您的位置:首页 > 编程语言 > C语言/C++

c++编译过程理解

2016-05-06 20:12 411 查看
1.Makefile就知道了。先直接用命令行操作,然后用集成的IDE来写代码。

2.对于编译过程,总体上是这样

<1>源代码(*.h,*.cpp/c)经过预编译,编译,生成目标文件(Windows下应该是.obj文件,Linux/unix下是.o文件)

<2>然后通过链接(将各种目标文件.obj(.o) 和 目标文件的集合(动态静态库dll(windows下),so(linux/unix下)))

<3>最终成功可执行文件(Windows下叫exe,Linux/unix下随便以什么结尾了)。

*.obj,*.pch,*.dsp,*.ncb,*.plg 这些,除了obj,其他都是微软集成的编译器做的事情了,微软其实也有个类似makefile的东西,其实你可以不用去关注的。这些C++本身无关。

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

c++程序在编译后,在目标路径下会生成多个文件:

Debug文件夹(*.exe,*.ilk,*.obj,*.pch,*.pdb,*.idb,*,pdb),*.cpp,*.dsp,*.ncb,*.plg

*.exe:是生成的可执行文件

*.ilk:当选定渐增型编译连接时,连接器自动生成ILK文件,记录连接信息

*.obj:是目标文件,源程序编译后的产物

*.pch:全称是PreCompiled Header,就是预先编译好的头文件

*.idb:文件保存的信息,使编译器在重新编译的时候只重编译最新改动过的函数和只对最新类定义改动过的源文件进行重编译,以提高编译速度

*.pdb:全称是Program DataBase,即程序数据库文件,用来记录调试信息

*.dsp:(全称是Developer Studio Project)也是一个配置文件

*.ncb:(全称No Compile Browser)的缩写,其中存放了供ClassView、WizardBar和Component Gallery使用的信息,由VC开发环境自动生成

*.plg:实际上是一个超文本文件,可以用Internet Explorer打开,记录了Build的过程

*.cpp:就是C++源代码文件.

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Part1

这里讲下C++文件的编译过程及其中模板的编译过程;

一:一般的C++应用程序的编译过程。

一般说来,C++应用程序的编译过程分为三个阶段。模板也是一样的。

在cpp文件中展开include文件。
将每个cpp文件编译为一个对应的obj文件。
连接obj文件成为一个exe文件(或者其它的库文件)。

下面分别描述这几个阶段。

1.include文件的展开。

include文件的展开是一个很简单的过程,只是将include文件包含的代码拷贝到包含该文件的cpp文件(或者其它头文件)中。被展开的cpp文件就成了一个独立的编译单元。在一些文章中我看到将.h文件和.cpp文件一起看作一个编译单元,我觉得这样的理解有问题。至于原因,看看下面的几个注意点就可以了。

1):没有被任何的其它cpp文件或者头文件包含的.h文件将不会被编译。也不会最终成为应用程序的一部分。先看一个简单的例子:

1 ==========test.h文件==========

2 // 注意,后面没有分号。也就是说,如果编译的话这里将产生错误。

3 void foo()
在你的应用程序中添加一个test.h文件,如上面所示。但是,不要在任何的其它文件中include该文件。编译C++工程后你会发现,并没有报告上面的代码错误。这说明.h文件本身不是一个编译单元。只有通过include语句最终包括到了一个.cpp文件中后才会成为一个编译单元。

2):存在一种可能性,即一个cpp文件直接的或者间接的包括了多次同一个.h文件。下面就是这样的一种情况:



1 // ===========test.h============

2 // 定义一个变量

3 int i;

4

5 // ===========test1.h===========

6 // 包含了test.h文件

7 #include "test.h"

8

9 // ===========main.cpp=========

10 // 这里同时包含了test.h和test1.h,

11 // 也就是说同时定义了两个变量i。

12 // 将发生编译错误。

13 #include "stdafx.h"

14 #include "test.h"

15 #include "test1.h"

16

17 void foo();

18 void foo();

19

20 int _tmain(int argc, _TCHAR* argv[])

21 {

22 return 0;

23 }



上面的代码展开后就相当于同时在main.cpp中定义了两个变量i。因此将发生编译错误。解决办法是使用#ifndef或者#pragma once宏,使得test.h只能在main.cpp中被包含一次。关于#ifndef和#pragma once请参考这里

3):还要注意一点的是,include文件是按照定义顺序被展开到cpp文件中的。关于这个,请看下面的示例。



1 // ===========test.h============

2 // 声明一个函数。注意后面没有分号。

