1. 字符指针变量
出自《剑指offer》书中
c
#inckude <stdio.h>
int main()
{
char str1[] = "hello bit."; //数组名表示首元素地址
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("strl and str2 are same\n");
else
printf("strl 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;
}
这段代码的核心目的是演示和比较两种不同的字符串存储方式(字符数组 vs. 字符指针指向字符串常量)在内存地址上的差异。
代码分析:
-
定义字符串变量:
char str1[] = "hello bit.";: 定义了一个字符数组str1并用字符串字面量"hello bit."初始化它。数组str1在栈内存中拥有自己的存储空间,用来存放字符串"hello bit."的副本(包括结尾的空字符\0)。char str2[] = "hello bit.";: 类似地,定义了另一个独立的字符数组str2,也用相同的字符串字面量初始化。它同样在栈内存中拥有自己独立的空间来存放字符串的副本。const char* str3 = "hello bit.";: 定义了一个指向字符常量的指针str3。它被初始化为指向字符串字面量"hello bit."。关键点 :字符串字面量(如"hello bit.")通常存储在程序的只读数据区(.rodata段)。str3存储的是这个字符串常量在内存中的地址。const char* str4 = "hello bit.";: 类似地,定义了另一个指针str4,也被初始化为指向相同的字符串字面量"hello bit."。使用了const是因为字符串字面量的内容是不可修改的。
-
比较字符串地址:
if (str1 == str2) ... else ...: 这里比较的是两个数组名str1和str2。在C语言中,数组名在大多数表达式中会退化成指向其首元素的指针。因此,这行代码实际上是在比较str1数组的首元素地址和str2数组的首元素地址。由于str1和str2是两个独立的数组,它们拥有不同的内存地址,所以这个比较结果是false,输出"str1 and str2 are not same\n"。- 重点:这比较的是存储字符串副本的数组的地址是否相同,而不是字符串的内容是否相同。
if (str3 == str4) ... else ...: 这里比较的是两个指针变量str3和str4的值,也就是它们所指向的内存地址。编译器(通常)会将相同的字符串字面量优化存储在同一个内存位置(只读数据区的同一个地址)。因此,str3和str4都被初始化为指向这个相同的地址,所以这个比较结果是true,输出"str3 and str4 are same\n"。- 重点:这比较的是指向同一个字符串常量的指针值(地址)是否相同。
运行结果解释:
c
strl and str2 are not same
str3 and str4 are same
- 第一行输出 (
str1 and str2 are not same): 因为str1和str2是两个独立的数组变量,各自在栈内存中分配了空间来存储字符串"hello bit."的副本。比较str1 == str2是在比较这两个不同数组的起始地址,它们必然不同。 - 第二行输出 (
str3 and str4 are same): 因为str3和str4都是指针变量,它们被赋予的值是字符串常量"hello bit."在内存中的地址。编译器优化使得相同的字符串字面量共享同一个存储位置(在只读数据区),所以str3和str4都指向这个相同的地址,因此str3 == str4比较的是相同的指针值。
总结与重点:
- 字符数组 (
char str[]) : 在栈内存中分配空间,存储字符串的副本。即使初始化内容相同,每个数组都有自己独立的地址。比较数组名是比较地址,不是内容。 - 字符指针指向字符串常量 (
const char* str) : 指针变量本身存储在栈内存,但它存储的是指向只读数据区中字符串常量的地址。相同的字符串字面量通常共享同一个内存地址(由编译器优化)。比较指针是比较它们存储的地址值。 ==运算符 : 当用于数组名或指针时,比较的是内存地址 ,而不是字符串的内容 。要比较字符串内容是否相同,必须使用标准库函数strcmp()。const关键字 : 在声明指向字符串常量的指针时使用const(如const char*) 是一个良好的习惯,因为它表明你承诺不通过这个指针去修改它所指向的(不可变的)字符串常量内容,有助于避免运行时错误(尝试修改只读内存)和提高代码可读性。
这段代码清晰地展示了C语言中字符串存储的两种方式及其在内存地址比较上的不同行为。
2. 数组指针变量
2.1 数组指针变量是什么?
数组指针变量是指向整个数组的指针。它的声明语法需要特别注意运算符的优先级。例如:
c
int (*p)[10];
- 解释 :在这里,
p是一个指针变量。由于[]的优先级高于*,必须使用括号()来确保p先与*结合,表示p是一个指针。然后,[10]表示这个指针指向一个大小为10的整型数组。因此,p是一个指向整型数组的指针,称为数组指针。 - 重点 :
[]的优先级高于*,如果不加括号,如int *p[10];,这会被解释为指针数组(一个包含10个整型指针的数组),而不是数组指针。括号是必需的,以避免歧义。
2.2 数组指针变量怎么初始化?
初始化数组指针变量时,需要将它指向一个实际的数组。关键点是:
- 初始化步骤 :
- 定义一个目标数组,例如一个大小为10的整型数组。
- 使用取地址运算符
&获取数组的地址(因为数组指针指向整个数组的地址)。 - 将指针变量赋值给这个地址。
- 重点 :
- 数组指针存储数组的首地址,但不同于普通指针(指向单个元素),它指向整个数组结构。
- 初始化时,必须确保指针的类型与数组的类型和大小匹配,否则会导致类型不匹配错误。
- 使用
&运算符是必要的,因为数组名本身在大多数情况下会退化为指向首元素的指针,但这里我们需要数组的地址。
下面是一个C语言代码示例,详细演示数组指针的初始化和使用:
c
#include <stdio.h>
int main() {
// 步骤1: 定义一个大小为10的整型数组
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 步骤2: 声明并初始化数组指针变量
int (*p)[10]; // p是指向大小为10的整型数组的指针
p = &arr; // 使用&获取arr的地址,赋值给p
// 验证初始化:通过指针访问数组元素
printf("第一个元素: %d\n", (*p)[0]); // 输出: 第一个元素: 1
printf("第五个元素: %d\n", (*p)[4]); // 输出: 第五个元素: 5
// 重点: 使用指针遍历数组
for (int i = 0; i < 10; i++) {
printf("元素 %d: %d\n", i, (*p)[i]);
}
return 0;
}
代码解释:
- 在代码中,
int (*p)[10];声明了p为数组指针。 p = &arr;将p初始化为指向数组arr的地址。- 访问元素时,使用
(*p)[index]语法,因为*p解引用后得到数组本身,然后可以用索引访问元素。 - 错误示例 :如果错误地写成
p = arr;(不加&),编译器可能会警告或报错,因为arr退化为指向首元素的指针(类型为int*),而p的类型是int (*)[10],类型不匹配。
3. ⼆维数组传参的本质
二维数组传参的本质详解
在C语言中,二维数组本质上是一个"数组的数组",它在内存中以连续的方式存储。例如,一个二维数组 int arr[3][5] 表示有3行(每个行是一个一维数组),每行有5个整数元素。在内存中,元素按行优先顺序排列:先存储第一行的5个元素,然后是第二行,最后是第三行。
3.1 二维数组传参的核心机制
当我们将二维数组作为参数传递给函数时,实际上传递的是数组名(即数组首元素的地址)。对于二维数组,数组名代表第一行的地址,而不是整个数组的副本。这意味着:
- 传递的是指针:函数参数接收的是一个指针,指向二维数组的第一行。
- 参数类型必须匹配 :在函数声明中,参数类型需指定为指向数组的指针,例如
int (*arr)[5],其中5表示每行的列数(即一维数组的大小)。 - 避免数组复制:这种方式高效,因为它只传递地址,而不是复制整个数组数据。
在您的代码示例中:
c
void print(int (*arr)[5], int a, int b) {
// ...
}
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;
}
arr在main函数中是二维数组名,它代表第一行{1,2,3,4,5}的地址,类型为int (*)[5]。- 在
print函数调用时,arr被传递为指针,函数参数int (*arr)[5]正确匹配了这个类型。 - 参数
a和b分别表示行数和列数(这里是3和5),用于控制遍历范围。
3.2 访问元素的三种方式解析
在 print 函数内部,您展示了三种访问二维数组元素的方法:
c
printf("%d ", arr[i][j]); // 方式1:下标访问
printf("%d ", *(*(arr + i) + j)); // 方式2:指针算术(双重解引用)
printf("%d ", *(arr[i] + j)); // 方式3:指针算术(单重解引用)
这些方法本质上是等价的,都基于指针算术:
arr[i][j]:这是最直观的下标访问。arr[i]获取第i行的指针(类型为int *),然后[j]访问该行的第j个元素。*(*(arr + i) + j):arr + i计算第i行的地址(指针偏移i行)。*(arr + i)解引用得到第i行的指针(类型int *)。*(arr + i) + j计算第i行第j个元素的地址。*(*(arr + i) + j)解引用得到元素值。
*(arr[i] + j):arr[i]等价于*(arr + i),得到第i行的指针。arr[i] + j计算第i行第j个元素的地址。*(arr[i] + j)解引用得到元素值。
所有方法都依赖于二维数组的连续内存布局:arr 指向第一行,arr + i 跳过 i 行(每行大小为 sizeof(int) * 5),然后通过列索引 j 访问具体元素。
代码输出解释
运行代码后,输出为:
c
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
这是因为:
print函数遍历从第0行到第2行(a=3),每行从第0列到第4列(b=5)。- 使用指针算术正确访问每个元素,输出数组内容。
重点总结
- 传参本质 :二维数组传参传递的是第一行的地址,参数类型必须为指向固定大小数组的指针(如
int (*arr)[5])。 - 高效性:只传递指针,避免数据复制,节省内存和时间。
- 访问灵活性:通过指针算术,下标访问和指针解引用可以互换使用。
- 维度匹配 :函数参数中列数(如
5)必须与数组定义一致,否则会导致未定义行为。
4. 函数指针变量
函数指针变量解释
函数指针变量在C语言中是一种特殊的指针类型,它可以指向函数,而不是数据。这允许动态调用函数,实现回调机制、函数表等高级功能。
4.1 函数指针变量的创建
函数指针变量的创建涉及声明和定义。声明时需指定指针指向的函数类型,包括返回类型和参数列表。语法格式为:返回类型 (*指针变量名)(参数类型列表)。
重点:
- 声明语法必须匹配函数签名(返回类型和参数)。
- 指针变量名需用括号括起,避免与函数声明混淆。
C语言代码示例:
c
#include <stdio.h>
// 定义一个函数,用于示例
int add(int a, int b) {
return a + b;
}
int main() {
// 创建函数指针变量,指向add函数
int (*func_ptr)(int, int) = add; // 声明并初始化
// 验证指针创建
printf("函数指针创建成功,指向add函数\n");
return 0;
}
在此示例中,func_ptr是一个函数指针,它指向一个返回int类型、并接受两个int参数的函数。我们将其初始化为add函数的地址。
4.2 函数指针变量的使用
使用函数指针变量可以直接调用它指向的函数,语法类似于普通函数调用:指针变量名(参数列表)。这提供了灵活性,允许运行时动态选择函数。
重点:
- 调用时需确保参数类型和数量匹配函数签名。
- 函数指针常用于回调函数或事件处理。
C语言代码示例:
c
#include <stdio.h>
// 定义两个函数
int multiply(int a, int b) {
return a * b;
}
int main() {
// 创建函数指针变量
int (*func_ptr)(int, int);
// 赋值给指针(选择不同函数)
func_ptr = multiply; // 指向multiply函数
// 使用指针调用函数
int result = func_ptr(5, 3); // 调用multiply(5, 3)
printf("结果: %d\n", result); // 输出: 15
return 0;
}
在此示例中,我们创建func_ptr,先指向multiply函数,然后通过func_ptr(5, 3)调用它,实现动态函数执行。
4.3 两段有趣的代码
为了展示函数指针的实用性,这里提供两个有趣的应用:回调函数和函数指针数组。这些代码演示了函数指针在真实场景中的灵活使用。
代码1:回调函数示例
回调函数允许将函数作为参数传递,实现事件驱动或自定义行为。
c
#include <stdio.h>
// 回调函数类型定义
void greet() {
printf("你好!\n");
}
// 接受函数指针作为参数的函数
void execute(void (*callback)()) {
callback(); // 调用传入的函数
}
int main() {
// 传递greet函数作为回调
execute(greet); // 输出: 你好!
return 0;
}
重点 :这里execute函数接受一个函数指针参数callback,并在内部调用它,实现通用执行逻辑。
代码2:函数指针数组
函数指针数组可用于实现状态机或命令表,根据索引调用不同函数。
c
#include <stdio.h>
// 定义几个函数
void task1() { printf("执行任务1\n"); }
void task2() { printf("执行任务2\n"); }
int main() {
// 创建函数指针数组
void (*tasks[2])() = {task1, task2};
// 通过索引调用函数
for (int i = 0; i < 2; i++) {
tasks[i](); // 依次调用task1和task2
}
return 0;
}
重点 :数组tasks存储函数指针,通过循环动态调用,展示了函数指针在批量操作中的应用。
4.3.1 typedef关键字
typedef关键字用于创建类型别名,简化函数指针的声明语法。它可以定义函数指针类型,使代码更易读和维护。
重点:
typedef定义一个新类型名,代表特定函数签名。- 使用时直接声明变量,避免复杂的指针语法。
C语言代码示例:
c
#include <stdio.h>
// 使用typedef定义函数指针类型
typedef int (*MathFunc)(int, int);
// 定义函数
int subtract(int a, int b) {
return a - b;
}
int main() {
// 使用typedef类型声明函数指针变量
MathFunc func_ptr = subtract;
// 调用函数
int result = func_ptr(10, 4);
printf("结果: %d\n", result); // 输出: 6
return 0;
}
在此示例中,typedef定义了MathFunc类型,它代表一个返回int并接受两个int参数的函数指针类型。声明func_ptr时直接使用MathFunc,简化了代码。
总结
函数指针变量是C语言中强大的工具,用于实现动态函数调用、回调机制和复杂数据结构。创建时需注意声明语法,使用时确保参数匹配,typedef可简化代码。
- 函数指针数组
那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组
5. 函数指针数组
函数指针数组与转移表的详细解释
在C语言中,函数指针数组和转移表是高级编程技巧,用于实现灵活的函数调用机制。
5.1 函数指针数组
函数指针数组是一个数组,其元素都是指向函数的指针。这意味着数组中的每个元素存储的是函数的地址,而不是普通数据。通过这种方式,我们可以将多个函数组织在一个数组中,便于统一管理和调用。
重点:
- 定义方式 :函数指针数组的声明需要指定函数的签名(即参数类型和返回类型)。例如,如果所有函数都接受一个
int参数并返回int,则可以声明为int (*func_ptr_array[size])(int);,其中size是数组大小。 - 用途:它允许将函数作为"数据"处理,常用于实现回调机制、插件系统或动态函数调度。
- 优势:提高了代码的模块化,便于扩展和维护,因为添加新函数只需更新数组即可。
6. 转移表
转移表(也称为跳转表或调度表)是函数指针数组的一个典型应用。它通过一个数组来"转移"控制流到不同的函数,基于输入值(如索引或枚举)来选择调用哪个函数。这类似于查表操作,能简化复杂的条件分支(如switch-case语句)。
重点:
- 工作原理 :转移表使用一个索引(如整数)来访问函数指针数组,并直接调用对应的函数。这避免了冗长的
if-else或switch结构,使代码更简洁高效。 - 应用场景:常用于状态机、命令处理器、菜单系统或事件驱动编程,其中输入值映射到特定操作。
- 优势:提升代码可读性和性能,因为函数调用是直接的指针解引用,减少了条件判断的开销。
C语言代码示例
下面是一个简单的C语言程序,演示如何定义函数指针数组并实现转移表。假设我们有三个函数,分别执行加、减、乘操作,用户输入一个索引来选择操作。
c
#include <stdio.h>
// 定义三个简单的数学函数
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int main() {
// 步骤1: 声明函数指针数组
// 数组元素是指向函数的指针,函数签名: int (*)(int, int)
int (*func_ptr_array[3])(int, int) = {add, subtract, multiply};
// 步骤2: 定义转移表索引和输入值
int operation_index;
int x = 10, y = 5; // 示例输入值
printf("选择操作 (0: 加, 1: 减, 2: 乘): ");
scanf("%d", &operation_index);
// 步骤3: 使用转移表调用函数
// 检查索引有效性,然后通过数组索引解引用函数指针
if (operation_index >= 0 && operation_index < 3) {
int result = func_ptr_array[operation_index](x, y); // 转移表调用
printf("结果: %d\n", result);
} else {
printf("无效索引\n");
}
return 0;
}
代码解释:
- 函数指针数组声明 :
int (*func_ptr_array[3])(int, int)定义了一个大小为3的数组,每个元素指向一个接受两个int参数并返回int的函数。初始化时,我们将add,subtract,multiply三个函数的地址赋给数组。 - 转移表实现 :用户输入
operation_index(如0,1,2),程序直接使用这个索引访问数组,并调用对应的函数。例如,如果输入0,则调用add(x,y)。 - 重点突出 :通过函数指针数组,转移表实现了高效的函数调度,避免了使用
switch-case(如switch(operation_index) { case 0: add(...); ... }),使代码更简洁、易扩展。
实现一个计算机
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("1.add 2.sub\n");
printf("3.mul 4.div\n");
printf("0.eixt\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int r = 0;
do
{
menu();
printf("请输入:");
scanf("%d", &input);
int(*pArr[])(int, int) = {NULL,Add,Sub,Mul,Div };
if (input >= 1 && input <= 4)
{
printf("请输入2个数字:");
scanf("%d %d", &x, &y);
r = pArr[input](x, y);
printf("结果是:%d\n", r);
}
else if(input == 0)
{
printf("退出计算机\n");
}
else
{
printf("请输入0------4\n");
}
} while (input);
return 0;
}
-
运算函数定义 (
Add,Sub,Mul,Div):- 定义了四个函数,分别执行加法、减法、乘法和除法运算。
- 每个函数都接受两个整数参数
x和y,并返回一个整数结果。 Div函数没有处理除数为零的情况(在实际代码中需要增加判断)。
-
菜单显示函数 (
menu):- 这个函数负责打印出用户可选择的选项菜单。
-
主函数 (
main):- 变量声明 :
input:存储用户选择的菜单项。x,y:存储用户输入的两个操作数。r:存储运算结果。
- 函数指针数组 (
int(*pArr[])(int, int) = {NULL,Add,Sub,Mul,Div};):- 这是这段代码的核心技巧。
- 它定义了一个数组
pArr,数组的元素都是指向函数的指针。 - 这些函数必须符合特定的格式:接受两个
int参数并返回一个int值。 - 数组被初始化为:
{NULL, Add, Sub, Mul, Div}。 - 下标 0 的位置是
NULL(空指针),不使用。 - 下标 1 对应
Add函数(加法)。 - 下标 2 对应
Sub函数(减法)。 - 下标 3 对应
Mul函数(乘法)。 - 下标 4 对应
Div函数(除法)。 - 这样,用户输入的
1可以直接对应数组下标1去调用加法函数,非常巧妙。
- 主循环 (
do ... while (input);):- 使用
do-while循环确保程序至少运行一次。 - 循环条件为
input != 0。当用户输入0时,循环结束。
- 使用
- 循环体内部 :
- 调用
menu()显示菜单。 - 提示用户输入选择 (
scanf("%d", &input))。 - 判断用户输入 :
- 如果
input在 1 到 4 之间 :- 提示用户输入两个数字 (
scanf("%d %d", &x, &y))。 - 关键调用 :
r = pArr[input](x, y); - 这行代码根据用户输入的
input值,从pArr数组中取出对应下标(input)的函数指针。 - 然后通过该指针调用相应的运算函数 (
Add,Sub,Mul,Div),并将x和y作为参数传递进去。 - 函数的返回值被赋给
r。 - 打印出结果 (
printf("结果是:%d\n", r);)。
- 提示用户输入两个数字 (
- 如果
input等于 0 :- 打印退出信息 (
printf("退出计算机\n");)。 - 这将导致循环条件
input != 0不成立,从而退出循环,结束程序。
- 打印退出信息 (
- 如果
input是其他值 :- 提示用户输入范围错误 (
printf("请输入0------4\n");)。
- 提示用户输入范围错误 (
- 如果
- 调用
- 变量声明 :
总结: 这段代码利用函数指针数组 pArr 将用户输入的菜单编号 (input) 直接映射到对应的运算函数 (Add, Sub, Mul, Div)。通过 pArr[input](x, y) 这一行代码,避免了使用冗长的 switch-case 或 if-else 语句来判断该调用哪个函数,使代码更加简洁高效。