目录
字符指针变量
在指针的类型中我们知道有⼀种指针类型为字符指针 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); // 错误!
问题:
p1是char*指针,其值是字符串的起始地址(如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++程序员。
指针的世界博大精深,本文只是揭开了其神秘面纱的一角。期待读者能够继续深入学习,探索更多指针的奥秘,在编程的道路上不断前进!