C语言-- 深入理解指针(3)

C语言-- 深入理解指针(3)

前言: 回顾上篇,我们学习了一维数组传参的本质,二级指针和指针数组等指针有关知识:
C语言-- 深入理解指针(2)

今天我们继续深入了解指针世界:

一、字符指针变量

字符指针变量就是用来存放字符类型变量地址的。

c 复制代码
int main()
{
	char ch = 'w';
	char* pc = &ch;//字符指针变量
	return 0;
}

我们知道通过指针访问数组可以通过数组名的方式,因为数组名是数组首元素的地址。字符数组也是如此:

c 复制代码
int main()
{
	char str[10] = "abcdefg";
	char* p = str;//str表示第一个字符的地址
	*p = 'w';
	printf("%s\n", str);
	return 0;
}

结果:str表示数组首元素的地址,也就是第一个字符的地址,解引用赋值可以改变为其他值。

那么如果我们使用指针变量来存放字符串的地址呢?是否也可以改变?

c 复制代码
int main()
{
	char* p = "hello world!";
	*p = 'w';
	return 0;
}

结果却是错误的,调试也会报错:

因为这种字符串是常量字符串,不是放在字符数组中的, 常量字符串的内容不能够被改变。指针变量p指向字符串,存放的是字符串中第一个字符的地址。

所以一般使用常量字符串会在前面加上const 修饰指针变量,来更好地约束

c 复制代码
const char* p = "hello world!";

下面我们来看一道有趣的题目:

问下面的输出结果是什么?

c 复制代码
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;
}

首先对于str1str2是两个不同的数组 ,数组名代表首元素的地址,虽然它们的内容存放的一样,但是地址是肯定不一样的;

然后是str3str4,这两个字符指针变量都指向常量字符串 "hello world.",因为常量字符串是不能够被修改的 ,所以在 内存中一个常量字符串只会存在唯一的一个 ,没有必要存放多个浪费空间。所以存放在字符指针变量str3和str4中的内存是一样的,都是指向第一个字符的地址。

二、数组指针变量

我们之前学习了指针数组,是一个数组,数组中的每一个元素是指针变量;那么数组指针就是一个指针变量,存放的是数组的地址。

c 复制代码
int main()
{
	int a[10] = { 0 };
	int(*p)[10] = &a;// p -> 数组指针
	return 0;
}

一定要加括号,不要和指针数组弄混了!
解释:[ ]的优先级比*高,所以一定要加括号,让p先与*结合,说明p是一个指针,指针指向的是一个存放10个元素的数组[10],每个元素的类型是int类型。

c 复制代码
int(*p1)[10];// p1 -> 数组指针
int* p2[10];// p2 -> 指针数组

数组指针变量把变量名去掉就是它的类型

c 复制代码
int(*p1)[10];// p1-> 数组指针变量
int(*)[10];// p1数组指针 的类型

三、二维数组传参的本质

在上一篇文章中,我们学习了一维数组传参的本质,知道了一维数组传参本质上是传递数组首元素的地址,所以一维数组传参既可以写成指针的形式,也可以写成一维数组的形式。

那么二维数组传参和一位数组传参是否类似呢?

首先我们刚开始学习二维数组时,就知道二维数组传参可以直接写成二维数组的形式:

c 复制代码
void print(int a[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 ", a[i][j]);
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][5] = { {1,1,1,1,1}, {2,2,2,2,2}, {3,3,3,3,3} };
	print(arr, 3, 5);
	return 0;
}

那么是否可以写成其他形式呢?

我们可以把二维数组看作是一维数组,原本二维数组中的每一行的一维数组就是一维数组中的一个元素,数组名代表首元素的地址,那么二维数组的的首元素是就是第一行的一维数组,那么 二维数组的数组名就表示第一行一维数组的地址。 类型是int(*)[5]

所以二维数组传参的本质就是传递的是第一行的一维数组的地址

所以二维数组传参可以写成二维数组的形式,也可以写成一维数组的数组指针的形式

