这是指针复习的第二篇,主要介绍指针与数组、函数的应用。
一、指针与数组
1.1 数组名的理解
在说明指针与数组的关系之前,我们必须要先了解「数组名」的意义
我们在指针复习 ( 下 ) 中,指针±整数的部份有写过下面这段代码
int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];
这里我们是使用 &arr[0]
来取得数组首元素的地址,但事实上,数组名本就代表了首元素的地址
#include <stdio.h>int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];printf("%p\n", &arr[0]);printf("%p\n", arr);return 0;
}
从输出结果可以看到,直接使用数祖名打印地址的结果是和取首元素地址打印的结果相同。这也就印证了我们上面所述,数组名代表了首元素的地址
那如果使用 sizeof(数组名)
结果会是多少呢 ?
如果依照我们在指针复习 ( 下 ) 所说,地址的大小不是 4 就是 8。 那这里的结果是 4 或 8 吗 ?
#include <stdio.h>int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];printf("%zd\n", sizeof(arr));return 0;
}
输出结果发现,sizeof(arr)
的结果是整个数组的大小而不是地址大小。因此我们就需要提到两个数组名的例外
- sizeof(数组名) : 当 sizeof 中单独放数组名,这里的数组名不是代表首元素的地址,而是整个数组,因此计算的是整个数组的大小,单位为字节。
- &数组名 : 这里的数组名也是代表整个数组,取出的是整个数组的地址
除了这两个例外,其他地方如果看到数组名原则上都是代表数组首元素地址
#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("&arr[0] = %p\n", &arr[0]);printf("arr = %p\n", arr);printf("&arr = %p\n", &arr);return 0;
}
我们前面说, &arr 取出的是整个数组的地址,但是代码中打印地址的结果却又是相同的,那 arr 和 &arr 到底有什么区别 ?
这和我们在指针复习 ( 一 ) 所提到,取出的地址是取出最小字节的地址。但如果我们去对地址进行± 就会体现这两者的不同
#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("&arr[0] = %p\n", &arr[0]);printf("&arr[0]+1 = %p\n", &arr[0] + 1);printf("arr = %p\n", arr);printf("arr+1 = %p\n", arr + 1);printf("&arr = %p\n", &arr);printf("&arr+1 = %p\n", &arr + 1);return 0;
}
也就说明了 &arr 是取出整个数组地址而不是首元素地址,当然往后偏移多少字节是取决于数据类型
1.2 使用指针访问数组
如果我们访问数组内容可以使用**数组名[下标]
来进行访问,那可以使用指针变量[下标]
** 的方式来访问吗 ?
#include <stdio.h>
int main()
{int arr[10] = {0};//输⼊int i = 0;int sz = sizeof(arr)/sizeof(arr[0]);//输⼊int* p = arr;for(i=0; i<sz; i++){scanf("%d", p+i);//scanf("%d", arr+i);//也可以这样写}//输出for(i=0; i<sz; i++){printf("%d ", p[i]); // 也可以写成 printf("%d ",*(p+i));// 同理,事实上我们如果打印 arr[i] 相当于 *(arr+i); // 数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的}return 0;
}
1.3 一维数组传参的本质
我们知道函数中是可以传入数组的,但是传入的真的是整个数组吗 ?
#include <stdio.h>
void test(int arr[])
{int sz2 = sizeof(arr)/sizeof(arr[0]);printf("sz2 = %d\n", sz2);
}
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int sz1 = sizeof(arr)/sizeof(arr[0]);printf("sz1 = %d\n", sz1);test(arr);return 0;
}
输出结果
sz1 = 10 sz2 = 1
我们发现函数内部并没有正确获得数组的元素个数,但我们不是将数组传入了吗 ? 这时就必须要说明一维数组传参的本质了
在本质上,数组传参传递的是首元素的地址,既然传入的是地址,那函数形参理论上也就应该使用指针变量来接收首元素的地址。
因此如果我们 test 函数中测试 sizeof(arr)
得到的会是 4 or 8。而不是数组的大小。
这也就说明了,当我们使用一维数组当做函数的形参时,这个一维数组会退化为指针
以 test 函数为例 形参是 int arr[]
事实上会退化成 int* arr
1.4 指针数组
指针数组是指针还是数组呢 ?
/*
我们称 int a[5] = {0}; 做整型数组 -- 用来存储整型的数组
我们称 char a[] = "abc"; 做字符数组 -- 用来存储字符的数组
因此如果我们写了这样的一行代码
int* a[5] = {...}; 就叫做指针数组 -- 也就是用来存储 「指针」 的数组*/
1.5 指针数组模拟二维数组
我们可以把**二维数组看作一维数组的数组,**如此一来,二维数组中的每个一维数组也有首元素地址。
#include <stdio.h>
int main()
{int arr1[] = {1,2,3,4,5};int arr2[] = {2,3,4,5,6};int arr3[] = {3,4,5,6,7};//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中int* parr[3] = {arr1, arr2, arr3};int i = 0;int j = 0;for(i=0; i<3; i++){for(j=0; j<5; j++){printf("%d ", parr[i][j]);// 相当于 printf("%d ", *(*(parr+i)+j));}printf("\n");}return 0;
}
它是如何体现的呢 ?
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数 组中的元素
上述的代码模拟出⼆维数组的效果,实际上并非完全是⼆维数组,因为每⼀⾏并非是连续的
后面会更正式的探讨二维数组传参的本质
1.6 字符指针变量
指针类型中有一个 char*
一般使用
int main()
{char ch = 'w';char *pc = &ch;*pc = 'w';return 0;
}
另一种使用方式
int main()
{const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?printf("%s\n", pstr);return 0;
}
实际上, const char* pstr = "hello bit.";
意思是把⼀个常量字符串的⾸字符 h 的地址存放到指针变量 pstr 中。而不是把整个字符串存放到 pstr 中
1.7 数组指针变量
我们前面有提到,指针数组是用来存放指针的数组。那数组指针呢 ? 数组指针就是用来存放数组的指针 !
// p1 p2 个别是什么 ?
int *p1[10]; // p1先和[10]结合,因此 p1是指针数组
int (*p2)[10]; // p2先和*结合,因此 p2是数组指针
// 这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合
数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是我们之前说的 &数组名
如果要存放数组的地址
#include <stdio.h>int main(){int arr[10] = {0};int (*p)[10] = &arr;return 0;
}
我们可以透过调试看到, &arr
和 p
的类型是一样的 !
数组指针的定义
int (*p) [10] = &arr;| | || | || | p指向数组的元素个数| p是数组指针变量名p指向的数组的元素类型
有了数组指针的概念,我们就可以进一步的探讨二维数组传参的本质了
1.8 二维数组传参的本质
一般情况下,我们要传递二维数组到函数中,是这样写的
#include <stdio.h>
void test(int a[3][5], int r, int c)
{int i = 0;int j = 0;for(i=0; i<r; i++){for(j=0; j<c; j++){printf("%d ", a[i][j]);}printf("\n");}
}
int main()
{int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};test(arr, 3, 5);return 0;
}
那可以使用其他方式写吗 ?
我们先再次理解一下二维数组
⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维 数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组
根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址。因此形参也可以写成指针的形式
#include <stdio.h>
void test(int (*p)[5], int r, int c)
{int i = 0;int j = 0;for(i=0; i<r; i++){for(j=0; j<c; j++){printf("%d ", *(*(p+i)+j));}printf("\n");}
}
int main()
{int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};test(arr, 3, 5);return 0;
}
二、指针与函数
2.1 函数指针变量
什么是函数指针变量呢 ? 根据前面的说明了这么多指针变量应该不难理解,函数指针变量就是用来存放函数地址的指针
当然,函数名就是函数的地址。因此 &函数名 和 直接写函数名都是可以的两者等价
不过要定义一个函数指针变量就不是像前面一样这么简单了
// 函数指针变量的定义
int (*pf3) (int x, int y) // 形参名称可以不写,但是类型和个数都必须和指向的函数相同| | ------------| | || | pf3指向函数的参数类型和个数| 函数指针变量名pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型
void test()
{printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;
2.2 函数指针变量的使用
#include <stdio.h>
int Add(int x, int y)
{return x+y;
}
int main()
{int(*pf3)(int, int) = Add;printf("%d\n", (*pf3)(2, 3));printf("%d\n", pf3(3, 5));return 0;
}
输出结果
5 8
使用函数指针变量的作用就是可以透过函数指针去调用目标函数,而不是直接透过函数名调用。
2.3 函数指针数组
把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组。要注意函数的类型要是一样的!
函数指针数组该如何定义呢 ?
int (*parr1[3])(); // parr1先和[]结合 说明 parr1是数组 数组的类型就是除掉parr1和[] 也就是 int(*)()
// 因此parr1 是数组,类型是函数指针
2.4 函数指针数组的使用
#include <stdio.h>
int add(int a, int b)
{return a + b;
}
int sub(int a, int b)
{return a - b;
}
int mul(int a, int b)
{return a*b;
}
int div(int a, int b)
{return a / b;
}
int main()
{int x, y;int input = 1;int ret = 0;int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表do{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");printf( "请选择:" );scanf("%d", &input);if ((input <= 4 && input >= 1)){printf( "输⼊操作数:" );scanf( "%d %d", &x, &y);ret = (*p[input])(x, y); // 我们可以直接透过解引用数组中所存储的函数地址,来调用目标函数!printf( "ret = %d\n", ret);}else if(input == 0){printf("退出计算器\n");}else{printf( "输⼊有误\n" );}}while (input);return 0;
}
2.5 回调函数
回调函数的概念
什么是回调函数呢 ? 回调函数就是通过函数指针调用的函数。
如果我们将某一个函数的指针 ( 地址 ) 作为参数传递给另外一个函数,这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。
回调函数不是由函数实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用,对于该事件或条件的响应。
简单说,将A函数地址传递给B,在B函数内通过指针调用A函数实现操作,那么A函数就称为回调函数,也就是不直接调用A,而是通过B调用A。
我们以上面的简易计算机代码为例。除了使用函数指针数组的方式之外,还能不能再简化一些 ?
//使⽤回到函数改造后
#include <stdio.h>
int add(int a, int b)
{return a + b;
}
int sub(int a, int b)
{return a - b;
}
int mul(int a, int b)
{return a * b;
}
int div(int a, int b)
{return a / b;
}
void calc(int(*pf)(int, int))
{int ret = 0;int x, y;printf("输⼊操作数:");scanf("%d %d", &x, &y);ret = pf(x, y);printf("ret = %d\n", ret);
}
int main()
{int input = 1;do{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf("*************************\n");printf("请选择:");scanf("%d", &input);switch (input){case 1:calc(add);break;case 2:calc(sub);break;case 3:calc(mul);break;case 4:calc(div);break;case 0:printf("退出程序\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}
// 这段代码中的 add、sub、mul、div 都可以称作回调函数
三、二级指针
我们都说 指针变量 指针变量。既然它是变量,它就应该要有地址,有地址我就应该可以使用指针来存储。
那存储普通变量 我们使用指针变量。要存储指针变量是不是就应该使用二级指针指针变量 ?
/*
其实就是这样的概念
存储 int变量 -> 用int*
存储 char变量 -> 用char*要存储 int*变量 -> 用 int** -- 二级指针*/
3.1 二级指针视图
如果对
ppa
解引用则得到的会是pa
的地址 (*ppa
)如果再对
*ppa
解引用 得到的就是 a 的内容 (**ppa
)
我们在指针复习 ( 一 ) 中也有说过,如果希望在函数内部改变函数外部的变量内容,就使用传址传递。
那如果我们希望在函数内部改变一级指针的指向内容呢 ? 也就是把一级指针的地址传到函数当中,此时,形参就要使用二级指针 !