问个 C 语言的问题

2018-09-26 11:02:17 +08:00
 emonber

a.c文件中定义了数组int a[100],在b.c里用extern int *a定义,然后在两个文件里分别打印&aa,前者的打印结果一致,后者打印结果不一样:

a.c: &a 0x601060, a 0x601060.
b.c: &a 0x601060, a (nil).

a.c代码:

int a[100];

void func_a()
{
        printf("%s: &a %p, a %p.\n", __FILE__, &a, a);
}

b.c代码:

extern int *a;
extern void func_a();

int main(void)
{
        func_a();
        printf("%s: &a %p, a %p.\n", __FILE__, &a, a);
        return 0;
}
2116 次点击
所在节点    程序员
19 条回复
glacer
2018-09-26 11:07:22 +08:00
你声明的是 int 指针而不是 int 数组,这不等价的。
正确声明方式是 extern int a[]
emonber
2018-09-26 11:08:25 +08:00
@glacer 那为什么`&a`的地址是一样的呢?
Andiry
2018-09-26 11:09:35 +08:00
这是一个典型的“数组就是指针”的误解
wizardoz
2018-09-26 11:09:53 +08:00
第一个数数组,第二个是指针,怎么感觉是未定义的行为啊,编译器把指针 extern 到数组指针了。
如果我的话会在 b.c 里面
extern int a[100];
没必要关心这些隐晦的行为。
实际上我写了几年 C,都没怎么用过 extern 变量的情况。
wizardoz
2018-09-26 11:11:34 +08:00
extern 函数对于框架代码的实现很有用。
extern 变量则很容易变成全局变量的滥用,完全可以在设计上避免 extern 变量。
pkokp8
2018-09-26 11:15:22 +08:00
a 文件将变量 a 当作数组解释,因此 a 地址为 a 数组首地址的地址,a 为数组 a 的首地址
b 文件将 a 当作指针解释,因此 a 地址为指针 a 的地址,同数组解释。a 为指针的值
你在 a 文件中给数组赋初值试试
别写这种代码,历史代码就改掉
emonber
2018-09-26 11:16:09 +08:00
@wizardoz 感谢回复,我最近在多核实时操作系统下开发,用 extern 可以容易实现核间的变量共享。
我理解了 int a[]和 int *a 是两个类型,但是不理解为什么&a 的值是一样的。
emonber
2018-09-26 11:20:10 +08:00
@pkokp8 感谢解答。所以&a 值一样是编译器实现的原因(我用 gcc 和 clang 的结果都是一样的)?

在 a.c 里定义 int a[100] = {1};后,打印结果如下:

```bash
a.c: &a 0x601030, a 0x601030.
b.c: &a 0x601030, a 0x1.
```
wizardoz
2018-09-26 11:20:35 +08:00
@emonber 我的理解是这样,extern 并不区分变量类型,而只是区分变量名。所以数组 a 和指针 a 放在了同一内存区域。
这就是&a 一样的原因
而 a 也是一样,只不过 a.c 的 a 是数组,所以打印出来就是它的实际内存地址。而 b.c 的 a 是变量,它所在的区域的值就是 null

你可以试试 a.c 中 a[0] = 1,然后在打印 b.c 中的 a,看看是否为 1.
bp0
2018-09-26 11:20:55 +08:00
extern int *a; 在 b.c 中声明了一个弱符号。连接的时候连接器使用了 a.c 中的 a 进行了替换。

另外,全局变量未初始化时,会被放在 bss 段,也就是说被默认初始化成 0.


然后在 b.c 中使用&a,就是 a.c 中数组 a 的地址,因此相同。

但是在 b.c 中使用 a,是需要按照指针的方式进行的。也就是说用 a 中的值作为指针。因为 a.c 中 a 数组被初始化成 0。所以这个地方打印出(nil)。


总结一下,指针是要额外占用一个空间保存指向的地址的。而数组名称并不占用额外的空间,直接表示数组首地址。
innoink
2018-09-26 11:22:09 +08:00
extern int* a
这句话,编译器在编译 b.c 时,认为在 a.c 中的符号 a 是一个 int*变量,即符号 a 所在的内存后面 4 字节(32 位)或 8 字节(64 位)里面存放的内容是一个地址,因为全局变量给你初始化成 0 了,所以就是个 nil
但是,实际在 a.c 中的符号 a 其实是 int [100],a 是一个数组名,数组名取地址和不取地址是一样的,都是首地址,因此 a.c 中,a 和&a 是一样的

int* a 是指,有个变量 a,它指向一块连续内存,注意 a 本身也有内存
int a[100],这里的 a 可以理解为一个字面量,本身不占内存
所以 b.c 中,编译器试图在 a.c 中找 a 所占的内存,即会认为 a.c 中的符号 a 所在的内存后面 4 字节(32 位)或 8 字节(64 位)就是一个 int* 变量,而实际没有这么个变量,当然就错了
emonber
2018-09-26 11:25:13 +08:00
@wizardoz 感谢~
@bp0 感谢~解释很清晰。
bp0
2018-09-26 11:40:54 +08:00
@innoink extern 的变量只是声明,并不是定义,所以不会被分配空间。如果最后连接时找不到 a.c 中的数组 a,那么连接会失败。
fcten
2018-09-26 11:41:20 +08:00
```
#include <stdio.h>

int *a;

int main(void)
{
printf("%s: &a %p, a %p.\n", __FILE__, &a, a);
return 0;
}
```

400559: 48 8b 05 00 0b 20 00 mov 0x200b00(%rip),%rax # 601060 <a>

```
#include <stdio.h>

int a[100];

int main(void)
{
printf("%s: &a %p, a %p.\n", __FILE__, &a, a);
return 0;
}
```

40052a: b9 60 10 60 00 mov $0x601060,%ecx

数组是数组,指针是指针。
chiu
2018-09-26 11:45:22 +08:00
个人愚见:
如果 a 本身是一个数组,那么 a 等于&a,就像函数名本身是指向这个函数的指针,对函数名取址&func 也是相同的值。
如果 a 是一个指针,那么对 a 取址&a 的值就是存放这个指针变量的地址。
innoink
2018-09-26 11:49:40 +08:00
@bp0 是这样的,所以我说的有问题吗
bp0
2018-09-26 13:08:14 +08:00
@innoink 不好意思,上面的回复 @错人了,应该 @wizardoz

你的回复有个不严谨的地方是,编译 b.c 的时候编译器并不知道 a 在什么地方,也就是说不知道 a 在 a.c 中。所以只能按照声明的类型进行编译。如果编译器知道 a 的位置,那么就能检查出类型不对,直接编译报错了。
XiaoxiaoPu
2018-09-26 13:28:19 +08:00
@chiu a 不等于 &a,类型不一样,差距大了
chiu
2018-09-26 14:36:14 +08:00
@XiaoxiaoPu 嗯,我表述不严谨,我指值相等,既楼主所说的前者打印一样。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/492660

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX