【数组指针】
讲完指针数组后,我们就来讲讲它的双胞胎兄弟 —— 【数组指针】
💬首先还是这个问题,数组指针是指针?还是数组?
1、数组指针的定义
- 我们通过指针初阶中所学习的整型指针和字符指针来做一个对比
int a = 10; char ch = 'x'; int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* pa = &a; —— 整型指针 - 存放整型地址的指针 char* pc = &ch; —— 字符指针 - 存放字符地址的指针 int(*parr)[10] = &arr; —— 数组指针 - 存放数组地址的指针
- 也是一样来分析一下这三个指针
- 对于
pa
,它是一个整型指针,里面存放的是一个整型的地址 - 对于
pc
,它是一个字符型指针,里面存放的是一个字符的地址 - 对于
parr
,它是一个数组指针,里面存放的是一个数组的地址
- 通过这么的对比相信你对【数组指针】有了一初步的概念,它也是一个指针,它所指向的是一个数组的地址
然后就来仔细介绍一下数组指针
- 下面有一个arr数组,数组里面有5个元素,每个元素都是一个
int
类型。那现在我要将这个数组的地址存起来,那肯定需要一个指针来接收,那既然是一个指针的话我们肯定会想要用*
做修饰,不过这还不够,因为接收的是一个数组的地址,所以我们还会想要再加上[10]
,而且这个10还不能像我们定义数组的可以省略调用,一定要加上 - 但是像下面这样真的可以吗?或许你应该去了解一下运算符优先级,因为
[]
的优先级是最高的,所以这个【pa】会首先和[]
结合,而不是先和*
,那么它就是一个数组,而不是指针了!
int arr[5] = { 1,2,3,4,5 }; int* pa[10] = &arr;
- 若是想要【pa】和这个
*
先结合的话,在它们的外面加上一个()
即可,如下所示👇
int (*pa)[10] = &arr;
==这才是一个完整又正确的【数组指针】==
2、&数组名VS数组名
对于数组名是首元素地址这个说法我们已经是耳熟于心了,不过上面看到了一个新的写法
&数组名
,这和数组名
存在着什么关联呢?本模块我们就来探讨一下这个
- 可以看到,在下面我分别打印了三种情形,那可以预测第一种和第二种是一样的,而第三种可能就不一样
int arr[5] = { 1,2,3,4,5 }; printf("%p\n", arr); printf("%p\n", &arr[0]); printf("%p\n", &arr);
但是从运行结果可以看到它们都是一样的,这是为什么呢?
- 在数组章节我就有讲到过
&数组名
值得是取出整个数组的地址,而&arr[0]
则是数组首元素的地址。不过从下图可以看,它们的位置是一样的,所以打印出来的地址就是一样的
💬那有同学说:难道它们就完全相同吗,那&数组名
还有什么意义呢?
- 但此时我将当前取到的地址再去 + 1的话,会有什么变化呢?
printf("%p\n", arr); printf("%p\n", arr + 1); puts("---------------"); printf("%p\n", &arr[0]); printf("%p\n", &arr[0] + 1); puts("---------------"); printf("%p\n", &arr); printf("%p\n", &arr + 1); puts("---------------");
可以看到,最后一个&数组名
和上面两个的结果不同
- 对于
arr
和&arr[0]
都一样,取到的是首元素的地址,这是一个整型数组,首元素是一个int
类型的数据,那么其地址就是int*
类型,那在【指针初阶部分】我有讲到过一个int*
的指针一次可以访问4个字节的大小,那在这数组中每个元素都占4个字节,所以 + 1就会跳过一个元素也就是4个字节 - 对于
&arr
来说,取出的是整个数组的大小,虽然它的位置和首元素地址是一样的,但是它 + 1跳过的确是整个数组的大小,上面说到过一个数组的地址给到【数组指针】来接收int (*parr)[5] = &arr;
,此时去掉它的变量名后这个指针的类型就是int(*)[10]
,上面我们也有讲过一个指针一次可以访问的字节取决于它的类型
具体可以看看这张图👇
💬在知晓了这一点后许多同学就明白了这个地址的偏移为何是这样,但是仔细一算好像也不对呀,整个数组所占的字节数不是20吗,这里是14呀?
- 要知道,编译器对于一块地址的表示形式是以十六进制的形式,所以我们计算出的差值应该再转换为十进制才对,那么14转换为十进制后刚好就是20,不清楚规则的同学可以去了解一下十六进制转十进制
3、数组指针的使用【⭐】
讲了这么多后,这个数组指针到底有什么用呢?
1.数组指针在一维数组的使用场景
- 之前我们在使用函数封装一个打印数组时有着下面两种写法,一个就是使用数组做接收,一个则是使用指针做接收。因为外界所传入的都是数组名,数组名就是首元素地址
void print1(int arr[], int n) { int i = 0; for (i = 0; i < n; ++i) { printf("%d ", arr[i]); } printf("\n"); } void print2(int* arr, int n) { int i = 0; for (i = 0; i < n; ++i) { printf("%d ", arr[i]); } printf("\n"); } print1(a, sz); print2(a, sz);
- 那在学习了【数组指针】后,我们还可以把形参写成下面这种样子
void print3(int (*p)[5], int n) { int i = 0; for (i = 0; i < n; ++i) { printf("%d ", (*p)[i]); //a[i] } }
- 实参就要以下面这种形式进行传递,那此时形参p接收到的就是整个数组的地址,那么此时
*p
也就取到了这个一维数组的数组名,那我们平常用数组名来访问数组中的每个元素时,都是用的arr[i]
这样的形式,那么用解引用后的数组指针来访问就可以写成(*p)[i]
print3(&a, sz);
💬但这样不是很别扭吗?传进来数组的地址,然后再解引用获取到数组名,还不如直接传递数组名呢🤨
- 是的,一般数组指针我们不会用在一维数组的情况下,但是我们一般直接会用数组名或者指针来接收。但数组指针在二维数组中使用的还是比较的多的
2.数组指针在二维数组的使用场景
- 下面是我们之前在使用函数封装二维数组打印的时候所需要的传参
void print4(int arr[3][5], int row, int col) { int i = 0; for (i = 0; i < row; i++) { int j = 0; for (j = 0; j < col; j++) { printf("%d ", arr[i][j]); } printf("\n"); } } int a[3][5] = { {1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7} }; print4(a, 3, 5);
- 那采用【数组指针】的写法也是像上面这样,但是有同学却疑惑说:传进来的不是一个二维数组吗?
void print5(int (*p)[5], int row, int col) { int i = 0; for (i = 0; i < row; i++) { int j = 0; for (j = 0; j < col; j++) { printf("%d ", *(*(p + i) + j)); } printf("\n"); } }
- 这一块的话我就来重点分析一下了:首先你要知道知道对于一维数组而言,它的首元素地址即为数组中第一个元素的地址,那么二维数组的首元素地址相当于什么呢?如果你仔细看过数组章节的话就可以知道为第一行的地址,此时形参
p
接收到的即为第一行的地址。对于二维数组把每一行看做是一个元素,那么对于这个数组来说三行就有三个元素,那么要如何访问到每一行呢?那就是使用p + i
,随着【i】的不断变化就可以取到每一行的地址 - 但是我们要访问的是二维数组中的每一个元素,那取到这一行的地址后还不够,因为我们访问数组中元素时使用的都是数组名,此时
*(p + i)
也就拿到了当前的这一行的数组名,假设现在要访问第一行,那它的数组名那就是a[0],或者是*(a + 0)
,以此类推后面的几行数组名就是a[1]、a[2]
。那数组名我们知道,意味着首元素地址,现在先访问第一行中的每个元素,那么首先拿到的就是【1】的地址,那要访问到后面的每一个元素首先要对地址进行一个偏移,*(p + i) + j
就可以拿到每个元素的地址,那此时就简单了,再解引用*(*(p + i) + j)
也就取到了当前行中的每个元素,根据数组名和指针的转换规则,即为p[i][j]
来看一下运行结果
在学习了【指针数组】和【数组指针】后,来看一下这四个指针 or 数组?
int arr[5]; int *parr1[10]; int (*parr2)[10]; int (*parr3[10])[5];
- 第一个【arr】首先和
[]
结合,表明它是是一个数组,数组有五个元素,每个元素都是int类型的,说明这是一个一维数组 - 第二个【parr】首先和
[]
结合,表明它是一个数组,数组的每个元素都是一个int类型的指针,说明这是一个指针数组 - 第三个【parr2】首先和
*
结合,表明它是一个指针,然后往后一看,它指向一个数组,该数组有10个元素,每个元素都是int类型,说明这是一个数组指针 - 第四个【parr3】首先和
[]
结合,表明它是一个数组,数组有十个元素,把parr3[10]
去掉后就可以看出它的类型,是int(*)[5]
,说明数组中存放着的都是数组指针,每个数组指针都指向一个存有5个元素,每个元素都是int类型的数组。最后我们判定其为==数组指针数组==
第四个的图示如下:
【数组传参与指针传参】
相信有很多同学对于数组传参、指针传参都是搞的稀里糊涂的
1、 一维数组传参
代码:
/*一维数组传参*/ void test(int arr[]) //ok? {} void test(int arr[10]) //ok? {} void test(int* arr) //ok? {} int main() { int arr[10] = { 0 }; test(arr); }
解析:
- 首先来看一维数组的传参,test传进来一个arr数组名,那第一个利用
arr[]
接收这是我们最常见的,没有问题✔ - 第二个和第一个类似,只是在
[]
里加上了一个10,不过我们知道对于一维数组里面的数组大小声明是可以省略的,所以没有关系 - 第三个是采用
*arr
的方式进行接收,那传递进来的arr为数组名,数组名是首元素地址,那给到一个指针作为接收也没什么问题
代码:
void test2(int* arr[20]) //ok? {} void test2(int** arr) //ok? {} int main() { int* arr2[20] = { 0 }; test2(arr2); }
解析:
- 接下去看到我向test2传递了一个指针数组,那使用
* arr[20]
合情合理 ✔ - 那么第二个
** arr
是都可以呢?这点我们可以通过画图来分析,因为arr2是一个指针数组,而且里面存放的每个元素都是int类型的, 那我们传递【指针数组】的数组名过去的话,那其实就是首元素地址,即这个一级指针int*
的地址,那么形参部分使用二级指针来接收也是正确的 ✔
总结:
最后总结一下一维数组传参形参可以是哪些内容
- 形参可以是数组
- 形参可以是指针
- 形参可以是一个二级指针,指针数组的地址可以给到二级指针做接收,==因为指针数组里面存放的都是一级指针==
2、 二维数组传参
代码:
/*二维数组传参*/ void test(int arr[3][5])//ok? {} void test(int arr[][])//ok? {} void test(int arr[][5])//ok? {} int main() { int arr[3][5] = { 0 }; test(arr); }
解析:
- 接下去我们再来看看二维数组的传参,第一个无需多说。第二个的话形参这种写法是不可以的,因为二维数组必须确定它的列,也就是每行有多少个元素,但是有多少行可以不用知道❌
- 那对于第三个来说就是正确的,虽然省略了第一个
[]
的数组,但是指明了列的个数,就没有关系 ✔
代码:
void test2(int* arr)//ok? {} void test2(int* arr[5])//ok? {} void test2(int(*arr)[5])//ok? {} void test2(int** arr)//ok? {} int main() { int arr[3][5] = { 0 }; test2(arr); }
解析:
- 上面的代码是采取形参部分指针进行接收,上面我们有分析到,二维数组的数组名是首行的地址,那可以使用一个一级指针来接收吗?很显然是不可以的❌
- 第二个
int* arr[5]
可以吗?首先你要分析看它是个什么,我们传递过来的是一个地址,那地址就要使用指针来进行接收,但是可以看到这很明显是一个指针数组,因为arr和[]
先结合了,所以也是错误的❌ - 那么第三个呢?通过观察可以判断出它是一个数组指针, 接收一个二维数组第一行的地址,那肯定是不会有问题的 ✔
- 最后是一个二级指针,但是二级指针只能接收一个一级指针的地址,不过我们传递过来的是一个二维数组中某一行的地址,根本牛头不对马嘴❌
总结:
最后总结一下二维数组传参形参可以是哪些内容
- 直接用二维数组做接收
- 二维数组的数组名是首行的地址,是一个一维数组的地址,要使用数组指针来接收
3、 一级指针传参
代码:
void print(int* p, int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d\n", *(p + i)); } } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9 }; int* p = arr; int sz = sizeof(arr) / sizeof(arr[0]); //一级指针p,传给函数 print(p, sz); return 0; }
解析:
- 接下去我们来看看一级指针的传参,那其实这很明确,在main函数中指针指向arr数组的首元素地址,传递过去后形参部分的p也指向这个地址,那么通过解引用就访问到了数组中的每一个元素
思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
- 可以直接是一个变量的地址
- 可以是一级指针
- 一维数组的数组名(数组名是首元素地址,数组中的每一个元素都是一个变量)
4、 二级指针传参
代码:
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; }
解析:
- 接下去我们来看看一级指针的传参,那其实这很明确,在main函数中指针指向arr数组的首元素地址,传递过去后形参部分的p也指向这个地址,那么通过解引用就访问到了数组中的每一个元素
思考:
当一个函数的参数部分为二级指针的时候,函数能接收什么参数?
- 可以直接是一个一级指针的地址
- 可以是二级指针
- 指针数组的数组名(数组名是首元素地址,数组中的每一个元素都是一个一级指针)
四、指针函数与函数指针
【指针函数】
1、定义
指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针
【格式】:返回类型* 函数名(参数表)
- 指针函数还是很好理解的,通过基本的函数来做个对比
int func(int x, int y)
int* func(int x, int y)
- 很清楚地可以看出,【指针函数】就是普通的一个函数,只是它的返回值类型为一个指针罢了
2、示例
下面展示一个指针函数的相关案例
Open()
函数从外界接收一个值,用于在函数内部开辟出一块大小为n的空间,然后return返回,返回类型为int*
,此时外界使用int*
来进行接收,就获取到了函数内部开辟出这个数组的首元素地址,然后通过循环为数组中n个元素初始化- 这里无需担心在函数内部开辟的这块空间的地址,因为它存放在堆上,而不是在栈上,所以不会随着函数栈帧的销毁而消亡,所以这里在举例的时候我专门去堆上面申请空间然后返回,若是返回函数中局部变量的地址,就会有很大的风险!
int* Open(int n) { int* a = (int*)malloc(sizeof(int) * n); if (NULL == a) { perror("fail malloc"); exit(-1); } return a; } int main(void) { int n = 10; int* arr = Open(n); memset(arr, 0, sizeof(int) * n); for (int i = 0; i < n; ++i) { *(arr + i) = i + 1; } printf("Initialized Successfully\n"); return 0; }
通过运行结果可以看出确实可以起到初始化数组的效果
【函数指针】
讲完指针函数,我们也来说说它的双胞胎兄弟 —— 函数指针
1、概念理清
经过上面所讲的字符指针、数组指针,相信你马上就能类比出函数指针:没错,它就是一个指针,所指向的就是一个函数
- 在【数组指针】中我有讲到过
数组名
和&数组名
的区别,虽然它们都指向数组的首元素地址,但是在它们往后偏移时,访问的字节数却不同;既然一个数组可以取出它的地址,那么函数是否可以取出它的地址呢?一起来看看
- 从打印结果可以看出无论是
函数名
还是&函数名
,它们的地址都是相同的,这是为什么呢?这就是语法规定的,一个函数名取不取地址都是这个函数的地址,因为对于函数来说也没有什么首函数的地址,是吧
对于数组的地址,我们可以用数组指针保存起来,那函数可以吗?当然可以,使用到的就是【函数指针】
- 那我现在想问,下面那种形式可以将函数的地址存放起来呢
//下面pfun1和pfun2哪个有能力存放test函数的地址? void (*pfun1)(); void *pfun2();
💡答案揭晓,就是第二个,解析如下
- 回忆我们数组指针的写法,为了不让指针变量和
[]
先结合,所以在*
和指针变量外加了一()
,其实对于函数指针也是一样的, 若是不加这个括号的话,就会变成* pf()
,pf就会优先和后面的()
结合,那么这会被编译器当成是一个函数的声明 - 加上括号后,
(*pf)
就会是一个指针,向外一看有个()
,说明它指向一个函数,这个函数的参数就是Add形参部分两个参数的类型 - 最后是它的返回类型,也就是这个函数的返回类型
int
所以Add函数的函数指针应该写成下面这种形式
int (*pf)(int, int) = &Add;
2、如何调用函数指针?
清楚了函数该如何去声明后,那既然有了这个指针,而且它指向一个函数,是否可以通过这个指针去调用这个函数呢?
- 调用函数肯定得传参,那我们为刚才声明的形参部分传入两个参数试试,然后再拿返回值接收一下
- 可以看到确实可以调用Add函数进行求和计算
- 不过这个编译器到底是怎么根据这个函数指针来判断去调用的Add函数,我们来对比一下
int ret = (*pf)(3, 4); printf("ret = %d\n", ret); int ret2 = Add(1, 2); printf("ret = %d\n", ret2);
通过调试来观察可以发现,编译器很智能,确实是通过函数指针的指向去找到函数的地址
也可以通过汇编来看,很清晰地看出它们都去call
了这个函数的地址
- 上面说到无论是
函数名
还是&函数名
,它们所取到的地址都是一样的,所以我们可以将函数指针的声明写成下面这种形式,读者可以自己去试一下,效果也是一样的
int (*pf)(int, int) = Add;
- 那观察上面这样的声明形式,把指针变量单独抽离出来其实就是把
Add
赋给了pf
,然后调用的时候在前面加上一个*
作为解引用,取到这个函数,那其实Add和pf就是一样的,所以我们可以像pf(1, 2)这样去调用函数,具体如下
//int ret = (*pf)(3, 4); int ret = pf(3, 4); int ret2 = Add(1, 2);
通过运行可以发现效果也是一样的,所以前面的*
其实是可以省略的,甚至你多加几个像(****pf)(3, 4)
都是可以的