目录
一. 字符指针变量
二. 数组指针变量
三. 二维数组传参
3.1 二维数组的本质
3.2 访问方式与地址计算
3.3 二维数组的传参方式
3.4 深入解析 *(*(arr+i)+j) 与 arr[i][j] 的等价性
四. 函数指针变量
4.1 函数指针变量的创建
4.2 函数指针变量的使用
4.3 两段"有趣"代码的讲解
4.3.1 解析 (*(void(*)())0)();
4.3.2 解析void(* signal(int, void(*)(int)))(int);
4.4 typedef关键字讲解
4.4.1 基本语法
4.4.2 常见用法
4.4.3 结合 typedef 解析 signal 函数
4.4.4 typedef 的优点
4.4.5 总结
五. 函数指针数组
5.1 基本概念
5.2 详细用法
5.3 实际应用示例
六. 转移表
一. 字符指针变量
什么是字符指针变量呢?
字符指针变量是指向字符类型数据的指针,在C/C++中通常用于处理字符和字符串和字符数组。
例如以下代码 处理字符
int main() {char ch = 'w';char* pc = &ch; ///字符指针变量return 0; }
pc存放的是字符w的地址
例如以下代码 处理字符串
int main() {const char* pc = "Hello, World!";//常量字符串 不可被修改printf(" %c\n", *pc);printf(" %s", pc); //注意: 打印字符串的时候 需要的参数是字符串的起始地址return 0; }
pc存放的是首字符H的地址 打印验证结果如下
注意: 打印字符串的时候 需要的参数是字符串的起始地址
注意:pc此时指向的是常量字符串 即*pc现在是一个左值 无法被修改
由于*pc现在是一个左值 无法被修改 使用我们使用const来修饰它
例如以下代码 处理字符数组
int main() {char arr[] = "ABCDE";char* pc = arr;printf(" %c\n", *pc);printf(" %s", pc); //注意: 打印字符串的时候 需要的参数是字符串的起始地址return 0; }
pc存放的是数组第一个元素a的地址 同理 运行结果如下
注意:pc此时指向的是数组 与指向字符串的区别是*pc现在可以被修改
现在让我们来认真阅读以下代码 运行结果会是什么呢?
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=str2\n");elseprintf("str1!=str2\n");if (str3 == str4)printf("str3=str4\n");elseprintf("str3!=str4\n");return 0;
}
运行结果如下
可以看出str1与str2不相等 而str3和str4却相等 这是为什么呢?
原因分析:
str1
和str2
的比较 (str1 == str2
)
str1
和str2
是两个独立的字符数组,分别存储"Hello world"
。- 数组名在比较时会被转换为指向数组首元素的指针(即
&str1[0]
和&str2[0]
)。- 由于
str1
和str2
是两个不同的数组,它们的地址不同,所以str1 == str2
为false
,输出str1!=str2
。
str3
和str4
的比较 (str3 == str4
)
str3
和str4
是指向字符串常量的指针,且它们的值都是"Hello world"
。- 编译器会对相同的字符串常量进行优化(称为 字符串池化,String Interning),即多个相同的字符串常量在内存中只存储一份。
- 因此,
str3
和str4
实际上指向同一个内存地址,所以str3 == str4
为true
,输出str3=str4
。
关键区别:
- 字符数组 (
char[]
) 会分配独立的内存空间,即使内容相同,地址也不同。- 字符串常量 (
const char*
) 可能被优化为共享同一内存,因此相同内容的字符串常量可能指向同一地址。
而如果想比较字符串的内容是否相同,应该使用 strcmp
函数,而不是直接比较指针:
if (strcmp(str1, str2) == 0) // 比较内容是否相同printf("str1 和 str2 内容相同\n");
elseprintf("str1 和 str2 内容不同\n");
strcmp函数简要功能如下
具体了解请访问strcmp - C++ Reference
二. 数组指针变量
在学习数组指针前 让我们来回顾一下 字符指针 整型指针
字符指针: char* p ----指向字符的指针 存放的是字符的地址 char *p=&ch;
整形指针: int* p ----指向整型的指针 存放的是整形的地址 int a=10; p=&a;
数组指针: ----指向数组的指针 存放的是数组的地址
即数组指针变量是指向数组的指针,且在C/C++中用于处理多维数组和动态数组操作。
而数组的地址我们也曾经遇见过 同学们不妨通过以下代码回想一下
int main()
{int arr[10] = { 0 };arr; //首元素的地址&arr[0];&arr;//取出的是整个数组的地址---数组的地址return 0;
}
相信已经有同学分不清了 现在让我们来区别一下指针数组和数组指针
特性 | 数组指针 (int (*p)[N] ) | 指针数组 (int *p[N] ) |
---|---|---|
本质 | 一个指针,指向整个数组 | 一个数组,元素全是指针 |
声明方式 | int (*p)[5]; | int *p[5]; |
内存占用 | 指针大小(通常8字节) | N个指针的大小(如5个指针=40字节) |
存储内容 | 存储数组的首地址 | 存储多个指针(地址) |
典型用途 | 处理二维数组 | 存储多个字符串/动态数组 |
sizeof结果 | sizeof(p) =指针大小 | sizeof(p) =N×指针大小 |
直观理解:
数组指针 → 指向数组的指针
int arr[3][4];
int (*p)[4] = arr; // p指向arr的第一行(一个包含4个int的数组)
p+1
会跳过整个子数组(移动4*sizeof(int)
字节)
指针数组 → 存放指针的数组
char *strs[3] = {"Hello", "World", "!"};
strs[1]
返回第二个字符串的地址("World"
的首地址)
关键区别:
操作 | 数组指针 (int (*p)[4] ) | 指针数组 (int *p[4] ) |
---|---|---|
定义 | 指向int[4] 的指针 | 包含4个int* 的数组 |
p+1的偏移量 | 16字节(假设int =4字节) | 8字节(指针大小) |
记忆口诀
“星号括起来是指针,星号不括是数组”
int (*p)[N]
→ 星号被括号括住,强调是指针int *p[N]
→ 星号没被括,强调是数组
如何使用数组指针来打印数组里的值呢? 如下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{int arr[] = {1,2,3,4,5,6,7,8,9};int (*p)[9] = arr;//(*p)得到arr的地址 [i]表示调用arr里的第几个元素for (int i = 0;i < 9;i++){printf("%d ", (*p)[i]);}
}
但这种写法似乎更加复杂了 并没有什么优势
但其实我们并不会在这种情况使用数组指针 下面让我们继续深入学习
三. 二维数组传参
3.1 二维数组的本质
二维数组是 “数组的数组”,在内存中仍然是连续存储的线性结构。例如:
int arr[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}
};
内存布局:
[1][2][3][4] [5][6][7][8] [9][10][11][12]
- 每行
arr[i]
是一个一维数组,类型是int[4]
arr
本身是 “指向int[4]
的指针”(即int (*)[4]
)
3.2 访问方式与地址计算
arr[i][j]
的地址:&arr[0][0] + i * 4 + j
(假设int
占4字节,4是列数)- 行指针
arr[i]
:等价于*(arr + i)
- 元素
arr[i][j]
:等价于*(*(arr + i) + j)
3.3 二维数组的传参方式
(1) 标准方式(必须指定列数)
void print(int arr[][4], int rows){for(int i=0; i<rows; i++){for(int j=0; j<4; j++){printf("%d ", arr[i][j]);}printf("\n");}
}
本质:arr[][4]
会被编译器转换为 int (*)[4]
(2) 数组指针方式
void print(int (*arr)[4], int rows) {// 与上述代码完全等价
}
关键点:
arr+1
会跳过 16字节(4个int
)- 必须指定列数,否则无法计算步长
(3) 错误方式
void print(int **arr, int rows, int cols) { // 错误!静态二维数组不是二级指针
}
下面让我们具体运行来观察一下二维数组传参
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
text(int(*arr)[5], int r,int c)
{for (int i = 0;i < r;i++){for (int j = 0;j< c;j++){printf("%d ", *(*(arr + i)+j));}}
}
int main()
{int arr[3][5] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};text(arr, 3, 5);return 0;
}
注意:
*(arr+i) = arr[i] 因为二维数组的数组名表示第一行的地址 +i表示跳过 i 行 并且*(*(arr+i)+j) = arr[ i ][ j ]
二维数组传参本质上也是传递了地址 传递的是第一行这个一维数组的地址
3.4 深入解析 *(*(arr+i)+j)
与 arr[i][j]
的等价性
1. 关键概念拆解
表达式 | 类型 | 含义 |
---|---|---|
arr | int (*)[3] | 指向第一行({1,2,3} )的指针 |
arr + i | int (*)[3] | 指向第i行的指针 |
*(arr + i) | int * | 第i行的首元素地址(退化成一维) |
*(arr+i) + j | int * | 第i行第j个元素的地址 |
*(*(arr+i)+j) | int | 第i行第j个元素的值 |
2. 与下标访问的对应关系
arr[i][j] ≡ *(*(arr + i) + j)
编译器实际处理:
所有 arr[i][j]
最终都会被转换为指针运算形式。
3. 为什么需要列数?
arr + i
的步长取决于列数(sizeof(int[N])
)- 若未指定列数(如
int arr[][]
),编译器无法计算arr + i
的偏移量
4. 典型考题示例
题目:以下代码输出什么?
int arr[2][3] = {{1,2,3}, {4,5,6}};
printf("%d\n", *(*(arr + 1) + 2));
答案:6
解析:
*(arr + 1)
指向第二行 {4,5,6}
,*(arr + 1) + 2
指向 6
,解引用后得到值 6
。
掌握这个核心等价关系,就能彻底理解二维数组的指针运算! 🎯
四. 函数指针变量
4.1 函数指针变量的创建
让我们回忆一下之前的指针内容
字符指针: 存放的是字符的地址 指向的就是字符变量
整型指针: 存放的是整型的地址 指向的是整行变量
数组指针: 存放的是数组的地址 指向的是数组
函数指针: 存放的是函数的地址 指向的是函数
那函数的地址怎么得到的呢?
我们知道数组的地址是通过&+数组名得到的 那函数的地址是通过&+函数名得到的吗?
答案是 是的 函数的地址就是通过&+函数名获得
可以看到 地区打印了函数的地址
那函数和数组一样吗? 数组名代表了数组首元素的地址 那函数名代表什么呢? 让我们来试试
可以看到 打印出来的一模一样 那函数名是首函数的地址吗? 显然没有这个说法
他们俩所得到的都是函数的地址 并没有什么区别
那什么是函数指针变量呢?
函数指针变量是一个指向函数的指针,它存储了函数的地址,可以通过该指针间接调用函数。函数指针的类型由函数的返回类型和参数列表决定。
1. 函数指针的声明
函数指针的声明语法如下:
返回类型 (*指针变量名)(参数类型1, 参数类型2, ...);
示例:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{int(*pf)(int,int) = &Add;//pf就是函数指针变量return 0;
}
那存放下面这个函数的函数指针变量该如何写呢?
int* text(int n, char* p)
{}
通过类比 不难写出
int* (*pf)(int, char*)=&text;//pf就是函数指针变量
4.2 函数指针变量的使用
我们知道通过解引用操作符 可以通过地址来找到存放的变量 那解引用函数指针变量是否就可以使用函数了呢? 答案是 是的 例子如下
可以看到 我通过*pf 实现了对函数的调用 并且传入参数( 3 , 5 )用r来接受 因为函数返回值是int类型 所以r也是int类型
其次 我们知道我们可以通过函数名来调用函数 即Add( 3 , 5 ) 那函数指针变量存放的又是函数名
那我们是否可以直接通过函数指针变量来使用函数呢? 不妨让我们试试
可以看到 无论是函数名调用 还是函数指针变量调用 还是解引用函数指针变量调用 都可以实现对函数的使用
4.3 两段"有趣"代码的讲解
那让我们来思考思考下面这段代码的含义是什么呢?
(*(void(*)())0)();
相信大家看了之后都会感觉 浑身不自在吧 现在让我们一起来解读一下这段代码
4.3.1 解析 (*(void(*)())0)();
这个表达式看起来复杂,但它实际上是一个 函数指针强制转换 + 调用 的典型例子。我们可以一步步拆解它的含义。
1. 表达式拆解
(*(void(*)())0)();
可以分解为:
(void(*)())0
:将0
强制转换为一个 函数指针。*(void(*)())0
:解引用 这个函数指针,得到函数本身。(*(void(*)())0)();
:调用 这个函数。
2. 详细分析
(1) void(*)()
是什么?
void(*)()
是一个 函数指针类型,表示:- 返回类型:
void
(无返回值) - 参数列表:
()
(无参数)
- 返回类型:
- 所以,
void(*)()
是一个 指向无参无返回值函数的指针。
(2) (void(*)())0
:将 0
强制转换为函数指针
0
是一个整数,代表 内存地址0x0
(NULL 指针)。(void(*)())0
表示 把0
强制转换为一个函数指针,即:void (*func_ptr)() = (void(*)())0; // 现在 func_ptr 指向地址 0
(3) *(void(*)())0
:解引用函数指针
*(void(*)())0
相当于:void (*func_ptr)() = (void(*)())0; *func_ptr; // 解引用,得到函数本身
- 在 C 语言中,函数指针解引用后仍然是函数,所以
*func_ptr
和func_ptr
是等价的(见上一节分析)。
(4) (*(void(*)())0)();
:调用这个函数
- 最终,
(*(void(*)())0)();
相当于:void (*func_ptr)() = (void(*)())0; (*func_ptr)(); // 调用地址 0 处的函数
- 或者更简单的写法(因为
func_ptr()
和(*func_ptr)()
等价):((void(*)())0)(); // 直接调用
4.3.2 解析void(* signal(int, void(*)(int)))(int);
这个声明 void(* signal(int, void(*)(int)))(int);
是一个函数声明,它定义了一个名为 signal
的函数。为了理解这个声明,我们可以逐步解析它:
-
最内层部分
void(*)(int)
:-
这是一个函数指针类型,指向一个接受
int
参数并返回void
的函数。 -
例如,
void handler(int sig);
这样的函数可以匹配这个指针类型。
-
-
中间部分
signal(int, void(*)(int))
:-
signal
是一个函数,它接受两个参数:-
第一个参数是
int
类型。 -
第二个参数是
void(*)(int)
类型(即上述的函数指针)。
-
- 因此,
signal
的函数原型可以理解为:void (*signal(int sig, void (*handler)(int)))(int);
-
-
最外层部分
void(* ... )(int)
:-
signal
函数的返回值也是一个函数指针,类型为void(*)(int)
。 -
也就是说,
signal
函数返回一个指向“接受int
参数并返回void
的函数”的指针。
-
简化理解:
signal
是一个函数,它接受一个int
和一个函数指针,并返回一个同类型的函数指针。
总结:
void(* signal(int, void(*)(int)))(int);
声明了一个函数 signal
,它:
- 接受两个参数:
int
和void(*)(int)
(函数指针)。 - 返回一个
void(*)(int)
类型的函数指针。
这种写法在 C 语言中很常见,尤其是在处理回调函数或函数指针时。
4.4 typedef关键字讲解
typedef
关键字解释
typedef
是 C/C++ 中的一个关键字,用于为现有的数据类型(包括基本类型、结构体、联合体、枚举、函数指针等)定义一个新的别名,使代码更易读、更简洁。
4.4.1 基本语法
typedef <原类型> <新别名>;
<原类型>
:可以是int
、float
、char
、struct
、union
、enum
或函数指针等。<新别名>
:你给这个类型取的新名字。
4.4.2 常见用法
(1) 为基本类型定义别名
typedef unsigned int uint; // 定义 uint 代替 unsigned int
typedef float real; // 定义 real 代替 floatuint age = 25; // 等同于 unsigned int age = 25;
real weight = 65.5f; // 等同于 float weight = 65.5f;
(2) 为结构体定义别名
传统写法(需要 struct
关键字):
struct Point {int x;int y;
};struct Point p1; // 必须写 struct Point
使用 typedef
简化:
typedef struct {int x;int y;
} Point; // 定义 Point 代替 struct { ... }Point p1; // 直接使用 Point,不需要写 struct
(3) 为指针类型定义别名
typedef int* IntPtr; // IntPtr 是 int* 的别名int a = 10;
IntPtr p = &a; // 等同于 int* p = &a;
(4) 为函数指针定义别名
原始写法(复杂):
void (*funcPtr)(int); // funcPtr 是一个指向 void(int) 函数的指针
使用 typedef
简化:
typedef void (*FuncPtr)(int); // FuncPtr 是 void(*)(int) 的别名void foo(int x) { printf("%d\n", x); }FuncPtr fp = foo; // 等同于 void (*fp)(int) = foo;
fp(10); // 调用 foo(10)
4.4.3 结合 typedef
解析 signal
函数
原声明:
void (*signal(int, void(*)(int)))(int);
使用 typedef
简化:
typedef void (*SignalHandler)(int); // 定义 SignalHandler 代替 void(*)(int)SignalHandler signal(int sig, SignalHandler handler); // 更清晰的声明
SignalHandler
是一个函数指针类型,指向void(int)
函数。signal
是一个函数,接受int
和SignalHandler
,并返回SignalHandler
。
4.4.4 typedef
的优点
- 提高可读性:复杂的类型(如函数指针)可以用更直观的名字表示。
- 减少重复代码:避免反复写冗长的类型声明。
- 便于维护:修改类型时只需改
typedef
定义,而不需要修改所有使用的地方。
4.4.5 总结
用途 | 示例 |
---|---|
基本类型别名 | typedef int Int32; |
结构体别名 | typedef struct { ... } Point; |
指针别名 | typedef int* IntPtr; |
函数指针别名 | typedef void (*FuncPtr)(int); |
typedef
是 C/C++ 中非常重要的关键字,能显著提升代码的可读性和可维护性,尤其是在处理复杂类型(如函数指针)时非常有用。
五. 函数指针数组
首先我们要明白什么是函数指针数组
5.1 基本概念
1. 函数指针
函数指针是指向函数的指针变量。声明一个函数指针需要指定它指向的函数的返回类型和参数类型。
// 函数原型
int add(int a, int b);
int subtract(int a, int b);// 函数指针声明
int (*funcPtr)(int, int);// 指向add函数
funcPtr = add;
2. 函数指针数组
函数指针数组是存储多个函数指针的数组。
// 声明一个包含两个函数指针的数组
int (*funcArray[2])(int, int);// 初始化数组
funcArray[0] = add;
funcArray[1] = subtract;
5.2 详细用法
1. 声明函数指针数组
// 返回类型 (*数组名[数组大小])(参数列表)
double (*operations[4])(double, double);
2. 初始化函数指针数组
double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double div(double a, double b) { return a / b; }// 初始化数组
operations[0] = add;
operations[1] = sub;
operations[2] = mul;
operations[3] = div;
3. 使用函数指针数组
double result;
int choice = 2; // 假设用户选择乘法
double x = 5.0, y = 3.0;// 通过索引调用函数
result = operations[choice](x, y);
printf("结果: %.2f\n", result); // 输出: 15.00
5.3 实际应用示例
1. 计算器实现
#include <stdio.h>// 定义运算函数
double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double div(double a, double b) { return a / b; }int main() {// 声明并初始化函数指针数组double (*ops[4])(double, double) = {add, sub, mul, div};int choice;double x, y;printf("选择运算:\n0. 加\n1. 减\n2. 乘\n3. 除\n");scanf("%d", &choice);printf("输入两个数字: ");scanf("%lf %lf", &x, &y);// 调用选中的函数double result = ops[choice](x, y);printf("结果: %.2f\n", result);return 0;
}
运行结果如下:
六. 转移表
转移表(Jump Table)
转移表(也称为跳转表)是一种使用函数指针数组来实现多路分支的技术,它比传统的switch-case
语句更高效、更灵活。
基本概念
转移表本质上是一个函数指针数组,通过数组索引来选择和调用不同的函数,避免了冗长的条件判断。
转移表 vs switch-case
switch-case实现
void handleCommand(int cmd) {switch(cmd) {case 0: cmd0(); break;case 1: cmd1(); break;case 2: cmd2(); break;// ...default: defaultHandler();}
}
转移表实现
// 定义命令处理函数
void cmd0() { /* ... */ }
void cmd1() { /* ... */ }
void cmd2() { /* ... */ }
void defaultHandler() { /* ... */ }// 创建转移表
typedef void (*CommandHandler)(void);
CommandHandler jumpTable[] = {cmd0, cmd1, cmd2};void handleCommand(int cmd) {if (cmd >= 0 && cmd < sizeof(jumpTable)/sizeof(jumpTable[0])) {jumpTable[cmd]();} else {defaultHandler();}
}
转移表优势
-
效率更高:直接通过索引访问,时间复杂度O(1),而switch-case可能需要多次比较
-
代码更简洁:特别是当分支很多时
-
更易维护:添加新功能只需扩展数组,不需要修改逻辑结构
-
动态性:可以在运行时修改函数指针
实际应用示例
1. 简单计算器
#include <stdio.h>// 运算函数
double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double div(double a, double b) { return a / b; }// 定义转移表
typedef double (*Operation)(double, double);
Operation operations[] = {add, sub, mul, div};int main() {int choice;double x, y;printf("选择运算(0-3): ");scanf("%d", &choice);printf("输入两个数字: ");scanf("%lf %lf", &x, &y);if (choice >= 0 && choice < sizeof(operations)/sizeof(operations[0])) {double result = operations[choice](x, y);printf("结果: %.2f\n", result);} else {printf("无效选择\n");}return 0;
}
以上就是本篇内容 希望能对你有所帮助