C 语言指针和数组区别(附 3 个要点),新手避坑一次讲清:从内存到误区

猿友 2026-09-24 15:44:15 浏览数 (50)
反馈

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

C 语言指针与数组在内存中的布局区别示意图,展示连续内存与地址存放

一、先看结论:C 语言指针和数组怎么选

下面这张表直接给出三个核心区别的对比,避免你在不该混用的地方踩坑。

对比项 指针 数组
内存布局 本身只占一个机器字长,存的是别的变量的地址 连续分配,长度是元素个数乘元素大小
sizeof 得到指针本身大小,64 位下通常是 8 字节 得到整个数组的字节数,如 4 个 int 为 16
可修改性 指向可改,p++ 合法 数组名是常量地址,a++ 非法
边界 不携带长度,越界无提示,最易静默出错 长度在编译期确定,越界仍靠人守

指针与数组在内存布局、sizeof 与可修改性上的对比示意图

讲清指针和数组区别之前,先把一句话记住:需要灵活移动或共享地址时用指针,需要固定一批同类型数据且关心总大小时用数组。两者下标访问结果相同,但本质不同。下面的示例都基于 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"; 让字符串存在可写的栈数组里,再对它做修改就安全了。

0 人点赞