C语言指针深度剖析(2):从“数组名陷阱”到“二级指针操控”的进阶指南

C语言中的指针是其灵魂,但也让无数人感到困惑。本篇博客将带你深入理解指针学习中的四大核心难点:

  1. 彻底搞懂数名 的两个特殊例外(sizeof&操作),认清数组名的"双重身份"。
  2. 掌握指针访问数组的原理,理解下标访问和指针解引用间的等价关系。
  3. 揭示一维数组传参 时发生的"指针退化"本质,解释sizeof为何在函数内部失效。
  4. 详细解析二级指针的原理与解引用操作,学会间接控制一级指针。

读完本文,你将能清晰掌握指针与数组之间的微妙关系,为后续学习数组指针、函数指针打下坚实基础!

目录

  • 一、数组名的双重身份
    • [1.1 数组名的"通用"身份:数组首元素的地址](#1.1 数组名的“通用”身份:数组首元素的地址)
    • [1.2 数组名的"特殊"身份:不可忽略的两个例外](#1.2 数组名的“特殊”身份:不可忽略的两个例外)
  • 二、使用指针访问数组:更灵活、更强大
    • [2.1 核心等价关系:下标与指针的转换](#2.1 核心等价关系:下标与指针的转换)
    • [2.2 利用指针进行数组遍历](#2.2 利用指针进行数组遍历)
  • 三、一维数组传参的本质:传递的是地址,而非整个数组!
    • [3.1 传参的真相:数组退化为指针](#3.1 传参的真相:数组退化为指针)
    • [3.2 函数形参的两种写法与本质](#3.2 函数形参的两种写法与本质)
    • [3.3 sizeof 失效的根本原因](#3.3 sizeof 失效的根本原因)
  • 四、二级指针:管理指针的指针
    • [4.1 什么是二级指针?](#4.1 什么是二级指针?)
    • [4.2 二级指针的解引用与运算](#4.2 二级指针的解引用与运算)

一、数组名的双重身份

C语言中的数组名 是一个极易令人混淆的概念,它既可以被视为一个指针 (地址),又可以代表整个数组实体。理解数组名,就抓住了C语言指针和数组关系的核心。

1.1 数组名的"通用"身份:数组首元素的地址

在C语言中,这是数组名最常见的用法,也是我们学习指针时被告知的默认规则
默认规则:数组名就是数组首元素的地址:

在绝大多数表达式中,当我们使用一个数组名时(例如 arr),它会自动被编译器解释为数组第一个元素的地址

这意味着:arr 的值与 &arr[0] 的值是完全相同的。
代码验证:

c 复制代码
int main()
{
    int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    printf("数组首元素地址: %p\n", &arr[0]);
    printf("数组名arr的值: %p\n", arr);
    return 0;
}

运行结果 (地址值是示例,每次分配都有差异,但两者值相等):

总结: arr 的类型是一个指向整型(int)的指针(即 int*)。有了这个地址,我们就可以使用指针方式 *(arr + i) 来访问数组的任意元素。

1.2 数组名的"特殊"身份:不可忽略的两个例外

正是以下这两种特殊情况,让数组名区别于普通的指针变量,从而可以进行一些特殊的运算。

例外一: sizeof(数组名) --- 计算整个数组大小

当数组名作为 sizeof() 运算符的唯一操作数时,它会"回归"到数组的本质,代表整个数组实体。

此时,sizeof(arr) 计算的是整个数组所占用的总字节数,而不是一个地址(指针)的大小。
示例:

c 复制代码
    int arr[10]; // 包含 10 个 int 元素的数组
    size_t size_of_arr = sizeof(arr);
    size_t size_of_element = sizeof(arr[0]);
    printf("整个数组的大小 (sizeof(arr))  = %zu 字节\n", size_of_arr);
    printf("首元素的大小 (sizeof(arr[0])) = %zu 字节\n", size_of_element);
    printf("数组元素的个数                = %zu\n", size_of_arr / size_of_element);


提示: 这是在函数外部唯一能够直接计算出数组元素个数的方法。如果你在函数内部对作为形参传入的数组使用 sizeof(),它将退化为指针,计算的是指针变量的大小(4或8字节)。

例外二: &数组名 --- 获取整个数组的地址

当对数组名使用取地址运算符 & 时,&arr 也表示整个数组。它取出的是整个数组的地址。
关键区别: 类型和步长

虽然 &arrarr 的值(地址值)是相同的,都指向数组的起始位置,但它们的类型和指针运算规则是完全不同的:

表达式 类型 (Data Type) 指针 + 1 的步长 (Offset)
arr int* (指向一个 int 元素的指针) 移动一个 int 元素的大小(4字节)
&arr int(*)[10] (指向一个有 10 个 int 元素的数组的指针,即数组指针类型) 移动整个数组的大小(40字节)

代码演示它们在指针运算上的本质区别:

c 复制代码
int main()
{
    int arr[10] = { 0 }; // 假设 int = 4字节
    printf("arr 的值:        %p\n", arr);
    printf("arr + 1 的值:    %p\n", arr + 1);   // arr + 1 跳过一个 int (4字节)
    printf("\n&arr 的值:       %p\n", &arr);
    printf("&arr + 1 的值:   %p\n", &arr + 1);  // &arr + 1 跳过整个数组 (40字节)
    return 0;
}

运行结果展示:

这⾥我们发现arrarr+1 相差4个字节,是因为&arr[0]arr 都是⾸元素的地址,+1就是跳过⼀个元素。

但是&arr&arr+1相差40个字节,这就是因为&arr数组的地址,+1 操作是跳过整个数组的。

到这⾥⼤家应该搞清楚数组名的意义了吧。

数组名是数组⾸元素的地址,但是就只有这两个操作符例外。

二、使用指针访问数组:更灵活、更强大

理解了数组名就是数组首元素的地址,我们就可以使用指针更灵活、更高效地访问数组的元素。

2.1 核心等价关系:下标与指针的转换

在C语言中,数组的下标访问 (arr[i]) 本质上就是通过指针运算来实现的。

对于一个数组 arr 和一个指向其首元素的指针 p (int* p = arr;),以下四种访问方式在编译器眼中是完全等价的:

访问方式 表达式 描述
下标法 (数组名) arr[i] 最常用的数组访问方法。
指针法 (数组名) *(arr + i) 数组名作为地址,加上偏移量 i 后解引用。
下标法 (指针变量) p[i] 将指针变量 p 当作数组名来使用,非常灵活。
指针法 (指针变量) *(p + i) 指针变量 p 加上偏移量 i 后解引用。

代码验证:

c 复制代码
int main()
{
    int arr[5] = { 10, 20, 30, 40, 50 };
    int* p = arr; // p 存储 arr 的首元素地址
    int i = 2; // 访问第三个元素(30)
    // 1. 下标法 (数组名)
    printf("arr[2]     = %d\n", arr[i]);
    // 2. 指针法 (数组名)
    printf("*(arr + 2) = %d\n", *(arr + i));
    // 3. 下标法 (指针变量)
    printf("p[2]       = %d\n", p[i]);
    // 4. 指针法 (指针变量)
    printf("*(p + 2)   = %d\n", *(p + i));
    return 0;
}

结果展示:

可以发现将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i] 是等价于 *(p+i)

同理arr[i] 应该等价于 *(arr+i),数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。

深入理解: 编译器在处理 arr[i] 时,会将其悄悄转换为 *(arr + i) 的指针运算形式。因此,用指针访问数组,只是让这种底层机制显性化。

2.2 利用指针进行数组遍历

使用指针进行数组遍历,是C语言中常见的优化手段,尤其在性能要求较高的场景。

我们可以通过递增指针的方式,依次访问每个元素。

c 复制代码
int main()
{
    int arr[5] = {10, 20, 30, 40, 50};
    int num = sizeof(arr) / sizeof(arr[0]); // 元素个数
    int* p = arr; // 指向首元素
    printf("使用指针遍历数组:\n");
    // 遍历循环
    for (int i = 0; i < num; i++)
    {
        // 关键操作:对 p 进行解引用,获取当前元素的值
        printf("%d ", *p);
        // 关键操作:指针 p 自增,p++ 使得指针跳过一个 int 大小(4字节),指向下一个元素
        p++; 
    }
    // 输出:10 20 30 40 50
    return 0;
}

通过这种方式,我们可以看到指针在数组操作中的强大灵活性。由于指针自增是按照它所指向的类型(如 int)的大小进行步进的,因此可以精确地指向数组中的下一个元素。

三、一维数组传参的本质:传递的是地址,而非整个数组!

这是C语言初学者最容易产生疑惑的地方之一:为什么我们不能在函数内部通过 sizeof(arr) 来计算传入数组的元素个数?

3.1 传参的真相:数组退化为指针

C语言在进行一维数组传参 时,遵循一个核心原则:形参中的数组名会被编译器自动退化(decay)为一个指向其首元素的指针

当你调用一个函数并传入一个数组名时,实际上传递给函数的是数组首元素的地址

数组作为实参 数组名 传递的是
int arr[10]; arr arr (数组首元素的地址)

因此,在函数内部接收这个地址的形参 ,本质上就是一个指针变量

3.2 函数形参的两种写法与本质

无论你将函数形参写成数组形式 还是指针形式,它们的本质都是一样的:接收一个地址。

c 复制代码
void test(int arr[])     // 写法一:数组形式
{
    // ...
}
void test_ptr(int* p)   // 写法二:指针形式
{
    // ...
}
// 在编译器看来,test 函数和 test_ptr 函数的形参是等效的!

3.3 sizeof 失效的根本原因

正是这种"退化"现象,导致了在函数内部无法通过 sizeof 获取数组的原始大小。

让我们看一个经典的错误示例:

c 复制代码
void print_array_size(int arr[]) // arr[] 在这里是 int* arr 的别名
{
    // 假设在 64 位环境下,指针大小是 8 字节
    int size_in_func = sizeof(arr); 
    printf("函数内 sizeof(arr) 的结果: %d 字节\n", size_in_func);
    // 这里的 size_in_func 会是 4 或 8(指针大小),而不是 40!
}
int main()
{
    int arr[10] = {0}; // 原始数组大小是 10 * 4 = 40 字节
    printf("函数外 sizeof(arr) 的结果: %zu 字节\n", sizeof(arr)); // 输出 40
    print_array_size(arr);
    return 0;
}

结果展示:

结论:

  • 在函数外部(定义数组的作用域内),sizeof(arr) 触发例外一,计算整个数组的大小(40字节)。
  • 在函数内部,由于数组名 arr 退化成了一个指针变量,sizeof(arr) 计算的是指针变量自身的大小(4或8字节),与数组的元素个数无关。

因此,如果你需要在函数内部知道数组的元素个数,必须显式地将元素个数作为另一个参数传入。

四、二级指针:管理指针的指针

理解了指针(一级指针)是用来存放变量地址 的,那么二级指针又是什么呢?

4.1 什么是二级指针?

指针变量也是一种变量,既然是变量,它就和普通变量一样,有自己的存储空间地址
二级指针(Pointer to a Pointer) ,就是用来存放一级指针变量的地址的变量。

指针级别 变量类型 存放内容 声明方式
普通变量 int 实际数据 (e.g., 10) int a = 10;
一级指针 int* 普通变量 a 的地址 int *pa = &a;
二级指针 int** 一级指针 pa 的地址 int **ppa = &pa;

代码示例:

c 复制代码
int main()
{
    int a = 10;        // 1. 普通变量 a,存储数据 10
    int* pa = &a;      // 2. 一级指针 pa,存储 a 的地址
    int** ppa = &pa;   // 3. 二级指针 ppa,存储 pa 的地址
    printf("变量 a 的值: %d\n", a);
    // 我们可以通过三种方式访问变量 a
    printf("通过 a 访问:   %d\n", a);
    printf("通过 *pa 访问: %d\n", *pa);
    printf("通过 **ppa 访问: %d\n", **ppa); // 多次解引用
    return 0;
}

这里给个图解,便于理解:

4.2 二级指针的解引用与运算

二级指针的强大之处在于它提供了间接修改一级指针的能力。通过两次解引用,我们可以精确地修改最终的数据。
第一次解引用:*ppa 访问一级指针

对二级指针 ppa 进行第一次解引用(*ppa),找到的是它所指向的变量------一级指针 pa 本身。

用途: 可以通过 *ppa 来改变 pa 变量所存储的地址。

c 复制代码
int a = 10;
int b = 20;
int *pa = &a; 
int **ppa = &pa;
// 1. *ppa 等价于 pa
*ppa = &b; // 语句解读:将 b 的地址赋值给 *ppa (即 pa)
           // 结果:pa 现在指向 b,而不是 a     
printf("现在 *pa 的值: %d\n", *pa); // 输出 20

这在函数传参中非常重要,如果你想在函数内部修改一个指针变量(例如在函数中给 pa 重新分配内存),你就必须传入它的地址,即二级指针。
第二次解引用:**ppa 访问最终数据

*ppa(即 pa)再进行一次解引用(**ppa),找到的就是最终的数据------变量 a 本身。

用途: 通过二级指针修改它最终指向的数据的值。

c 复制代码
int a = 10;
int *pa = &a; 
int **ppa = &pa;
// 1. **ppa 等价于 *pa,也等价于 a
**ppa = 30; // 语句解读:将 30 赋值给 **ppa (即 a)
           // 结果:a 的值被修改为 30   
printf("现在 a 的值: %d\n", a); // 输出 30

二级指针为我们提供了一个通过两层间接访问来控制数据的能力,这是高级C编程中绕不开的关键概念。

相关推荐
luoganttcc2 小时前
介绍一下 机器人坐标转换的 RT 矩阵
算法
程序员大雄学编程2 小时前
定积分的几何应用(一):平面图形面积计算详解
开发语言·python·数学·平面·微积分
Evand J2 小时前
【MATLAB例程】二维平面的TOA定位,几何精度因子GDOP和克拉美罗下界CRLB计算与输出
开发语言·matlab·平面·crlb·gdop
草莓火锅3 小时前
用c++求第n个质数
开发语言·c++·算法
snakecy3 小时前
自然语言处理(NLP)算法原理与实现--Part 1
人工智能·算法·自然语言处理
aniden3 小时前
Swagger从入门到实战
java·开发语言·spring
萌新彭彭3 小时前
vLLM主要模块Scheduler详解
算法·源码阅读
灵动小溪3 小时前
时频信号分析总结
算法
CoovallyAIHub3 小时前
让Qwen-VL的检测能力像YOLO一样强,VLM-FO1如何打通大模型的视觉任督二脉
深度学习·算法·计算机视觉