• 欢迎访问开心洋葱网站,在线教程,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站,欢迎加入开心洋葱 QQ群
  • 为方便开心洋葱网用户,开心洋葱官网已经开启复制功能!
  • 欢迎访问开心洋葱网站,手机也能访问哦~欢迎加入开心洋葱多维思维学习平台 QQ群
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏开心洋葱吧~~~~~~~~~~~~~!
  • 由于近期流量激增,小站的ECS没能经的起亲们的访问,本站依然没有盈利,如果各位看如果觉着文字不错,还请看官给小站打个赏~~~~~~~~~~~~~!

编译链接处理

嵌入式开发 弦苦 3130次浏览 0个评论

预处理器编译器汇编链接器

预处理器会处理相关的预处理指令,一般是以“#”开头的指令。如:#include “xx.h” #define等。

编译器把对应的*.cpp翻译成*.s文件(汇编语言);

汇编器则处理*.s生成对应的*.o文件(obj目标文件);

最后链接器把所有的*.o文件链接成一个可执行文件(?.exe)。

 

1.部件:

首先要知道部件(可以暂且狭义地理解为一个类)一般分为头文件(我喜欢称为接口,如:*.h)及实现文件(:*.cpp)

一般头文件会是放一些用来作声明的东东作为接口而存在的,而实现文件主要是实现的具体代码。

 

2.编译单个文件:

记住IDEbulid文件时只编译实现文件(*.cpp)来产生obj,在vc下你可以对某个?.cpp按下ctrl+f7单独编译它生成对应一个?.obj文件。在编译?.cppIDE会在?.cpp中按顺序处理用#include包括进来的头文件(如果该头文件中又#include有文件,同样会按顺序跟进处理各个头文件,如此递归。)

 

3.内部链接与外部链接:

内、外链接是比较基础的东东,但是也是新手最容易错的地方,所以这里有必要详细讨论一下。

内部链接static)产生的符号只在本地?.obj中可见,而外部链接extern)的符号是所有*.obj之间可见的。如:inline的是内部链接,在文件头中直接声明的变量、不带inline的全局函数都是外部链接。

在文件头中类的内部声明的函数(不带函数体)是外部链接,而带函数体一般会是内部链接(因为IDE会尽量把它作为内联函数)。

认识内部链接与外部链接有什么作用呢?下面用vc6举个例子:

// a.h void Foo( ){ } // t1.cpp #include “a.h” void Test1(){ Foo(); } // t2.cpp #include “a.h” void Test2(){ Foo(); } // main.cpp int main(){ return 0; }

好,用vc生成一个空的console程序(File – new – projects – win32 console application),并关掉预编译选项开关(projectàsettingàC/C++(CagegoryPrecompiled Headers)àNot using precompiled headers)

1)打开t1.cpp,按ctrl+f7编译生成t1.obj通过。

Compiling…

t1.cpp

 

t1.obj – 0 error(s), 0 warning(s)

2)打开t2.cppctrl+f7编译生成t2.obj通过。

Compiling…

t2.cpp

 

t2.obj – 0 error(s), 0 warning(s)

3)按F7build链接时会发现:

Linking…

t2.obj : error LNK2005: “void __cdecl Foo(void)” (?Foo@@YAXXZ) already defined in t1.obj

Debug/main.exe : fatal error LNK1169: one or more multiply defined symbols found

Error executing link.exe.

 

main.exe – 2 error(s), 0 warning(s)

这是因为:

1.编译t1.cpp在处理到#include “a.h”中的Foo时看到的Foo函数原型定义是外部链接的,所以在t1.obj中记录Foo符号是外部的。

2.编译t2.cpp在处理到#include “a.h”中的Foo时看到的Foo函数原型定义是外部链接的,所以在t2.obj中记录Foo符号是外部的。

3.最后在链接 t1.obj t2.obj, vc发现有两处地方(t1.objt2.obj中)定义了相同的外部符号(注意:是定义,外部符号可以多处声明但不可多处定义,因为外部符号是全局可见的,假设这时有t3.cpp声明用到了这个符号就不知道应该调用t1.obj中的还是t2.obj中的了),所以会报错。

解决的办法有几种:

a.将a.h中的定义改写为声明,而用另一个文件a.cpp来存放函数体。(提示:把上述程序改来试试)(函数体放在其它任何一个cpp中如t1.cpp也可以,不过良好的习惯是用对应cpp文件来存放)。

这时包括a.h的文件除了a.obj中有函数体代码外,其它包括a.hcpp生成的obj文件都只有对应的符号而没有函数体,如t1.objt2.obj就只有符号,当最后链接时IDE会把

a.objFoo()函数体链接进exe文件中,并把t1.objt2.obj中的Foo符号转换成对应在函数体exe文件中的地址。

另外:当变量放在a.h中会变成全局变量的定义,如何让它变为声明呢?

例如:我们在a.h中加入:class CFoo{};CFoo* obj;

这时按f7进行build时出现:

Linking…

t2.obj : error LNK2005: “class CFoo * obj” (?obj@@3PAVCFoo@@A) already defined in t1.obj

一个好办法就是在a.cpp中定义此变量CFoo* obj,然后拷贝此定义到a.h文件中并在前面加上externextern CFoo* obj,如此就可通过了。当然extern也可以在任何调用此变量的位置之前声明,不过强烈建议不要这么作,因为到处作用extern,会导致接口不统一。良好的习惯是接口一般就放到对应的头文件。

b.将a.h中的定义修改成内部链接,即加上inline关键字,这时每个t1.objt2.obj都存放有一份Foo函数体,但它们不是外部符号,所以不会被别的obj文件引用到,故不存在冲突。(提示:把上述程序改来试试)

在上述代码中的a.hvoid Foo()前添加inline修饰符,作个实验来验证“vc是把是否是外部符号的标志记录在obj文件中的”(有点绕口)

(2)t1.cppctrl+f7单独编译,并把编译后的t1.obj修改成t1.obj_inline

(3)t2.cppctrl+f7单独编译,并把编译后的t2.obj修改成t2.obj_inline

(4)把除了t1.obj_inlinet2.obj_inline外的其它编译生成的文件删除。

(5)修改a.h内容为:void Foo( ){ },使之变为非内联函数作测试

(6) rebuild all所有文件。这时提示:

Linking…

t2.obj : error LNK2005: “void __cdecl Foo(void)” (?Foo@@YAXXZ) already defined in t1.obj

Debug/cle.exe : fatal error LNK1169: one or more multiply defined symbols found

(7)好,看看工程目录下的debug目录中会看到新生成的obj文件。

下面我们来手工链接看看,

打开菜单中的project – setting – Link,拷贝Project options下的所有内容,如下:

kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /incremental:yes /pdb:”Debug/cle.pdb” /debug /machine:I386 /out:”Debug/cle.exe” /pdbtype:sept

把它修改成:

Link.exe kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /incremental:yes /pdb:”Debug/cle.pdb” /debug /machine:I386 /out:”Debug/cle.exe” /pdbtype:sept Debug/t1.obj Debug/t2.obj Debug/main.obj

Pause

注意前面多了Link.exe,后面多了Debug/t1.obj Debug/t2.obj Debug/main.obj以及最后一个pause批处理命令,然后把它另存到工程目录(此目录下会看到debug目录)下起名为link.bat,运行它,就会看到:

t2.obj : error LNK2005: “void __cdecl Foo(void)” (?Foo@@YAXXZ) already defined in t1.obj

Debug/cle.exe : fatal error LNK1169: one or more multiply defined symbols found

很好,我们链接原来的obj文件得到的效果跟在vc中用rebuild all出来的效果一样。那么现在如果我们把备份出来的t1.obj_inline覆盖t1.objt2.obj_inline覆盖t2.obj再手动链接应该会是不会出错的,因为原t1.obj_inlinet2.obj_inline中存放的是内部链接符号。好,运行Link.bat,果然不出所料,链接成功了,看看debug目录下多出了一个exe文件。这就说明了内或外符号在obj有标志标识!(提示:上述为什么不用vcF7build)链接呢,因为文件时间改变了,build会重新生成新的obj,所以我们用手动链接保证obj不变)【注obj信息可用dumpbin.exe查看】

 

4.#include规则:

有很多人不知道#include文件该放在何处?

1).增强部件自身的完整性:

为了保证部件完整,部件的cpp实现文件(test.cpp)中第一个#include的应当是它自身对应的头文件(test.h)

(除非你用预编译头文件,预编译头必须放在第一个)。这样就保证了该部件头文件(test.h)所必须依赖的其它接口(a.h)要放到它对应的文件头中(test.h),而不是在cpp(test.cpp)把所依赖的其它头文件(a.h)移到其自身对应的头文件(test.h)之前(因为这样强迫其它包括此部件的头文件(test.h)的文件(b.cpp)也必须再写一遍include(b.cpp若要#include “test.h”也必须#include “a.h”)”。另外我们一般会尽量减少文件头之间的依赖关系,看下面:

2).减少部件之间的依赖性:

1的基础上尽量把#include到的文件放在cpp中包括。

这就要求我们一般不要在头文件中直接引用其它变量的实现,而是把此引用搬到实现文件中。

例如:

// foo.h class CFoo{ void Foo(){} }; // test.h #include “foo.h” class CTest{ CFoo* m_pFoo; public: CTest() : m_pFoo(NULL){} void Test(){ if(m_pFoo){ m_pFoo->Foo();} } // ……….. }; // test.cpp #include “test.h” // …..

 

如上文件test.h中我们其实可以#include “foo.h”移到test.cpp文件中。因为CFoo* m_pFoo我们只想在部件CTest中用到,而将来想用到CTest部件而包括test.h的其它部件没有必要见到foo.h接口,所以我们用前向声明修改原文件如下:

// foo.h class CFoo{ public: void Foo(){} }; // test.h class CFoo; // 引用性声明 class CTest{ CFoo* m_pFoo; public: CTest(); void Test(); //…….. }; // test.cpp #include “test.h” // 这里第一个放该部件自身对应的接口头文件 #include “foo.h” // 该部件用到了foo.h CTest::CTest() : m_pFoo(0){ m_pFoo = new CFoo; } void CTest::Test(){ if(m_pFoo){ m_pFoo->Foo(); } } // main.cpp #include “test.h” // 这里我们就不用见到#include “foo.h”了 CTest test; void main(){ test.Test(); }

3).双重包含卫哨:

在文件头中包括其它头文件时(如:#include “xx.h”)建议也加上包含卫哨:

// test.h文件内容: #ifndef __XX1_H_ #include “xx1.h” #endif #ifndef __XX2_H_ #include “xx2.h” #endif // ……

虽然我们已经在xx.h文件中开头已经加过,但是因为编译器在打开#include文件也是需要时间的,如果在外部加上包含卫哨,对于很大的工程可以节省更多的编译时间。

 

5.待续(还有很多相关的东东,比如不同dll工程之间符号导出问题等等,有空再写)

 

原文:

   编译器相关


开心洋葱 , 版权所有丨如未注明 , 均为原创丨未经授权请勿修改 , 转载请注明编译链接处理
喜欢 (0)

您必须 登录 才能发表评论!

加载中……