C 语言里 &Send_buf 取的是数组地址还是首元素:从类型上讲透

编程狮(w3cschool.cn) 2026-10-21 07:07:38 浏览数 (13)
反馈

先给结论:&Send_buf 取的是整个数组的地址,类型是"指向长度为 8 的 unsigned char 数组的指针";Send_buf 与 &Send_buf[0] 取的是首元素地址,类型是 unsigned char *。三者的数值完全相同,类型不同,因此做指针运算时的步进完全不同。

取数组地址还是首元素地址?

看到 &Send_buf 这种写法会犹豫是正常的,因为它在数值上和 Send_buf 一模一样,打印出来是同一个地址,很容易被认为是多余的一次取址。真正让它们不同的不是值,是类型——而类型决定了 +1 走多远、以及传给函数时编译器要不要告警。

本文用一段可以编译运行的最小程序把三种写法的结果打出来,再讲清数组名在哪些场合会退化、哪些场合不会,最后给出函数传参的正确写法与验证清单。示例用 C99,gcc 与 clang 下行为一致。

先看结论

  • 数值上三者相等:&Send_buf、Send_buf、&Send_buf[0] 打印出的地址是同一个。
  • 类型上三者不同:只有 &Send_buf 的类型是 unsigned char (*)[8],另外两个是 unsigned char *。
  • 指针运算最能暴露差异:&Send_buf + 1 前进 8 个字节,Send_buf + 1 只前进 1 个字节。

数组与指针的关系还有不少细节,C 教程 里有专门的章节,遇到边界情况可以对照着查。

一、最小场景与预期行为

把任务定死:有一个 8 字节的发送缓冲区,把它传给一个发送函数,要求函数里能正确拿到 8 个字节。

输入是一段 C 源码。预期行为是:编译无告警、函数里拿到的长度确实是 8、打印出的三种地址数值相同但 +1 之后不同。

#include <stdio.h>
#include <string.h>

unsigned char Send_buf[8] = {1, 2, 3, 4, 5, 6, 7, 8};

int main(void) {
    printf("Send_buf      = %p\n", (void *)Send_buf);
    printf("&Send_buf[0]  = %p\n", (void *)&Send_buf[0]);
    printf("&Send_buf     = %p\n", (void *)&Send_buf);

    printf("Send_buf + 1  = %p\n", (void *)(Send_buf + 1));
    printf("&Send_buf + 1 = %p\n", (void *)(&Send_buf + 1));

    printf("sizeof Send_buf = %zu\n", sizeof Send_buf);
    return 0;
}

这段做的是:把三种写法转成 void 后打印地址,再各加 1 打印一次,最后打印数组的大小。转成 void 是因为 %p 要求对应参数是 void ,直接传 unsigned char 属于未定义行为,虽然多数平台能正常打印,但开启严格告警时会被指出。

预期结果(未在本机执行,依据 C 标准中数组到指针的转换规则与指针运算定义)是:

  • 前三行输出同一个地址值;
  • Send_buf + 1 比首地址大 1;
  • &Send_buf + 1 比首地址大 8;
  • sizeof Send_buf 输出 8。

最后这一行是关键:sizeof 作用在数组上时不做退化,得到的是整个数组的字节数。

二、值与类型为什么不是一回事

数组名在表达式中出现时,有一个名为"数组到指针转换"的规则:除了三种例外,数组名会被转换成指向其首元素的指针。

三种例外是:

  1. 作为 sizeof 的操作数——得到整个数组的字节数。
  2. 作为 & 的操作数——得到指向整个数组的指针,而不是指向首元素的指针。
  3. 作为字符串字面量初始化字符数组时——这是一条独立规则,与本题无关。

正因为第 2 条例外,&Send_buf 才会得到 unsigned char (*)[8] 这个类型。它指向的是"一个 8 字节的数组对象",而不是"一个字节",所以加 1 会跨过整个数组。

这也解释了为什么下面这两行的值相同:数组的地址就是它第一个字节的地址,任何对象的地址都等于它起始处的地址。值相同不代表类型相同,这是 C 里最容易被忽略的一条常识。

三种写法的类型与加一之后的步进

三、三种写法与各自的适用场合

写法 类型 适用场合
Send_buf unsigned char * 传给按字节处理的函数
&Send_buf[0] unsigned char * 同上,语义更明确
&Send_buf unsigned char (*)[8] 需要保留数组长度信息时

3.1 传给发送函数:用数组名

void send_bytes(const unsigned char *buf, size_t len);

send_bytes(Send_buf, sizeof Send_buf);

这段做的是把数组名退化成的指针与显式算出的长度一起传进去。这是最常用也最推荐的写法:函数不关心数据来自数组还是堆,只要地址与长度。

