指针深入第二弹--字符指针、数组指针、函数指针、函数指针数组、转移表的理解加运用

目录

字符指针变量

打印时解引用

打印字符串(%s格式)

无需解引用指针

错误示例:解引用指针

打印单个字符(%c格式)

必须解引用指针

错误示例:不解引用指针

数组指针变量

数组指针变量的初始化

⼆维数组传参的本质

函数指针变量

函数指针变量的创建

函数指针变量的使用

typedef关键字

函数指针数组

转移表

总结

字符指针变量

在指针的类型中我们知道有⼀种指针类型为字符指针 char*

cpp 复制代码
int main()
{
	char ch = 'w';
	char* pc = &ch;//pc是指针变量
	char arr[10] = "abcdef";//字符数组
	char* p = arr;
	//%s打印字符串时,需要提供起始地址
	printf("%s\n", arr);
	printf("%s\n", p);
	return 0;
}

字符串的存储空间是连续的,所以可以通过数组首元素的地址来访问全部的字符串

字符数组的内容是可变的

常量字符串的内容是不可变的

cpp 复制代码
char* p="abcdef";//常量字符串

字符串字面量char* p1 = "abcdef"):

  • 存储在只读内存区。
  • 内容不可修改。
  • 指针 p1 可以重新指向其他地址(如 p1 = "xyz")。

字符数组char arr[] = "abcdef"):

  • 存储在栈或静态数据区(可读写)。
  • 内容可修改(如 arr[0] = 'x')。
  • 数组名 arr 是常量地址,不可重新赋值

打印时解引用

打印字符串(%s格式)

无需解引用指针
cpp 复制代码
char* p1 = "abcdef";

printf("%s", p1); // 正确:传递指针本身

原理

  • %s格式说明符期望接收一个char*类型的指针(即字符串的起始地址)。
  • printf会从该地址开始逐个读取字符,直到遇到空字符\0为止。
  • 不需要解引用 (即不需要*p1),因为传递的是指针值(地址),而不是指针指向的内容。
错误示例:解引用指针
cpp 复制代码
printf("%s", *p1); // 错误!

问题

  • *p1是解引用操作,得到的是第一个字符'a',类型为char)。
  • %s期望一个char*指针,但传入的是char类型(ASCII值)。
  • printf会将该ASCII值(如'a'的ASCII码97)误认为是一个内存地址 ,尝试访问地址0x00000061,导致未定义行为(通常是程序崩溃)。

打印单个字符(%c格式)

必须解引用指针
cpp 复制代码
printf("%c", *p1); // 正确:解引用得到字符

原理

  • %c格式说明符期望接收一个char类型的字符
  • *p1解引用指针,得到指针指向的第一个字符('a')。
  • 必须解引用,否则传递的是指针值(地址),而不是字符。
错误示例:不解引用指针
cpp 复制代码
printf("%c", p1); // 错误!

问题

  • p1char*指针,其值是字符串的起始地址(如0x400000)。
  • %c期望一个char,但传入的是指针(地址),导致类型不匹配(编译器警告),输出可能是乱码或截断的地址值。
cpp 复制代码
#include <stdio.h>

int main() {
    char* p1 = "abcdef";  // 字符串字面量(常量)
    char arr[] = "hello"; // 字符数组(可修改)

    // 打印整个字符串(传递指针,不解引用)
    printf("String p1: %s\n", p1);   // 输出: abcdef
    printf("String arr: %s\n", arr); // 输出: hello

    // 打印单个字符(必须解引用)
    printf("First char of p1: %c\n", *p1);   // 输出: a
    printf("First char of arr: %c\n", arr[0]); // 输出: h

    // 打印指针地址(不解引用)
    printf("Address of p1: %p\n", p1);   // 输出: 0x400000(示例地址)
    printf("Address of arr: %p\n", arr); // 输出: 0x7ffd1234(栈地址)

    // 错误示例(不要尝试!)
    // printf("%s", *p1); // 崩溃:将字符'a'(ASCII 97)当作地址访问
    // printf("%c", p1);  // 警告:将指针传递给%c(输出乱码)

    return 0;
}

下面我们来看一个与字符串相关的笔试题

cpp 复制代码
#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两个数组名是首元素的地址,代表了两个独立的空间

常量字符串在代码段里,栈区,堆区,静态区中存放的数据是可变的

数组指针变量

数组指针变量是指针变量?还是数组?

答案是:指针变量。

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

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

cpp 复制代码
int main()
{
	int* p1[10];//p1是指针数组-存放整型指针的数组
	int (*p2)[10];//p2是指针变量,指向的是数组
	return 0;
}

p先和*结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组,所以 p是⼀个指针,指向⼀个数组,叫数组指针

这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。

数组指针变量的初始化

cpp 复制代码
int main()
{
	int arr[10] = { 0 };
	int(* p) [10] = arr;
	return 0;
}

查看内存为

我们调试能看见&arr和p的类型是一致的

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

⼆维数组传参的本质

二维数组的数组名也是数组首元素的地址,那首元素到底是谁的地址?

首元素的地址就是第一行的地址,第一行的地址就是一维数组的地址,类型是数组指针类型

打印二维数组(用数组的方式传参)

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

用指针传参来改一下上面这个代码

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

数组名是数组首元素的地址,但有两个例外

  • &数组名
  • sizeof(数组名)

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

