.预处理器-编译器-汇编器-链接器 预处理器会处理相关的预处理指令,一般是以"#"开头的指令。如:#include "xx.h" #define等。 编译器把对应的*.cpp翻译成*.s文件(汇编语言)。 汇编器则处理*.s生成对应的*.o文件(obj目标文件) 最后链接器把所有的*.o文件链接成一个可执行文件(?.exe) 1.部件: 首先要知道部件(可以暂且狭义地理解为一个类)一般分为头文件(我喜欢称为接口,如:*.h)及实现文件(如:*.cpp)。 一般头文件会是放一些用来作声明的东东作为接口而存在的。而实现文件主要是实现的具体代码。 2.编译单个文件: 记住IDE在bulid文件时只编译实现文件(如*.cpp)来产生obj,在vc下你可以对某个?.cpp按下ctrl+f7单独编译它 生成对应一个?.obj文件。在编译?.cpp时IDE会在?.cpp中按顺序处理用#include包括进来的头文件 (如果该头文件中又#include有文件,同样会按顺序跟进处理各个头文件,如此递归。。) 3.内部链接与外部链接: 内、外链接是比较基础的东东,但是也是新手最容易错的地方,所以这里有必要祥细讨论一下。 内部链接产生的符号只在本地?.obj中可见,而外部链接的符号是所有*.obj之间可见的。 如:用inline的是内部链接,在文件头中直接声明的变量、不带inline的全局函数都是外部链接。 在文件头中类的内部声明的函数(不带函数体)是外部链接,而带函数体一般会是内部链接(因为IDE会尽量把它作为内联函数) 认识内部链接与外部链接有什么作用呢?下面用vc6举个例子: // 文件main.cpp内容: void main(){} // 文件t1.cpp内容: #include "a.h" void Test1(){ Foo(); } // 文件t2.cpp内容: #include "a.h" void Test2(){ Foo(); } // 文件a.h内容: void Foo( ){ } 好,用vc生成一个空的console程序(File - new - projects - win32 console application),并关掉预编译选项开关 (project - setting - Cagegoryrecompiled Headers - Not using precompiled headers) 现在你打开t1.cpp按ctrl+f7编译生成t1.obj通过 打开t2.cpp按ctrl+f7编译生成t2.obj通过 而当你链接时会发现: Linking... t2.obj : error LNK2005: "void __cdecl Foo(void)" (?Foo@@YAXXZ) already defined in t1.obj 这是因为: 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.obj和t2.obj中)定义了相同的外部符号(注意:是定义,外部符号可以多处声明但不可多处定义,因为外部符号是全局可见的,假设这时有t3.cpp声明用到了这个符号就不知道应该调用t1.obj中的还是t2.obj中的了),所以会报错。 解决的办法有几种: a.将a.h中的定义改写为声明,而用另一个文件a.cpp来存放函数体。(提示:把上述程序改来试试) (函数体放在其它任何一个cpp中如t1.cpp也可以,不过良好的习惯是用对应cpp文件来存放)。 这时包括a.h的文件除了a.obj中有函数体代码外,其它包括a.h的cpp生成的obj文件都只有对应的符号而没有函数体,如t1.obj、t2.obj就只有符号,当最后链接时IDE会把a.obj的Foo()函数体链接进exe文件中,并把t1.obj、t2.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文件中并在前面加上extern(extern CFoo* obj如此就可通过了。当然extern也可以在任何调用此变量的位置之前声明,不过强烈建议不要这么作,因为到处作用extern,会导致接口不统一。良好的习惯是接口一般就放到对应的头文件。 b. 将a.h中的定义修改成内部链接,即加上inline关键字,这时每个t1.obj和t2.obj都存放有一份Foo函数体,但它们不是外部符号,所以不会被别的obj文件引用到,故不存在冲突。(提示:把上述程序改来试试) 另外我作了个实验来验证”vc是把是否是外部符号的标志记录在obj文件中的“(有点绕口)。可以看看,如下: (1)文件内容: // 文件main.cpp内容: void main(){} // 文件t1.cpp内容: #include "a.h" void Test1(){ Foo(); } // 文件t2.cpp内容: #include "a.h" void Test2(){ Foo(); } // 文件a.h内容: inline void Foo( ){ } (2) 选t1.cpp按ctrl+f7单独编译,并把编译后的t1.obj修改成t1.obj_inline (3) 选t2.cpp按ctrl+f7单独编译,并把编译后的t2.obj修改成t2.obj_inline (4) 把除了t1.obj_inline及t2.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.obj而t2.obj_inline覆盖t2.obj再手动链接应该会是 不会出错的,因为原t1.obj_inline及t2.obj_inline中存放的是内部链接符号。好运行Link.bat,果然 不出所料,链接成功了,看看debug目录下多出了一个exe文件。这就说明了内或外符号在obj有标志标识! (提示:上述为什么不用vc的f7build链接呢,因为文件时间改变了,build会重新生成新的obj, 所以我们用手动链接保证obj不变)[注bj信息可用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工程之间符号导出问题等等,有空再写) 转自http://www.donews.net/xzwenlan/archive/2004/12/23/211668.aspx 转载请注明.
------------------------------------------------
最近我抽空研究、整理了一下VC中几个以前比较模糊的问题,写成这篇短文,希望和碰到过类似问题的朋友共享。 如果我的理解有不正确的地方,欢迎大家指正。 文章的3、4小节参照了vcforever的专栏(http://blog.csdn .net /vcforever/archive/2004/12/14/215936.aspx)。其它信息来源于MSDN和自己的摸索。 1、Run-Time Library Run-Time Library是编译器提供的标准库,提供一些基本的库函数和系统调用。 我们一般使用的Run-Time Library是C Run-Time Libraries。当然也有Standard C++ libraries。 C Run-Time Libraries实现ANSI C的标准库。VC安装目录的CRT目录有C Run-Time库的大部分源代码。 C Run-Time Libraries有静态库版本,也有动态链接库版本;有单线程版本,也有多线程版本;还有调试和非调试版本。 可以在"project"-"settings"-"C/C++"-"Code Generation"中选择Run-Time Library的版本。 动态链接库版本: /MD Multithreaded DLL 使用导入库MSVCRT.LIB /MDd Debug Multithreaded DLL 使用导入库MSVCRTD.LIB 静态库版本: /ML Single-Threaded 使用静态库LIBC.LIB /MLd Debug Single-Threaded 使用静态库LIBCD.LIB /MT Multithreaded 使用静态库LIBCMT.LIB /MTd Debug Multithreaded 使用静态库LIBCMTD.LIB C Run-Time Library的标准io部分与操作系统的关系很密切,在Windows上,CRT的io部分代码只是一个包装,底层要用到操作系统内核kernel32.dll中的函数,在编译时使用导入库kernel32.lib。这也就是为什么在嵌入式环境中,我们一般不能直接使用C标准库。 在Linux环境当然也有C标准库,例如: ld -o output /lib/crt0.o hello.o -lc 参数"-lc"就是在引用C标准库libc.a。猜一猜"-lm"引用哪个库文件? 2、常见的编译参数 VC建立项目时总会定义"Win32"。控制台 程序 会定义"_CONSOLE",否则会定义"_WINDOWS"。Debug版定义"_DEBUG",Release版定义"NDEBUG" 。 与MFC DLL有关的编译常数包括: _WINDLL 表示要做一个用到MFC的DLL _USRDLL 表示做一个用户DLL(相对MFC扩展DLL而言) _AFXDLL 表示使用MFC动态链接库 _AFXEXT 表示要做一个MFC扩展DLL 所以: Regular, statically linked to MFC _WINDLL,_USRDLL Regular, using the shared MFC DLL _WINDLL,_USRDLL,_AFXDLL Extension DLL _WINDLL,_AFXDLL,_AFXEXT CL.EXE编译所有源文件,LINK.EXE链接EXE和DLL,LIB.EXE产生静态库。 3、subsystem和可执行文件的启动 LINK的时候需要指定/subsystem,这个链接选项告诉Windows如何运行可执行文件。 控制台程序是/subsystem:"console" 其它程序一般都是/subsystem:"windows " 将 subsystem 选成"console"后,Windows在进入可执行文件的代码前(如mainCRTStartup),就会产生一个控制台窗口。 如果选择"windows",操作系统就不产生console窗口,该类型应用程序的窗口由用户自己创建。 可执行文件都有一个Entry Point,LINK时可以用/entry指定。缺省情况下,如果subsystem是“console”,Entry Point是 mainCRTStartup(ANSI)或wmainCRTStartuup(UNICODE),即: /subsystem:"console" /entry:"mainCRTStartup" (ANSI) /subsystem:"console" /entry:"wmainCRTStartuup" (UNICODE) mainCRTStartup 或 wmainCRTStartuup 会调用main或wmain。 值得一提的是,在进入应用程序的Entry Point前,Windows的装载器已经做过C变量的初始化,有初值的全局变量拥有了它们的初值,没有初值的变量被设为0。 如果subsystem是“windows”,Entry Point是WinMain(ANSI)或wWinMain(UINCODE),即: /subsystem:"windows" /entry:"WinMainCRTStartup" (ANSI) /sbusystem:"windows" /entry:"wWinMainCRTStartup" (UINCODE) WinMainCRTStartup 或 wWinMainCRTStartup 会调用 WinMain 或 wWinMain。 如果使用MFC框架,WinMain也会被埋藏在MFC库中(APPMODUL.CPP): extern "C" int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { // call shared/exported WinMain return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow); } "_t"是一个宏,对于ANSI版本,"_tWinMain"就是"WinMain";对于UINCODE版本,"_tWinMain"就是"wWinMain"。 全局C++对象的构造函数是在什么地方调用的?答案是在进入应用程序的Entry Point后,在调用main函数前的初始化操作中。所以MFC的theApp的构造函数是在_tWinMain之前调用的。 4、不显示Console窗口的Console程序 在默认情况下/subsystem 和/entry开关是匹配的,也就是: "console"对应"mainCRTStartup"或者"wmainCRTStartup" "windows"对应"WinMain"或者"wWinMain" 我们可以通过手动修改的方法使他们不匹配。例如: #include "windows.h" #pragma comment( linker, "/subsystem:/"windows/" /entry:/"mainCRTStartup/"" ) // 设置入口地址 void main(void) { MessageBox(NULL, "hello", "Notice", MB_OK); } 这个Console程序就不会显示Console窗口。如果选/MLd的话,这个程序只需要链接LIBCD.LIB user32.lib kernel32.lib。 5、VC中缺省库冲突的 解决 VC的编译器在编译程序时有两个习惯: a、在从头开始编译时,将源文件名按字母排序后,依次处理; b、一边编译一边决定需要哪些缺省库。 它的这些习惯有时会造成奇怪的编译错误,例如项目中有两个文件: charutil.c gbnni.cpp 其中gbnni.cpp用到了MFC库。 它老兄当然是先处理charutil.c,然后觉得需要link一个C Runtime库,根据项目设置选择了LIBCMTD.lib。 然后又处理gbnni.cpp,因为要用MFC,又决定要link nafxcwd.lib。 最后link的时候,就会出现以下冲突: nafxcwd.lib(afxmem.obj) : error LNK2005: "void __cdecl operator delete(void *)" (??3@YAXPAX@Z) already defined in LIBCMTD.lib(dbgdel.obj) 其实,如果先link了nafxcwd.lib,就没有必要再link LIBCMTD.lib,也就不会产生冲突。 解决这类问题有两个办法。 a、让项目的第一个文件包含MFC的头文件,这样编译器就不会想到找C Runtime库。这样就要把c文件改成cpp了。 b、将需要link C Runtime库的文件的名字改大一些,让它排在后面。 使用IDE当然很方便,但既然使用了别人写的工具,有时就不得不琢磨、迁就它的习性。
1. | 在“项目”菜单上,单击“设置”。 |
2. | 在“项目设置”对话框的“以下项目的设置”视图中,单击以选中出现链接错误的项目配置。 |
3. | 在“链接”选项卡上,单击以选中“类别”组合框中的“输入”。 |
4. | 在“忽略库”框中,插入库名(例如,Nafxcwd.lib;Libcmtd.lib)。 注意:等效的链接器命令行是:/NOD:<library name>。 |
5. | 在“对象/库模块”框中,插入库名。必须确保这些库按顺序列出,而且是行中的前两个库(例如,Nafxcwd.lib 和 Libcmtd.lib)。 |
1. | 在“项目”菜单上,单击“设置”。 |
2. | 在“项目设置”对话框的“以下项目的设置”视图中,单击以选中出现链接错误的项目配置。 |
3. | 在“链接”选项卡上的“项目选项”框中键入 /verbose:lib。 |
4. | 重新生成项目。在链接过程中,这些库将在输出窗口中列出。 |