3 void foo()

4 c++程序在编译后,在目标路径下会生成多个文件:

Debug文件夹(*.exe,*.ilk,*.obj,*.pch,*.pdb,*.idb,*,pdb),*.cpp,*.dsp,*.ncb,*.plg

*.exe:是生成的可执行文件

*.ilk:当选定渐增型编译连接时,连接器自动生成ILK文件,记录连接信息

*.obj:是目标文件,源程序编译后的产物

*.pch:全称是PreCompiled Header,就是预先编译好的头文件

*.idb:文件保存的信息,使编译器在重新编译的时候只重编译最新改动过的函数和只对最新类定义改动过的源文件进行重编译,以提高编译速度

*.pdb:全称是Program DataBase,即程序数据库文件,用来记录调试信息

*.dsp:(全称是Developer Studio Project)也是一个配置文件

*.ncb:(全称No Compile Browser)的缩写,其中存放了供ClassView、WizardBar和Component Gallery使用的信息,由VC开发环境自动生成

*.plg:实际上是一个超文本文件,可以用Internet Explorer打开,记录了Build的过程

*.cpp:就是C++源代码文件.

5 // ===========test1.h===========

6 // 仅写了一个分号。

7 ;

8

9 // ===========main.cpp=========

10 // 注意,这里按照test.h和test1.h的顺序包含了头文件。

11 #include "stdafx.h"

12 #include "test.h"

13 #include "test1.h"

14

15 int _tmain(int argc, _TCHAR* argv[])

16 {

17 return 0;

18 }



如果单独看上面的代码中,test.h后面需要一个分号才能编译通过。而test1.h中定义的分号刚好能够补上test.h后面差的那个分号。因此,安这样的顺序定义在main.cpp中后都能正常的编译通过。虽然在实际项目中并不推荐这样做,但这个例子能够说明很多关于文件包含的内容。

有的人也许看见了,上面的示例中虽然声明了一个函数,但没有实现且仍然能通过编译。这就是下面cpp文件编译时的内容了。

2.CPP文件的编译和链接。

大家都知道,C++的编译实际上分为编译和链接两个阶段,由于这两个阶段联系紧密。因此放在一起来说明。在编译的时候,编译器会为每个cpp文件生成一个obj文件。obj文件拥有PE[Portable Executable,即windows可执行文件]文件格式,并且本身包含的就已经是二进制码,但是,不一定能够执行,因为并不保证其中一定有main函数。当所有的cpp文件都编译好了之后将会根据需要,将obj文件链接成为一个exe文件(或者其它形式的库)。看下面的代码:



1 // ============test.h===============

2 // 声明一个函数。

3 void foo();

4

5 // ============test.cpp=============

6 #include "stdafx.h"

7 #include <iostream>

8 #include "test.h"

9

10 // 实现test.h中定义的函数。

11 void foo()

12 {

13 std::cout<<"foo function in test has been called."<<std::endl;

14 }

15

16 // ============main.cpp============

17 #include "stdafx.h"

18 #include "test.h"

19

20 int _tmain(int argc, _TCHAR* argv[])

21 {

22 foo();

23

24 return 0;

25 }

c++程序在编译后,在目标路径下会生成多个文件:

Debug文件夹(*.exe,*.ilk,*.obj,*.pch,*.pdb,*.idb,*,pdb),*.cpp,*.dsp,*.ncb,*.plg

*.exe:是生成的可执行文件

*.ilk:当选定渐增型编译连接时,连接器自动生成ILK文件,记录连接信息

*.obj:是目标文件,源程序编译后的产物

*.pch:全称是PreCompiled Header,就是预先编译好的头文件

*.idb:文件保存的信息,使编译器在重新编译的时候只重编译最新改动过的函数和只对最新类定义改动过的源文件进行重编译,以提高编译速度

*.pdb:全称是Program DataBase,即程序数据库文件,用来记录调试信息

*.dsp:(全称是Developer Studio Project)也是一个配置文件

*.ncb:(全称No Compile Browser)的缩写,其中存放了供ClassView、WizardBar和Component Gallery使用的信息,由VC开发环境自动生成

*.plg:实际上是一个超文本文件,可以用Internet Explorer打开,记录了Build的过程

*.cpp:就是C++源代码文件.

