指针的主题,我们在初级阶段的【C指针详解】初阶篇 章节已经接触过了,我们知道了指针的概念:
- 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
- 指针的大小是固定的4/8个字节(32位平台/64位平台)。
- 指针是有类型的,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
- 指针的运算。
- 野指针。
- 二级指针。
这个章节,我们继续探讨指针的高级主题。
我们知道在指针的类型中有一种指针类型为字符指针 char* ;
顾名思义,字符指针就是用来存放字符的地址。
一般使用:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
还有一种使用方式如下:
int main()
{
const char* pstr = "hello bit.";
printf("%s\n", pstr);
return 0;
}
大家思考一下,这里是把一个字符串"hello bit."放到pstr指针变量里了吗?
应该不是的,pstr是一个字符指针,是用来存放字符的地址的,而"hello bit."是一个字符串,即使我们想把它放到pstr中,也是不可行的。
那这句代码的结果是啥呢?
const char* pstr = “hello bit.”; ——本质上是把字符串 “hello bit.” 首字符 h 的地址放到了pstr中,使得指针pstr指向该字符串。
那就有这样的面试题:
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是不一样的,而str3和str4本质上是一样的,那为什么呢?
首先我们来分析一下"hello bit.",这里的"hello bit."是一个常量字符串,而常量字符串是不能被修改的,在内存中仅保留一份。
这也是有时候在代码最前面加上const的原因(const char* str3 = “hello bit.”;)。
C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候会开辟出不同的内存块。
了解了常量字符串的存储机制之后,我相信大家就明白结果是怎么回事了。
1. 我们用相同的常量字符串"hello bit."去初始化不同的数组str1和str2的时候,数组str1和str2会开辟出不同的内存块(相当于我们只是借用常量字符串"hello bit."去初始化了两个数组,但这两个数组各自开辟了自己的空间),而数组名str1和str2表示的是数组首元素的地址,那两个数组的空间是不同的,它们首元素的地址自然也就不同了。
2. 而指针str3和str4指向的是同一个常量字符串,它们存储的都是字符串"hello bit."的首字符’h’的地址,所以str3和str4本质上是一样的
先问大家一个问题,指针数组是指针还是数组?
答案:是数组。是存放指针的数组。
我们已经知道了整型数组,字符数组等。
int arr1[5];
char arr2[6];
那指针数组是什么样子的,我们举个例子,定义这样一个指针数组:
int* arr3[5];
应该是这样的意思:arr3是一个数组,有五个元素,每个元素是一个整形指针。
来看几个其它类型的指针数组:
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3 [5]; //二级字符指针的数组
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针,用来存放数组的地址。
下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];
答案是:int (*p2) [10]是数组指针,int *p1[10]是指针数组。
解释:p先和*结合,说明p是一个指针变量,然后指向一个数组,数组有10个元素,每个元素的类型是 int 。
所以p是一个指针,指向一个数组,叫数组指针。
这里要注意: [ ]的优先级要高于 * 号的,所以必须加上()来保证p先和 * 结合。
当然,我们知道数组有很多类型,那自然就有不同类型的数组指针,比如:
char (*p3) [20];
double (*p4) [5];
float (*p5) [8];
对于下面的数组:
int arr[10];
arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥?
我们看一段代码:
#include
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
结果会是什么呢?
可见数组名和&数组名以地址的形式打印出来是一样的。
难道两个是一样的吗?
我们再看一段代码:
#include
int main()
{
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr= %p\n", &arr);
printf("arr+1 = %p\n", arr + 1);
printf("&arr+1= %p\n", &arr + 1);
return 0;
}
这次结果还会一样吗?
我们看到,arr和&arr打印出来虽然是一样,但是arr+1和&arr+1,却完全不一样,这说明,&arr和arr,虽然值是一样的,但是意义应该不一样的。
实际上:
&arr 表示的是整个数组的地址,而arr是数组首元素的地址。
所以,arr和&arr打印出来才会不一样。
arr和arr+1的差值是4,即数组arr(整型数组)一个元素的大小,因为arr是一个整型元素的地址,是int * 类型的指针,步长为4个字节。
而本例中 &arr 的类型是: int (*) [10] ,是一种数组指针类型,加1就应该跳过一个该数组的大小,而int arr[10]的大小是40个字节,我们看到&arr和&arr+1的差值是68-40=28,但我们要知道编译器给我们打印出来的地址是以16进制展示的,而16进制数28转换为10进制就是40(2x16 ^ 1+8x16 ^ 0=40) 。
最后,再给大家补充一点:
补充:
1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
2. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。
那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
看代码:
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
return 0;
}
但是我们一般很少这样写代码,通常用在二维数组中。
#include
void print_arr1(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");
}
}
//数组指针的方法
void print_arr2(int(*arr)[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("%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);
print_arr2(arr, 3, 5);
return 0;
}
大家先仔细看看这段代码,然后我们一起来分析一下:
上面的代码我们实现了2个函数,功能都是打印数组arr [3][5]的元素,而且我们传过去的实参也一样,两个函数的不同之处在于接收参数arr 的方式不同:
函数print_arr1用的是还是数组来接收,我们知道这样当然是可以的。
函数print_arr2用的是指针来接收,而且用的就是我们刚学的数组指针。
看到这里大家可能会有一个疑惑,我们传过去的是数组名arr,是数组首元素的地址,但是我们为什么用了一个数组指针(int(*arr)[5])来接收呢?
首先,数组名arr是数组int arr[3][5]首元素的地址,这肯定是没问题的。
但是,我们要注意,这里的int arr[3][5]是一个二维数组,二维数组的首元素是二维数组的第一行(相当于一个一维数组),所以这里传递的arr其实相当于第一行的地址,是一维数组的地址,既然是数组的地址,当然要用数组指针来接收了。
而数组int arr[3][5]的第一行有5个元素,每个元素的类型是int ,所以我们就用一个整型数组指针(int(*arr)[5])来接收。然后我们就可以访问数组 int arr[3][5] 并打印它的元素了。
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
int arr [5] ;
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
我们一起看一下:
- int arr [5] ——整型数组
- int *parr1[10] ——整型指针数组,10个元素,每个元素类型为整型指针
- int (*parr2) [10] ——数组指针,指向一个数组,10个元素,元素类型为int
- int (*parr3[10]) [5] ——数组指针数组:parr3是一个存放数组指针的数组,能存放10个数组指针,每个数组指针指向一个整型数组,5个元素。
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
比如,有这样一个一维数组:
int arr[10];//一维整型数组
int* arr2[20];//一维整型指针数组
我们把它们作为参数传给两个函数:
test(arr);
test2(arr2);
那现在函数test和test2的参数应该如何设计呢?
1. 先来看函数test(接收arr):
test函数要接收arr,首先我们想到,arr是一个一维数组,那我们是不是可以用一个同类型一维数组来接收,这当然是没问题的,所以test(假设不需要返回值)的参数可以这样设计:
void test (int arr[10])
当然【】里的10其实可以省略的:
void test (int arr[])
,这样也是可以的,因为这里设计的形参我们只是写成数组的形式,本质上还是指针(因为接收的是地址),所以不要求必须指定大小。
然后,
因为arr是数组名,表示的是数组首元素的地址,所以我们当然也可以把直接设计成指针,那传过来的是数组首元素(整型变量)的地址,我们应当用一个整型指针变量来接收:
void test (int* arr)
所以。函数test的形参,我们可以设计成这三种:
void test (int arr[10])
void test (int arr[])
void test (int* arr)
2. 然后我们来看函数test2(接收arr2):
那test2其实还是一个一维数组,只不过是整型指针数组,那我们的参数设计还是用同类型的数组数组,或者用指针:
同类型的指针数组:
void test2(int* arr[20]
void test2(int* arr[]
数组arr2的首元素是一个一级整型指针变量,一级指针的地址我们要用一个二级指针来接收:
void test2(int** arr)
那现在我们要把二维数组作为参数传递给函数:
int main()
{
int arr[3][5] = {0};
test(arr);
}
此时,函数test的参数可以如何设计呢?
- 首先,传过去的是二维数组,我们当然可以用一个同类型的二维数组来接收:
void test(int arr[3][5])
或
void test(int arr[][5]
但注意不能写成int arr[][],
因为二维数组的列数是不能省略的,二维数组传参,函数形参的设计只能省略第一个[]的数字。- 那然后我们当然也可以用指针接收。
在【3.3 数组指针的使用】我们已经知道了,二维数组的首元素是二维数组的第一行(相当于一个一维数组),所以这里传递的arr其实相当于第一行的地址,是一维数组的地址,既然是数组的地址,当然要用数组指针来接收了。
所以我们可以这样设计:
void test(int (*arr)[5])
如果我们调用一个函数传过去的实参是一级指针,那当然要用一个同类型的一级指针作为形参来接收:
比如:
#include
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;
}
那现在我们思考这样一个问题:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
void test1(int *p)
{}
test1函数能接收什么参数?
- 首先实参传一个同类型的一级指针变量,这肯定是没问题的。
int a=9; int *p=&a; test1(p);
- 然后我们是不是还可以传一个变量的地址,形参为int *p,当然可以接收一个整型变量的地址了。
int b=0;test1(&b);
- 那我们是不是还可以传一个一维数组的数组名,因为数组名也是一个地址,是数组首元素的地址,那形参为int *p,当然我们要传一个整型数组的数组名。
int arr[10];test1(arr);
若实参为二级指针,那形参应该是同类型的二级指针,这样肯定是可以的。
举个例子:
#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;
}
那现在我们要讨论的是,当函数的参数(形参)为二级指针时,可以接收什么样的参数(实参)?
看这段代码:
void test(char** p)
{
}
int main()
{
char c = 'b';
char* pc = &c;
char** ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);//Ok?
return 0;
}
调用test函数,我们可以传什么参数?
1.形参为二级指针,实参也传二级指针,这样肯定可以:
test(ppc);
2. 二级指针,当然可以接收一级指针变量的地址:
test(&pc);
我们是不是还可以传一个一级指针数组的数组名,因为它是该数组首元素的地址,即还是一级指针变量的地址:
test(arr);
什么时函数指针呢?
函数指针,即指向一个函数的指针,用来存放函数的地址。
那既然要存放函数的地址,那函数的地址怎么来表示呢?
int arr[10]={0};
数组名和&数组名的意义是不同的:
数组名arr表示数组首元素地址,而&arr才是整个数组的地址。
void test()
{
printf("hehe\n");
}
函数test的地址要如何表示呢?
会不会像数组一样,&test表示函数地址呢?那函数名test表示啥呢?函数可没有首元素这一说。
我们来看一段代码:
#include
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
我们一起来看一下test和&test打印出来是什么:
输出的是两个地址,且两个地址都是 test 函数的地址。
因为对于函数来说,函数名和&函数名表示的意义是完全一样的,都表示函数的地址。
即函数名==&函数名
那函数指针又应该怎么写呢?
现在有这样一个函数:
int add(int x, int y)
{
return x + y;
}
如果我们要写一个函数指针来存储上面add函数的地址,我们可以这样写:
int (*p) (int,int)=&add;
或
int (*p) (int ,int)=add;
这里的P就是一个函数指针,解释一下:
首先,p和*结合,说明p是一个指针变量,然后该指针指向的是一个函数,函数有两个参数,都是 int 类型,函数的返回值类型也是 int 。
其它类型的函数指针书写也是同样的方法,大家按函数自己的参数类型,返回值类型写就行了。
那么,接下来我们怎么通过函数指针去调用上面的add函数呢?
我们知道,如果我们要通过函数名调用的话,可以这样写:
int ret=add(3,5);
那我们现在要通过函数指针调用add函数,怎么写呢?
int ret=(*p)(3,5);
解释一下:因为p里面存的是函数add的地址,所以我们先对p解引用找到函数add,然后就可以传参调用了。- 但是我们要知道,在函数指针这一块,通过函数指针调用函数时,可以不对指针解引用,即我们可以直接写成这样:
int ret=p(3,5);
这样也可以成功调用函数。
不过带上解引用操作符的话可我们更容易理解。
但是要注意:如果加解引用操作符的话,必须加上括号。
我们一起来阅读两段有趣的代码:
(*(void (*)())0)();
给大家解释一下:
我们先来看中间这一部分
(void (*)())0
的意思:
数字0前面一个括号,括号里面放的是啥,是不是一个函数指针类型啊,首先一个(*)表明是一个指针,指针指向一个函数,该函数没有参数,也不需要返回值(void)。
也就是说将0强制类型转换为一个函数指针。
然后我们再看整个表达式,(*(void (*)())0)();
:
其实是 对该函数指针解引用,并调用该函数。
在《C陷阱与缺陷》这本书中提及该代码,我们来看一下:
void (*signal(int , void(*)(int)))(int);
解释一下:
我们直接去看这句代码可能不容易理解,我们可以将这句代码写成这样:
void(*)(int)
signal
(int,void(*)(int));
函数返回类型 、函数名、 参数类型
这样相信大家很容易就看懂了,就是一个函数声明。
但是我们要知道,这种写法是语法不支持的。
void (*signal(int , void(*)(int)))(int);
这句代码看上去可能太复杂了,不过我们可以简化一下它:
我们使用关键字 typedef 对
void(*)(int)
进行一个类型重命名。
typedef void(*)(int) pfun_t;
,
将void(*)(int)
重命名为pfun_t
,这样写对吗?
错误的!!!
语法规定正确的写法是这样的:
正确的:typedef void (*pfun_t) (int);
那现在我们就可以这样写了:
pfun_t signal (int, pfun_t);
这句代码同样在《C陷阱与缺陷》中提及:
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组。
比如:
int *arr[10];
//数组的每个元素是int*
那函数指针数组就是存放函数指针(或函数地址)的数组,那函数指针的数组如何定义呢?
int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];
这3句代码那一句正确定义了一个函数指针数组?
答案是:parr1
parr1 先和 [] 结合,说明 parr1是数组,数组有10个元素,每个元素的类型是
int (*)()
类型的函数指针。
(把数组名及元素个数parr1[10]
去掉剩下的就是元素类型。)
函数指针数组的用途:转移表
比如我们想要写代码实现一个计算器的功能(加减乘除),在没学函数指针数组之前,我们可能会这样写:
#include
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;
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:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
使用switch、case语句选择相应的功能,就去调用对应的函数来实现对操作数的加减乘除。
但这样写好像不是特别好。
那有没有更好的办法呢?
当然有,那我们就可以使用函数指针数组去实现。
#include
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 }; //转移表
while (input)
{
printf("*************************\n");
printf(" 1.add 2.sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf("输入有误\n");
printf("ret = %d\n", ret);
}
return 0;
}
这次代码就没有那么多重复的部分了,更加简洁。
解释一下:我们定义了一个函数指针数组int(*p[5])(int x, int y)
,5个元素,每个元素是一个函数指针,指向的函数两个参数为int类型,返回类型也是int。
然后对数组初始化:{ 0, add, sub, mul, div };
,把加减乘除4个函数的地址存入数组。
为啥数组最前面要加一个0呢?
因为我们四个函数对应的选项是1,2,3,4,这样使得它们的下标正好是1,2,3,4。
我们可以通过下标直接找到并调用函数。
什么是指向函数指针数组的指针?
即指向函数指针数组的指针,用来存放函数指针数组的地址。
那 指向函数指针数组的指针 如何定义呢?
举个例子:
#include
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
函数指针和函数指针数组我们已经知道怎么回事了。
解释一下:
void (*(*ppfunArr)[5])(const char*)
首先,ppfunArr和*
结合(*ppfunArr)
,说明它是一个指针。
然后指向一个数组,数组有5个元素(*ppfunArr)[5]
,每个元素是一个函数指针void (*) (const char*)
。
该函数指针指向一个函数,函数一个参数,参数类型为const char* str
类型,不需要返回值。
以上就是对指针进阶内容的讲解,希望能帮助到大家,如果有写的不好的地方,欢迎大家指正!!!