一、开发环境
本文所有代码均基于标准 C99 规范,测试环境如下:
- 示例代码:64 位 Ubuntu 20.04,GCC 9.4.0
- 实战场景:Keil MDK 5.38,STM32F407(Cortex-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 地址运算的底层逻辑差异
当数组名出现在普通运算中时,会先隐式转换为指向首元素的指针,此时的运算步长为单个元素的大小,和同类型指针的运算表现完全一致,这也是很多人会混淆二者的主要原因。而指针运算的步长始终由指针自身的类型决定,和指向的内容无关。
五、示例代码验证

以下所有示例均可直接编译运行,通过实际结果验证上述结论:
示例 1:sizeof操作对比实验
#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,验证了数组名的常量属性。
示例 3:arr和&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
可以看到arr+1偏移了 4 字节,而&arr+1偏移了 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/2=2,长度计算完全错误
// 正确写法:额外传递数组长度参数
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 语言入门到编写稳定可靠代码的关键一步,能帮你避开很多难以排查的低级错误,新手只要掌握核心差异和两个特殊例外,就能解决绝大多数日常开发中的混淆问题。


5380

被折叠的 条评论
为什么被折叠?