函数指针变量

函数指针变量的创建

数组指针--指针-指向数组的--存放的是数组的地址

函数指针--指针-指向函数的--存放的是函数的地址

  • &数组名是数组的地址
  • 数组名是数组首元素的地址

两个地址的值是一样的

cpp 复制代码
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int x = 10;
	int y = 20;
	Add(x, y);
	printf("&Add=%p\n", &Add);
	printf("Add=%p\n", Add);
	return 0;
}

运行结果为:

可见,函数是有地址的,函数名就是函数的地址

cpp 复制代码
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf)(int, int) = &Add;//pf是专门存放函数的地址的,pf就是函数指针变量
	return 0;
}
cpp 复制代码
int (*pf3) (int x, int y)
 | | ------------ 
 | | |
 | | pf3指向函数的参数类型和个数的交代
 | 函数指针变量名
 pf3指向函数的返回类型
 
 int (*) (int x, int y) //pf3函数指针变量的类型

函数指针变量的使用

通过函数指针调⽤指针指向的函数

cpp 复制代码
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf)(int, int) = Add;//pf是函数指针
	int c = Add(2, 3);//函数名调用
	printf("%d\n", c);
	int d = (*pf)(3, 4);//函数指针调用
	printf("%d\n", d);
	int e = pf(4,5);//函数指针调用
	printf("%d\n", e);
	return 0;
}

函数名在非 sizeof& 操作下,会自动转为函数指针。因此:

  • Add(2,3) 等价于 (&Add)(2,3)
  • pf(4,5) 等价于 (*pf)(4,5)

typedef关键字

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

指针类型,能否重命名呢?其实也是可以的,⽐如,将 int* 重命名为 ptr_t ,这样写:

cpp 复制代码
typedef int* ptr_t;

但是对于数组指针和函数指针稍微有点区别:

⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:

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

下面我们来看一下这四个变量类型是否相同

cpp 复制代码
typedef int* ptr_t;
#define PTR_T int*
ptr_t p1, p2;
PTR_T p3, p4;

p1,p2都是指针变量,p3是指针变量,p4是整形变量

原因:PTR_T是符号,而ptr_t是类型

cpp 复制代码
PTR_T p3, p4; == int* p3, p4;  ==  int (*p3), (p4);

函数指针数组

那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组

cpp 复制代码
int (*parr1[3])();

转移表

函数指针数组的⽤途:转移表

举例:计算器的⼀般实现

cpp 复制代码
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 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");
		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 = Add(x, y);
			printf("ret=%d\n", ret);
			break;
		case 3:
			printf("请输入操作数:");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("ret=%d\n", ret);
			break;
		case 4:
			printf("请输入操作数:");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("ret=%d\n", ret);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}
cpp 复制代码
printf("请输入操作数:");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("ret=%d\n", ret);
break;

这段代码在这里显得非常的冗余,下面我们来用函数指针数组来改变这一点

cpp 复制代码
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");
		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;
}

不用Switch...case,可以减少代码行数

总结

通过本文的学习,我们系统地了解了C/C++中指针的高级应用,从基础的字符指针变量到复杂的函数指针变量和转移表,全面掌握了指针这一强大工具的精髓。指针作为C/C++语言的核心特性,不仅能够帮助我们实现高效的内存管理,还能让我们编写出更加灵活和强大的程序。

我们首先探讨了字符指针变量的使用,特别是如何通过%s格式打印字符串,以及打印时解引用的重要性。随后,我们深入研究了数组指针变量,理解了指针与数组之间的紧密联系。函数指针变量的学习让我们看到了如何将函数作为参数传递,以及如何创建动态的函数调用机制,这为设计灵活的程序架构提供了可能。

转移表的概念更是将函数指针的应用推向了新的高度,使我们能够构建出高效且易于扩展的程序结构。这些知识点虽然看似复杂,但一旦掌握,将极大地提升我们的编程能力和解决问题的思路。

在实际开发中,熟练运用这些指针技巧能够帮助我们编写出更加高效、简洁且可维护的代码。希望读者能够将这些知识应用到实际项目中,通过不断的实践和探索,真正领会指针的强大之处,成为一名优秀的C/C++程序员。

指针的世界博大精深,本文只是揭开了其神秘面纱的一角。期待读者能够继续深入学习,探索更多指针的奥秘,在编程的道路上不断前进!

相关推荐
蓝色汪洋7 小时前
数码和easy
算法
熊猫_豆豆7 小时前
嫦娥号地月轨道、环月(一个月)MATLAB仿真
开发语言·matlab
wjs20247 小时前
MongoDB Java:深入解析与应用实践
开发语言
散峰而望7 小时前
基本魔法语言数组 (二) (C语言)
c语言·开发语言·github·visual studio
mit6.8247 小时前
[VT-Refine] 强化学习工作流 | 分布式-近端策略优化(DPPO)
分布式·算法
逻极7 小时前
Rust之结构体(Structs):构建自定义数据类型
开发语言·后端·rust
小二·7 小时前
深入解析 Rust 并行迭代器:Rayon 库的原理与高性能实践
开发语言·算法·rust
四念处茫茫8 小时前
Rust:复合类型(元组、数组)
开发语言·后端·rust
l1t8 小时前
对luasql-duckdb PR的测试
c语言·数据库·单元测试·lua·duckdb