详解C语言指针的使用方法(上)https://developer.aliyun.com/article/1389156
5、指针数组?数组指针?
看下面的例子,你能分辨出哪个是指针数组,哪个是数组指针吗?
int* p1[5]; int(*p2)[5];
单个的我们都可以判断,但是组合起来就有些难度了。
答案:
int* p1[5];//指针数组 int(*p2)[5];//数组指针
我们挨个来分析。
指针数组
数组下标[]
的优先级是最高的,因此p1
是一个有5个元素的「数组」。那么这个数组的类型是什么呢?答案就是int*
,是「指向整型变量的指针」。因此这是一个「指针数组」。
那么这样的数组应该怎么样去初始化呢?
你可以定义5个变量,然后挨个取地址来初始化。
不过这样太繁琐了,但是,并不是说指针数组就没什么用。
比如:
//Example 07 #include int main(void) { char* p1[5] = { "人生苦短,我用Python。", "PHP是世界上最好的语言!", "One more thing...", "一个好的程序员应该是那种过单行线都要往两边看的人。", "C语言很容易让你犯错误;C++看起来好一些,但当你用它时,你会发现会死的更惨。" }; int i; for (i = 0; i < 5; i++) { printf("%s\n", p1[i]); } return 0; }
结果如下:
//Consequence 07 人生苦短,我用Python。 PHP是世界上最好的语言! One more thing... 一个好的程序员应该是那种过单行线都要往两边看的人。 C语言很容易让你犯错误;C++看起来好一些,但当你用它时,你会发现会死的更惨。
这样是不是比二维数组来的更加直接更加通俗呢?
数组指针
()
和[]
在优先级里面属于「同级」,那么就按照「先后顺序」进行。
int(*p2)
将p2
定义为「指针」, 后面跟随着一个5个元素的「数组」,p2
就指向这个数组。因此,数组指针是一个「指针」,它指向的是一个数组。
但是,如果想对数组指针初始化的时候,千万要小心,比如:
//Example 08 #include int main(void) { int(*p2)[5] = {1, 2, 3, 4, 5}; int i; for (i = 0; i < 5; i++) { printf("%d\n", *(p2 + i)); } return 0; }
Visual Studio 2019报出以下的错误:
//Error and Warning in Example 08 错误(活动) E0146 初始值设定项值太多 错误 C2440 “初始化”: 无法从“initializer list”转换为“int (*)[5]” 警告 C4477 “printf”: 格式字符串“%d”需要类型“int”的参数,但可变参数 1 拥有了类型“int *”
这其实是一个非常典型的错误使用指针的案例,编译器提示说这里有一个「整数」赋值给「指针变量」的问题,因为p2
归根结底还是指针,所以应该给它传递一个「地址」才行,更改一下:
//Example 08 V2 #include int main(void) { int temp[5] = {1, 2, 3, 4, 5}; int(*p2)[5] = temp; int i; for (i = 0; i < 5; i++) { printf("%d\n", *(p2 + i)); } return 0; }
//Error and Warning in Example 08 V2 错误(活动) E0144 "int *" 类型的值不能用于初始化 "int (*)[5]" 类型的实体 错误 C2440 “初始化”: 无法从“int [5]”转换为“int (*)[5]” 警告 C4477 “printf”: 格式字符串“%d”需要类型“int”的参数,但可变参数 1 拥有了类型“int *”
可是怎么还是有问题呢?
我们回顾一下,指针是如何指向数组的。
int temp[5] = {1, 2, 3, 4, 5}; int* p = temp;
我们原本以为,指针p
是指向数组的指针,但是实际上「并不是」。仔细想想就会发现,这个指针实际上是指向的数组的「第一个元素」,而不是指向数组。因为数组里面的元素在内存中都是挨着个儿存放的,因此只需要知道第一个元素的地址,就可以访问到后面的所有元素。
但是,这么来看的话,指针p
指向的就是一个「整型变量」的指针,并不是指向「数组」的指针。而刚刚我们用的数组指针,才是指向数组的指针。因此,应该将「数组的地址」传递给数组指针,而不是将第一个元素的地址传入,尽管它们值相同,但是「含义」确实不一样:
//Example 08 V3 //Example 08 V2 #include int main(void) { int temp[5] = {1, 2, 3, 4, 5}; int(*p2)[5] = &temp;//此处取地址 int i; for (i = 0; i < 5; i++) { printf("%d\n", *(*p2 + i)); } return 0; }
程序运行如下:
//Consequence 08 1 2 3 4 5
6、指针和二维数组
在上一节《C语言之数组》我们讲过「二维数组」的概念,并且我们也知道,C语言的二维数组其实在内存中也是「线性存放」的。
假设我们定义了:int array[4][5]
array
array作为数组的名称,显然应该表示的是数组的「首地址」。由于二维数组实际上就是一维数组的「线性拓展」,因此array应该就是指的指向包含5个元素的数组的指针
。
如果你用sizeof()
去测试array
和array+1
的话,就可以测试出来这样的结论。
*(array+1)
首先从刚刚的问题我们可以得出,array+1
同样也是指的指向包含5个元素的数组的指针
,因此*(array+1)
就是相当于array[1]
,而这刚好相当于array[1][0]
的数组名。因此*(array+1)
就是指第二行子数组的第一个元素的地址。
*(*(array+1)+2)
有了刚刚的结论,我们就不难推理出,这个实际上就是array[1][2]
。是不是感觉非常简单呢?
总结一下,就是下面的这些结论,记住就好,理解那当然更好:
*(array + i) == array[i] *(*(array + i) + j) == array[i][j] *(*(*(array + i) + j) + k) == array[i][j][k] ...
7、数组指针和二维数组
我们在上一节里面讲过,在初始化二维数组的时候是可以偷懒的:
int array[][3] = { {1, 2, 3}, {4, 5, 6} };
刚刚我们又说过,定义一个数组指针是这样的:
int(*p)[3];
那么组合起来是什么意思呢?
int(*p)[3] = array;
通过刚刚的说明,我们可以知道,array
是指向一个3个元素的数组的「指针」,所以这里完全可以将array
的值赋值给p
。
其实C语言的指针非常灵活,同样的代码用不同的角度去解读,就可以有不同的应用。
那么如何使用指针来访问二维数组呢?没错,就是使用「数组指针」:
//Example 09 #include int main(void) { int array[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} }; int(*p)[4]; int i, j; p = array; for (i = 0, i < 3, i++) { for (j = 0, j < 4, j++) { printf("%2d ", *(*(p+i) + j)); } printf("\n"); } return 0; }
运行结果:
//Consequence 09 0 1 2 3 4 5 6 7 8 9 10 11
第三:void指针
void
实际上是无类型
的意思。如果你尝试用它来定义一个变量,编译器肯定会「报错」,因为不同类型所占用的内存有可能「不一样」。但是如果定义的是一个指针,那就没问题。void
类型中指针可以指向「任何一个类型」的数据,也就是说,任何类型的指针都可以赋值给void
指针。
将任何类型的指针转换为void
是没有问题的。但是如果你要反过来,那就需要「强制类型转换」。此外,不要对void
指针「直接解引用」,因为编译器其实并不知道void
指针会存放什么样的类型。
//Example 10 #include int main(void) { int num = 1024; int* pi = # char* ps = "TechZone"; void* pv; pv = pi; printf("pi:%p,pv:%p\n", pi, pv); printf("*pv:%d\n", *pv); pv = ps; printf("ps:%p,pv:%p\n", ps, pv); printf("*pv:%s\n", *pv); }
这样会报错:
//Error in Example 10 错误 C2100 非法的间接寻址 错误 C2100 非法的间接寻址
如果一定要这么做,那么可以用「强制类型转换」:
//Example 10 V2 #include int main(void) { int num = 1024; int* pi = # char* ps = "TechZone"; void* pv; pv = pi; printf("pi:%p,pv:%p\n", pi, pv); printf("*pv:%d\n", *(int*)pv); pv = ps; printf("ps:%p,pv:%p\n", ps, pv); printf("*pv:%s\n", pv); }
当然,使用void指针一定要小心,由于void指针几乎可以「通吃」所有类型,所以间接使得不同类型的指针转换变得合法,如果代码中存在不合理的转换,编译器也不会报错。
因此,void指针能不用则不用,后面讲函数的时候,还可以解锁更多新的玩法。
第四:NULL指针
在C语言中,如果一个指针不指向任何数据,那么就称之为「空指针」,用「NULL」来表示。NULL其实是一个宏定义:
#define NULL ((void *)0)
在大部分的操作系统中,地址0通常是一个「不被使用」的地址,所以如果一个指针指向NULL,就意味着不指向任何东西。为什么一个指针要指向NULL呢?
其实这反而是一种比较指的推荐的「编程风格」——当你暂时还不知道该指向哪儿的时候,就让它指向NULL,以后不会有太多的麻烦,比如:
//Example 11 #include int main(void) { int* p1; int* p2 = NULL; printf("%d\n", *p1); printf("%d\n", *p2); return 0; }
第一个指针未被初始化。在有的编译器里面,这样未初始化的变量就会被赋予「随机值」。这样指针被称为「迷途指针」,「野指针」或者「悬空指针」。如果后面的代码对这类指针解引用,而这个地址又刚好是合法的话,那么就会产生莫名其妙的结果,甚至导致程序的崩溃。因此养成良好的习惯,在暂时不清楚的情况下使用NULL,可以节省大量的后期调试的时间。
第五:指向指针的指针
开始套娃了。其实只要你理解了指针的概念,也就没什么大不了的。
//Example 12 #include int main(void) { int num = 1; int* p = # int** pp = &p; printf("num: %d\n", num); printf("*p: %d\n", *p); printf("**p: %d\n", **pp); printf("&p: %p, pp: %p\n", &p, pp); printf("&num: %p, p: %p, *pp: %p\n", &num, p, *pp); return 0; }
程序结果如下:
//Consequence 12 num: 1 *p: 1 **p: 1 &p: 004FF960, pp: 004FF960 &num: 004FF96C, p: 004FF96C, *pp: 004FF96C
当然你也可以无限地套娃,一直指下去。不过这样会让代码可读性变得「很差」,过段时间可能你自己都看不懂你写的代码了。
第六:指针数组和指向指针的指针
那么,指向指针的指针有什么用呢?
它可不是为了去创造混乱代码,在一个经典的实例里面,就可以体会到它的用处:
char* Books[] = { "《C专家编程》", "《C和指针》", "《C的陷阱与缺陷》", "《C Primer Plus》", "《Python基础教程(第三版)》" };
然后我们需要将这些书进行分类。我们发现,其中有一本是写Python的,其他都是C语言的。这时候指向指针的指针就派上用场了。首先,我们刚刚定义了一个指针数组,也就是说,里面的所有元素的类型「都是指针」,而数组名却又可以用指针的形式来「访问」,因此就可以使用「指向指针的指针」来指向指针数组:
... char** Python; char** CLang[4]; Python = &Books[5]; CLang[0] = &Books[0]; CLang[1] = &Books[1]; CLang[2] = &Books[2]; CLang[3] = &Books[3]; ...
因为字符串的取地址值实际上就是其「首地址」,也就是一个「指向字符指针的指针」,所以可以这样赋值。
这样,我们就利用指向指针的指针完成了对书籍的分类,这样既避免了浪费多余的内存,而且当其中的书名要修改,只需要改一次即可,代码的灵活性和安全性都得到了提升。
第七:常量和指针
常量,在我们目前的认知里面,应该是这样的:
520, 'a'
或者是这样的:
#define MAX 1000 #define B 'b'
常量和变量最大的区别,就是前者「不能够被修改」,后者可以。那么在C语言中,可以将变量变成像具有常量一样的特性,利用const
即可。
const int max = 1000; const char a = 'a';
在const
关键字的作用下,变量就会「失去」本来具有的可修改的特性,变成“只读”的属性。
第八:指向常量的指针
强大的指针当然也是可以指向被const
修饰过的变量,但这就意味着「不能通过」指针来修改它所引用的值。总结一下,就是以下4点:
❝
- 指针可以修改为指向不同的变量
- 指针可以修改为指向不同的常量
- 可以通过解引用来读取指针指向的数据
- 不可以通过解引用来修改指针指向的数据
❞
第九:常量指针
指向非常量的常量指针
指针本身作为一种「变量」,也是可以修改的。因此,指针也是可以被const
修饰的,只不过位置稍稍「发生了点变化」:
... int* const p = # ...
这样的指针有如下的特性:
❝❞
- 指针自身不能够被修改
- 指针指向的值可以被修改
1、指向常量的常量指针
在定义普通变量的时候也用const
修饰,就得到了这样的指针。不过由于限制太多,一般很少用到:
... int num = 100; const int cnum = 200; const int* const p = &cnum; ...