《C 语言指针高级指南:字符、数组、函数指针的进阶攻略》

目录

一. 字符指针变量

二. 数组指针变量

三. 二维数组传参

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");
	else
		printf("str1!=str2\n");

	if (str3 == str4)
		printf("str3=str4\n");
	else
		printf("str3!=str4\n");
	return 0;
}

运行结果如下

可以看出str1与str2不相等 而str3和str4却相等 这是为什么呢? 

原因分析:

  1. str1 和 str2 的比较 (str1 == str2)

    • str1 和 str2 是两个独立的字符数组,分别存储 "Hello world"
    • 数组名在比较时会被转换为指向数组首元素的指针(即 &str1[0] 和 &str2[0])。
    • 由于 str1 和 str2 是两个不同的数组,它们的地址不同,所以 str1 == str2 为 false,输出 str1!=str2
  2. 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");
else
    printf("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. 关键概念拆解

表达式类型含义
arrint (*)[3]指向第一行({1,2,3})的指针
arr + iint (*)[3]指向第i行的指针
*(arr + i)int *第i行的首元素地址(退化成一维)
*(arr+i) + jint *第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)();

可以分解为:

  1. (void(*)())0:将 0 强制转换为一个 函数指针
  2. *(void(*)())0解引用 这个函数指针,得到函数本身。
  3. (*(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 的函数。为了理解这个声明,我们可以逐步解析它:

  1. 最内层部分 void(*)(int):

    • 这是一个函数指针类型,指向一个接受 int 参数并返回 void 的函数。

    • 例如,void handler(int sig); 这样的函数可以匹配这个指针类型。

  2. 中间部分 signal(int, void(*)(int)):

    • signal 是一个函数,它接受两个参数:

      • 第一个参数是 int 类型。

      • 第二个参数是 void(*)(int) 类型(即上述的函数指针)。

    • 因此,signal 的函数原型可以理解为:
      void (*signal(int sig, void (*handler)(int)))(int);
      
  3. 最外层部分 void(* ... )(int):

    • signal 函数的返回值也是一个函数指针,类型为 void(*)(int)

    • 也就是说,signal 函数返回一个指向“接受 int 参数并返回 void 的函数”的指针。

简化理解:

  • signal 是一个函数,它接受一个 int 和一个函数指针,并返回一个同类型的函数指针。

总结:

void(* signal(int, void(*)(int)))(int); 声明了一个函数 signal,它:

  1. 接受两个参数:int 和 void(*)(int)(函数指针)。
  2. 返回一个 void(*)(int) 类型的函数指针。

这种写法在 C 语言中很常见,尤其是在处理回调函数或函数指针时。

4.4 typedef关键字讲解

typedef 关键字解释

typedef 是 C/C++ 中的一个关键字,用于为现有的数据类型(包括基本类型、结构体、联合体、枚举、函数指针等)定义一个新的别名,使代码更易读、更简洁。


4.4.1 基本语法
typedef <原类型> <新别名>;
  • <原类型>:可以是 intfloatcharstructunionenum 或函数指针等。
  • <新别名>:你给这个类型取的新名字。

4.4.2 常见用法

(1) 为基本类型定义别名

typedef unsigned int uint;  // 定义 uint 代替 unsigned int
typedef float real;         // 定义 real 代替 float

uint 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 的优点
  1. 提高可读性:复杂的类型(如函数指针)可以用更直观的名字表示。
  2. 减少重复代码:避免反复写冗长的类型声明。
  3. 便于维护:修改类型时只需改 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();
    }
}

转移表优势

  1. 效率更高:直接通过索引访问,时间复杂度O(1),而switch-case可能需要多次比较

  2. 代码更简洁:特别是当分支很多时

  3. 更易维护:添加新功能只需扩展数组,不需要修改逻辑结构

  4. 动态性:可以在运行时修改函数指针

实际应用示例

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;
}

以上就是本篇内容 希望能对你有所帮助


前期回顾

《指针进阶之旅:解锁 C 语言中指针的更多奥秘》

《初探指针世界:揭开内存管理与编程优化的第一篇章》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿方猛敲c嘎嘎

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值