关键点在第二个参数:函数内部无法知道传进来的指针指向多少字节,长度必须由调用方给。忘记传长度是这类代码最常见的越界来源。

3.2 想让函数自己知道长度:用数组指针

void send_frame(unsigned char (*frame)[8]);

send_frame(&Send_buf);

这段做的是把"指向 8 字节数组的指针"作为参数类型,调用方必须写 &Send_buf 才能匹配。函数里用 (*frame)[i] 访问第 i 个字节,长度信息在类型里,不会传错。

代价是函数签名被写死了长度:只能接收 8 字节的数组,传 16 字节的数组会类型不匹配。只在长度确实是协议固定值时才这么写。

3.3 千万别这么写:形参写成数组

void send_bytes(unsigned char buf[8]) {
    printf("%zu\n", sizeof buf);   // 输出的不是 8
}

这段做的是把一个"看起来像数组"的形参传给函数。这里的 buf 实际上是指针——形参声明里的数组会被调整为指针,所以 sizeof buf 得到的是指针大小(64 位平台上是 8),恰好和数组大小 8 相同,极具迷惑性。

换成 16 字节的数组就会立刻暴露:sizeof buf 仍然是 8。这就是为什么不要依赖形参里的数组长度,长度必须单独传。

四、常见失败与验证清单

4.1 编译告警:incompatible pointer types

函数期望 unsigned char *,却传了 &Send_buf。修复办法是去掉取址符,直接传 Send_buf;如果函数确实要数组指针,就改函数签名而不是强转类型。

4.2 用 %p 打印出现告警

%p 要求 void 。修复办法是显式转换:`printf("%p\n", (void )Send_buf);`。这是少数必须写强转的场合。

4.3 函数里 sizeof 拿不到真实长度

已在 3.3 说明。修复办法是把长度作为参数传进去,或者在结构体里带上长度字段。

4.4 验证清单

  • 检查:gcc -Wall -Wextra -std=c99 demo.c -o demo,确认无告警
  • 运行:./demo
  • 预期:三种地址相同,Send_buf + 1 前进 1 字节,&Send_buf + 1 前进 8 字节
  • 失败:&Send_buf + 1 也只前进 1 字节 → 检查是不是被强转成了 unsigned char *
  • 修复:保留原始类型,只在打印时转 void *
  • 验证:把数组改成 16 字节再跑一次,确认 &Send_buf + 1 前进 16 字节

上面这些命令做的是开全告警编译、运行、比对地址步进。预期结果(未在本机执行)是步进等于数组长度;若等于 1,说明类型被抹平了。

编译运行后的地址与步进输出

总结

&Send_buf 取的是数组对象的地址,类型是数组指针;Send_buf 取的是首元素地址,类型是指向元素的指针。数值一样,类型不同,加 1 的步进也不同。传参时用数组名加显式长度最稳妥。

三条要点带走:

  • 数组名在 sizeof 和 & 两个场合不退化,这是理解一切的起点;
  • 函数形参里的数组会被调整成指针,sizeof 拿不到真实长度;
  • 打印地址要转成 void *,否则 %p 是未定义行为。

下一步建议在项目的公共头文件里约定一条:凡是接收字节缓冲的函数,签名一律写成"指针加长度",禁止只写指针。指针运算的细节记不清时,翻 C 语言 C Tongyilingma 教程 最快。

常见问题

Q:&Send_buf 和 Send_buf 打印出来一样,是不是可以随便换?

A:不能。值相同但类型不同,加 1 之后一个跨 8 字节、一个跨 1 字节,传参时的类型检查也不同。

Q:为什么 sizeof 在函数里拿不到数组长度?

A:形参声明里的数组会被调整为指针,函数收到的是指针。长度必须由调用方以参数形式传进来。

Q:什么场合才应该用 &Send_buf?

A:需要把数组长度写进类型时,比如协议规定固定 8 字节的帧。其余场合用数组名就够了。

Q:printf 用 %p 为什么要强转 void *?

A:标准规定 %p 对应 void * 类型的参数。传其他对象指针属于未定义行为,虽然多数平台能正常输出,但严格告警会指出。

Q:数组退化成指针的三条例外是什么?

A:作为 sizeof 的操作数、作为 & 的操作数、以及用字符串字面量初始化字符数组这三种场合不退化。

延伸学习

想把数组与指针这块补完整,可以按这个顺序来:

  1. 读一篇相关笔记 C语言指针和数组区别,把两者的关系彻底理清;
  2. 再读一篇 C语言数组长度,对照 sizeof 在不同场合的结果;
  3. 想动手编译验证,先看 c语言编译器有哪些 选一个顺手的工具。
C

0 人点赞