这是指针复习的第二篇,主要介绍指针与数组、函数的应用。
一、指针与数组
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
)
我们在指针复习 ( 一 ) 中也有说过,如果希望在函数内部改变函数外部的变量内容,就使用传址传递。
那如果我们希望在函数内部改变一级指针的指向内容呢 ? 也就是把一级指针的地址传到函数当中,此时,形参就要使用二级指针 !