c 复制代码
void print(int(*a)[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 ", *(*(a + i) + j));
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][5] = { {1,1,1,1,1}, {2,2,2,2,2}, {3,3,3,3,3} };
	print(arr, 3, 5);
	return 0;
}

a是二维数组首元素的地址,也就是第一行一维数组a[0]的地址;
a + i可以分别找到每一行的一维数组的地址;

解引用*(a + i)就等价于分别得到了每一行的一维数组的数组名a[i]

一维数组的数组名相当于一维数组首元素的地址,一维数组的首元素是一个int类型的整型数组元素;

所以+j就可以每一次跳过一个int类型的大小,得到每一个数组元素的地址(*(a + i) + j)

最后再解引用,就可以依次遍历得到二维数组的每一个数组元素*(*(a + i) + j)

四、函数指针变量

4.1 函数指针变量的创建

既然指针可以用来存放数组的地址,用来指向和访问数组,那么函数自然也"难逃一死"。

既然叫做函数指针,那么本质上就是一个指针变量,存放的是一个函数的地址,可以通过函数指针来调用函数。

在数组中,数组名和&数组名,这两者是虽然直接输出的地址可能都是第一个元素的地址,但是还是有区别的,但是对于函数来说, 函数名和&函数名,这两者都是函数的地址,没有任何分别。

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

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

结果:

既然函数指针变量是指向函数的,存放函数的地址,那么变量的类型就和函数的参数和返回类型等有关:

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

int main()
{
	int (*p)(int, int) = Add;
	//int (*p)(int x, int y) = Add; -- 形参名x 和 y可以省略不写
	return 0;
}

Add是一个有两个形参(int x, int y),返回类型是int类型的函数。

那么函数指针变量首先是一个指针,因为函数的括号比*优先级要高,所以要 先使用括号让变量名与*号结合,说明是一个指针变量(*p)

(*p)的后面加上(int x, int y),在前面加上int,说明指针指向的是一个参数为(int x, int y)(参数名x和y可以省略不写),返回类型是int类型的函数。

如果函数指针变量指向的是一个没有参数,返回类型是void的函数,就可以按照下面这样写:

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

int main()
{
	void (*p)() = test;
	return 0;
}

函数指针变量把函数名去掉就是函数指针变量的类型:

c 复制代码
int (*p)(int, int)//函数指针变量
int (*)(int, int)//类型

4.2 函数指针变量的使用

函数指针变量是指向函数的,所以我们可以通过它来调用函数。

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

int main()
{
	int (*pf)(int, int) = Add;
	int a = 6;
	int b = 8;
	printf("%d\n", Add(a, b));
	printf("%d\n", (&Add)(a, b));
	printf("%d\n", (*pf)(a, b));
	printf("%d\n", pf(a, b));
	return 0;
}
  1. 既然Add和&Add一样,都可以表示函数的地址,所以也可以使用&函数名来调用函数;
  2. 函数指针变量pf存放的就是函数Add的地址,所以解引用可以得到Add函数名 ,然后进行调用,因为函数括号的优先级比*高,所以注意要先使用括号让变量名与*结合进行解引用(*pf)(a, b)
  3. 因为Add和&Add等价,所以使用(*pf)可以,直接使用函数指针变量pf也可以调用函数pf(a, b)

结果:

4.3 两段奇葩代码

NO.1 :请问下面这段代码什么含义?

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

很多人第一眼看到这都是懵的,这是什么奇葩代码啊?

分析:

  1. 首先,我们在这里面最熟悉的数字0开始下手,0是一个数字然后前面有一个括号,括号中放的是一个void (*)()
  2. void (*)()这不就是我们上面学习的函数指针变量的类型吗,所以这就是将数字0强制类型转换void (*)()这样一个函数指针类型,也就是说拿到了地址为0的一个函数的地址
  3. 然后对将这个地址与*号使用括号结合起来,进行解引用(*(void (*)())0),得到函数名,对函数调用 ,函数没有参数 ,所以最后面的单独的一个括号中什么也没有(),函数也返回类型是void,所以不需要使用变量来接收返回值;
    类似于:
c 复制代码
(*pf)();//pf是函数指针变量,存放函数的地址
  1. 所以总的来说就是调用一个函数 :地址为0处的,返回类型为void,没有参数的函数。

NO.2 :请问下面这段代码什么含义?

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

分析:

  1. 首先我们可以看到intvoid(*)(int)分别是整型和函数指针类型,共同放在一个括号里面,很明显这两个是函数的参数 ,函数名是signal,组成才一起就是signal(int, void(*)(int))

  2. 但是缺少函数返回类型;

  3. 然后我们把整个函数从中去掉,发现剩下的是void (*)(int);,这不也是一个函数指针类型吗,其实这就是函数的返回类型

c 复制代码
void (*)(int);
  1. 所以总的来说这句代码的含义就是声明函数 ,声明了一个函数名为signal,参数是(int, void(*)(int)),返回类型是void (*)(int)的一个函数。

所以要注意:
如果一个函数的返回类型是函数指针变量类型,不能直接按照常规函数的声明一样,将类型直接放在函数名前面,要放在函数指针变量内部,放在*号的右边

比如:

返回类型是void (*)(int)

这种写法是错误的:

c 复制代码
void (*)(int)函数名(参数1, 参数2,......,(或者没有参数));

这种写法是正确的:

c 复制代码
void (*      函数名(参数1, 参数2,......,(或者没有参数))  )(int);

当然可以使用typedef来对函数指针类型进行重定义,使得代码简化易懂,具体会在后面讲解typedef关键字中讲到。

五、typedef关键字

我们再来学习一个C语言中的关键字typedef,是用来进行类型重定义的,简化代码。

5.1 typedef的使用

5.1.1 重定义普通变量类型

如果我们创建一个无符号的整型可以使用unsigned int

c 复制代码
unsigned int n = 10;

但是我们有时可能嫌这个类型太长了,使用起来需要敲的代码量有点大,我们想简化一下,就可以使用typedef来对这个类型重定义:

c 复制代码
typedef unsigned int u_int;
int main()
{
	u_int n = 10;
	return 0;
}

这就将unsigned int类型重定义为了u_int,在代码中就可以直接使用u_int来创建无符号整型变量了。

5.1.2 重定义指针变量类型

同理也可以重定义指针类型:

c 复制代码
typedef unsigned int u_int;
typedef int* p_int;

int main()
{
	u_int n = 10;//等价于 unsigned int n = 10;
	p_int pn = &n;//等价于 int* pn = &n;
	printf("%d\n", n);
	printf("%d\n", *pn);
	return 0;
}

5.1.3 重定义数组指针变量类型

重定义数组指针变量类型和普通指针变量类型有所不同,比如我们知道有整型数组变量类型是:

c 复制代码
int(*)[5]

如果对其重定义,就要将新的类型名放在*号的右边,括号()的里面

c 复制代码
typedef int(*p_arr)[5];

int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int(*p1)[5] = &arr;
	p_arr p2 = &arr;//p1和p2类型等价
	return 0;
}

5.1.4 重定义函数指针变量类型

在上面我们提到了,如果一个函数的返回类型是函数指针变量类型,不能直接按照常规函数的声明一样,将类型直接放在函数名前面,要放在函数指针变量内部,放在*号的右边

但是我们这样不便于我们编写函数和理解函数,所以我们可以对函数指针变量类型进行重定义:

比如对void(*)(int)类型进行重定义:

c 复制代码
typedef void(*p_fun)(int);//p_fun就是新的类型名

上面的4.3中的第二个代码就可以换成下面这种方式写:

c 复制代码
typedef void(*p_fun)(int);

p_fun signal(int, p_fun);

这样就可以避免一些复杂的代码,使函数更加清晰易懂了。

5.2 typedef 和 define 的区别

详见文章:
typedef 和 #define 的区别

六、函数指针数组

我们之前学习过指针数组,是存放指针的数组。那么很显然,函数指针数组就是存放函数指针的数组。

