深⼊理解指针(4)

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. 字符指针指向字符串常量)在内存地址上的差异

代码分析:

  1. 定义字符串变量:

    • 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 是因为字符串字面量的内容是不可修改的。
  2. 比较字符串地址:

    • if (str1 == str2) ... else ...: 这里比较的是两个数组名 str1str2。在C语言中,数组名在大多数表达式中会退化成指向其首元素的指针。因此,这行代码实际上是在比较 str1 数组的首元素地址和 str2 数组的首元素地址。由于 str1str2 是两个独立的数组,它们拥有不同的内存地址,所以这个比较结果是 false,输出 "str1 and str2 are not same\n"
    • 重点:这比较的是存储字符串副本的数组的地址是否相同,而不是字符串的内容是否相同。
    • if (str3 == str4) ... else ...: 这里比较的是两个指针变量 str3str4 的值,也就是它们所指向的内存地址。编译器(通常)会将相同的字符串字面量优化存储在同一个内存位置(只读数据区的同一个地址)。因此,str3str4 都被初始化为指向这个相同的地址,所以这个比较结果是 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): 因为 str1str2 是两个独立的数组变量,各自在栈内存中分配了空间来存储字符串 "hello bit." 的副本。比较 str1 == str2 是在比较这两个不同数组的起始地址,它们必然不同。
  • 第二行输出 (str3 and str4 are same): 因为 str3str4 都是指针变量,它们被赋予的值是字符串常量 "hello bit." 在内存中的地址。编译器优化使得相同的字符串字面量共享同一个存储位置(在只读数据区),所以 str3str4 都指向这个相同的地址,因此 str3 == str4 比较的是相同的指针值。

总结与重点:

  1. 字符数组 (char str[]) : 在栈内存中分配空间,存储字符串的副本。即使初始化内容相同,每个数组都有自己独立的地址。比较数组名是比较地址,不是内容。
  2. 字符指针指向字符串常量 (const char* str) : 指针变量本身存储在栈内存,但它存储的是指向只读数据区中字符串常量的地址。相同的字符串字面量通常共享同一个内存地址(由编译器优化)。比较指针是比较它们存储的地址值。
  3. == 运算符 : 当用于数组名或指针时,比较的是内存地址 ,而不是字符串的内容 。要比较字符串内容是否相同,必须使用标准库函数 strcmp()
  4. const 关键字 : 在声明指向字符串常量的指针时使用 const (如 const char*) 是一个良好的习惯,因为它表明你承诺不通过这个指针去修改它所指向的(不可变的)字符串常量内容,有助于避免运行时错误(尝试修改只读内存)和提高代码可读性。

这段代码清晰地展示了C语言中字符串存储的两种方式及其在内存地址比较上的不同行为。


2. 数组指针变量

2.1 数组指针变量是什么?

数组指针变量是指向整个数组的指针。它的声明语法需要特别注意运算符的优先级。例如:

c 复制代码
int (*p)[10];
  • 解释 :在这里,p 是一个指针变量。由于 [] 的优先级高于 *,必须使用括号 () 来确保 p 先与 * 结合,表示 p 是一个指针。然后,[10] 表示这个指针指向一个大小为10的整型数组。因此,p 是一个指向整型数组的指针,称为数组指针。
  • 重点[] 的优先级高于 *,如果不加括号,如 int *p[10];,这会被解释为指针数组(一个包含10个整型指针的数组),而不是数组指针。括号是必需的,以避免歧义。

2.2 数组指针变量怎么初始化?

初始化数组指针变量时,需要将它指向一个实际的数组。关键点是:

  • 初始化步骤
    1. 定义一个目标数组,例如一个大小为10的整型数组。
    2. 使用取地址运算符 & 获取数组的地址(因为数组指针指向整个数组的地址)。
    3. 将指针变量赋值给这个地址。
  • 重点
    • 数组指针存储数组的首地址,但不同于普通指针(指向单个元素),它指向整个数组结构。
    • 初始化时,必须确保指针的类型与数组的类型和大小匹配,否则会导致类型不匹配错误。
    • 使用 & 运算符是必要的,因为数组名本身在大多数情况下会退化为指向首元素的指针,但这里我们需要数组的地址。

下面是一个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;
}
  • arrmain 函数中是二维数组名,它代表第一行 {1,2,3,4,5} 的地址,类型为 int (*)[5]
  • print 函数调用时,arr 被传递为指针,函数参数 int (*arr)[5] 正确匹配了这个类型。
  • 参数 ab 分别表示行数和列数(这里是3和5),用于控制遍历范围。

