ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C 语言中指针和数组名到底有什么区别

C 语言中指针和数组名到底有什么区别 一、开发环境本文所有代码均基于标准 C99 规范测试环境如下示例代码64 位 Ubuntu 20.04GCC 9.4.0实战场景Keil MDK 5.38STM32F407Cortex-M4 32 位架构二、常见误区引入很多 C 语言入门教材会简化表述为 “数组名就是指向首元素的指针”但实际写代码时总能遇到各种不符合这个结论的奇怪问题为什么尝试给数组名赋值会直接编译报错给指针变量赋值就完全没问题同样是打印sizeof结果数组名得到的是整个数组的字节数指针得到的却只是地址的大小为什么数组定义的地方sizeof结果正常一旦传进函数内部sizeof得到的结果就不对了本文就把这个最容易混淆的核心概念掰扯清楚帮你避开绝大多数相关陷阱。三、核心知识点首先要明确一个核心误区“数组名就是指针” 是不严谨的简化表述二者本质是完全不同的两种类型。3.1 本质特性数组名代表整个数组对象的常量仅在多数普通表达式中会隐式转换为指向首元素的常量指针本身不占用独立的存储内存只是标识数组所在连续内存块的起始位置。指针是独立的变量专门用来存储内存地址自身会占用独立的内存空间指向可以随意修改可以指向任意同类型的内存地址。3.2 两个关键特殊例外数组名的隐式转换规则存在两个唯一例外只有在这两个场景下数组名会保留其 “整个数组” 的类型属性不会转换为指针对数组名使用sizeof操作符时返回整个数组的总字节数对数组名使用取地址操作符时返回指向整个数组的指针步长为整个数组的大小3.3 数组退化的概念当数组作为函数参数传递时会隐式退化为指向首元素的指针此时函数接收到的只是一个普通指针丢失了原数组的长度信息这就是为什么在函数内部用sizeof计算数组参数会得到错误结果的核心原因。3.4 核心差异对比表特性数组名指针变量本质代表整个数组对象的常量存储内存地址的独立变量存储内容不单独存储标识数组起始地址存储另一块内存的起始地址内存分配静态分配连续内存大小固定单独分配存储地址的内存sizeof结果整个数组的总字节数指针自身的大小通常 4/8 字节赋值权限是常量不能作为左值被赋值是变量可以随意修改指向取地址结果返回指向整个数组的指针返回指针变量自身的存储地址四、底层原理4.1 内存分配层面的差异数组编译器会在栈 / 静态存储区分配一块连续的内存空间直接存储数组的所有元素数组名只是这块内存的一个符号标识本身不占用额外的存储内存。比如定义int arr[5]编译器会直接分配 20 字节假设 int 为 4 字节的连续内存arr就代表这 20 字节内存的起始地址。指针编译器会单独分配一块内存用来存储指针变量自身这块内存里存储的是另一块内存的起始地址。比如定义int *p编译器会先分配 4/8 字节的内存给p本身p里可以存储任意 int 类型内存的地址。4.2 编译器处理逻辑的差异对于数组名编译器会额外记录数组的总长度和元素类型信息当遇到sizeof和操作时会直接使用数组的完整类型信息不做隐式转换其他场景下才会将数组名转换为指向首元素的指针。对于普通指针变量编译器只会记录指针自身的类型不会关心它指向的内存是单个变量还是数组也不会管理指向内存的长度。4.3 地址运算的底层逻辑差异当数组名出现在普通运算中时会先隐式转换为指向首元素的指针此时的运算步长为单个元素的大小和同类型指针的运算表现完全一致这也是很多人会混淆二者的主要原因。而指针运算的步长始终由指针自身的类型决定和指向的内容无关。五、示例代码验证以下所有示例均可直接编译运行通过实际结果验证上述结论示例 1sizeof操作对比实验#include stdio.h int main() { int arr[5] {1, 2, 3, 4, 5}; int *p arr; // sizeof数组名得到整个数组的总大小sizeof指针得到指针自身的大小 printf(sizeof(arr) %zu字节\n, sizeof(arr)); // 5 * 4 20字节 printf(sizeof(p) %zu字节\n, sizeof(p)); // 64位系统指针占8字节 return 0; }运行结果sizeof(arr) 20字节 sizeof(p) 8字节示例 2赋值操作对比实验#include stdio.h int main() { int arr[5] {1, 2, 3, 4, 5}; int *p arr; int a 10; p a; // 合法操作指针是变量可以修改指向 // arr a; // 取消注释会编译报错数组名是常量不能作为左值 printf(*p %d\n, *p); return 0; }取消注释后编译会得到明确错误error: assignment to expression with array type验证了数组名的常量属性。示例 3arr和arr的地址输出对比#include stdio.h int main() { int arr[5] {1, 2, 3, 4, 5}; printf(arr %p\n, arr); // 首元素地址 printf(arr 1 %p\n, arr 1); // 步长一个int大小4字节 printf(arr %p\n, arr); // 整个数组的地址数值与首元素地址相同 printf(arr 1 %p\n, arr 1); // 步长整个数组大小20字节 return 0; }运行结果示例arr 0x7ffdabcdef10 arr 1 0x7ffdabcdef14 arr 0x7ffdabcdef10 arr 1 0x7ffdabcdef24可以看到arr1偏移了 4 字节而arr1偏移了 20 字节二者步长完全不同。示例 4数组传参退化实验#include stdio.h // C语言中int arr[]作为参数等价于int *arr会发生数组退化 void print_array_size(int arr[]) { printf(函数内部sizeof(arr) %zu字节\n, sizeof(arr)); // 退化为指针返回8字节 } int main() { int arr[5] {1, 2, 3, 4, 5}; printf(函数外部sizeof(arr) %zu字节\n, sizeof(arr)); // 原数组返回20字节 print_array_size(arr); return 0; }运行结果函数外部sizeof(arr) 20字节 函数内部sizeof(arr) 8字节直接验证了数组传参时会发生退化丢失原数组长度信息。六、工程实战温度传感器采样处理我们以嵌入式开发中最常见的温度传感器采样平均计算场景为例演示如何正确区分和使用数组与指针。6.1 场景问题描述我们需要实现一个函数处理 STM32 ADC 采样得到的 10 个温度数据计算平均温度。如果错误理解数组和指针的区别很容易出现长度计算错误、数组越界等问题导致系统异常。6.2 开发环境硬件STM32F407 Cortex-M4 开发板32 位架构指针大小 4 字节软件Keil MDK 5.38符合 C99 标准6.3 完整操作流程1. 定义固定长度采样数组采样次数固定为 10 次使用数组可以静态分配连续内存无需动态管理适合嵌入式场景的确定性要求// 定义存储10个16位ADC采样值的数组 uint16_t adc_sample_buf[10] {0};2. 预判数组退化正确设计函数接口数组作为函数参数传递时一定会发生退化丢失长度信息必须主动传递长度参数// 错误写法试图在函数内用sizeof获取数组长度 // float calc_average_temp(uint16_t sample_buf[]) { // uint8_t len sizeof(sample_buf) / sizeof(sample_buf[0]); // // 这里会得到 4/22长度计算完全错误 // 正确写法额外传递数组长度参数 float calc_average_temp(uint16_t *sample_buf, uint8_t buf_len);3. 在定义作用域内预计算数组长度只有在数组定义的作用域内sizeof才能正确得到整个数组的大小我们在这里预先计算长度并做合法性校验#include assert.h // 计算数组实际长度sizeof(adc_sample_buf) 10 * 2 20字节 uint8_t buf_len sizeof(adc_sample_buf) / sizeof(adc_sample_buf[0]); // 调试阶段断言校验长度提前发现错误 assert(buf_len 10);4. 填充采样数据并调用处理函数模拟 ADC 采样填充数组然后将数组和预计算的长度一起传递给处理函数// 这里是硬件ADC采样函数由HAL库提供 extern uint16_t get_adc_sample(void); // 填充10个采样值 for(uint8_t i 0; i buf_len; i) { adc_sample_buf[i] get_adc_sample(); } // 调用平均计算函数 float avg_temp calc_average_temp(adc_sample_buf, buf_len);5. 函数内部正确遍历计算函数接收到的已经是退化成指针的首元素地址直接用传入的长度遍历即可float calc_average_temp(uint16_t *sample_buf, uint8_t buf_len) { // 禁止在此处使用sizeof(sample_buf)计算长度32位系统会得到4字节结果错误 if(sample_buf NULL || buf_len 0) { // 增加异常处理 return 0.0f; } uint32_t sum 0; for(uint8_t i 0; i buf_len; i) { // 下标访问本质就是指针偏移语法等价 sum sample_buf[i]; } // 简化换算公式实际开发中替换为对应的NTC温度换算即可 return (float)sum / buf_len * 0.1f; }6.4 结果验证正确写法编译无错误无警告运行计算得到的平均温度和实际采样值平均值完全一致无内存异常。如果使用错误写法不传递长度函数内用 sizeof 计算会得到错误长度2计算结果完全错误甚至可能触发越界访问导致硬件异常。尝试给数组名adc_sample_buf赋值其他地址会直接触发编译报错符合数组名是常量的特性。七、实战开发建议不要刻意混淆使用定义数组就按数组规则使用定义指针就按指针规则使用不要过度依赖隐式转换的语法糖提高代码可读性。不确定就做实验遇到结果不符合预期时可以用sizeof打印结果验证自己做实验比死记结论更可靠不同架构下指针大小可能不同。传递数组必须带长度这是嵌入式开发的通用规范永远不要试图在函数内部用sizeof获取数组长度数组退化一定会丢失长度信息。记住两个例外就够只有sizeof和对数组名不做隐式转换其他所有场景数组名都会转成首元素的常量指针记不住复杂规则记这个就够。八、实战总结核心结论总结数组名不是普通指针只是在多数表达式中会隐式转换为指向首元素的常量指针本质上数组和指针是完全不同的两种类型。核心差异可以简化为三句话数组存数据指针存地址数组名是常量不能改指针是变量可以改sizeof数组得总大小sizeof指针得地址大小。理解指针和数组名的区别是从 C 语言入门到编写稳定可靠代码的关键一步能帮你避开很多难以排查的低级错误新手只要掌握核心差异和两个特殊例外就能解决绝大多数日常开发中的混淆问题。
返回列表