c 复制代码
int(*parr[3])(int, int) = { 0 };
  1. []的优先级比*高,所以parr先与[3]结合,说明这是这是一个存放三个数据的一个数组,数组名是parr;
  2. 然后把parr[3]去掉,剩下int(*)(int, int),就是数组元素类型,表示数组中存放的都是函数指针。

函数指针数组也是一个数组,所以数组元素类型都相同,都是。那么函数指针数组中存放的函数指针的类型都必须一样

比如:

下面这段代码中,函数指针数组parr中存放的函数指针类型都是int(*)(int, int),即指向的都是返回类型是int,参数是两个int类型的整型的函数

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 main()
{
	int(*parr[3])(int, int) = { Add, Sub, Mul };
	return 0;
}

七、转移表

我们学习了函数指针数组,那么它到底有何用处呢?我们可以使用它来实现转移表。

比如我们来实现一个计算器,可以按照下面这段代码写:

c 复制代码
#include <stdio.h>

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

但是我们会发现,在switch语句中,涉及到计算的case 1case 4 中的除了调用的函数不一样其他都是相同的,感觉上过于冗余。

思考:

  1. 既然只有调用的函数不一样,那么我们就可以将函数放在一个数组里,用到哪个就可以通过下标来访问到哪一个函数,拿到函数名然后进行调用,这就可以使用我们的函数指针数组了。
  2. 将加减乘除四个函数的函数指针(可以直接是函数名)放入函数指针数组中int(*parr[5])(int, int) = { 0, Add, Sub, Mul, Div };,但是为了和我们菜单页面中的选择(1:Add 2:Sub 3 :Mul 4 :Div)保持一致,所以要在数组的最前面加上一个0;
  3. 然后就可以直接使用if-else语句来实现:
    输入的input为
    1~4:根据输入的input的具体值,进行具体的运算,得出计算结果,再进入循环重新选择;
    0:退出计算机,程序结束;
    其他:输入的是错误值,打印错误信息,再进入循环重新选择。

方法一:使用函数指针数组,完整代码:

c 复制代码
#include <stdio.h>

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;//放在循环外面,只需创建一次,节省内存
	int(*parr[5])(int, int) = { 0, Add, Sub, Mul, Div };//放在循环外面
	do
	{
		menu();
		printf("请选择您的操作:\n");
		scanf("%d", &input);
		if (input >= 1 && input<= 4)
		{
			printf("请输入两个操作数:\n");
			scanf("%d %d", &x, &y);
			ret = parr[input](x, y);
			printf("%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出计算器\n");
		}
		else
		{
			printf("选择错误,请重新选择:\n");
		}
	} while (input);
	return 0;
}

方法二:使用回调函数

会在下一章节中讲到。

结语:typedef 和 #define 的区别 章节到这里就结束了。

本人才疏学浅,文章中有错误和有待改进的地方欢迎大家批评和指正,非常感谢您的阅读!如果本文对您又帮助,可以高抬贵手点点赞和关注哦!

相关推荐
阿桂天山几秒前
实现批量图片文字识别(python+flask+EasyOCR)
开发语言·python·flask
天天进步20157 分钟前
Python跨平台桌面应用程序开发
开发语言·python
时光话16 分钟前
Lua 第9部分 闭包
开发语言·lua
时光话16 分钟前
Lua 第7部分 输入输出
开发语言·lua
海上彼尚21 分钟前
使用Autocannon.js进行HTTP压测
开发语言·javascript·http
勇敢牛牛@22 分钟前
Python flask入门
开发语言·python·flask
万水千山走遍TML41 分钟前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能
三体世界2 小时前
Linux 管道理解
linux·c语言·开发语言·c++·git·vscode·visual studio
我命由我123452 小时前
Android Cordova 开发 - Cordova 快速入门(Cordova 环境配置、Cordova 第一个应用程序)
android·开发语言·前端框架·android studio·h5·安卓·android-studio
Mikey_n2 小时前
深入理解依赖、Jar 包与 War 包:Java 开发基石探秘
java·开发语言·jar