指针和数组区别是 C 语言里最容易混淆的一对概念,二者下标访问结果相同,本质却完全不同。指针本身只占一个机器字长、存放别的变量的地址,可自增、可改指向;数组是一整片连续数据,长度在编译期确定,数组名是常量地址不能自增。这种差异直接导致 sizeof 算错、把数组当指针反复自增编译报错、以及数组作参数退化为指针等高频坑。本文用三段最小示例讲清内存布局、sizeof 差异与可修改性三个核心区别,并给出可直接对照的排错清单。先理解内存模型,再写代码,最后用对比输出验证你的理解,就能避开绝大多数新手错误。

一、先看结论:C 语言指针和数组怎么选
下面这张表直接给出三个核心区别的对比,避免你在不该混用的地方踩坑。
| 对比项 | 指针 | 数组 |
|---|---|---|
| 内存布局 | 本身只占一个机器字长,存的是别的变量的地址 | 连续分配,长度是元素个数乘元素大小 |
| sizeof | 得到指针本身大小,64 位下通常是 8 字节 | 得到整个数组的字节数,如 4 个 int 为 16 |
| 可修改性 | 指向可改,p++ 合法 |
数组名是常量地址,a++ 非法 |
| 边界 | 不携带长度,越界无提示,最易静默出错 | 长度在编译期确定,越界仍靠人守 |

