C语言指针和数组有什么区别?从地址、sizeof到函数传参

编程狮 2026-09-18 17:05:05 浏览数 (18)
反馈

C 语言数组和指针经常写出相似表达式,但它们不是同一种东西:数组是连续元素组成的对象,指针是保存地址的变量。数组在多数表达式中会转换为首元素指针,这个自动转换才是混淆的来源。

指针和数组到底差在哪?

本文用一个 int scores[4] 贯穿地址、sizeof、赋值、步进和函数传参。示例按标准 C11/C17 语义编写;当前环境没有 C 编译器,因此输出会明确标为预期结果(未在本机执行)。不同平台的指针字节数可能不同,不应把某个数字当语言规定。

一、先看结论:数组和指针的核心区别

对比项 数组 int scores[4] 指针 int *cursor
本质 连续元素组成的对象 保存地址的变量
能否赋值 数组名不能被赋值 指针变量可以重新指向
sizeof 得到整个数组字节数 得到指针变量字节数
步进单位 scores + 1 跨一个元素 cursor + 1 跨一个元素
取地址 &scores 类型是 int (*)[4] &cursor 类型是 int **
函数参数 会调整为指针 本身就是指针
生命周期 由定义作用域决定 取决于指向对象和存储期
长度信息 在定义作用域内可由 sizeof 得到 不携带长度,必须另传

一句话:数组是对象,指针是变量;数组名在多数表达式中会退化为首元素指针,但数组本身没有消失。

二、先把数组对象和指针变量分开

#include <stdio.h>

int main(void) {
    // scores 是一个包含 4 个 int 的数组对象
    int scores[4] = {70, 80, 90, 100};

    // cursor 是指针变量,指向 scores 的第一个元素
    int *cursor = scores;

    // scores[0] 和 *cursor 都访问第一个元素
    printf("%d %d\n", scores[0], *cursor);

    // 指针后移,指向下一个 int
    cursor++;

    // 此时 cursor 指向 80
    printf("%d\n", *cursor);

    return 0;
}

预期结果(未在本机执行)是第一行 70 70,第二行 80

scores 代表一整块能放 4 个 int 的数组对象;cursor 只保存其中一个元素的地址。cursor++ 会让指针移到下一个 int,不会移动数组本身。

C 语言数组与指针的第一条边界是可修改性:指针变量可以重新指向别处,数组名不能被赋值。

int scores[4] = {70, 80, 90, 100};
int *cursor = scores;

// 合法:指针变量可以重新指向
cursor = NULL;

// 非法:数组名不能被赋值
// scores = cursor;

若你对声明和解引用还不熟,可以先回看 C语言基础教程 的变量与指针章节。

三、地址看起来相同,含义和步进仍不同

scores 在多数表达式中转换为 &scores[0],所以打印二者常得到相同数值。可是 &scores 是“指向整个四元素数组的指针”,类型不同,步进也不同。

#include <stdio.h>

int main(void) {
    int scores[4] = {70, 80, 90, 100};

    // scores 在多数表达式中转换为 &scores[0]
    printf("%p\n", (void *)scores);

    // 显式取第一个元素的地址
    printf("%p\n", (void *)&scores[0]);

    // 分别计算 scores + 1 和 &scores + 1 的字节偏移
    printf("%td %td\n",
           (char *)(scores + 1) - (char *)scores,
           (char *)(&scores + 1) - (char *)&scores);

    return 0;
}

在常见 sizeof(int)==4 的平台,前两个地址数值相同;最后一行预期为 4 16。第一个加一跨过一个元素,第二个加一跨过整个数组。

这里的 4 和 16 只是该假设下的结果,严谨代码应使用 sizeof 计算,而不是写死。

表达式 类型 加一步进 说明
scores 多数表达式中转为 int * 跨 1 个 int 首元素指针
&scores[0] int * 跨 1 个 int 首元素地址
&scores int (*)[4] 跨整个数组 指向数组对象
scores + 1 int * 跨 1 个 int 指向第二个元素
&scores + 1 int (*)[4] 跨 4 个 int 指向数组之后

💡 小提示:数组名转换有例外。作为 sizeof 的操作数、取地址运算符 & 的操作数时,它仍表示数组类型,而不是先变成元素指针。

四、sizeof 最能暴露 C 语言数组与指针差异

#include <stdio.h>

int main(void) {
    int scores[4] = {70, 80, 90, 100};
    int *cursor = scores;

    // 分别输出数组大小、指针大小、元素大小和数组长度
    printf("array=%zu pointer=%zu element=%zu length=%zu\n",
           sizeof scores,
           sizeof cursor,
           sizeof scores[0],
           sizeof scores / sizeof scores[0]);

    return 0;
}

int 是 4 字节、指针是 8 字节,预期输出为:

array=16 pointer=8 element=4 length=4

sizeof scores 计算完整数组,sizeof cursor 只计算指针变量。数组长度公式只能在数组仍是数组的作用域使用。

场景 写法 sizeof 结果 说明
数组定义作用域 sizeof scores 整个数组字节数 16(假设 int 为 4 字节)
指针变量 sizeof cursor 指针变量字节数 8(假设指针为 8 字节)
单个元素 sizeof scores[0] 一个元素字节数 4
数组长度 sizeof scores / sizeof scores[0] 元素个数 4
函数参数内 sizeof values 指针大小 不是原数组长度
动态内存 sizeof pointer 指针大小 无法恢复分配长度

所谓 sizeof 数组,本质是让编译器读取数组类型里保存的元素数量,而不是运行时沿内存寻找结束位置。字符数组可能用 \0 作为字符串终止标记,普通整数数组没有哨兵;即使字符数组里有终止标记,sizeof 也统计分配的全部字节,strlen 才统计终止标记前的字符数。把两者混用会造成长度少一位,或把未使用容量算进有效数据。

