C语言笔记归纳12:指针(3)

C语言指针进阶核心解析

指针(3)

目录

指针(3)

1. 📝 字符指针:常量字符串的 “只读陷阱” 与存储秘密

1.1 字符指针 vs 字符数组:可变 vs 只读

1.2 为什么str1≠str2但str3=str4?

2. 📚 数组指针:指向数组的 “专属导航仪”

2.1 数组指针 vs 指针数组:别再搞混双胞胎!

2.2 数组指针的 “步长”:一跳就是整个数组

3. 🧩 二维数组传参:数组指针的实战舞台

3.1 二维数组的地址逻辑:行地址≠元素地址

3.2 传参的两种写法:数组形式 vs 指针形式

4. 🔧 函数指针:存储函数地址的 “特殊名片”

4.1 函数地址的获取:函数名 =(& 函数名)

4.2 函数指针的调用:*可省可加

4.3 两段 “天书代码” 的拆解

代码 1:(*(void(*)())0)();

代码 2:void (*signal(int, void(*)(int)))(int);

4.4 typedefvs#define:类型重命名的坑

5. 🗂️ 函数指针数组:打造函数的 “工具箱”

6. ⚙️ 转移表:函数指针数组的经典实战(计算器案例)

传统 switch 实现(冗余)

转移表实现(简洁高效)

7. 🔄 回调函数:指针驱动的 “灵活响应机制”

定义与示例

实际应用:

🎉 总结:指针进阶的核心逻辑


✨ 引言:

指针是 C 语言的 “灵魂进阶篇”🤯—— 字符指针、数组指针、函数指针、回调函数…… 这些概念层层递进,新手容易绕晕,但一旦吃透,代码效率和灵活性会翻倍!

1. 📝 字符指针:常量字符串的 “只读陷阱” 与存储秘密

字符指针不仅能指向单个字符,还能指向字符串 —— 但这里藏着新手最容易踩的 “只读坑”,还有存储的小秘密!

1.1 字符指针 vs 字符数组:可变 vs 只读

字符数组是 “可修改的草稿纸”,常量字符串是 “贴了只读标签的打印件”:

int main()
{
	char ch = 'w';
	char* pc = &ch;//指向单个字符的指针

	char arr[10] = "abcdef";//字符数组(栈区,内容可修改)
	//内存布局:a b c d e f \0 0 0 0(像可擦写的草稿纸)
	char* p1 = arr;
	*p1 = 'w';//合法:修改数组内容(擦改草稿纸)

	const char* p2 = "abcdef";//指向常量字符串(代码段,只读)
	//*p2 = 'w'//err:修改只读内存会崩溃!(试图改打印件)
	printf("%s\n", p1);//wbcdef
	printf("%s\n", p2);//abcdef
	return 0;
}

📌 核心区别(必记!):

类型存储区域内容是否可修改
字符数组栈区
常量字符串代码段否(const是提醒,不加也报错)

1.2 为什么str1≠str2str3=str4

就像 “两个独立小区 vs 共享一个公共设施”:

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和str2相同\n");//❌ 输出不同
	else printf("str1和str2不同\n");//✅

	if (str3 == str4) printf("str3和str4相同\n");//✅
	else printf("str3和str4不同\n");//❌
	return 0;
}

🤔 通俗解释:

  • str1str2栈区的两个独立数组(两个小区),各占一块内存,首地址自然不同;
  • str3str4指向代码段的同一个常量字符串(共享一个公园)—— 系统为了节省空间,相同的常量字符串只存储一份,所以地址相同。

💡 小贴士:%s打印字符串时,只需传入起始地址,会自动打印到\0结束,不用手动循环!

2. 📚 数组指针:指向数组的 “专属导航仪”

数组指针不是 “指针数组”!它是专门指向整个数组的指针—— 就像导航仪专门定位 “整个小区”,而不是单个 “住户”。

2.1 数组指针 vs 指针数组:别再搞混双胞胎!

int main()
{
	int arr[10] = { 0 };
	int(*p)[10] = &arr;//p是数组指针:指向包含10个int的数组
	//int* p1[10];//p1是指针数组:数组里存的是10个int*指针
	return 0;
}

⚠️ 优先级陷阱(关键!):

  • int(*p)[10]()优先级高,p先和*结合,是指针,指向 “10 个 int 的数组”;
  • int* p1[10][]优先级高,p1先和[]结合,是数组,存的是 “int * 指针”。

2.2 数组指针的 “步长”:一跳就是整个数组

普通指针跳 “一个元素”,数组指针跳 “整个数组”:

