文章目录
Ⅰ指针的概念
指针的两个要点
- 指针是内存中一个最小单元的编号,也就是地址
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
- 总结:指针就是地址,口语中说的指针通常指的是指针变量。
内存是如何存放变量的?
- 要彻底搞懂指针的概念,首先要知道内存是如何存放变量的。由于内存的最小索引单元式 1 字节,所以可以把整个内存想想为一个超级大的字符数组。
- 数组有索引下标,内存也是,只是我们把它称为地址。每个地址可以存放 1 字节的数据,所以对于占 4 字节的整型变量来说,就需要使用 4 个连续的存储单元来存放。
- 因为编译器知道具体每一个变量名对应的存放地址,所以当读取某个变量的时候,编译器就会找到变量名所在的地址,并根据变量的类型读取相应范围的数据。
- 如下图所示:找到变量名 f 所在的地址,并根据变量 f 的类型连续读取四个字节的地址。
指针和指针变量
- 通常我们所说的指针,就是地址的意思。C 语言中有专门的指针变量用于存放指针,跟普通变量不同,指针变量存储的是一个地址。
- 指针变量,里边存放的是地址,而通过这个地址,就可以找到一个内存单元。
地址的产生
- 访问内存首先得有地址,有了地址才能找到内存单元,那地址是哪里来的? 地址是从地址线上传过来的。
- 32 位系统中,有 32 根地址线,每根地址线上过来的信号有 0/1 两种情况;
- 32 根地址线所产生的所有二进制序列的组合就有 232 ,也就是会产生 232 个地址;
-
232 个地址,1个地址管理1个字节,总共能管理 232 个字节(4GB)的空间。
-
同样的,在 64 位系统中,就有 264 个地址。
总结
- 指针变量是用来存放地址(指针)的,地址是唯一标示一块地址空间的。
- 指针得我大小在 32 位机器上是 4 个字节,在 64 位上是 8 个字节。
Ⅱ 指针和指针类型
- 指针变量也有类型,它的类型就是存放的地址指向的数据类型。
- 如下图所示,变量 a ~ g 都是普通变量,其中变量 a ~ e 和 变量 g 都是字符变量,它们所在的地址存放的都是字符类型的数据,只占 1 个字节;
- 变量 f 是整型变量,存放的数据是一个整型,占 4 字节的的空间;
- 还有两个指针变量—— pa 和 pf,这两个变量存放的数据是地址,在这里它们分别存放了 变量 a 和变量 f 的地址。
指针的具体类型
- 当有这样的代码:
int num = 10;
p = #
- 要将 &num(num的地址)保存到 p 中,我们知道 p 就是一个指针变量,我们在定义指针变量 p 的时候就需要给它相应的类型了。
- 各类型指针变量的定义:
char* pc = NULL;//char* 就是 pc 的类型
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
float* pf = NULL;
double* pd = NULL;
- 可以看出,指针的类型就是指针所指向的变量类型+ *
- short* 说明了 ps 是一个指向 short 类型的变量的指针,*表示 ps是一个指针。
- int* 说明了 pi 是一个指向 int 类型的变量的指针。
指针类型的意义
- 不管是什么类型的指针,在 32 位机器上都是 4 个字节,那么定义不同的指针类型好像就显得很憨了。
- 然鹅,C 语言并没有把所有的指针类型都整合成一个同一的类型,那么指针类型自然是有其存在的意义的:指针 ± 整数、指针的解引用
⒈指针 ± 整数
先说结论
- 指针的类型决定了指针 ± 1 操作的时候跳过几个字节。
- int* 类型的指针 + 1 会让地址向后走 4 个字节;
- char* 的 + 1 会向后走 1 个字节;其余同理。
举个栗子
#include <stdio.h>
int main()
{
int a = 0;
char b = 'c';
int* pa = &a;
char* pb = &b;
printf("%p\n", &a);
printf("%p\n", pa);
printf("%p\n", pa + 1);//int* 类型的指针一步跨 4 字节
printf("----------------\n");
printf("%p\n", &b);
printf("%p\n", pb);
printf("%p\n", pb + 1);//char* 类型的指针一步跨 1 字节
return 0;
}
⒉指针的解引用
- 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
- 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
- 对 char* 类型的指针解引用只能修改一个字节的内存(只将一个字节的空间置为 0)。
- 对 int* 类型的指针 pi 解引用就能将 4 个字节的空间全部改为 0。
Ⅲ 野指针
野指针的概念
- 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
举个栗子
#include <stdio.h>
int main()
{
int* a;
*a = 123;
return 0;
}
- 类似于上面这样的代码是很危险的,因为指针变量 a 到底指向哪里,我们没办法知道。就和访问未初始化的变量一样,它的值是随机的。这在指针变量里是很危险的,因为后边代码对一个未知的地址进行赋值,那么就可能会覆盖到系统的一些关键代码。
- 偶尔这个指针变量里随机存放的是一个合法的地址,那么接下来的赋值会导致那个位置的值莫名其妙的被修改。
- 这种类型的 BUG 是非常难以排查的,所以在对指针进行解引用操作时,必须确保它们已经被正确的初始化了。
⒈野指针成因
1. 指针未初始化
- 指针没有初始化,就意味着指针没有明确的指向。
- 一个局部变量不初始化的话,放的是随机值。指针也一样,只不过放的时随机的地址。
#include <stdio.h>
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;//非法访问内存,将随机一个地址中存的值改成 20
return 0;
}
2. 指针越界访问
- 一般访问数组元素都是通过下标来访问的;
- 但有时候也会使用指针来访问,这种情况就有可能发生指针的越界访问了。
int main()
{
int i = 0;
int arr[10] = { 0 }; //数组名就是首元素地址 arr = &arr[0]
int* p = arr; //p 指向了数组的第一个元素,p + 1 指向第二个元素
for (i = 0; i <= 10; i++)
{
*p = i;
p++; //弄到最后会让 p + 10 相当于 arr[10],直接就越界了
}
return 0;
}
3. 指针指向的空间释放
int* test()
{
int a = 10;
return &a;
//因为 a 是局部变量,当把 a 的地址返回之后,变量 a 自动销毁
//原来的地址存放的就不再是变量 a 了,
}
int main()
{
int* p = test();
//用 p 来接收返回的 a 的地址
//当接收了 p 的地址后,再往下使用 p 就成了野指针
//当变量 a 销毁之后,p 还是记得传过来的地址,但此时这个地址已经不再指向 a 了
//当 p 用这个地址往回找的时候,就不晓得找的到底是谁了
return 0;
}
⒉规避野指针
- 指针初始化。
- 小心指针越界。
- 指针指向空间释放及时置NULL。
- 避免返回局部变量的地址。
- 指针使用之前检查有效性。
指针初始化
- 在使用指针的时候,如果知道要给指针变量什么值,就一定要把这个值赋给它。
int a = 110;
int* p1 = &a;//明确把 a 的地址赋给 p1
- 有时候确实不知道该让某个指针指向哪里的时候,一定要将其置为空指针。
int* p2 = NULL;//p2 哪都没指向,它是个空指针
指针指向空间释放及时置NULL
free(p); //释放 p 所指向的空间
p = NULL; //释放完之后要及时将该指针置空
指针使用之前检查有效性
if(p != NULL)
{
*p = 100;//不是空指针就可以对 p 进行解引用
}
Ⅳ 指针运算
⒈指针 ± 整数
- 当指针指向数组元素的时候,允许对指针变量进行加减运算,这样做的意义相当于指向举例指针所在位置向前或向后第 n 个元素。
- 例如:p + 1 表示指向 p 指针所指向的元素的下一个元素,p - 1 则表示指向上一个元素。
⒉指针 - 指针
- 指针之间也可以进行减法运算。
- 指针 - 指针得到的绝对值是指针和指针之间元素的个数。
- 注意:指向同一块空间(同个数组)的两个指针才能相减。
指针 - 指针的用途
- 求字符串长度。
- 只要拿到 \0 的地址,以及首字符的地址,两个指针相减,就是字符的长度。
⒊指针的关系运算
- 指针之间也可以比较大小。
#define N_VALUES 5
float values[N_VALUES];
float *vp;
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}
- 当 vp 指向的地址小于 values[N_VALUES] 的地址时,就会让 vp 一直+1,指向数组的下一个元素。
Ⅴ 指针和数组
- 指针和数组在很多方面都可以相互替换,给人的感觉它们似乎是一样的东西。
- 然而,指针终归是指针,数组终归还是数组。
指针和数组的区别
- 数组:一组相同类型元素的集合。
- 指针变量:是一个变量,存放的是地址。
举个栗子
- 下面代码试图计算一个字符串的字符个数:
int main()
{
int count = 0;
char str[] = "hello word!";
while (*str != '\0')
{
str++;//数组名是个常量,不可以自增
count++;
}
printf("总共有 %d 个字符\n", count);
return 0;
}
- 当试图运行的时候你可以看到,编译器毫不留情的报错了。
- 编译器提示了自增运算符的操作对象需要一个左值,这个表达式的自增运算符的操作对象是 str ,str 实际上是数组名,不是一个左值。
左值的定义
- 如果是左值的话,有两点要求:
- 是一个用于识别和定位一个存储位置的标识符。
- 这个值必须是可修改的。
- 第一点数组名是满足的,因为数组名就是定位一个数组的位置。第二点就无法满足了,因为数组名不是变量,它只是一个地址常量,没办法修改。
- 如果按照这个思路来写代码,应该这样修改:
int main()
{
int count = 0;
char str[] = "hello word!";
char* target = str;
while (*target != '\0')
{
target++;//指针是个左值(变量),可以修改
count++ ;
}
printf("总共有 %d 个字符\n",count);
return 0;
}
结论
- 数组名只是一个地址,而指针是一个左值(变量),可以存放地址。
Ⅵ 二级指针
- 指针变量也是变量,是变量就有地址,那么自然也有用来存放指针变量的地址的变量。
- 我们管这种指针叫做:指向指针的指针(二级指针)。
1. 二级指针的定义
int a = 10;
int* pa = &a; //pa 是一级指针变量,存放整型变量 a 的地址
int** ppa = &pa;//ppa 是二级指针变量,存放指针变量 pa 的地址
2. 二级指针的类型
- 二级指针的类型应该是指向的指针变量类型+*
- 如:int** 就是二级指针 ppa 的类型。
char** ppa = NULL;
int** ppb = NULL;
short** ppc = NULL;
long** ppd = NULL;
float** ppe = NULL;
double**ppf = NULL;
3. 二级指针解引用
- 已经知道了对一级指针解引用一次可以找到原来变量里存的值,那么同样的,对二级指针进行两次解引用也可有找到原来变量里边存的值。
4. 二级指针的用途
- 二级指针变量是用来存放一级指针变量的地址的。
Ⅶ 指针数组
- 指针数组本质上是个数组,是用来存放指针的数组。
1. 指针数组的定义
int* p[5];
//p 先和 [5] 结合,表明 p 是一个指针数组
//数组的每个元素都是一个 int* 类型的指针
- 数组下标的优先级要比取值运算符的优先级高,所以先入为主,p 被定义为具有 5 个元素的数组。
- 数组元素的类型是指向整型变量的指针。
- p 是一个数组,有五个元素,每个元素是一个整形指针。
2. 指针数组的用途
- 就像如果有很多 int 类型的值,可以放在一个整型数组里;
- 同样的,如果定义的同类型的指针太多了,也可以放在指针数组里。
int a = 10;
int b = 20;
int c = 30;
......
int arr[5] = {10,20,30,......};
/可以用整型数组将多个同类型的值存储起来
int* pa = &a;
int* pb = &b;
int* pc = &c;
......
int* parr[5] = {&a,&b,&c,......};
//也可以用数组将多个同类型的指针存储起来
3. 指针数组的访问
- 知道了怎么往指针数组里存东西之后,也要知道怎么把里面的东西拿出来。
- 只要能找到数组下标为 0 的位置,就能拿到 a 的地址,再对这个地址进行解引用就可以找到 10 这个值了。其余同理
- 先取出对应下标内存放的地址,然后再解引用。
- 现在我只想拿到前三个地址所指向的元素,请看代码:
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* parr[5] = { &a,&b,&c };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", *(parr[i]));
//找到数组对应下标内存放的地址,然后解引用
}
putchar('\n');
return 0;
}
4. 使用指针数组模拟二维数组
- 一般的一维数组的数组名就是首元素的地址,那么如果把数组名放到指针数组里自然就能形成二维数组的效果。
int arr1[4] = { 1,2,3,4 };
int arr2[4] = { 2,2,3,4 };
int arr3[4] = { 3,2,3,4 };
int* parr[3] = { arr1,arr2,arr3 };
- 将三个一维数组关联起来,造成一种二维数组的感觉。
- 想把这些元素打印出来,依然可以使用二维数组的方式。
int main()
{
int arr1[4] = { 1,2,3,4 };//第一行
int arr2[4] = { 2,2,3,4 };//第二行
int arr3[4] = { 3,2,3,4 };//第三行
int* parr[3] = { arr1,arr2,arr3 };
//parr[i],访问指针数组的每个元素的时候,
//就相当于拿到了上面三行每一行的第一个元素
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d ", parr[i][j]);
}
putchar('\n');
}
return 0;
}
- 此处可能有人会好奇了,为什么不解引用呢?
- 因为 [ ] 就是解引用:arr[i] <==> *(arr + i)