当前位置: 首页 > news >正文

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

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

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

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

目录

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

一、数组名的双重身份

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

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

在C语言中,这是数组名最常见的用法,也是我们学习指针时被告知的默认规则
默认规则:数组名就是数组首元素的地址:
在绝大多数表达式中,当我们使用一个数组名时(例如 arr),它会自动被编译器解释为数组第一个元素的地址
这意味着:arr 的值与 &arr[0] 的值是完全相同的。
代码验证:

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) 计算的是整个数组所占用的总字节数,而不是一个地址(指针)的大小。
示例:

    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)
arrint* (指向一个 int 元素的指针)移动一个 int 元素的大小(4字节)
&arrint(*)[10] (指向一个有 10 个 int 元素的数组的指针,即数组指针类型)移动整个数组的大小(40字节)

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

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 后解引用。

代码验证:

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语言中常见的优化手段,尤其在性能要求较高的场景。
我们可以通过递增指针的方式,依次访问每个元素。

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 50return 0;
}

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

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

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

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

C语言在进行一维数组传参时,遵循一个核心原则:形参中的数组名会被编译器自动退化(decay)为一个指向其首元素的指针
当你调用一个函数并传入一个数组名时,实际上传递给函数的是数组首元素的地址

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

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

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

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

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

3.3 sizeof 失效的根本原因

正是这种“退化”现象,导致了在函数内部无法通过 sizeof 获取数组的原始大小。
让我们看一个经典的错误示例:

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)); // 输出 40print_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;

代码示例:

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

在这里插入图片描述
这里给个图解,便于理解:
在这里插入图片描述

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

二级指针的强大之处在于它提供了间接修改一级指针的能力。通过两次解引用,我们可以精确地修改最终的数据。
第一次解引用:*ppa 访问一级指针
对二级指针 ppa 进行第一次解引用(*ppa),找到的是它所指向的变量——一级指针 pa 本身。

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

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 本身。

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

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编程中绕不开的关键概念。

http://www.dtcms.com/a/601951.html

相关文章:

  • 中企动力做网站 知乎网站后台系统是用什么做的
  • Linux内核信号传递机制完全解析:从force_sig_info到kick_process的完整路径
  • 佛山新网站建设哪家好建筑方案设计流程步骤
  • 计算机工作原理
  • 北京做网站建设比较好的公司上海网站建设企业名录
  • AEC-Q100 stress实验详解#3——HTSL(高温储存寿命测试)
  • 洋洋点建站wordpress判断是否登录
  • 做的好的农产品网站怎样开通微商城平台
  • Python | 变量如何定义,数据类型介绍
  • 12. 2 雅可比法
  • 【OpenCV + VS】图像通道的均值和方差计算
  • (5)框架搭建:Qt实战项目之主窗体菜单栏
  • 网页C语言在线编译 | 快速、便捷的编程体验
  • 网站免费注册建站培训班
  • WebSpoon9.0(KETTLE的WEB版本)编译 + tomcatdocker部署 + 远程调试教程
  • 万网的网站建设好吗北京模板网站建站
  • Leaflet入门,Leaflet如何修复瓦片之间有白线问题
  • Unity一分钟思路---UI任务条:宝箱位置如何准确卡在百分比位置上
  • 在线做爰a视频网站个人网站搭建详细步骤
  • 网站开发 工作量云台山旅游景区网站建设内容
  • Android 使用MediaMuxer+MediaCodec编码MP4视频异步方案
  • 第14章 智能指针
  • GSV6128E/ACP---嵌入式Display port 1.4到 LVDS转换器,带音频提取和嵌入式MCU
  • 网站建设ftp上传是空目录仿做网站的网站
  • c 网站开发代码辅助色网站
  • 无法下载依赖:pentaho-aggdesigner-algorithm/5.1.5-jhyde
  • sward实战教程系列(1) - 安装与配置
  • C语言编译环境 | 配置和优化你的开发环境,让编程更加高效
  • 《Vue项目开发实战》第五章:组件封装--Form
  • 数据管理战略|流程与IT变革、量化闭环