C语言入门:指针与数组的关系
数组名不是指针,可它偏偏总被当指针用,那它到底算个啥呢。
我学C语言快两个月了,光是“a
到底怎么算出来的”这个问题,就反复查了三回标准文档,问了两个同学,还扒了GCC的汇编输出。不是我不想弄懂,是网上太多说法自相矛盾——有的说“数组就是指针”,有的又说“根本不是”,连老师PPT上写的和教材小字批注都对不上。
后来我干脆拿纸笔画内存:`int a
= {1,2,3,4,5};`。我标出每个元素地址,手动算`a+2`是多少,再对照`&a
`,发现真的一样。但`&a`写在纸上时,我愣住了:地址数字和`&a
`并排写着,可我给它标类型时,硬生生多画了个括号,写成`int (*)
`——这玩意儿编译器认识,我一开始真不认识。
我试了`sizeof(a)`和`sizeof(&a)`,结果一个是20,一个是8。就这俩数字,让我盯了十分钟。20是5个int占的总空间,8是地址本身大小。原来“值一样”和“东西一样”真不是一回事。`a`不是变量,它没内存地址可存;`&a`也不是取“a这个符号”的地址,是取整个数组块的起始位置。
我写了个最简单的程序:
```c
int a
= {10,20,30};
printf("%p %p\n", (void*)a, (void*)&a);
```
运行结果两行地址一模一样。我差点又信了“它俩就是一回事”。直到我把`a+1`和`&a+1`打出来——前者是`a
`地址+4,后者直接跳了12字节(3×4)。这才明白,指针加1走多远,全看它指的“东西”有多大。`&a`指的不是int,是一整个`int
`,所以加1就跨过整个数组。
下标`a
`这事,课本说等于`*(a+i)`。我原来以为这只是写法不同,直到看godbolt.org里生成的汇编,`a
`和`*(a+i)`编译出来真的一条指令都不差。它不是“看起来像”,是编译器规定它就必须是。但`a++`为啥错?因为`a`根本不是能放地址的盒子,它连盒子都不是,是贴在内存块上的一张标签。你不能让标签自己动,但你能拿个新盒子(比如`int *p = a;`),把标签上的地址抄进去,再让盒子动。
函数传数组最骗人。我写`void f(int arr
10
)`,还傻乎乎在函数里`sizeof(arr)`,结果永远是8。我删掉10写成`int arr
`,还是一样。问了助教才懂:C标准白纸黑字写了,形参里的数组声明,编译器当场就替换成指针。它不存长度,不存边界,只存首地址。你传进去的是`a`,函数里拿到的是`a`转换后的`int *`,仅此而已。
陷阱真容易踩。有次我把`&a`直接赋给`int *p`,GCC警告我“类型不匹配”,我没理,结果程序跑一半崩了。后来加`-Wall`重编,警告清清楚楚写“initialization from incompatible pointer type”。还有一次越界访问`a
`,程序居然没报错,还打印出一个鬼数字,我真以为它“能用”。直到换台机器跑,直接段错误。UB不是吓唬人,是它真不管你会不会出事。
我拿`int (*p)
10
= &a;`和`int *q = a;`对比着写了几遍。前者`p+1`跳40字节,后者`q+1`跳4字节;`*p`是整个数组,`*q`只是一个int。要访问`a
`,用`q`是`q
`,用`p`得写成`(*p)
`——多一对括号,少一对就错。这不是绕口令,是类型系统在拉警报。
结构体里的数组让我又卡了一次。`struct { int x
; } s;`,我打`&s.x`和`&s.x
`,值一样,但类型一个是`int (*)
`,一个是`int *`。我试了`printf("%d", sizeof(&s.x));`,输出8;`sizeof(&s.x
)`也是8。但把它们传给函数,参数类型写错,编译器立马翻脸。
现在写代码前,我先想三件事:这个东西类型是啥?`sizeof`它多大?`&`它得到什么?想清楚了,`a
`、`p
`、`*(p+i)`在我脑子里就真成了一回事——不是“差不多”,是底层指令一模一样,只是写法不同。
我删掉了所有笔记里“数组就是指针”这几个字。改成:“数组名,在大部分地方,会自动变成指向第一个元素的指针,但它自己从来不是。”
今天用`-O2`编译三个遍历写法,看了汇编,三条`mov`指令排得整整齐齐,没差一个字节。