注意到22行对foo函数进行了调用。上面的代码的实际操作过程是编译器首先为每个cpp文件生成了一个obj,这里是test.obj和main.obj(还有一个stdafx.obj,这是由于使用了VS编辑器)。但这里有个问题,虽然test.h对main.cpp是可见的(main.cpp包含了test.h),但是test.cpp对main.cpp并不可见,那么main.cpp是如何找到foo函数的实现的呢?实际上,在单独编译main.cpp文件的时候编译器并不先去关注foo函数是否已经实现,或者在哪里实现。它只是把它看作一个外部的链接类型,认为foo函数的实现应该在另外的一个obj文件中。在22行调用foo的时候,编译器仅仅使用了一个地址跳转,即jump
0x23423之类的东西。但是由于并不知道foo具体存在于哪个地方,因此只是在jump后面填入了一个假的地址(具体应该是什么还请高手指教)。然后就继续编译下面的代码。当所有的cpp文件都执行完了之后就进入链接阶段。由于.obj和.exe的格式都是一样的,在这样的文件中有一个符号导入表和符号导出表[import table和export table]其中将所有符号和它们的地址关联起来。这样连接器只要在test.obj的符号导出表中寻找符号foo[当然C++对foo作了mapping]的 地址就行了,然后作一些偏移量处理后[因为是将两个.obj文件合并,当然地址会有一定的偏移,这个连接器清楚]写入main.obj中的符号导入表中foo所占有的那一项。这样foo就能被成功的执行了。

简要的说来,编译main.cpp时,编译器不知道f的实现,所有当碰到对它的调用时只是给出一个指示,指示连接器应该为它寻找f的实现体。这也就是说main.obj中没有关于f的任何一行二进制代码。编译test.cpp时,编译器找到了f的实现。于是乎foo的实现[二进制代码]出现在test.obj里。连接时,连接器在test.obj中找到foo的实现代码[二进制]的地址[通过符号导出表]。然后将main.obj中悬而未决的jump
XXX地址改成foo实际的地址。

现在做个假设,foo()的实现并不真正存在会怎么样?先看下面的代码:



1 #include "stdafx.h"

2 //#include "test.h"

3

4 void foo();

5

6 int _tmain(int argc, _TCHAR* argv[])

7 {

8 foo();

9

10 return 0;

11 }



注意上面的代码,我们把#include "test.h"注释掉了,重新声明了一个foo函数。当然也可以直接使用test.h中的函数声明。上面的代码由于没有函数实现。按照我们上面的分析,编译器在发现foo()的调用的时候并不会报告错误,而是期待连接器会在其它的obj文件中找到foo的实现。但是,连接器最终还是没有找到。于是会报告一个链接错误。

LINK : 没有找到 E:\CPP\CPPTemplate\Debug\CPPTemplate.exe 或上一个增量链接没有生成它;

再看下面的一个例子:



1 #include "stdafx.h"

2 //#include "test.h"

3

4 void foo();

5

6 int _tmain(int argc, _TCHAR* argv[])

7 {

8 // foo();

9

10 return 0;

11 }



这里只有foo的声明,我们把原来的foo的调用也去掉了。上面的代码能编译通过。原因就是由于没有调用foo函数,main.cpp没有真正的去找foo的实现(main.obj内部或者main.obj外部),编译器也就不会在意foo是不是已经实现了。

二:模板的编译过程。

在明白了C++程序的编译过程后再来看模板的编译过程。大家知道,模板需要被模板参数实例化成为一个具体的类或者函数才能使用。但是,类模板成员函数的调用且有一个很重要的特征,那就是成员函数只有在被调用的时候才会被初始化。正是由于这个特征,使得类模板的代码不能按照常规的C++类一样来组织。先看下面的代码:



1 // =========testTemplate.h=============

2 template<typename T>

3 class MyClass{

4 public:

5 void printValue(T value);

6 };

7

8 // =========testTemplate.cpp===========

9 #include "stdafx.h"

10 #include "testTemplate.h"

11

12 template<typename T>

13 void MyClass<T>::printValue(T value)

14 {

15 //

16 }



下面是main.cpp的文件内容:



1 #include <iostream>

2 #include "testTemplate.h"

3

4 int main()

5 {

6 // 1:实例化一个类模板。

7 // MyClass<int> myClass;

8

9 // 2:调用类模板的成员函数。

10 // myClass.printValue(2);

11

12 std::cout << "Hello world!" << std::endl;

13 return 0;

14 }




注意到注释掉的两句代码。我们将会按步骤说明模板的编译过程。

1):我们将testTemplate.cpp文件从工程中拿掉,即删除testTemplate.cpp的定义。然后直接编译上面的文件,能编译通过。这说明编译器在展开testTemplate.h后编译main.cpp文件的时候并没有去检查模板类的实现。它只是记住了有这样的一个模板声明。由于没有调用模板的成员函数,编译器链接阶段也不会在别的obj文件中去查找类模板的实现代码。因此上面的代码没有问题。

