Bootstrap

C语言指针超详解——进阶篇

C语言指针系列文章目录

入门篇
强化篇


1. 字符指针变量

在指针的类型中我们知道有一种指针类型为字符指针 char* 。
一般的是使用方式:

int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'w';
	return 0;
}

这里介绍另一种使用方式:

#include<stdio.h>
int main()
{
	const char* pstr = "hello world.";//这里是把一个字符串放到pstr指针变量里了吗?
	printf("%s\n", pstr);
	return 0;
}

代码 const char* pstr ="hello world.";特别容易让同学以为是把字符串 hello world 放到字符指针 pstr 里了,但是本质是把字符串 hello world. 的首字符的地址放到了pstr中
图解
所以上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中

《剑指offer》中收录了一道和字符串相关的笔试题,我们一起来学习一下!

#include <stdio.h>
int main()
{
	char str1[] = "hello world.";
	char str2[] = "hello world.";
	const char* str3 = "hello world.";
	const char* str4 = "hello world.";
	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 一样。

因为这里str3和str4指向的是一个同一个常量字符串。C / C++ 会把常量字符串存储到单独的一个内存区域(文字常量区),当几个指针指向同一个字符串的时候,他们实际会指向同一块内存,所以str3和str4相同。

2. 数组指针变量

2. 1 概念

之前我们学习了指针数组:
指针数组是一种数组,数组中存放的是地址(指针)

那么数组指针变量是指针变量还是数组?
答案是:指针变量

我们已经熟悉:

整形指针变量  :int *pint;存放整形变量的地址,能够指向整形数据的指针变量。
浮点型指针变量:float *pf;存放浮点型变量的地址,能够指向浮点型数据的指针变量。

那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。

下面代码哪个是数组指针变量?

int *p1[10];
int (*p2)[10];

想一想,这两个变量分别是什么类型的?

int (*p2)[10];是数组指针变量。
解释:p先和 * 结合,说明 p 是一个指针变量,然后指针指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
这里要注意:[] 的优先级要高于 * 号,所以必须加上()来保证p先和 * 结合

p的类型是什么?
我们类比 int double 等我们熟悉的变量,可以发现:在变量声明中去掉变量名就是变量类型,所以 p 的类型就是:int (*)[10]

2. 2 数组指针变量的初始化

数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?就是之前的博客中提及的 &数组名。

int arr[10] = { 0 };
&arr;//得到的就是数组的地址

如果要存放单个数组的地址,就得存放在数组指针变量中,比如:

int (*p)[10] = &arr;

我们通过调试来看一看
调试可以发现,p 和 &arr 的类型是相同的。

总结:

int(*p)[10] = &arr;
 |   |  |
 |   |  |
 |   |  p指向数组的元素个数
 |   p是数组指针变量名
 p指向的数组的元素类型

3. 二维数组传参的本质

理解了数组指针的概念,就可以来讲一讲二维数组传参的本质了。
过去我们有一个二维数组的需要传参给一个函数的时候,我们是这样写的:

#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;
}

实际上,这也可以解开一个疑惑:为什么写成二维数组的形式传参时,形参的行可以省略,而列不行,因为这是和以指针的形式传参保持一致的。

总结:二维数组传参,形参的部分可以写成数组,也可以写成指针形式。

4. 函数指针变量

4. 1 函数指针变量的创建

什么是函数指针变量呢?

根据前面学习整型指针,数组指针的时候,我们的利用的类比关系,我们不难得出结论:
函数指针变量应该是用来存放函数地址的,未来通过地址能够调用函数的。
那么函数是否有地址呢?
我们做个测试:

#include <stdio.h>
void test()
{
	printf("hehe\n");
}
int main()
{
	printf("test : %p\n", test);// %p 打印地址
	printf("&test: %p\n", &test);
	return 0;
}

测试结果
确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的方式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法其实和数组指针非常类似。如下:

我们以一个简单的加法函数做例子:

int Add(int a, int b)
{
	return a + b;
}

我们将它的声明变成指针:

int (*Add)(int a,int b);

那么按照之前类比出来的规则:在变量声明中去掉变量名就是变量类型,可以得知 Add 的类型为:

int (*)(int a,int b)

除此之外,函数形参里的变量名称也是可以省略的,即:

int (*)(int ,int)

这样就知道函数指针变量的类型了。

那么就有:

#include<stdio.h>
void test()
{
	printf("hehe\n");
}

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	void (*pf1)() = &test;
	void (*pf2)() = test;
	
	int(*pf3)(int, int) = Add;
	int(*pf4)(int x, int y) = &Add;//x和y写上或者省略都是可以的
	return 0;
}

函数指针变量分析:

int (*pf3) (int x, int y)
 |     |    ------------ 
 |     |          |
 |     |          pf3指向函数的参数类型和个数的交代
 |     函数指针变量名
 pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型

4. 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));   //因为函数名也是地址,所以 pf 无论是否解引用
	return 0;                    //都可以进行传参
}

4. 3 两个有趣的代码

4. 3. 1 代码一

(*(void (*)())0)();

来分析一下
分析
实际上,这个代码是用来模拟实现开机的(开机时会调用 0 地址处的函数)

4. 3. 1 代码二

void (*signal(int , void(*)(int)))(int);

分析
所以说,上面的代码实际上是对函数 signal 的声明,至于为什么不把整个返回类型放在函数名的前面,是因为C语言的语法要求(函数名放在 * 的右边)。
signal 返回类型为 void ,两个参数分别为 int 和 函数指针变量(这个函数指针变量返回类型为 void ,参数为 int),返回类型为函数指针类型(这个函数指针变量的类型为:void(*)(int) 返回类型为 void ,参数为 int)。

两段代码均出自《C陷阱和缺陷》这本书

4. 3. 3 typedef 关键字

typedef 是用来类型重命名的,可以将复杂的类型简单化。
比如说可以将 unsigned int 这个很长的变量名称改短:

typedef unsigned int uint;
//将unsigned int 重命名为uint

这样在之后就可以用 unit 代替 unsigned int 了。

typedef 同样适用于指针类型。

 typedef int* ptr_t;

但是对于数组指针和函数指针稍微有点区别:
比如:有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:

typedef int(*parr_t)[5]; //新的类型名必须在*的右边

函数指针类型的重命名也是一样的,比如,将 void(*)(int)类型重命名为 pf_t,就可以这样写:

typedef void(*pfun_t)(int);//新的类型名必须在*的右边

这样,我们可以尝试简化一下第两个有趣的代码:

typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

这样就简单多了,更方便代码阅读了。

5. 函数指针数组

如果把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
应该是下面这三个中的哪一个呢?

int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];

答案是第一个。
我们来分析一下第一个:
parr1 是数组名,首先与[3]结合,说明它是一个数组,那么剩下的部分就是这个数组存储的变量类型(int (*)())。

注意:只有类型返回类型,形参完全相同的几个函数的指针才能放进一个函数指针数组中。

6. 转移表

那么函数指针数组有什么用呢?
我们来实现一个计算器:
不使用函数指针数组:

#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;
	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);
		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;
}

可以发现,这样写的话,在 main 函数中会十分得臃肿,有大量的相同的重复内容,我们可以使用函数指针数组优化这个代码:

#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;
}

那么在 main 函数中使用的函数数组就是转移表,是函数指针数组的最主要功能。

谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会尽快更新完毕指针全系列!

;