讲清指针和数组区别之前,先把一句话记住:需要灵活移动或共享地址时用指针,需要固定一批同类型数据且关心总大小时用数组。两者下标访问结果相同,但本质不同。下面的示例都基于 GCC 与 C99 标准,逻辑在 Clang、MSVC 上也一致,只是编译命令略有差别,可以放心对照练习。
二、环境确认与最小示例
写代码前先确认运行环境,能少走很多弯路。本文示例基于 GCC 与 C99 标准,用 gcc 编译、./demo 运行;语法不熟可以先看 C 语言入门教程 把编译流程跑通,再回到这里复制示例。确认 gcc --version 能看到支持 C99 的版本即可,更老的编译器对 for 内声明变量可能不兼容,遇到报错就把循环变量提到函数开头。
下面是一段能直接编译运行的最小示例,把指针和数组指向同一处内存,并对比它们的 sizeof:
#include <stdio.h>
int main(void) {
int arr[4] = {10, 20, 30, 40}; // 定义长度为 4 的整型数组,在栈上连续存放
int *p = arr; // 指针 p 指向数组首元素,等价于 &arr[0]
int i;
// 打印地址:数组名和指针指向同一处起始位置
printf("arr 地址=%p, p 地址=%p\n", (void*)arr, (void*)p);
// 打印大小:数组是整个空间,指针只是自身一个地址
printf("sizeof(arr)=%zu, sizeof(p)=%zu\n", sizeof(arr), sizeof(p));
for (i = 0; i < 4; i++) {
// 两种下标写法结果一致,都展开成 *(起始 + i)
printf("arr[%d]=%d, p[%d]=%d\n", i, arr[i], i, p[i]);
}
return 0;
}
关于输出:本机未实际执行,按官方文档推断,不同机器地址值会变,但结构一致。预期结果(未在本机执行)如下,注意 sizeof(arr) 是 16、sizeof(p) 是 8:
arr 地址=0x7ffd..., p 地址=0x7ffd...
sizeof(arr)=16, sizeof(p)=8
arr[0]=10, p[0]=10
arr[1]=20, p[1]=20
arr[2]=30, p[2]=30
arr[3]=40, p[3]=40
若编译报 warning: format '%p',确认占位符与参数类型匹配即可;若想看内存是否越界,可借助 GDB 检查。先用预期输出建立直觉,再用你自己机器上的真实结果验证,理解会更扎实。
三、要点一:内存布局不同(指针存地址,数组存数据)
指针本身只是一块存地址的内存,而数组是一整片连续的数据区。下面这段程序把两者的地址和移动过程打印出来,看清本质差异。
#include <stdio.h>
int main(void) {
int arr[3] = {1, 2, 3}; // 数组在栈上连续分配 3 个 int,共 12 字节
int *p = arr; // 指针 p 只占一个地址大小,存的是 arr 首地址
int i;
printf("arr 首地址=%p\n", (void*)arr);
printf("p 首地址=%p\n", (void*)p); // 与 arr 指向同一处起始位置
for (i = 0; i < 3; i++) {
// 指针算术:p+i 跳过 i 个 int 大小,正好走到第 i 个元素
printf("第%d个元素:arr=%p, p+%d=%p, 值=%d\n",
i, (void*)&arr[i], i, (void*)(p + i), *(p + i));
}
return 0;
}
这段代码的边界是:p + i 移动的字节数由元素类型决定,不是简单加 i;数组名 arr 在表达式里会隐式转成 &arr[0],但它背后仍是一整片连续内存。理解这一点,就能解释为什么 arr 和 p 打印出的起始地址相同、却一个是「整片数据」一个是「一个地址」。安全上,指针算术越界不会报错,需要你自己在循环里守住长度。
四、要点二:sizeof 差异与可修改性
这是新手最易混淆的点:指针的 sizeof 是它自己,数组的 sizeof 是整个空间;同时指针能自增、数组名不能。下面程序一次看全。
#include <stdio.h>
int main(void) {
int arr[5] = {0}; // 长度 5 的数组,共 20 字节
int *p = arr; // 指针指向数组首元素
printf("sizeof(arr)=%zu 字节\n", sizeof(arr)); // 整个数组:5*4=20
printf("sizeof(p)=%zu 字节\n", sizeof(p)); // 指针本身:64 位下为 8
p++; // 合法:指针是可变变量,移动到下一个元素
// arr++; // 非法:数组名是常量地址,不能自增,编译报错
printf("p 移动后指向 arr[1]=%d\n", *p);
return 0;
}
这里的失败常见于把数组当指针反复自增,结果编译直接报错。修复方向是保留数组名不动,另起一个指针变量去遍历;需要长度时永远用 sizeof(arr)/sizeof(arr[0]) 在本作用域内求值。注意这个技巧只在数组名还「是数组」时有效,一旦传给函数就退化成指针,下面要点三会讲。
五、要点三与常见排错
要点三是:数组名作为函数参数时会退化为指针,函数内拿到的不再是整个数组。这直接解释了前面 sizeof 算错的现象,也是排错的重点。遇到拿不准的代码,可直接把片段贴到 在线代码实例 核对编译结果。
#include <stdio.h>
// 数组作参数会退化为指针,sizeof 拿到的是指针大小而非数组大小
void print_size(int a[]) {
printf("函数内 sizeof(a)=%zu 字节(已退化为指针)\n", sizeof(a));
}
int main(void) {
int arr[4] = {1, 2, 3, 4};
printf("函数外 sizeof(arr)=%zu 字节(整个数组)\n", sizeof(arr));
print_size(arr);
return 0;
}
| 现象 | 常见原因 | 修复方向 |
|---|---|---|
| 函数里 sizeof(arr) 变成 8 | 数组名作参数退化为指针 | 额外传长度参数,或改用结构体包裹 |
| 修改字符串常量崩段错误 | 指针指向 "abc" 这种只读区 |
改用字符数组 char s[]="abc" 再改 |
| 指针越界读到脏数据 | 指针不携带长度,循环条件写错 | 显式保存长度并核对边界 |
排错时优先看编译告警:把数组当指针自增、sizeof 用错这类问题,编译器往往已经给了提示。把长度作为参数显式传递,比依赖数组名携带的信息更可靠,也更符合 C 语言的设计习惯。
总结
指针和数组区别可以一句话收住:内存布局上指针只存地址、数组连续存数据;sizeof 上指针是本身大小、数组是整个大小;可修改性上指针能自增、数组名不能。把数组传给函数会退化为指针,这是 sizeof 算错和越界失败最常出现的边界。写代码前先确认环境用 gcc 编译,再用预期输出验证,对照上面的排错表处理异常,就能避开绝大多数新手坑。更多练习可以看编程狮整理的 C 语言入门教程,把三个要点各敲一遍比只读更管用。
延伸学习
常见问题
Q:为什么数组名不能做 p++ 这样的自增?
A:数组名在多数表达式里会转换为指向首元素的常量指针,它是只读的地址,不能当变量自增;指针 p 是独立变量,p++ 改的是 p 自己,所以合法。需要遍历数组时,应另起指针变量移动。
Q:函数参数为什么 sizeof(arr) 算出来是指针大小?
A:C 语言规定数组作为函数参数时会退化为指针,函数拿到的是地址而非整个数组,因此 sizeof 得到指针本身的字节数。修复办法是额外传入长度,或用结构体把数组和长度绑在一起一起传。
Q:指针和数组的下标访问到底是不是一回事?
A:在访问元素时 a[i] 和 p[i] 结果等价,都展开成 *(a+i);但本质不同:数组的 a 是常量地址、自带长度信息(仅在本作用域),指针的 p 是变量、不携带长度,越界时更容易静默出错。
Q:为什么修改字符串常量会段错误?
A:char *s = "abc"; 里 s 指向只读数据区,写 *s = 'x' 会触发段错误。修复办法是改用 char s[] = "abc"; 让字符串存在可写的栈数组里,再对它做修改就安全了。

TRAE-AI编程



