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

一、开发环境

本文所有代码均基于标准 C99 规范,测试环境如下:

  • 示例代码:64 位 Ubuntu 20.04,GCC 9.4.0
  • 实战场景:Keil MDK 5.38,STM32F407(Cortex-M4 32 位架构)

二、常见误区引入

很多 C 语言入门教材会简化表述为 “数组名就是指向首元素的指针”,但实际写代码时总能遇到各种不符合这个结论的奇怪问题:

  1. 为什么尝试给数组名赋值会直接编译报错,给指针变量赋值就完全没问题?
  2. 同样是打印sizeof结果,数组名得到的是整个数组的字节数,指针得到的却只是地址的大小?
  3. 为什么数组定义的地方sizeof结果正常,一旦传进函数内部,sizeof得到的结果就不对了?

本文就把这个最容易混淆的核心概念掰扯清楚,帮你避开绝大多数相关陷阱。


三、核心知识点

首先要明确一个核心误区:“数组名就是指针” 是不严谨的简化表述,二者本质是完全不同的两种类型

3.1 本质特性

  • 数组名:代表整个数组对象的常量,仅在多数普通表达式中会隐式转换为指向首元素的常量指针,本身不占用独立的存储内存,只是标识数组所在连续内存块的起始位置。
  • 指针:是独立的变量,专门用来存储内存地址,自身会占用独立的内存空间,指向可以随意修改,可以指向任意同类型的内存地址。

3.2 两个关键特殊例外

数组名的隐式转换规则存在两个唯一例外,只有在这两个场景下,数组名会保留其 “整个数组” 的类型属性,不会转换为指针:

  1. 对数组名使用sizeof操作符时,返回整个数组的总字节数
  2. 对数组名使用&取地址操作符时,返回指向整个数组的指针,步长为整个数组的大小

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 结果验证

  1. 正确写法编译无错误无警告,运行计算得到的平均温度和实际采样值平均值完全一致,无内存异常。
  2. 如果使用错误写法(不传递长度,函数内用 sizeof 计算),会得到错误长度2,计算结果完全错误,甚至可能触发越界访问导致硬件异常。
  3. 尝试给数组名adc_sample_buf赋值其他地址会直接触发编译报错,符合数组名是常量的特性。

七、实战开发建议

  1. 不要刻意混淆使用:定义数组就按数组规则使用,定义指针就按指针规则使用,不要过度依赖隐式转换的语法糖,提高代码可读性。
  2. 不确定就做实验:遇到结果不符合预期时,可以用sizeof打印结果验证,自己做实验比死记结论更可靠,不同架构下指针大小可能不同。
  3. 传递数组必须带长度:这是嵌入式开发的通用规范,永远不要试图在函数内部用sizeof获取数组长度,数组退化一定会丢失长度信息。
  4. 记住两个例外就够:只有sizeof&对数组名不做隐式转换,其他所有场景数组名都会转成首元素的常量指针,记不住复杂规则记这个就够。

八、实战总结

核心结论总结:数组名不是普通指针,只是在多数表达式中会隐式转换为指向首元素的常量指针,本质上数组和指针是完全不同的两种类型。核心差异可以简化为三句话:

  • 数组存数据,指针存地址; 
  • 数组名是常量不能改,指针是变量可以改;
  • sizeof数组得总大小,sizeof指针得地址大小。

理解指针和数组名的区别,是从 C 语言入门到编写稳定可靠代码的关键一步,能帮你避开很多难以排查的低级错误,新手只要掌握核心差异和两个特殊例外,就能解决绝大多数日常开发中的混淆问题。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值