指针可以说是C语言中特别重要的内容了,对于指针的介绍,笔者由浅入深写过两篇博文,如果想对指针有一个系统全面的认识,欢迎阅读:
初阶:初识C语言(目录八–初识指针)
中阶:指针必备的7大知识点
进阶:指针,这还拿不下你?(本篇)
前面我们介绍了指针类型中有整形指针、字符指针、浮点型指针……这里为什么要把字符指针特别拿出来介绍呢?因为字符指针还有一种特殊用法。
int main()
{
const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);
return 0;
}
注释:这段代码很容易误认为是把字符串 hello bit 放到字符指针 pstr 里了,但是本质是把字符串 hello bit. 首字符的地址放到了pstr中。这里用const修饰是因为"hello bit."是常量字符串,不能修改。
一道面试题
#include
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
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
和str2
两个数组被初始化相同的值,str1和str2为数组名代表首元素的地址,对于数组来说每次初始化都会开辟独立的内存空间,由于数组str1和数组str2分别占据不同的内存空间,即首元素地址各不相同,所以str1不等于str2。对于str3
和str4
,初始化为常量字符串,由于常量字符串不可修改,所以从内存优化的角度来说,在内存中每个常量字符串只存在一份即可,即str3和str4对应的是同一块内存空间,故str3和str4相等。
指针数组在初阶指针中有详细介绍,这里就简单复习一下:
int* arr1[10]; //整形指针的数组//arr1为数组有10个元素,每个元素为整形指针类型
char *arr2[4]; //一级字符指针的数组//arr2为数组有4个元素,每个元素为字符指针类型
char **arr3[5];//二级字符指针的数组//arr3位数组有5个元素,每个元素为二级指针类型
数组指针是指针?还是数组?答案是:指针。像我们熟悉的整形指针 int * pint;
是能够指向整形数据的指针。浮点型指针: float * pf;
能够指向浮点型数据的指针。那数组指针应该是:能够指向数组的指针。
对于数组指针:
int (*p)[10];
p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
注意:[]
的优先级要高于*
号的,所以必须加上()来保证p先和*结合。如果写成int *p[10];则p先和[ ]结合,则成了指针数组。
我们先看一段代码:
#include
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("\n%p\n", arr + 1);
printf("\n%p\n", &arr[0]);
printf("\n%p\n", &arr[0] + 1);
printf("\n%p\n", &arr);
printf("\n%p\n", &arr + 1);
return 0;
}
结果分析:
根据上面的代码我们发现,&arr
和arr
,虽然值是一样的,但是意义应该不一样的。
实际上:
arr数组名是数组首元素的地址,仅有2个例外:sizeof(数组名)、&数组名 。
&arr表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
本例中 &arr 的类型是: int(*)[10],是一种数组指针类型。数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40。而arr为整形指针,加1跳过一个整形,差值为4。
使用1:
#include
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
//但是我们一般很少这样写代码
return 0;
}
使用2:
#include
//形参为数组
void print_arr1(int arr[3][5], int row, int col) {
int i = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
//形参为指针
void print_arr2(int(*arr)[5], int row, int col) {
int i = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
学习了指针数组和数组指针你能看得出以下代码的意思吗?
int arr[5];
arr和[ ]结合为数组,有5个元素,每个元素为int类型。
int *parr1[10];
*和[ ]相比[ ]的优先级更高,parr1和[ ]结合为数组,有10个元素,每个元素为int*类型.
int (*parr2)[10];
parr2先和*结合为指针,指向一个含有10个元素的整形数组。
int (*parr3[10])[5];
parr3先和[ ]结合为数组,有10个元素,每个元素为数组指针类型-int(* )[5]。
#include
//arr1一维数组传参——数组接收
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
//arr1一维数组传参——指针接收
//arr1整形数组的首元素为第一个一级指针
void test(int* arr)//ok
{}
//arr2一维数组传参——数组接收
void test2(int* arr[])//ok
{}
void test2(int* arr[20])//ok
{}
//arr2一维数组传参——指针接收
//arr2指针数组的首元素为一个二级指针
void test2(int** arr)//ok
{}
int main()
{
int arr1[10] = { 0 };//整形数组
int* arr2[20] = { 0 };//指针数组
test(arr1);
test2(arr2);
}
总结:一维数组传参,形参可以是数组或指针。当形参是数组时元素个数可省略不写。
#include
//二维数组传参——数组接收
void test(int arr[3][5])//ok
{}
void test(int arr[][5])//ok
{}
//void test(int arr[][])//error
//{}
//二维数组传参——指针接收
void test(int(*arr)[5])//ok
{}
//void test(int* arr)//error
//{}
//void test(int* arr[5])//error
//{}
//void test(int** arr)//error
//{}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
总结:二维数组传参,形参可以是数组或指针。当形参为数组,函数形参的设计只能省略第一个[ ]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。
思考:当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
//test1函数能接收什么参数?
void test1(int* p)
{}
int main()
{
int a = 0;
test1(&a);//接收整形变量的地址
int* p = &a;
test1(p);//接收整形指针变量本身
int arr[10] = {0};
test1(arr);//接收一维数组数组名(首元素地址)
return 0;
}
思考:当函数的参数为二级指针的时候,可以接收什么参数?
//test2函数能接收什么参数?
void test2(int** p)
{}
int main()
{
int a = 0;
int* p = &a;
test1(&p);//接收一级变量的地址
int** p = &p;
test1(p);//接收二级指针变量本身
int* arr[10];
test1(arr);//接收指针数组数组名(首元素地址)
return 0;
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
//Add和&Add都是函数的地址,没有区别
printf("%p\n",Add);
printf("%p\n",&Add);
return 0;
}
结果展示:
上面代码输出的是两个地址,这两个地址是 test
函数的地址。那我们的函数的地址要想保存起来,怎么保存?首先,我们已知指针能够存储地址,那么我们该如何创建一个函数指针?
int Add(int x, int y)
{
return x + y;
}
int main()
{
//类比数组指针,我们可以写出函数指针
//int arr[10] = {0};
//int(*p)[10] = &arr;
//p先和* 结合,说明p是指针,指针指向的是一个数组,
//指向的数组有10个元素,每个元素为int类型。
int (*pf)(int x, int y) = &Add;//int (*pf)(int x, int y) = Add;
//pf为指针,指向的是一个函数
//指向函数参数为int类型,返回值类型为int
return 0;
}
图解:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int sum1 = Add(3, 5);
printf("sum1=%d\n", sum1);
int (*pf)(int x, int y) = Add;
//或int(*pf)(int x,int y)=&Add;
int sum2 = (*pf)(3, 5);
printf("sum2=%d\n", sum2);
int sum3 = (******pf)(3, 5);//在函数调用时*只是一个摆设,可省略
printf("sum3=%d\n", sum3);
int sum4 = pf(3,5);
printf("sum4=%d\n", sum4);
return 0;
}
结果展示:
代码1:(*(void (*)())0)();
代码2:void (*signal(int , void(*)(int)))(int);
代码分析:
在读代码时,代码2可谓是相当复杂,为了增加代码的可读性,我们可以使用typedfy-类型重定义关键字对其进行简化:
int main()
{
void(*signal(int, void(*)(int)))(int);
typedef void(*pf_t)(int) ;//语法规定pf_t在*后边
pf_t signal( int, pf_t);
//注释
//上述代码是一次函数声明
//声明的函数叫:signal
//signal函数的第一个参数是int类型的
//signal函数的第二个参数是一个函数指针类型,该函数指针指向的函数参数是int,返回类型是void
//signal函数的返回类型也是一个函数指针类型,该函数指针指向的函数参数是int,返回类型是void
//void (*)(int) signal(int, void(*)(int)); //err
return 0;
}
总结:像上面这样的表达式恐怕会令我们“不寒而粟”。然而我们大可不必对此望而生畏,我们只需要对其进行拆解,就能对其很好地理解。
数组是一个存放相同类型数据的存储空间,我们前面已经学习了指针数组如int *arr[10];
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
例如: 指针数组:
int (* pfArr[2])(int, int)
注释:pfArr先和 [2]结合,说明pfArr是数组,有两个元素。数组的内容是什么呢?是 int (*)(int,int) 类型的函数指针。
函数指针数组特点:函数指针数组可以存放多个参数相同
和返回类型相同
的函数的地址。
那么函数指针数组具体有什么用呢?
函数指针数组的用途——转移表
转移表就是函数指针数组的用途,通过函数指针可以得出,函数的入口地址可以被保存起来,这就意味着完全可以通过入口地址来调用函数,而函数指针又可以存入到一个指针数组(函数指针数组)中,即可以通过数组下标来调用函数。
比如我们写一个整数的加、减、乘、除计算器:
✍️常规写法:
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("***************************\n");
printf("***** 1.add 2. sub ****\n");
printf("***** 3.mul 4. div ****\n");
printf("***** 0.exit ****\n");
printf("***************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("%d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
}
使用函数指针数组:
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
//函数指针数组 - 转移表
int (*pfArr[])(int, int) = { 0, Add, Sub, Mul, Div };//将首元素设为0--可直接使用input访问
//如果后续想对此计算器功能进行拓展,仅需在元素列表中添加相应函数即可。
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器\n");
break;
}
if (input >= 1 && input <= 4)
{
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("%d\n", ret);
}
else
{
printf("选择错误\n");
}
} while (input);
}
观察上述两段代码,我们可以很明显的看出,通过使用指针数组,我们减少了大量常规写法中的冗余代码,并且省去了使用switch语句,很巧妙的起到了简化代码的效果。其实这就是转移表的简单应用!
定义:指向函数指针数组的指针是一个指针
,指针指向一个数组
,数组的元素都是函数指针
。
实例展示:
void test(const char* str) {
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;//test函数名代表函数地址
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
图解:
当然像这种函数指针数组的指针继续往下拓展的话还会有:函数指针数组的指针的数组、函数指针数组的指针的数组的指针……看起来是有种无限套娃的意思了对于这些知识点我们只做相应了解即可,实际开发运用并不多。
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
将函数地址作为参数传递给另一个函数,由函数指针接收之后,在适当的位置通过函数指针调用所指的函数的机制就叫回调函数的机制。
上面我们已经用函数指针数组对计算器进行了简化,下面我们用回调函数进行简化:
void calc(int (*p)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = p(x, y);
printf("%d\n", ret);
}
int main()
{
int input = 0;
do
{
menu();
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);
}
注释:上述代码中我们定义了一个calc()
函数,并将加、减、乘、除函数的函数名作为参数传递给calc()函数并以函数指针接收,在选择相应的运算时,calc()函数会通过函数指针调用相应所指函数。这就是一个回调函数的简单使用,同时起到了简化代码的作用。
理解了回调函数我们就可以使用C语言中一个重要的库函数——qsort()
,对于qsort函数,需要理解和掌握知识点较多,就集中放到下一期讲解了,期待一下叭!
本章重点介绍了C语言指针[进阶]内容,篇幅略长,重难点较多,建议反复食用。看到这,如果本篇对您有所帮助的话,还望多多支持,一起加油!