3.2 访问元素的三种方式解析

print 函数内部,您展示了三种访问二维数组元素的方法:

c 复制代码
printf("%d ", arr[i][j]);         // 方式1:下标访问
printf("%d ", *(*(arr + i) + j)); // 方式2:指针算术(双重解引用)
printf("%d ", *(arr[i] + j));     // 方式3:指针算术(单重解引用)

这些方法本质上是等价的,都基于指针算术:

  1. arr[i][j] :这是最直观的下标访问。arr[i] 获取第 i 行的指针(类型为 int *),然后 [j] 访问该行的第 j 个元素。
  2. *(*(arr + i) + j)
    • arr + i 计算第 i 行的地址(指针偏移 i 行)。
    • *(arr + i) 解引用得到第 i 行的指针(类型 int *)。
    • *(arr + i) + j 计算第 i 行第 j 个元素的地址。
    • *(*(arr + i) + j) 解引用得到元素值。
  3. *(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可简化代码。


  1. 函数指针数组

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

5. 函数指针数组

函数指针数组与转移表的详细解释

在C语言中,函数指针数组和转移表是高级编程技巧,用于实现灵活的函数调用机制。

5.1 函数指针数组

函数指针数组是一个数组,其元素都是指向函数的指针。这意味着数组中的每个元素存储的是函数的地址,而不是普通数据。通过这种方式,我们可以将多个函数组织在一个数组中,便于统一管理和调用。

重点:

  • 定义方式 :函数指针数组的声明需要指定函数的签名(即参数类型和返回类型)。例如,如果所有函数都接受一个int参数并返回int,则可以声明为int (*func_ptr_array[size])(int);,其中size是数组大小。
  • 用途:它允许将函数作为"数据"处理,常用于实现回调机制、插件系统或动态函数调度。
  • 优势:提高了代码的模块化,便于扩展和维护,因为添加新函数只需更新数组即可。

6. 转移表

转移表(也称为跳转表或调度表)是函数指针数组的一个典型应用。它通过一个数组来"转移"控制流到不同的函数,基于输入值(如索引或枚举)来选择调用哪个函数。这类似于查表操作,能简化复杂的条件分支(如switch-case语句)。

重点:

  • 工作原理 :转移表使用一个索引(如整数)来访问函数指针数组,并直接调用对应的函数。这避免了冗长的if-elseswitch结构,使代码更简洁高效。
  • 应用场景:常用于状态机、命令处理器、菜单系统或事件驱动编程,其中输入值映射到特定操作。
  • 优势:提升代码可读性和性能,因为函数调用是直接的指针解引用,减少了条件判断的开销。
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;
}
  1. 运算函数定义 (Add, Sub, Mul, Div):

    • 定义了四个函数,分别执行加法、减法、乘法和除法运算。
    • 每个函数都接受两个整数参数 xy,并返回一个整数结果。
    • Div 函数没有处理除数为零的情况(在实际代码中需要增加判断)。
  2. 菜单显示函数 (menu):

    • 这个函数负责打印出用户可选择的选项菜单。
  3. 主函数 (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),并将 xy 作为参数传递进去。
          • 函数的返回值被赋给 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-caseif-else 语句来判断该调用哪个函数,使代码更加简洁高效。


相关推荐
大大杰哥1 小时前
2025ccpc南昌补题笔记(前六题)
c++·笔记·算法
sheeta19981 小时前
LeetCode 每日一题笔记 日期:2026.05.14 题目:2784. 检查数组是否是好的
笔记·算法·leetcode
孬甭_1 小时前
顺序表详解
c语言·数据结构
Lucky_ldy1 小时前
C语言学习:数据在内存中的存储
c语言·开发语言·学习
AOwhisky1 小时前
Docker 学习笔记:Docker Compose 多容器编排
linux·运维·笔记·学习·docker·容器
qeen871 小时前
【算法笔记】各种常见排序算法详细解析(上)
c语言·数据结构·c++·学习·算法·排序算法
许长安2 小时前
gRPC 数据包传输格式解析:从 Protobuf 到 HTTP/2
c++·经验分享·笔记·http·rpc
YangWeiminPHD2 小时前
金水32051编译器下的AI8051U单片机入门:从点亮LED到“你好,世界,我来了!”
c语言·汇编·51单片机·编译器
问心无愧05132 小时前
ctf show web入门47
前端·笔记