int main()
{
	int arr[10] = { 0 };
	int* p1 = arr;//int*:指向首元素(住户导航)
	int(*p2)[10] = &arr;//int(*)[10]:指向整个数组(小区导航)

	printf("%p\n", p1);//000000983ED3FA28
	printf("%p\n", p1 + 1);//000000983ED3FA2C(+4字节:1个int,到下一户)

	printf("%p\n", p2);//000000983ED3FA28
	printf("%p\n", p2 + 1);//000000983ED3FA50(+40字节:10个int,到下一个小区)
	return 0;
}

📌 结论:数组指针的 “步长”= 指向数组的总大小(sizeof(数组))。

3. 🧩 二维数组传参:数组指针的实战舞台

二维数组传参的本质是 “传递行地址”—— 而行地址就是数组指针!

3.1 二维数组的地址逻辑:行地址≠元素地址

二维数组可以看作 “数组的数组”,首元素是 “第一行的一维数组”:

int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
// arr:二维数组名 → 第一行的地址(int(*)[5],数组指针)
// arr+i:第i行的地址(int(*)[5])
// *(arr+i) = arr[i] → 第i行的数组名 → 第i行首元素地址(int*)
// *(arr+i)+j → 第i行第j列元素地址(int*)
// *(*(arr+i)+j) = arr[i][j] → 第i行第j列元素(int)

🤔 比喻:二维数组是 “3 栋楼,每栋 5 户”:

  • arr是 1 号楼的地址(数组指针);
  • arr+1是 2 号楼的地址;
  • *(arr+1)是 2 号楼 1 单元的地址(普通指针);
  • *(arr+1)+2是 2 号楼 3 单元的地址;
  • *(*(arr+1)+2)是 2 号楼 3 单元的住户(元素值)。

3.2 传参的两种写法:数组形式 vs 指针形式

二维数组传参时,列数不能省略(因为要知道 “每栋楼有几户”):

// 写法1:数组形式(直观,本质是数组指针)
// void print(int arr[3][5], int r, int c)
// {
// 	// ...
// }

// 写法2:数组指针形式(更贴近本质)
void print(int(*arr)[5], int r, int c)
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{
			printf("%d ", *(*(arr + i) + j));//等价于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);
	// 输出:
	// 1 2 3 4 5 
	// 2 3 4 5 6
	// 3 4 5 6 7
	return 0;
}

📌 关键:二维数组名arr的类型是int(*)[5](数组指针),所以形参必须用相同类型接收!

4. 🔧 函数指针:存储函数地址的 “特殊名片”

函数也有地址!函数指针就是存放函数地址的变量 —— 就像用名片存着 “公司地址”,拿着名片就能找到公司(调用函数)。

4.1 函数地址的获取:函数名 =(& 函数名)

和数组名不同,函数名和&函数名完全等价,都表示函数的入口地址:

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

int main()
{
	printf("%p\n", &Add);//00DF10B9(函数地址)
	printf("%p\n", Add);//00DF10B9(和&Add完全相同)

	// 函数指针定义:返回值类型 (*变量名)(参数类型1, 参数类型2)
	int(*pf)(int,int) = &Add;//pf是函数指针变量
	return 0;
}

📌 函数指针类型格式:返回值类型 (*变量名)(参数类型列表)

4.2 函数指针的调用:*可省可加

调用函数指针时,*只是 “仪式感”,加不加都能调用:

int main()
{
	int(*pf)(int, int) = Add;
	int c = Add(2, 3);//直接调用(常用)
	int d = (*pf)(3, 4);//解引用调用(*可省)
	int e = pf(4, 5);//函数指针直接调用(更简洁)
	printf("%d\n", c);//5
	printf("%d\n", d);//7
	printf("%d\n", e);//9
	return 0;
}

💡 小贴士:函数指针的核心是 “通过地址调用函数”,*的作用是 “解引用地址”,但编译器会自动识别,所以可省略。

4.3 两段 “天书代码” 的拆解

代码 1:(*(void(*)())0)();
int main()
{
	(*(void(*)())0)();
	// 拆解(从内到外):
	// 1. void(*)() → 无参数、返回值为void的函数指针类型(“函数类型模板”)
	// 2. (void(*)())0 → 把0强制转换成该函数指针类型(假设0地址有个这样的函数)
	// 3. *(void(*)())0 → 解引用0地址的函数指针(找到这个函数)
	// 4. (*...)() → 调用这个函数(无参数)
	return 0;
}
代码 2:void (*signal(int, void(*)(int)))(int);

这是标准库signal函数的声明,返回值是 “函数指针”,用typedef简化后更清晰:

// typedef简化函数指针类型
typedef void(*pf_t)(int);//pf_t = void(*)(int)(给函数指针类型起别名)
pf_t signal(int, pf_t);//简化后:返回pf_t,参数是int+pf_t

