C++知识点之C++编译过程

C++知识点之C++编译过程

编译过程

  • 编译预处理
  • 编译 优化 汇编
  • 链接
    这里写图片描述

1 编译预处理

主要进行源码级别上的操作,预处理器执行源码中的预处理命令(以‘#’号开头的语句),其中预处理命令可以分为以下几类:

  • 宏定义命令[ #define 宏名 替换内容 、#undef 宏名]:进行代码替换, 凡是遇到标识符为宏名的都直接用“替换内容”进行替换。
  • 条件编译命令[ #if … 、 #else 、 #elseif …、 #ifdef … 、#ifndef … 、#endif 、#error messageStr]: 根据条件判断来选取代码块作为编译程序输入。
  • 包含文件命令[ #include< filename >]: 用文件内容替换这条命令。
  • 预定义宏名[ LINEDATETIMEFILE]
  • 预编译模块[[#pragma]:一般被用作编译器的拓展,用来设置编译器状态或指示编译器完成特定动作,比如VC++的[#pragma once]用于防止头文件被重复包含,[#pragma omp parallel for]用于VC++对OMP加速的支持。
  • 特殊符号,比如#line, 如果在源码中出现,将会被解释成当前行号。

指令

使用的gcc命令是:gcc –E
将.c 文件转化成 .i文件
对应于预处理命令cpp

2 编译过程——编译 优化 汇编

C++程序的编译过程是分模块进行的,每一个模块独立编译,生成相应的.obj或.o文件,一般情况下,每一个.c或.cpp文件作为一个独立的模块进行编译。需要注意的是,.h文件是不会被编译的。这个编译过程会检查变量和函数是否有被声明,进行词法检查,语法检查…,生成汇编代码,优化汇编代码,最后经过汇编程序翻译成目标机器代码,生成.obj或.o文件。

指令

a.编译
使用的gcc命令是:gcc –S
将.c/.h文件转换成.s文件
对应于编译命令 cc –S

b.汇编
使用的gcc 命令是:gcc –c
将.s 文件转化成 .o文件
对应于汇编命令是 as

因为每一个模块是独立编译的,所以对于定义在其它模块的函数或变量的使用都是未定义的,编译时检查到有声明只是告诉模块——这个函数或变量定义在其它模块中了,当然这样编译后生成的文件是无法直接执行的。因此,我们需要一个过程将各个编译后的模块合理组合起来,并将各个模块中对其它模块的函数调用或变量引用设置到正确的地址,这个过程就叫做链接。链接完各个.obj或.o文件后才能生成一个可执行文件。

链接过程需要编译时收集每一个模块的相关信息才能完成,编译每一个模块的同时还要生成三个表供链接过程使用:
1. 导出符号表:提供了本编译单元具有定义,并且愿意提供给其他编译单元使用的符号及其地址。
2. 未解决符号表:提供了所有在该编译单元里引用但是定义并不在本编译单元里的符号及其出现的地址。
3. 地址重定向表:因为每个模块的逻辑地址都是从0开始的,这样的地址是相对的,在链接成可执行文件之前需要知道每个.obj文件在可执行文件中的起始位置,这样(相应.obj的起始位置+导出符号表中导出符号对应的位置)才是一个正确的的地址。这样,我们就需要告诉链接器,哪些地址是需要重定位的。这样,链接器在进行链接时,首先会决定每一个.obj文件在可执行文件中的起始位置,然后查看模块的地址重定向表,在需要重定向的位置上加上相应模块实际在可执行文件中的起始位置,这样的地址才是正确的地址。

对于每个模块,一般而言:
1. 用extern关键字修饰的符号是外部链接,它告诉编译器,这个符号定义在其它模块,应该把这个符号放入未解决符号表。
2. 用static关键字修饰的符号都是内部链接符号,也就是说这些符号仅仅在模块内部可见,不会提供给其它模块引用,也就不会被放进导出符号表。
3. 默认情况下,const常量是内部链接,不会被加到导出符号表中。
4. 默认情况下,函数和全局变量都是外部链接符号,这些符号会被放入模块的导出符号表,以供其它模块使用,但是可以用static关键字修饰,把它变成内部链接,这样就不会被放进导出符号表。

对上述描述中几点疑问和解释:

  1. 为什么函数默认是外部链接?
    如果函数默认是内部链接,那么大家会倾向于把函数连同其定义都放入头文件中。然而,函数是多变的,可能会经常修改,这样一来,所以包含它的模块都需要被重新编译,很麻烦。另外一方面,如果函数中定义了静态变量,这样每一个包含该函数的模块都会有一个静态变量(因为假设是默认内部链接),导致不一致。
  2. 为什么const常量默认是内部链接而变量(全局)默认是外部链接?
    因为它是常量,初始化后就不能改变,这样即使每一个包含它的模块都有一份它的复制,那也不会导致不一致。如果变量默认是内部链接,它是可变的量,所以在每个包含它的模块中,它的值可能会被改变,从而导致不一致的状况出现。
  3. 为什么类的静态数据成员不可以就地初始化?
    因为类体一般是放在头文件中的,如果允许其静态成员就地初始化,那就相当于允许在头文件中定义变量了。(当实例化多个相同类对象后,会多次初始化该静态变量,造成重名问题)

3 链接过程

链接器一般会一次做出如下动作:
1. 决定每一个obj(模块)在可执行文件中的位置。
2. 查看每一个模块的重定向表,给需要重定向的值加上它所在模块在可执行文件中的起始位置,形成正确的地址。
3. 检查所有模块的导出符号表,如果发现导出符号有重复,会产生链接错误: duplicated external symbols…,然后停止链接过程。
4. 在导出符号表中搜索未解决符号表中的符号,找到后会在相应位置填上正确地址,如果找不到,就会产生链接错误: unresolved external link…,然后停止链接过程。
5. 完成链接过程,生成可执行文件。

4 C++编译中的特殊情况:模板函数/模板类和内联函数

在了解C++程序一般编译过程后,我们不难理解为什么头文件中一般只能放声明而不能放定义,这是因为头文件会被很多模块包含,如果头文件中有定义,那么链接这些包含这个头文件的模块时就会出现符号重定义的错误了。
当然,我说的是一般情况,当然也有特殊情况,那就是内联函数和模板类,它们的定义是允许并且必须放在头文件中的。

4.1 内联函数 — 默认是内部链接 — 需要把定义一起放在头文件中

用关键字inline修饰的函数称为内联函数,就效果上来说,内联函数和宏定义是一样的,它会将函数调用用函数体进行代码替换,这样省去了调用函数过程的开销,对于频繁使用并且短小精悍的函数来说,改成内联,可以提高程序效率。使用内联函数要注意一下几点:

  • 类体内实现的成员函数默认内联。
  • 用inline修饰只是建议编译器对函数使用内联,至于内不内联,由编译器视函数实际情况来决定,如果函数语句很多,或函数中有循环,条件判断等语句,编译器一般不会内联,如果内联函数的使用在内联函数定义之前,也不会内联。
  • inline是实现用的关键字,也就是说放在定义一起才有用,放在函数声明处是没用的。

为什么内联函数需要把定义一起放在头文件中?
与宏定义在预处理时就进行替换不同,内联函数的替换是在编译时执行的,因为每个模块都是独立编译的,此时如果模块本身不知道函数的定义,也就无法内联展开了,这就是为什么要知道内联函数定义的原因。那么就会存在问题了,头文件中包含了定义,就不怕重定义吗?大家不用担心这个,编译器会将内联函数视为内链接。

如果没有把内联函数的定义一起放在头文件里会发生什么?
会出现链接错误:unresolved external symbols …,这是因为在编译时,因为找不到内联函数的定义而无法内联展开,这时编译器会认为它应该是在其它模块定义了。前面说过,内联函数是内部链接,所以这个函数符号在导出符号表中找不到,于是会出现unresolved链接错误。

模板类与模板函数 — 需要把定义一起放在头文件中
要理解模板类和模板函数的编译过程,大家要记住以下几点:

  • 模板就是模板,它本身不能被编译成二进制代码,它的作用只是在编译时根据类型生成相应代码而已。
  • 只有在模板函数(不管是普通的还是类的成员函数)被调用时,函数模板才会被实例化,也就是才会生成相应类型的函数以供调用。
  • 函数模板实例化时会增大源文件代码量,并且生成的函数都是内链接。

和内联函数一样,为了能在编译时能生成相应实现代码,我们需要知道模板函数的定义,这也就要求模板类或模板函数的定义也要一起放在头文件中。

如果没有把模板类或模板函数的定义一起放在头文件中会怎样?
如果包含它的模块调用了模板函数,那么会出现Unresolved external simbols链接错误。因为如果编译时,模块找不到模板函数的定义,它会认为这个函数肯定是定义在其它模块了,把这个问题留给链接程序去解决。当然,链接程序在导出符号表中找不到这个函数(因为它压根没有被实例化过),所以出现以上错误。

关于链接的详解可见

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值