c语言输出symbol_C 语言课上不会讲的一些问题

d096e4ad6a56b97c39f365e789a4364c.png

基础认识:程序在内存中的基本结构

在大多数操作系统中,一个程序加载到内存后会被分配一个虚拟的内存地址,范围从0到一个非常大的不确定值。这个虚拟内存对应的物理内存映射是操作系统管理的,多数情况它在主存(main memory)中,但在空间不足时也有可能在硬盘里。作为一个应用程序的开发者,我们的看到的世界是被隔离开的。

程序运行时,代码区域(通常从0x00400000开始)的指令被依次执行,它可能操作一个内存区域,可能写入一个寄存器,可能做一个运算,也可能跳转到另一个指令等等。这些可能做的事情是完全是可以扩展的,可以是任何现在还想不到的功能,不过笔者认为这些具体的事情单独都不是那么了不起,计算机最核心的部分应该是提供了一套机制,按照某种清单样的方式执行那些功能,这个清单就是程序代码。

a395873a0ff5126a4fccf4b8c15a095a.png

区分变量的定义和声明

声明和定义在经常被混淆,这里尝试把这个问题讲清楚

  • 声明是指告诉编译器一个变量的名字、类型、如何初始化等信息
  • 定义是告诉编译器给这个变量分配内存

声明和定义听起来就是紧密结合的两个操作,我们通常同时需要两者,一般不会只需要分配内存,而不需要知道它的类型等基本信息

考虑都这种紧密关系,许多语言都没有把声明和定义完全分开,在c语言中定义就包含了声明,而声明不一定包含定义

以下,都是定义+声明

int 

有些时候,我们不需要定义(分配内存),只需要告诉编译器有这样一个变量就可以了,比如:

extern 

之所以不需要分配内存,是因为这个变量可能在别的地方(文件)定义过了,我们只需要告诉编译器相信有这个函数,有这个变量,并且告诉它怎么用这个变量(类型)就可以了。当然这样编译器是不可能知道这个变量的内存具体分配到哪里了,它会把这种位置“留空”,等链接器填上。

为什么要等链接器?编译器自己不可以补上吗?

不可以,c 语言的编译器只会编译一个个单独的文件, 那些不明位置的声明,和知道位置的全局变量/函数会被临时写到一个符号表,等链接器把所有编译好的文件串起来,并把所有的符号表对应起来,最终成为可执行的程序。

这个过程最重要的是要理解编译器在编译的时候是没有全局观的,它只知道它编译的那一个文件,这也是为什么我们需要单独的声明,而不是仅用定义的原因。

d79ce72399dba04f6769c73b161d654f.png

备注:上面说的变量和函数都是指全局的,局部变量作用在局部根本不存在跨文件的问题,也不会产生符号表。

好了,知道编译链接的基本流程后我们要问几个十分重要的问题

  1. 为什么需要头文件

​ a. 方便跨文件调用别的函数/变量

​ b. 方便使用共用的结构体/宏

2. 头文件中可以定义函数/变量吗,当作共享函数/变量来使用

不应该,不应该,不应该,重要的事情说三遍,链接时出现duplicate symbol 的问题很多时候是这样错误的编码方式导致的

如果头文件中定义一个变量 int a; 那么所有include 它的源文件 xx.c 都会在编译后输出符号 a, 相当于定义了很多次a,也就是让同一个变量分配到了几个不同的地址,这显然是有问题的,链接器遇到这种情况就会报错。

正确的做法是仅在头文件中包含声明,比如extern int a; 然后在其对应的.c文件中定义这个变量(函数同理)。

3. 头文件中可以定义结构体/宏吗

可以的,也应该如此,结构体/宏不属于要分配内存地址的对象(c++的inline函数同理)。结构体只是定义了一种类型的结构,宏只是定义了一种展开代码的结构,其本身都不需要分配内存地址,这和上面提到的函数本体/变量是有很大不同的, 函数需要分配到代码段,而变量需要分配到全局变量段。

4. 没有头文件就没有办法跨文件调用别文件中的函数/变量了吗?

不是,只要你在调用前声明(e.g. extern int a;)就可以了,这种方法虽然及其不推荐,但是我们需要知道有这种机制,才能在实际工程中发现问题。

5. 头文件需要作为编译器的输入的编译文件吗

​ 不需要,也不应该,编译器遇到#include 会自动导入头文件,作为正在处理的 .c 的一部分, 头文件本身不应该被编译成任何单独的目标文件参与后续的链接(按照之前讨论的,头文件里面都没有任何需要往分配内存地址的东西,编译出来本来应该就是空的)

6. 可以让全局变量不在链接时共享吗?

可以,用static全局变量,

更广为人知的static用法可能是函数里面的局部static变量,这样的变量在函数反复调用的时候会保留之前的值。这里说的static全局变量语义有所不同,这个相当于这个文件私有的全局变量,其它文件访问不到。

深入链接的过程,强弱定义

之前提到编译器会把每一个全局符号(函数和全局变量)写到符号表。我们已经区分过声明和定义了,这里我们进一步再深入一下定义,还可以分为弱定义和强定义,其实就是是否带有初始化的区别

int a;          //弱定义
int a = 10;     // 强定义
extern int a;   // 声明

强弱定义会影响链接器的行为:

  • 对于强定义,编译器会导出强符号(.text/.data/.bss)
  • 对于弱定义,编译器会导出弱符号(COMMON)
  • 对于声明,编译器会导出未定义符号(UND)

链接器首先会检查有没有重名的强符号和弱符号,规则如下:

  • 不允许有多个同名的强符号
  • 如果有一个强符号和一些弱符号同名,那么选择强符号
  • 如果有多个弱符号同名,那么随机选择一个

后两个十分危险,因为链接器是不知道变量类型的,它只关心地址!发生这种情况时许多链接器也会发出warning,但是不会停止链接,最终产生的程序可能会产生意想不到的效果。

比如下面,右边的x将给定左边x的地址,而编译的时候右边的编译器是把x当浮点数处理的,所有编译出来的操作也是针对浮点数的,这两边的不统一会导致许多意想不到的问题。

330802c655b0bd4576b97e2735652eec.png
Copy From CSAPP

为了避免上述问题,我们不要使用弱定义,也就是所有全局变量都一定要初始化

上面检查和选择成功后,链接器就会把UNF的符号,和这些已定义的符号匹配,如果没有匹配,则报错

ld: symbol(s) not found for architecture x86_64

解析变量定义和声明(未完待续)

你认识几个?

  • int a;
  • int * a;
  • int const * a;
  • int (*p(float q))(int a);
  • void (*signal(int sig, void (*handler)(int)))(int);
  • void (*(*(*funcArrays[10])[30])(int, void(*)(int)))(int);

识别步骤

1fbbd1c3d068d98ab5653272c8484e03.png
copy from c专家编程

比较核心的几点是

(1)分析从最左边的标识符开始,它就是变量名

(2)标识符右边的符号表示它是什么,() 就是函数, []就是数组, 剩下左边的部分对于函数来说就是返回值类型,对于数组就是内容类型

(3)向左边分析,*, const, volatile

(4)解析完一部分,比如解析完()中的内容,就把()当成一个整体,继续(2)解析

int 

区分常量指针和指针常量

这里中文有点歧义,常量指针是说指向常量的指针(pointer to constant), 指针常量是说常量存在的指针(constant pointer)

int const *a; //常量指针

int const (*a); //常量指针

const int *a; //常量指针

int * const a; //指针常量

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值