4.4 typedefvs#define:类型重命名的坑

typedef是 “真正的类型重命名”,#define只是 “文本替换”,别踩坑!

typedef int* ptr_t;
#define PTR_T int*

ptr_t p1, p2;//p1、p2都是int*(typedef是类型重命名,统一类型)
PTR_T p3, p4;//p3是int*,p4是int(#define文本替换:int* p3,p4)

📌 区别:typedef会把整个别名当作一个类型,#define只做简单替换,*只会绑定第一个变量。

5. 🗂️ 函数指针数组:打造函数的 “工具箱”

函数指针数组是 “存放多个函数指针的数组”—— 把功能相似的函数(如加减乘除)的地址 “打包”,像工具箱一样按需取用!

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

int main()
{
	// 函数指针数组定义:int(*数组名[元素个数])(参数类型)
	int(*pfarr[4])(int, int) = { Add, Sub, Mul, Div };
	
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		int r = pfarr[i](8, 4);//通过下标调用对应函数(工具箱取工具)
		printf("%d\n", r);//12 4 32 2
	}
	return 0;
}

✨ 优势:批量管理函数,避免重复的switch/if判断,代码更简洁!

6. ⚙️ 转移表:函数指针数组的经典实战(计算器案例)

计算器的加减乘除功能,用函数指针数组(转移表)实现,代码更简洁、扩展性更强!

传统 switch 实现(冗余)

// 原代码:重复的输入、计算逻辑
switch (input)
{
case 1:
	printf("请输入两个操作数:");
	scanf("%d %d", &x, &y);
	z = Add(x, y);
	printf("%d\n", z);
	break;
case 2:
	// 重复输入、计算逻辑(Sub)
	break;
// ...(case3/4重复类似代码)
}

转移表实现(简洁高效)

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 z = 0;
	// 函数指针数组(转移表):下标对应功能选项
	int(*pfArr[5])(int,int) = { 0 , Add , Sub , Mul , Div};
	//                          0     1    2     3     4        

	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if(input >= 1 && input <= 4)
		{
		  printf("请输入两个操作数:");
		  scanf("%d %d", &x, &y);
		  z = pfArr[input](x,y);//通过下标调用对应函数
		  printf("%d\n", z);
		}
		else if (input == 0)
		{
			printf("退出计算器\n");
		}
		else
		{
			printf("输入错误,请重新输入\n");
		}

	} while (input);

	return 0;
}

📌 优势:新增功能(如取模)只需添加函数和更新数组,不用修改switch结构,扩展性拉满!

7. 🔄 回调函数:指针驱动的 “灵活响应机制”

回调函数是 “通过函数指针调用的函数”—— 把函数作为参数传递给另一个函数,在需要时触发调用,像 “按需响应的开关”!

定义与示例

// 回调函数:具体的功能实现(Add/Sub等)
int Add(int x, int y)
{
	return x + y;
}

// 主调函数:接收函数指针参数,统一处理逻辑
void calc(int(*pf)(int,int))
{ 
	int x = 0;
	int y = 0;
	int z = 0;
	printf("请输入两个操作数:");
	scanf("%d %d", &x, &y);
	z = pf(x, y);//调用回调函数(触发功能)
	printf("%d\n", z);
}

int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			calc(Add);//把Add作为参数传给calc,calc中回调Add
			break;
		case 2:
			calc(Sub);//回调Sub
			break;
		// ...(case3/4类似)
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);

	return 0;
}

💡 核心特点:

  • 回调函数不由自己直接调用,而是由主调函数(如calc)在特定条件下触发;
  • 主调函数统一处理公共逻辑(如输入输出),回调函数处理具体功能,实现 “公共逻辑复用 + 功能灵活切换”。

实际应用:

qsort(快速排序函数)就是典型的回调函数应用 ——qsort负责排序框架,用户通过回调函数定义排序规则(升序 / 降序、结构体字段排序等)。

🎉 总结:指针进阶的核心逻辑

  1. 字符指针指向常量字符串时,牢记 “只读属性”,避免修改崩溃;
  2. 数组指针指向整个数组,步长 = 数组总大小,二维数组传参本质是传 “行地址”;
  3. 函数指针是函数的 “地址名片”,typedef能简化复杂声明;
  4. 函数指针数组(转移表)是批量管理函数的利器,提升代码扩展性;
  5. 回调函数通过函数指针实现 “灵活响应”,是模块化编程的关键。

指针进阶的本质是 “用地址管理一切”—— 字符、数组、函数都能通过地址灵活操作,掌握这些知识点,你就能写出更高效、更灵活的 C 语言代码!如果这篇博客帮你理清了思路,欢迎点赞收藏🌟~ 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值