原文地址

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

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

  1. 在cpp文件中展开include文件。
  2. 将每个cpp文件编译为一个对应的obj文件。
  3. 连接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 
 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 }

注意到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. c++应用程序文件的编译过程

    这里讲下C++文件的编译过程及其中模板的编译过程: 一:一般的C++应用程序的编译过程.     一般说来,C++应用程序的编译过程分为三个阶段.模板也是一样的. 在cpp文件中展开include文件 ...

  2. 用gcc编译c语言程序以及其编译过程

    对于初学c语言编程的我们来说,学会如何使用gcc编译器工具,对理解c语言的执行过程,加深对c语言的理解很重要!!! 1.预编译 --> 2.编译 --> 3.汇编 --> 4.链接- ...

  3. 【转】 Apk文件及其编译过程

    Apk文件概述 Android系统中的应用程序安装包都是以apk为后缀名,其实apk是Android Package的缩写,即android安装包. 注:apk包文件其实就是标准的zip文件,可以直接 ...

  4. 关于一个程序的编译过程 zkjg面试

    http://blog.csdn.net/gengyichao/article/details/6544266 一 以下是C程序一般的编译过程: 从图中看到: 将编写的一个c程序(源代码 )转换成可以 ...

  5. 第48章 MDK的编译过程及文件类型全解—零死角玩转STM32-F429系列

    第48章     MDK的编译过程及文件类型全解 全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn 野火视频教程优酷观看网址:http://i.youku.co ...

  6. 第48章 MDK的编译过程及文件类型全解

    Frm: http://www.cnblogs.com/firege/p/5806134.html 全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn 野火视频教 ...

  7. 后台程序编译过程报错PCC-F-02104, Unable to connect to Oracle

    偶然重新编译了一下后台程序,发现编译过程报错无法连接数据库.但通过sqlplus登录数据库是正常的.后台程序改动中也做了详细的分析,没有改动相关数据库的参数和配置. 最后通过浏览器查看了很多相关问题的 ...

  8. GCC编译过程

    以下是C程序一般的编译过程: gcc的编译流程分为四个步骤,分别为:· 预处理(Pre-Processing) 对C语言进行预处理,生成*.i文件.· 编译(Compiling) 将上一步生成的*.i ...

  9. Linux系统GCC常用命令和GCC编译过程描述

    前言: GCC 原名为 GNU C 语言编译器(GNU C Compiler),因为它原本只能处理 C语言.GCC 很快地扩展,变得可处理 C++.后来又 扩展能够支持更多编程语言,如Fortran. ...

随机推荐

  1. TCP Operational Overview and the TCP Finite State Machine (FSM) http://tcpipguide.com/free/t_TCPOperationalOverviewandtheTCPFiniteStateMachineF.htm

    http://tcpipguide.com/free/t_TCPOperationalOverviewandtheTCPFiniteStateMachineF.htm   http://tcpipgu ...

  2. cell.getCellType有几种

    CellType 类型 值CELL_TYPE_NUMERIC 数值型 0CELL_TYPE_STRING 字符串型 1CELL_TYPE_FORMULA 公式型 2CELL_TYPE_BLANK 空值 ...

  3. 【CJOJ】Contest4 - A+B Series

    Position:http://oj.changjun.com.cn/contest.php?cid=4 A经典题目 // <A.cpp> - Sun Oct 9 15:28:01 201 ...

  4. 【BZOJ 2456】 mode

    [题目链接] 点击打开链接 [算法] 此题初看是大水题,只需调用std :: sort即可 但是,n最大500000,显然会超时 而且,内存限制1MB,我们连数组也开不了! 那怎么做呢 ? 我们发现, ...

  5. robotframework执行用例时,报错selenium.common.exceptions.WebDriverException: Message: unknown error: cannot get automation extension from unknown error: page could not be found: chrome-extension://aapnijgdinl

    在用robotframework编写移动端测试用例(用chrome浏览器模拟手机浏览器),执行用例时, 报错selenium.common.exceptions.WebDriverException: ...

  6. 【WIP】Objective-C Foundation框架的主要对象

    创建: 2018/02/02 完成字符串: 2018/02/05 任务表: TODO 最新内容确认: 字符串,数据类, 数组类  可变与不变的对象  一览  种类  不变类  可变类  数组  NSA ...

  7. 洛谷P4114 Qtree1(树链剖分+线段树)

    传送门 LCT秒天秒地用什么树剖 这题可以算是树剖的比较裸的题目了 把每一条边的权值下放到他两边的点中深度较深的那个 然后直接用树剖+线段树带进去乱搞就可以了 //minamoto #include& ...

  8. 上帝造题的七分钟2/花神游历各国/GSS4 线段树维护区间开方 By cellur925

    题目传送门 或者 另一个传送门 询问区间和都好说.但是开方?? 其实是这样的,一个数(1e9)以内连续开方6次就会变成1,于是我们就可在开方操作上进行暴力修改.暴力修改的意思其实也就是找到叶子节点进行 ...

  9. [USACO4.1]麦香牛块Beef McNuggets By cellur925

    题目描述 农夫布朗的奶牛们正在进行斗争,因为它们听说麦当劳正在考虑引进一种新产品:麦香牛块.奶牛们正在想尽一切办法让这种可怕的设想泡汤.奶牛们进行斗争的策略之一是“劣质的包装”.“看,”奶牛们说,“如 ...

  10. SetViewportOrgEx(视口) 与 SetWindowOrgEx(窗口) 解析

    SetViewportOrgEx (hdc,x/2,y/2) 视口中心点设置到窗口中心 获取客户群x和y,x/2,y/2 ,这样中心点就到窗口中间了. SetWindowOrgEx(hdc,-x/2, ...