先给结论:&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 作用在数组上时不做退化,得到的是整个数组的字节数。
二、值与类型为什么不是一回事
数组名在表达式中出现时,有一个名为"数组到指针转换"的规则:除了三种例外,数组名会被转换成指向其首元素的指针。
三种例外是:
- 作为
sizeof的操作数——得到整个数组的字节数。 - 作为
&的操作数——得到指向整个数组的指针,而不是指向首元素的指针。 - 作为字符串字面量初始化字符数组时——这是一条独立规则,与本题无关。
正因为第 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 的操作数、作为 & 的操作数、以及用字符串字面量初始化字符数组这三种场合不退化。
延伸学习
想把数组与指针这块补完整,可以按这个顺序来:
- 读一篇相关笔记 C语言指针和数组区别,把两者的关系彻底理清;
- 再读一篇 C语言数组长度,对照 sizeof 在不同场合的结果;
- 想动手编译验证,先看 c语言编译器有哪些 选一个顺手的工具。

TRAE-AI编程



