【C语言】指针的进阶(一)
目录
前言
1. 字符指针
2. 指针数组
3. 数组指针
3.1 数组指针的定义
3.2 &数组名VS数组名
3.3 数组指针的使用
4. 数组参数、指针参数
4.1 一维数组传参
4.2 二维数组传参
4.3 一级指针传参
4.4 二级指针传参
5. 函数指针
前言
指针在C语言中可谓是有着举足轻重的存在,初学C语言的我们在《指针》章节已经接触过了一些指针的知识,知道了指针的概念:
- 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
- 指针的大小是固定的4/8个字节(32位平台/64位平台)。
- 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
- 指针的运算。
指针的基础知识已经了解完毕,那么在这一篇博客里,我们将深入探讨指针的一些高级使用。
1. 字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char*
一般使用:
int main() { char ch = 'w'; char* pc = &ch; return 0; }
还有一种使用方式:
int main() { const char* pstr = "abcdef"; printf("%s\n", pstr); return 0; }
上面这种用法很多人会有一个误区,以为是把字符串 abcdef 放到字符指针 pstr 里了,但是本质是把字符串 abcdef 中的首字符的地址放到了pstr中。
下面可以试着证明一下:
- 字符串"abcdef"的地址就是a所在地址,那么"abcdef"[3]相当于 ““a地址[3]”” ,侧面印证了确实是把首字符地址存入了指针pstr中。
- 数组名就是首元素地址,既然说字符指针存放的是首字符的地址,那么试着用数组下标的方式访问字符指针指向的内容,结果发现一样可以打印出来。
- 因此完全可以把常量字符串想象成一个数组,然后用字符指针接收,操作起来与数组一致。
注意:最好使用 const 修饰存放字符串的字符指针,因为字符串是常量,不允许被修改,如果修改了程序会崩溃。
一道经典【面试题】,出自《剑指offer》:
以下的最终输出是什么呢?
#include int main() { char str1[] = "nash."; char str2[] = "nash."; const char* str3 = "nash."; const char* str4 = "nash."; if (str1 == str2) printf("str1 and str2 are same\n"); else printf("str1 and str2 are not same\n"); if (str3 == str4) printf("str3 and str4 are same\n"); else printf("str3 and str4 are not same\n"); return 0; }
【答案】
str1 and str2 are not same
str3 and str4 are same
【解释】
- str1 和 str2 :用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块,str1和str2其实是各自创建了一个空间存放nash.,因此它们的地址是不一致的。
- str3 和 str4 :当用指针指向字符串时,因为nash是常量字符串,是不会被修改的,那么既然不能被修改,编译器就没必要保存多份,只需要一份,然后让指针都指向同一块内存即可,因此地址值是一致的。
【扩展】
int main() { char str1[] = "hello bit."; char str2[] = "hello bit."; const char* str3 = "hello bit."; const char* str4 = "hello bit."; if (&str3 == &str4) printf("Yes"); else printf("No"); return 0;
【答案】
结果为No,因为str3和str4本身的地址值是不一样的,只是str3和str4指向的内容是一样的。
2. 指针数组
这里我们复习一下,下面指针数组是什么意思?
int* arr1[10]; //整形指针的数组 char *arr2[4]; //一级字符指针的数组 char **arr3[5];//二级字符指针的数组
类比一下:
字符数组 —— 存放字符的数组
整型数组 —— 存放整形的数组
那么:
指针数组 —— 存放指针的数组,即存放在数组中的元素都是指针类型。
那么很多人有个疑问,指针数组到底有什么用呢?
很多人直观的感受就是定义abcd,然后将它们的地址都存入整型指针数组中。如下:
错误的使用方式,【没有意义】 int main() { int a = 0; int b = 1; int c = 2; int* arr[3] = { &a,&b,&c }; return 0; }
但是很少有人会这样使用的,没有这种使用场景,也没有人会这样使用,这样写的没有任何意义。
正确的使用场景之一:
【可以使用指针数组模拟一个二维数组】 int main() { int arr1[] = { 1,2,3,4,5 }; int arr2[] = { 2,3,4,5,6 }; int arr3[] = { 3,4,5,6,7 }; int* arr[3] = { arr1,arr2,arr3 }; //遍历三个数组 int i = 0; for ( i = 0; i
使用指针数组维护多个数组,模拟出一个二维数组,因此操作也与而二维数组类似。
还有使用指针数组维护多个字符串:
当然还有很多其他的应用场景,但是由于篇幅有限,这里就不再一一列举。
3. 数组指针
3.1 数组指针的定义
数组指针是指针?还是数组?
类比一下:
整型指针 —— 指向整形的指针
字符指针 —— 指向字符的指针
那么:
数组指针 —— 指向数组的指针
下面代码哪个是数组指针?
int *p1[10]; int (*p2)[10]; //p1, p2分别是什么?
【解释】
p2是数组指针。
p2先和*结合,说明p2是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证p2先和*结合。
3.2 &数组名VS数组名
在往期博客(链接:点击前往)中有提到过:
数组名就是地址,通常来说:数组名是数组首元素的地址。
但是,存在两种特殊情况:
1、sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
2、&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
除此之外,所有遇到的数组名都是数组首元素的地址。
由上图可以证明,第一组和第二组+1时都只是跳过四个字节,即arr表示的是首元素地址。
而第三组可以发现,&arr和&arr+1之间跳过了40个字节,即一整个数组的大小,因此证明&arr表示整个数组。
指针类型决定了指针+1到底+几个字节。
那么我们大胆猜测一下,arr的类型是int*,&arr[0]的类型也是int*,
那么&arr的类型就是int (*)[10],即数组指针存放数组。
既然看完了什么是数组指针,那么做一道题来检测一下自己是否理解明白。
【练习】
下面代码中pc的类型是什么?
int main() { char* arr[5]; pc = &arr; //给pc定义类型 return 0; }
【答案】
char* (*pc)[5] = &arr;
【解释】
首先pc得是个指针,即(*pc)
指向一个元素为5的数组,即(*pc)[5]
数组每个元素的类型是char*,即char* (*pc)[5]
那么数组指针有哪些使用场景呢?
3.3 数组指针的使用
错误使用场景:
这种场景一点也不方便,在使用数组时反而更麻烦了,有一种 “脱裤子放屁” 的感觉。这种使用方式基本上不会出现。
如果非要用指针接收数组,也应该使用指针接收首元素地址,而不是接收整个数组的地址。这才是正确的访问姿势。
其实,在一维数组传参过程中,形参既可以写成数组的形式,又可以写成指针的形式,因为本质上这两种方式都是传递的是数组首元素地址,是互等的。
既然本质上都是地址,那么为什么还要写成数组形式呢?
那是因为能够写成数组形式完全是为了照顾初学者,因为实参是一个数组,形参也定义一个数组来接收这个数组这种方式更能够让初学者理解。因此即使这么写,本质也还是指针。
同理,二维数组传参也有两种方式:
- 形参也是使用二维数组的形式。
- 形参使用数组指针的形式。
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
int arr[5]; int *parr1[10]; int (*parr2)[10]; int (*parr3[10])[5];
【答案】
- arr 是一个能够存放5个整型数据的数组。
- parr1 是一个数组,能够存放整型指针的数组,数组大小为10。
- parr2 是一个指针,指向元素为10的数组,每个元素的类型是int*
- parr3 是一个数组,数组有10个元素,指向一个指针,指针指向的内容是元素为5的数组,每个元素的类型是int,即是一个存放数组指针的数组。
当然parr3看不懂也不要着急,这种形式是很少用到的,只是作为拓展知识点了解即可。
4. 数组参数、指针参数
4.1 一维数组传参
4.2 二维数组传参
二维数组传参,函数形参的设计只能省略第一个[]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。
4.3 一级指针传参
一级指针传参,形参就写成一级指针就行。
#include void print(int* p, int sz) { int i = 0; for (i = 0; i
【思考】
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
【答案】
4.4 二级指针传参
#include void test(int** ptr) { printf("num = %d\n", **ptr); } int main() { int n = 10; int* p = &n; int** pp = &p; test(pp); test(&p); return 0; }
【思考】
当函数的参数为二级指针的时候,可以接收什么参数?
【答案】
5. 函数指针
函数指针 —— 指向函数的指针 —— 存放的是函数的地址
&Add和Add完全等价,下面的例子可以证明。
可以定义 int (*pf2)(int,int) = &Add; 将&Add赋值给pf2, pf2便是函数指针。
- Add引用函数返回ret1
- 此时对pf2解引用在调用函数返回ret2
- 对pf2直接调用函数返回ret3
- 在pf2前面加很多解引用返回ret4
最后打印结果居然发现,四个结果都是一样的。
所以得出结论:函数指针在调用所指向函数时,可以不写*直接和函数名一样调用函数,而*号在这里其实就只是一个摆设,同样是为了照顾初学者的使用习惯,所以才会导致当加了很多*号去解引用时得出来的结果依然是正确的结果。
下面来看两端有趣的代码:
1、
(*(void (*)())0)();
- void (*)() ———— 函数指针类型
- ( void (*)() )0 ———— 对0进行强制类型转换,转换成函数指针。即将地址0当做存放函数指针的地址。
- (*( void (*)() )0 )() ———— 通过函数指针调用函数,函数的参数为空。
即上面的代码实在调用0地址处的函数,这个函数没有参数,返回值是void。
该代码出自《C陷阱和缺陷》
2、
void (*signal(int , void(*)(int)))(int);
- signal(int, void(*)(int)) ———— signal是一个函数,它有两个参数,整型int和函数指针类型void(*)(int)。
- void (*signal(int , void(*)(int)) )(int); ———— signal函数的返回类型也是函数指针类型void (*)(int)
但是这个代码看着太复杂了,有没有办法简化呢?
有没有办法将代码写成符合我们习惯的形式这样:返回类型在前,函数名在中间,函数参数在后的:void (*)(int) signal(int , void(*)(int)),这样直接写是肯定不支持的,但是可以通过typedef优化一下:
typedef void(*pfun_t)(int); //对void(*)(int)重新起名为pfun_t pfun_t signal(int, pfun_t);
对void(*)(int)重新起名为pfun_t,这样写出来的代码就清楚多了。
如果觉得作者写的不错,求给作者一个大大的点赞支持一下,你们的支持是我更新的最大动力!