2):把main.cpp文件中,第7行的注释符号去掉。即加入类模板的实例化代码。在编译工程,会发现也能够编译通过。回想一下这个过程,testTemplate.h被展开,也就是说main.cpp在编译是就能找到MyClass<T>的声明。那么,在编译第7行的时候就能正常的实例化一个类模板出来。这里注意:类模板的成员函数只有在调用的时候才会被实例化。因此,由于没有对类模板成员函数的调用,编译器也就不会去查找类模板的实现代码。所以,上面的函数能编译通过。

3):把上面第10行的代码注释符号去掉。即加入对类模板成员函数的调用。这个时候再编译,会提示一个链接错误。找不到printValue的实现。道理和上面只有函数的声明,没有函数的实现是一样的。即,编译器在编译main.cpp第10行的时候发现了对myClass.PrintValue的调用,这时它在当前文件内部找不到具体的实现,因此会做一个标记,等待链接器在其他的obj文件中去查找函数实现。同样,连接器也找不到一个包括MyClass<T>::PrintValue声明的obj文件。因此报告链接错误。

4):既然是由于找不到testTemplate.cpp文件,那么我们就将testTemplate.cpp文件包含在工程中。再次编译,在VS中会提示一个链接错误,说找不到外部类型_thiscall
MyClass<int>::PrintValue(int)。也许你会觉得很奇怪,我们已经将testTemplate.cpp文件包含在了工程中了阿。先考虑一个问题,我们说过模板的编译实际上是一个实例化的过程,它并不编译产生二进制代码。另外,模板成员函数也只有在被调用的时候才会初始化。在testTemplate.cpp文件中,由于包含了testTemplate.h头文件,因此这是一个独立的可以编译的类模板。但是,编译器在编译这个testTemplate.cpp文件的时候由于没有任何成员函数被调用,因此并没有实例化PrintValue成员。也许你会说我们在main.cpp中调用了PrintValue函数。但是要知道testTemplate.cpp和main.cpp是两个独立的编译单元,他们相互间并不知道对方的行为。因此,testTemplate.cpp在编译的时候实际上还是只编译了testTemplate.h中的内容,即再次声明了模板,并没有实例化PrintValue成员。所以,当main.cpp发现需要PrintValue成员,并在testTemplate.obj中去查找的时候就会找不到目标函数。从而发出一个链接错误。

5):由此可见,模板代码不能按照常规的C/C++代码来组织。必须得保证使用模板的函数在编译的时候就能找到模板代码,从而实例化模板。在网上有很多关于这方面的文章。主要将模板编译分为包含编译和分离编译。其实,不管是包含编译还是分离编译,都是为了一个目标:使得实例化模板的时候就能找到相应的模板实现代码。大家可以参照这篇文章

最后,作一个小总结。C++应用程序的编译一般要经历展开头文件->编译cpp文件->链接三个阶段。在编译的时候如果需要外部类型,编译器会做一个标记,留待连接器来处理。连接器如果找不到需要的外部类型就会发生链接错误。对于模板,单独的模板代码是不能被正确编译的,需要一个实例化器产生一个模板实例后才能编译。因此,不能寄希望于连接器来链接模板的成员函数,必须保证在实例化模板的地方模板代码是可见的。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

最近,有同事向我多次问及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

[html]
view plain
copy





#ifndef A_H_
#define A_H_

static int a = 1;
void fun();

#endif

a.cpp

[cpp]
view plain
copy





#include "a.h"

static void hello_world()
{
}

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

[plain]
view plain
copy





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

查看tmp.cpp

[cpp]
view plain
copy





# 1 "a.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "a.cpp"
# 1 "a.h" 1

static int a = 1;
void fun();
# 2 "a.cpp" 2

static 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函数)

[cpp]
view plain
copy





int main()
{
hello_world();
}

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

[cpp]
view plain
copy





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函数前面,再次编译

[plain]
view plain
copy





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

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

[cpp]
view plain
copy





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

[html]
view plain
copy





void hello_world()
{
}

编译

[cpp]
view plain
copy





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

链接

[plain]
view plain
copy





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++为了支持重载,引入了函数重命名的机制,就像下面这样:

[cpp]
view plain
copy





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了。

reference link:

/article/7968979.html
http://blog.csdn.net/qq575787460/article/details/18671137
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: