理清二维数组的指针各种表达方式的思维导图
目录
内存
内存是电脑上的存储设备,程序运行时会加载到内存。
任务管理器可以查看内存使用情况:
内存中的每一个字节都有自己的编号:
这个编号是用十六进制表示的(“0x”是用来表示这个数是十六进制数而不是其他进制,可以对一个整形变量赋值一个十六进制数,如 int a = 0x11223344)。这个编号也可以叫做地址、指针,它们都是一回事。我们平时口头说的指针是指针变量。
指针是内存中一个最小存储单元的编号,也就是地址。
& ---- 取地址运算符
假如我们定义了一个整形变量 a ,a 占四个字节:
如果此时我们 &a (对 a 取地址),在上图中,四个红色方块的编号都是 a 的地址,那么对 a 取地址,取的是哪个地址呢?
答案是:取第一个字节的地址(较小的地址)
如果我们把这个地址赋值给一个变量,这个变量就叫指针变量。存放在指针变量中的值都被当做地址处理
编址
定义时只需要在类型名后加一个 * 就可以了,如:
int* p = &a ;
指针就是地址,我们平时口头说的指针是指针变量。
指针变量存放的地址用 %p 打印:
(编址)
对于32位的机器 , 假设有32根地址线 , 那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就有32个0或1,能够寻找2的32次方个地址(4GB),所以32位的机器的内存通常在1到4GB。
这里我们就明白:
● 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
● 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地
址。
总结:
● 指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
● 指针的大小在32位平台是4个字节,在64位平台是8个字节。
指针的类型
上面说了指针的大小在32位平台是4个字节,在64位平台是8个字节,那为啥我们在定义指针的时候要指定它的基类型呢?为啥不用一个统一的类型来定义指针变量呢,如 ptr i ?
分析以下代码:
将十六进制数赋值给整形变量 a 后,定义一个字符指针变量 p ,使它指向 a ,将 0 赋值给 *p 后,我们发现 a 的值改变为 0x11223300 ,为什么 a 的值不改变为 00 00 00 00 呢 ?
(上面的代码中,a 是整形变量,p 是字符指针变量,将 a 的地址赋值给 p 要将 a 的类型强制转换为 char* ,不然编译器会报出警告)
这是因为:
指针类型其实是有意义的
1. 指针类型决定了指针进行解引用操作的时候一次性访问几个字节
如果是char*的指针,解引用访问1个字节
如果是int*的指针,解引用访问4个字节
与指针指向的变量类型无关
2. 指针类型决定指针的步长(对指针+1跳过几个字节)
字符指针+1,跳过1个字节
整形指针+1, 跳过4个字节
指针的解引用
解引用操作符(*)用于访问指针所指向的内存位置中的数据,而不是地址本身。
例如,如果你有一个指向整数的指针 p,那么 p 将返回 p 所指向的整数的值。同样,如果你有一个指向字符的指针 q,那么 q 将返回 q 所指向的字符。
解引用操作可以用于读取和修改指针所指向的值。
需要注意的是,解引用操作符只能用于有效的指针,即指向有效内存地址的指针。如果指针是nullptr、未初始化的指针或者指向已经释放的内存块的指针,解引用它将导致未定义的行为,可能会引发程序崩溃或其他错误。因此,在使用指针时,应该确保指针是有效的,并且在解引用之前进行必要的检查。
危险的野指针
成因
1、指针未初始化
int* p;
*p = 20;
整形指针变量 p 未初始化,可能指向系统文件,随意篡改系统文件将导致严重的后果。
2、指针的越界访问
int a[10] = {0};
int*p = a;
for( int i = 0 , i <= 11 , i++ )
{
*p = i;
p++;
}
当指针的指向超出数组的范围时,该指针就是野指针。
3、指针指向的空间已被释放
int* test()
{
int a = 10;
return &a;|
}
int main()
{
int*p = test();
printf("%d\n",*p);
return 0;
}
退出 test 函数时,变量 a 已经被销毁,再用指针变量 p 去访问 a 的存储空间不合法。
但我们如果运行以上代码,会发现可以输出 10,这是因为在指针变量得到 a 的地址时立刻就打印 p 所指向的地址,test 函数的函数栈帧没有被覆盖,p 所指向的地址存储的还是 10。如果我们在printf(“%d\n”,*p) 前面加上printf(“hehe\n”),发现打印出来的就不是 10 了。
如何规避野指针
1. 指针初始化
在定义一个指针变量时,就指定它的指向。如 int*p = &a,
如果暂时不知道该指针该指向谁,
要使它成为一个空指针(NULL,NULL = (*void)0)
空指针有时也非常危险,如:int*p = NULL;*p = 10;(p 不指向任何一个变量,对 p “所指向的变量赋值 10 是非法的)
规避空指针可以用 if(p != NULL){...}
2. 小心指针越界
3. 指针指向空间释放,及时置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性(是不是空指针)
指针运算
1、指针加减一个整数的运算
指针加减一个整数的意义参考上文
2、指针减去指针(地址减去地址)
前提:两个指针指向同一个数组
意义:得到的结果的绝对值是两个指针之间的元素个数。
应用:模拟实现 string 函数
3、指针的关系运算
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
一维数组的指针
引用一维数组元素的方法:
1、下标法,如 a[1]、a[2]
a[i] 与 *(a + i)等价
[ ] 是变址运算符,编译时会把 A[B] 转化为 *(A + B)
2、指针法
一维数组的数组名是数组首元素的地址,这个地址是可以放在指针变量中的,可以通过该指针访问数组元素
int a[10];
int*p = a;//不能写成int*p = &a;
则
*(p + i)、 a[i] 、 p[i] 等价
(一维数组的数组名是指针常量,不能对它赋值)
综上所述,若 a 是一维数组名,p 是指针变量且 p = a,则:
a[i] 、 *(a + i)、 *(p + i)、 p[i] 等价
指针数组
指针数组是数组,是存储指针的数组。
int a = 1;
int b = 2;
int* arr[2] = { &a,&b};
arr 就是一个指针数组。
二维数组的指针
若 a 是二维数组名,则:
上图是各种有关二维数组的指针的表达,其中基类型可以理解为”步长“,即对该指针进行加减一个整数,指针跳过的字节数
用指针一维数组模拟二维数组:
定义了三个一维数组:
int a[4]; int b[4]; int c[4]; 其中 a ,b ,c 是一维数组名,表示数组首元素的地址。
又定义了一个指针一维数组:
int* arr[3] = { a,b,c };则 arr[1]表示 a 数组首元素的地址。
上图中,根据 [ ] 变址运算符的运算,arr[i][j] 等价于 *(a[i] + j ),而 a[i] 等价于 *(a + i),所以 arr[i][j] 等价于
*(*(a + i) + j )
[ ] 是变址运算符,编译时会把 A[B] 转化为 *(A + B)
这样子应该可以很好理解二维数组的指针。
二级指针
指针变量是变量,是变量就有地址,指针变量的地址可以存储到二级指针。
二级整形指针可以用 int** 定义,三级整形指针可以用 int*** 定义,以此类推。
怎么通过二级指针访问它最终指向的变量呢?
如上图:*ppa ----> pa ----> *pa ----> a
也可以用**ppa ----> a
(未完待续)