本文转载,尊重原创!受益良多!点击打开链接

最近,有同事向我多次问及C++关于编译链接方面的问题,包括如下:

1:什么样的函数以及变量可以定义在头文件中

2:extern "C"的作用

3:防止重复包含的宏的作用

4:函数之间是怎么链接起来的

我认为,这些问题不难,书上基本上都有,但要是没有真正思考过,就凭死记硬背,也就是只能“嘴上说说”而已,遇到问题还真棘手,所以我觉得有必要说一下。

C/C++的编译链接过程

其实,“编译”这个词大多数时候,我们指的是由一堆.h,.c,.cpp文件生成链接库或者可执行文件的过程。但是拿C/C++来说,其实这是很模糊的,由一堆C/C++文件生成应用程序包括预处理---编译文件---链接(写的比较粗糙,不影响本文论述)。

首先,要明白什么是编译单元,一个编译单元可以认为是一个.c或者.cpp文件,每一个编译单元首先会经过预处理得到一个临时的编译单元,这里称为tmp.cpp,预处理会把.c或者.cpp直接或者间接包含的其它文件(不只局限于.h文件,只要是#include即可)的内容替换进来,并展开宏调用等。

下面首先看一个例子:

a.h

#ifndef A_H_
#define A_H_                                                                                                          static int a = 1;
void fun();                                                                                                           #endif

a.cpp

#include "a.h"static void hello_world()
{
}

只有a.h和a.cpp这两个文件,及其简单。首先通过g++的-E参数得到a.cpp预处理之后的内容

coderchen@coderchen:~/c++$ g++ -E a.cpp > tmp.cpp

查看tmp.cpp

# 1 "a.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "a.cpp"
# 1 "a.h" 1static int a = 1;
void fun();
# 2 "a.cpp" 2static void hello_world()
{
}

tmp.cpp就是只经过预处理得到的文件,这个文件才是编译器能够真正看到的文件。这个过程就是 预处理。

其中#define A_H_的作用是防止重复包含a.h这个头文件,很多人都知道这一点,但是再仔细问,我见过大多数人都说不清楚。

这种宏是为了防止一个编译单元(cpp文件)重复包含同一个头文件。它在预处理阶段起作用,预处理器发现a.cpp内已经定义过A_H_这个宏的话,在a.cpp中再次发现#include "a.h"的时候就不会把a.h的内容替换进a.cpp了。
编译器看到tmp.cpp的时候,会编译成一个obj文件,最后由链接器对这一个对obj文件进行链接,从而得到可执行程序。

编译错误和连接错误

编译错误指的是一个cpp编译单元在编译时发生的错误,这种错误一般都是语法错误,拼写错误,参数不匹配等。

以main.cpp为例(只有一个main函数)

int main()
{                                                                                                                     hello_world();
}    

编译(加-c参数表示只编译不链接)

coderchen@coderchen:~/c++$ g++ -c -o main.o main.cpp
main.cpp: In function ‘int main()’:
main.cpp:4: error: ‘hello_world’ was not declared in this scope

这种错误就是编译,原因是hello_world函数未声明,把void hello_world();这条语句加到main函数前面,再次编译

coderchen@coderchen:~/c++$ g++ -c -o main.o main.cpp
coderchen@coderchen:~/c++$ 

编译成功,虽然我们调用了hello_world函数,却没有定义这个函数。好,接下来,我们把这个main.o文件链接下,

coderchen@coderchen:~/c++$ g++ -o main main.o
main.o: In function `main':
main.cpp:(.text+0x7): undefined reference to `hello_world()'
collect2: ld returned 1 exit status

看到了吧,链接器ld报出了链接错误,原因是hello_world这个函数找不到。这个例子很简单,基本上可以区分出编译错误和链接错误。我们再添加一个hello_world.cpp

void hello_world()
{
}

编译

coderchen@coderchen:~/c++$ g++ -c -o hello_world.o hello_world.cpp

链接

coderchen@coderchen:~/c++之所以$ g++ -o main main.o hello_world.o

ok,我们的main程序已经生成了,我们经历了预处理---编译---链接的过程。

有的人说为什么不需要写一个hello_world.h的头文件,声明hello_world函数,然后再让main.cpp包含hello_world.h呢?这样写自然是标准的做法,不过预处理过后,和我们现在写的一样的,预处理会把hello_world.h的内容替换到main.cpp中。

问题:在链接的时候,main.o怎么知道hello_world函数定义在hello_world.o中呢?

答案:main.o不知道hello_world函数定义在那个obj文件中,每个obj文件都有一个导出符号表,对于这个例子,hello_world.o的导出符号表中有hello_world这个函数,而main.o需要用到这个函数,可以想象就像几个插槽一样。链接器通过扫描obj文件发现这个函数定义在hello_world.o中,然后就可以链接了。

问题:为什么函数不能定义在头文件中?

这个问题是不恰当的,因为用inline和static修饰的函数可以定义在头文件中,而inline修饰的函数必须定义在头文件中。

如果函数定义在头文件中,并且有多个cpp文件都包含了这个头文件的话,那么这些cpp文件生成的obj文件的导出符号表中都有这个头文件中定义的函数,单文件编译的时候是不会出错的,但是链接的时候就会报错。链接器发现了多个函数实体,但却无法确定应该使用哪一个。这是一个链接错误。

inline修饰的函数,通常都不会存在函数实体,即便编译器没有对其内联,那么obj文件也不会导出inline函数,所以链接不会出错。

static修饰的函数,只能由定义它的编译单元调用,也不会导出。如果头文件中顶一个static修饰的函数,就相当于多个obj文件中都顶一个了一个一模一样的函数,大家各用各的,互补干扰。

问题:什么样的变量可以定义在头文件中?

其实变量于函数很类似,由static或const修饰的变量可以定义在头文件中。

static修饰的变量于static修饰的函数一样,道理同上。

const修饰的变量默认是不会进入导出符号表的,相当于每个obj中都定义了一个一模一样的const变量,各用各的。而const可以再用extern修饰,如果用extern const修饰的变量定义在头文件中,那么就会出现链接错误,原因就是“想一想extern是干嘛的”

问题:extern "C"是干嘛的?

如果有人回答“兼容C和C++”,我只能说“这是一个正确答案,但我不知道你是否真的知道”。

首先要知道C不支持重载,C++支持重载,C++为了支持重载,引入了函数重命名的机制,就像下面这样:

int hello_world(type1 param);
int hello_world(type2 param);

通常第一个函数会被编译成hello_world_type1这样子,第二个函数会被编译成hello_world_type2这样子。 不管是定义的地方还是调用的地方,都会把函数改成同样的名字,所以链接器可以正确的找到函数实体。

而我们写C++程序的时候,通常会引入由c编写的库(gcc编译的c文件),而c不支持重载,自然不会对函数重命名。而我们在C++中调用的地方很可能会重命名,这就造成了调用的地方(C++编译)和定义的地方(C编译)函数名不一致的情况,这也是一种链接错误。

所以我们经常会看到在C++中用extern "C" { #include "some_c.h" }这种代码。这就是告诉c++编译器,some_c.h中的函数要按照c的方式编译,不要重命名,这样在链接的时候就ok了。



C++编译链接的那些小事相关推荐

  1. 易语言静态连接器提取_易语言静态编译链接器切换工具

    使用说明 将exe程序和"链接器目录配置.ini"文件,复制到易语言安装目录的"tools"文件夹下 并且对"链接器目录配置.ini"进行修 ...

  2. 反编译与反汇编、C++编译过程,包括预编译--汇编--编译--链接

    参考:C/C++程序编译流程(预处理->编译->汇编->链接) - ProLyn - 博客园 反汇编和反编译的区别_代码小卒_新浪博客 反汇编与反编译: 汇编:是把汇编源程序转变为目 ...

  3. Boost 编译链接

    头文件就是库 使用者最常问的问题就是"我该怎么安装Boost",这个也是我一开始最关心的问题,Boost这点做的很好,将大部分实现都封装在头文件里,所以对于一些基本的Boost库, ...

  4. GNU ARM汇编--(二)汇编编译链接与运行

    GNU的汇编器是GNU Tools的一部分,可以用来ARM的汇编语言源代码编译为二进制文件.关于GNU汇编器的介绍可以搜索<GNU Assembler Manual>.这里我们只是做一个简 ...

  5. Linux环境下gcc编译链接库-lz -lrt -lm -lc都是什么库?

    编译链接库:-lz -lrt -lm -lc都是什么库 -lz      压缩库(Z) -lrt     实时库(real time):shm_open系列 -lm     数学库(math) -lc ...

  6. VC如何在编译链接程序过程中在输出窗口看到链接的顺序

    VC如何在编译链接程序过程中在输出窗口看到链接的顺序 具体操作:选择VC菜单Project->Settings->Link页,然后在Project Options的Edit栏中输入/ver ...

  7. VC++编译链接原理与过程

    简单编译链接过程 源文件先被预处理,分别编译,头文件不参与编译过程,生成目标文件,在生成可执行文件

  8. Gcc编译链接及常用选项总结

    转载文章:http://www.franktly.com 前言 GNU CC(简称Gcc)是GNU项目中符合ANSI C标准的编译系统,能够编译用C.C++和Object- C等语言编写的程序.Gcc ...

  9. linux 编译链接图

    Linux 文章目录 Linux 2:Java 3:.NET 5:GCC 编译链接图 源文件通过预处理 得到 纯C文件 找到这个文本文件,然后文本文件的内容替换#,贴在这里 纯C文件通过编译器 得到汇 ...

最新文章

  1. Excel 计算除法并显示为万分之几,如0.15‱
  2. 调用wasm_PDX Utopia区块链协议栈使用Solidity调用wasm智能合约
  3. Consul集群搭建
  4. 对象方法(包含es6)
  5. 【1】刷了1000道二级C语言的题,这些题竟然还是易错题!!(附C语言完整知识点)
  6. 自定义vue中的验证码组件
  7. 机器学习系列-tensorflow-03-线性回归Linear Regression
  8. amCharts: JavaScript/HTML5 charts 破解
  9. 态度决定一切细节决定成败_字体设计可以决定设计的成败:选择字体的过程
  10. 单片机(STM32)内部RC振荡器误差时间到底有多少
  11. 执行stap测试例报错:“insmod: can‘t insert ‘xx.ko‘: invalid module format”
  12. 计算机方面的缩写大全
  13. python 12306查询不到车次_(经典!!!详细解析!!!)python实现12306余票查询
  14. 【Oracle】建立关联三个表的视图
  15. P2916 [USACO08NOV]安慰奶牛Cheering up the Cow
  16. 洛谷:P6560 [SBCOI2020] 时光的流逝(博弈、拓扑序列)
  17. .xin 是什么域名?个人能使用吗?
  18. Go实现简易聊天室(群聊)
  19. CANFiber是什么?
  20. 闭包是什么?运用在那些地方?

热门文章

  1. 2018-2019-1 20165323 20165333 20165336 实验五 通讯协议设计
  2. 当数据库没有备份,redo或undo损坏
  3. PopWindow弹出在任意位置。
  4. oracle的imp和exp
  5. Windows7下手动搭建Apache+PHP+MySQL方法记录
  6. 恢复exchange2003的公共文件夹
  7. java StringBuffer常用方法
  8. dataframe常用处理
  9. 设置Django关闭Debug后的静态文件路由
  10. k8s通过label来控制pod的位置