C语言指针深入浅出4

目录


1.字符指针变量

指针类型为char*的指针叫做字符指针变量。

一般我们这样使用:

c 复制代码
int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'h';
	return 0;
}

还有一种使用方式如下:

c 复制代码
int main()
{
	//char str[] = "abcdef";
	//const 的作用是因为是常量字符串,不能随意修改内容
	const char* ps = "abcdef"; // "abcdef" 是常量字符串,这里的赋值是把首元素a的地址赋值给ps 
	printf("%s\n", ps); //注意,用字符%s来打印字符串时,不用加上*
	printf("%c\n", *ps); 
	return 0;
}

《剑指offer》中收录了⼀道和字符串相关的笔试题,通过这一段代码让我们更好认识字符指针数组:

c 复制代码
#include <stdio.h>
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;
}

这段代码打印出的结果是:

我们一起来分析这段代码:

str3和str4指向的是同一块常量字符串,C/C++会把常量字符串存储到单独的⼀个内存区域,当几个指针指向同一块字符串时,他们实际会指向同⼀块内存,但是⽤相同的常量字符串去初始化不同的数组 的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。


2.数组指针变量

2.1 数组指针变量是什么?

我们又可以来类比一下:

  • 整型指针变量 int* pa:指向整型变量的指针,存放的是整型变量的地址
  • 字符指针变量 char* pc:指向的是字符类型变量的指针,存放的是字符类型变量的地址

得出结论:数组指针变量就是指向数组类型变量的指针,存放的是数组的地址。

数组指针变量应该怎么写:

c 复制代码
int (*p)[10];

我们来拆分一下:

  1. p先和*结合在一起,说明p是指针变量。
  2. 去掉*p,剩下int [10],说明指针指向的是一个包含 10 个 int 元素的数组
  3. p是一个指针,指向的是一个数组,所以叫做数组指针

2.2 数组指针如何初始化?

数组指针存放的是整个数组的地址,而不是数组首元素的地址。

c 复制代码
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int (*p)[10] = &arr; //存放的是整个数组的地址
	return 0;
}

注意:数组指针变量的类型,就是把变量名去掉后剩下的部分。比如 int (*p)[10] 中,变量名是 p,去掉 p 后,类型就是 int (*)[10],表示"指向含有 10 个 int 元素数组的指针"。

数组指针类型的解析:


3.二维数组传参的本质

在之前我们的二维数组需要传参给一个函数时,我们是这样写的:

c 复制代码
void print(int arr[3][5], int r, int c)
{
	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; j++)
		{
			printf("%d ", arr[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} };
	print(arr, 3, 5);
	return 0;
}

二维数组可以理解为:由多个一维数组组成的数组 。例如 arr[0] 表示二维数组中的第一个一维数组,也就是第一行。

例如:

c 复制代码
int arr[3][5];

根据数组名是首元素的地址这个特点,我们可以得出二维数组中数组名其实就是首元素的地址,也就是第一行的地址,是一维数组的地址,由此可知,上面arr数组中第一行一维数组的类型就是int [5],所以第一行数组指针的类型就是int (*)[5].
结论:二维数组传参本质上传递的是地址,传递的是第一行这个一维数组的地址,形参也可以写成指针的形式,如下:

c 复制代码
//         一维数组中数组指针变量p的类型
void print(int (*p)[5],int  r, int c)
{
	for (int i = 0; i < r;i++)
	{
		for (int j = 0; j < c; j++)
		{
			printf("%d ", *(p[i] + j));
			//             p[i][j]
			//            *(*(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} };
	// arr传参时表示第一行 arr[0] 的地址,类型是 int (*)[5]
	print(arr, 3, 5);
	return 0;
}

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


4.函数指针变量

4.1 函数指针变量的创建

根据前面的学习,我们进行一下类比不难得出结论:函数指针变量是用来存放函数的地址的,指向的是函数的指针变量。

函数的地址是什么,看下面代码:

c 复制代码
void test()
{
	printf("hehe\n");
}

int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

输出结果:

从上述代码我们可以得出,函数名就是函数的地址,我们也可以通过&函数名的方式来获得函数的地址。那我们就需要创建函数指针变量来存放函数的地址,函数指针变量的写法跟数组指针非常相似。如下:

c 复制代码
void test()
{
	printf("hello world\n");
}

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int (*p1)(int, int) = Add;
	int (*p2)(int x, int y) = &Add; //x 和 y 写上或者省略都可以
	void (*p3)() = test;
	//void (*)() -> 函数指针变量p3的类型
	//int (*)(int, int) -> 函数指针变量p1,p2的类型
	return 0;
}

函数指针类型解析:

4.2 函数指针变量的使用

通过函数指针调用所指向的函数

c 复制代码
int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int (*p1)(int, int) = Add;
	int r = (*p1)(4, 5);
	int l = p1(4, 5);
	printf("%d\n", r);
	printf("%d\n", l);
	return 0;
}

输出结果:

函数指针变量调用函数的两种写法:

4.3 两段有趣的代码

代码1:

c 复制代码
(*(void (*)())0)();

代码解析:

  1. 上述代码完成的是一次函数调用,调用的是0地址处的函数,这个函数没有参数,也没有返回类型。
  2. 代码中的void (*)()是函数指针类型,void (*)()0类型放在括号中的意思是强制类型转化 ,将整型0强制类型转化这种函数指针类型,将0当作函数的地址。
  3. *(void (*)())0,前面加上一个*,表示调用0地址处的这个函数,根据函数指针变量来看,这个函数没有返回值,也没有参数。

画图演示:

代码2:

c 复制代码
void (*signal(int , void(*)(int)))(int);

代码解析:

  1. 上述代码其实是一次函数声明
  2. 声明的函数名叫做signal,函数的参数有两个,一个是int类型,另一个是void(*)(int)函数指针类型。函数的返回值类型也是void(*)(int)

画图演示:

4.3.1 typedef 关键字

typedef 是用来给类型重命名的,可以将复杂的类型简单化。

下面我来类举常见用typedef重命名的情况:

  1. 对于整型类型的重命名,用unit来重命名unsigned int ,用ll重命名long long ,用ull重命名unsigned long long .
c 复制代码
typedef unsigned int uint
  1. 对于指针变量的重命名,将int*重命名为ptr_t
c 复制代码
typedef int* ptr_t
  1. 对于数组指针和函数指针稍微有点区别:
  • 在数组指针类型中,比如数组指针类型是int(*)[5],需要重命名为parr_t,那么可以这么写:
c 复制代码
typedef int(*parr_t)[5]
//新的类型名必须在 * 的右边
  • 函数指针类型的重命名也是⼀样的,比如,将void(*)(int)重命名为pf_t,我们可以这么写:
c 复制代码
typedef void(*pf_t)(int)
//新的类型名必须在 * 的右边

这样我们就可以简化代码2了:

c 复制代码
typedef void(*pf_t)(int);
int main()
{
	void (* signal( int, void(*)(int) ) ) (int);
	pf_t signal(int, pf_t);
	return 0;
}

5.函数指针数组

之前我们学习了数组,数组是一个存放相同类型数据的存储空间,函数指针数组就是把函数的地址存放在一个数组中,函数指针数组该如何定义。

c 复制代码
int (*parr[3])()

函数指针数组的拆解:

parr 先和 [] 结合,说明 parr 是数组;再看 *,说明数组中的每个元素是指针(函数指针变量);最后看 () 和最前面的 int,说明这些指针指向返回类型为 int 的函数。因此,parr 是函数指针数组。

下面这张图是用来说明parr为什么先和[]结合,而不是(),相关知识点在C语言操作符详解中讲到过。


6.转移表

函数指针数组的用途:计算器的一般实现:

c 复制代码
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 x = 0;
	int y = 0;
	int r = 0;
	int input = 1;
	menu();
	do
	{
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入两个数字:");
			scanf("%d %d", &x, &y);
			r = Add(x,y);
			printf("结果是:%d\n", r);
			break;
		case 2:
			printf("请输入两个数字:");
			scanf("%d %d", &x, &y);
			r = Sub(x, y);
			printf("结果是:%d\n", r);
			break;
		case 3:
			printf("请输入两个数字:");
			scanf("%d %d", &x, &y);
			r = Mul(x, y);
			printf("结果是:%d\n", r);
			break;
		case 4:
			printf("请输入两个数字:");
			scanf("%d %d", &x, &y);
			r = Div(x, y);
			printf("结果是:%d\n", r);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (input);
	return 0;
}

使用函数指针数组的方式来实现:

c 复制代码
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 x = 0;
	int y = 0;
	int r = 0;
	int input = 1;
	//函数指针数组 - 转移表
	int (*p[])(int, int) = { NULL,Add,Sub,Mul,Div }; //这样做是为了在用户输入选项时,根据下标直接找到对应的函数指针
	//这里 p[0] 放的是 NULL,不是函数指针,目的是让菜单编号和数组下标对应:
					// 下标: 0     1    2    3    4
					// 功能:退出   加   减   乘   除
	menu();
	do
	{
		printf("请选择:>");
		scanf("%d", &input);
		if (input > 0 && input < 5)
		{
			printf("请输入两个数:");
			scanf("%d %d", &x, &y);
			printf("结果是:%d\n", p[input](x, y)); // 通过 input 作为下标,取出对应的函数指针并调用
		}
		else if (input == 0)
		{
			printf("退出计算器\n");
		}
		else
			printf("请重新输入\n");

	} while (input);
	return 0;
}

如果大家对于这段代码还有疑问的,可以看下面这张图片,我又补充讲解了函数指针数组的调用,其实和函数指针变量 的调用语法格式差不多:


相关推荐
asdfg12589631 小时前
Java 大型项目设计的“内功心法”---面向对象和接口编程
java·开发语言
叼烟扛炮1 小时前
C++第八讲:string 类
开发语言·c++·算法·string
ch.ju1 小时前
Java programming Chapter Three——Array
java·开发语言
灵哎惹,凌沃敏1 小时前
CM3/CM4内核总线知识总结
c语言·arm开发·单片机
努力努力再努力wz1 小时前
【Qt入门系列】第一个 Qt Widgets 程序:项目创建、UI 文件、Hello World、对象树与 qDebug 日志
java·c语言·开发语言·数据结构·c++·qt·ui
电子云与长程纠缠2 小时前
UE5 GameFeature创建与使用
开发语言·学习·ue5·游戏引擎
_Evan_Yao2 小时前
零基础学编程,第一门语言选Python还是C?
c语言·开发语言·python
凤凰院凶涛QAQ2 小时前
《C++转Java快速入手系列》抽象类和接口篇
java·开发语言·c++
河阿里2 小时前
Lambda表达式(Java):从语法本质到工程实践
java·开发语言