这一点与 C++ 中的原生数组也有关,但 C++ 还能用容器与模板保留更多类型信息。想对照差异时可查看 C++基础教程,不要把 std::array 的行为反推给 C 数组。

五、函数参数写成数组,进入函数后仍是指针

#include <stddef.h>

// 参数写成 const int values[],实际会调整为 const int *values
int sum_scores(const int values[], size_t length) {
    int total = 0;

    // 函数内部不知道数组长度,必须依赖调用者传入 length
    for (size_t i = 0; i < length; i++) {
        total += values[i];
    }

    return total;
}

参数 const int values[] 会调整为 const int *values。函数收到首元素地址,不知道调用者数组有几项。因此必须把 length 一起传入。

在函数内部写 sizeof values / sizeof values[0] 得到的是指针大小除以元素大小,通常不是原数组长度。

// 在函数内部,这种写法通常得不到原数组长度
// sizeof values 得到的是指针大小

C 语言函数参数中的 [] 更像“这里接收一段连续元素”的阅读提示,不会复制整个数组。二维数组还必须保留后续维度,例如 int a[][4],因为编译器需要知道跨一行要移动多少字节。

函数参数写法 实际类型 是否复制数组 能否得到原长度
int values[] int *values 不能
int values[10] int *values 不能
int *values int *values 不能
int values[][4] int (*values)[4] 不能

六、三个常见错误怎样定位

第一个错误是返回局部数组首地址。函数结束后数组生命周期结束,指针变成悬空指针。应由调用者提供缓冲区,或使用明确的动态分配与释放约定。

// 错误示例:返回局部数组首地址
// int *get_scores(void) {
//     int scores[4] = {70, 80, 90, 100};
//     return scores; // 函数结束后 scores 生命周期结束
// }

第二个错误是越过数组边界。指针算术只在同一数组对象及其尾后位置范围内有定义;尾后指针可以比较,不能解引用。

第三个错误是把字节数当元素数,特别是在函数参数里误用 sizeof

排错时依次记录变量声明、元素数量、每次索引范围和所有权。编译时开启警告,并使用 AddressSanitizer 等工具检查越界;本文没有可用编译器,不能把预期输出写成实测结果。

常见错误 现象 定位方法
返回局部数组首地址 悬空指针、随机值 检查返回指针指向的生命周期
越过数组边界 越界读写、崩溃 记录索引范围,开启 ASan
函数内误用 sizeof 长度算成指针大小 打印 sizeof values
把字节数当元素数 循环次数错误 区分字节数与元素个数
指针未初始化 野指针 声明后立即初始化或置 NULL

七、复杂声明与动态内存

还要区分三种容易看花的声明:const int *p 表示不能通过 p 修改元素;int *const p 表示 p 不能改指向;int (*p)[4] 是指向四元素数组的指针。它们分别约束数据、指针变量和步进单位。

int scores[4] = {70, 80, 90, 100};

// 不能通过 p 修改元素,但 p 可以重新指向
const int *p1 = scores;

// p2 不能改指向,但可以通过 p2 修改元素
int *const p2 = scores;

// 指向四元素数组的指针,加一步进跨整个数组
int (*p3)[4] = &scores;

阅读复杂声明时,从变量名向外按括号展开,再检查 const 修饰的是星号左侧还是右侧。

声明 含义 能否改指向 能否改元素
const int *p 指向常量的指针 不能
int *const p 常量指针 不能
int (*p)[4] 数组指针
int *p[4] 指针数组 不适用 不适用

动态内存也不会自动变成数组类型。malloc(4 * sizeof(int)) 返回一段足够大的存储,接收它的变量仍是指针,sizeof pointer 不能恢复分配长度。

#include <stdlib.h>

// 分配 4 个 int 的存储,但 ptr 仍然只是指针
int *ptr = malloc(4 * sizeof(int));

// sizeof ptr 是指针大小,不是 4 * sizeof(int)

工程代码应把指针与元素数量放进同一个结构体,统一创建、传递与释放,避免长度在调用链中丢失。

typedef struct {
    int *data;
    size_t length;
} IntArray;

C 语言数组与指针的四个差异

总结

C 语言数组和指针之所以像,是因为数组表达式经常自动转换为首元素指针;它们之所以不能混为一谈,是因为对象大小、能否赋值、生命周期和函数参数信息完全不同。

记住四点:

  • 数组拥有连续元素,指针只保存地址;
  • sizeof 在原作用域能得到数组总字节数;
  • 数组参数进入函数后按指针处理;
  • 长度和所有权必须显式传递。

掌握这些边界后,再看字符串、动态内存和二维数组会清晰很多。

延伸学习

  1. 想继续学习相近语法,可进入 C++入门课程 对照容器与原生数组;
  2. 需要练习声明和索引,可使用 编程题库 自测;
  3. 还没搭好编译环境时,先读 C语言编译器工作原理,理解源码到可执行文件的路径。

常见问题

Q:数组名就是常量指针吗?

A:不是。这个说法便于记忆,却不准确。数组名在多数表达式中会转换为指针,但数组本身仍是独立对象,也没有一个可被赋值的“常量指针变量”。

Q:为什么函数里算不出数组长度?

A:数组参数会调整为指针,函数只拿到首元素地址。把长度作为独立参数传入,或用结构体同时包装指针与长度。

Q:scores 和 &scores 地址为什么一样?

A:它们通常指向同一块起始内存,所以数值相同;类型不同,执行加一时分别跨一个元素和整个数组。

Q:指针一定是 8 字节吗?

A:不一定。它取决于目标平台和编译模型。代码需要指针大小时应写 sizeof(pointer),不要假设 32 位或 64 位结果。

0 人点赞