日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

转:c/c++ 运行库

發布時間:2025/3/15 c/c++ 62 豆豆
生活随笔 收集整理的這篇文章主要介紹了 转:c/c++ 运行库 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

轉:c/c++?運行庫

(2011-09-21 11:14:53) 轉載▼
標簽:

雜談

分類:Programming_Languages

http://book.csdn.net/bookfiles/1017/100101730949.shtml

11.2? C/C++運行庫

11.2.1? C語言運行庫

任何一個C程序,它的背后都有一套龐大的代碼來進行支撐,以使得該程序能夠正常運行。這套代碼至少包括入口函數,及其所依賴的函數所構成的函數集合。當然,它還理應包括各種標準庫函數的實現。

這樣的一個代碼集合稱之為運行庫(RuntimeLibrary)。而C語言的運行庫,即被稱為C運行庫(CRT)。

如果讀者擁有VisualStudio,可以在VC/crt/src里找到一份C語言運行庫的源代碼。然而,由于此源代碼過于龐大,僅僅.c文件就有近千個,并且和C++的STL代碼一起毫無組織地堆放在一起,以至于實際上沒有什么仔細閱讀的可能性。同樣,Linux下的libc源代碼讀起來也如同啃磚頭。所幸的是,在本章的最后,我們會一起來實現一個簡單的運行庫,讓大家更直觀地了解它。

一個C語言運行庫大致包含了如下功能:

l??????????啟動與退出:包括入口函數及入口函數所依賴的其他函數等。

l??????????標準函數:由C語言標準規定的C語言標準庫所擁有的函數實現。

l??????????I/O:I/O功能的封裝和實現,參見上一節中I/O初始化部分。

l??????????堆:堆的封裝和實現,參見上一節中堆初始化部分。

l??????????語言實現:語言中一些特殊功能的實現。

l??????????調試:實現調試功能的代碼。

在這些運行庫的組成成分中,C語言標準庫占據了主要地位并且大有來頭。C語言標準庫是C語言標準化的基礎函數庫,我們平時使用的printf、exit等都是標準庫中的一部分。標準庫定義了C語言中普遍存在的函數集合,我們可以放心地使用標準庫中規定的函數而不用擔心在將代碼移植到別的平臺時對應的平臺上不提供這個函數。在下一章節里,我們會介紹C語言標準庫的函數集合,并對一些特殊的函數集合進行詳細介紹。

標準庫的歷史

在計算機世界的歷史中,C語言在AT&T的貝爾實驗室誕生了。初生的C語言在功能上非常不完善,例如不提供I/O相關的函數。因此在C語言的發展過程中,C語言社區共同意識到建立一個基礎函數庫的必要性。與此同時,在20世紀70年代C語言變得非常流行時,許多大學、公司和組織都自發地編寫自己的C語言變種和基礎函數庫,因此當到了80年代時,C語言已經出現了大量的變種和多種不同的基礎函數庫,這對代碼遷移等方面造成了巨大的障礙,許多大學、公司和組織在共享代碼時為了將代碼在不同的C語言變種之間移植搞得焦頭爛額,怨聲載道。于是對此慘狀忍無可忍的美國國家標準協會(American National Standards Institute,ANSI)在1983年成立了一個委員會,旨在對C語言進行標準化,此委員會所建立的C語言標準被稱為ANSIC。第一個完整的C語言標準建立于1989年,此版本的C語言標準稱為C89。在C89標準中,包含了C語言基礎函數庫,由C89指定的C語言基礎函數庫就稱為ANSI C標準運行庫(簡稱標準庫)。其后在1995年C語言標準委員會對C89標準進行了一次修訂,在此次修訂中,ANSIC標準庫得到了第一次擴充,頭文件iso646.h、wchar.h和wctype.h加入了標準庫的大家庭。在1999年,C99標準誕生,C語言標準庫得到了進一步的擴充,頭文件complex.h、fenv.h、inttypes.h、stdbool.h、stdint.h和tgmath.h進入標準庫。自此,C語言標準庫的面貌一直延續至今。

11.2.2? C語言標準庫

在本章節里,我們將介紹C語言標準庫的基本函數集合,并對其中一些特殊函數進行詳細的介紹。ANSIC的標準庫由24個C頭文件組成。與許多其他語言(如Java)的標準庫不同,C語言的標準庫非常輕量,它僅僅包含了數學函數、字符/字符串處理,I/O等基本方面,例如:

l??????????標準輸入輸出(stdio.h)。

l??????????文件操作(stdio.h)。

l??????????字符操作(ctype.h)。

l??????????字符串操作(string.h)。

l??????????數學函數(math.h)。

l??????????資源管理(stdlib.h)。

l??????????格式轉換(stdlib.h)。

l??????????時間/日期(time.h)。

l??????????斷言(assert.h)。

l??????????各種類型上的常數(limits.h & float.h)。

除此之外,C語言標準庫還有一些特殊的庫,用于執行一些特殊的操作,例如:

l??????????變長參數(stdarg.h)。

l??????????非局部跳轉(setjmp.h)。

相信常見的C語言函數讀者們都已經非常熟悉,因此這里就不再一一介紹,接下來讓我們看看兩組特殊函數的細節。

1. 變長參數

變長參數是C語言的特殊參數形式,例如如下函數聲明:

int printf(const char* format, ...);

如此的聲明表明,printf函數除了第一個參數類型為constchar*之外,其后可以追加任意數量、任意類型的參數。在函數的實現部分,可以使用stdarg.h里的多個宏來訪問各個額外的參數:假設lastarg是變長參數函數的最后一個具名參數(例如printf里的format),那么在函數內部定義類型為va_list的變量:

va_list ap;

該變量以后將會依次指向各個可變參數。ap必須用宏va_start初始化一次,其中lastarg必須是函數的最后一個具名的參數。

va_start(ap, lastarg);

此后,可以使用va_arg宏來獲得下一個不定參數(假設已知其類型為type):

type next = va_arg(ap, type);

在函數結束前,還必須用宏va_end來清理現場。在這里我們可以討論這幾個宏的實現細節。在研究這幾個宏之前,我們要先了解變長參數的實現原理。變長參數的實現得益于C語言默認的cdecl調用慣例的自右向左壓棧傳遞方式。設想如下的函數:

int sum(unsigned num, ...);

其語義如下:

第一個參數傳遞一個整數num,緊接著后面會傳遞num個整數,返回num個整數的和。

當我們調用:

int n = sum(3, 16, 38, 53);

參數在棧上會形成如圖11-7所示的布局。

?

圖11-7?函數參數在棧上分布

在函數內部,函數可以使用名稱num來訪問數字3,但無法使用任何名稱訪問其他的幾個不定參數。但此時由于棧上其他的幾個參數實際恰好依序排列在參數num的高地址方向,因此可以很簡單地通過num的地址計算出其他參數的地址。sum函數的實現如下:

int sum(unsigned num, ...)

{

??? int* p =&num + 1;

??? int ret =0;

??? while(num--)

???????ret += *p++;

??? returnret;

}

在這里我們可以觀察到兩個事實:

(1)sum函數獲取參數的量僅取決于num參數的值,因此,如果num參數的值不等于實際傳遞的不定參數的數量,那么sum函數可能取到錯誤的或不足的參數。

(2)cdecl調用慣例保證了參數的正確清除。我們知道有些調用慣例(如stdcall)是由被調用方負責清除堆棧的參數,然而,被調用方在這里其實根本不知道有多少參數被傳遞進來,所以沒有辦法清除堆棧。而cdecl恰好是調用方負責清除堆棧,因此沒有這個問題。

printf的不定參數比sum要復雜得多,因為printf的參數不僅數量不定,而且類型也不定。所以printf需要在格式字符串中注明參數的類型,例如用%d表明是一個整數。printf里的格式字符串如果將類型描述錯誤,因為不同參數的大小不同,不僅可能導致這個參數的輸出錯誤,還有可能導致其后的一系列參數錯誤。

【小實驗】

printf的狂亂輸出

#include<stdio.h>

int main()

{

???printf("%lf/t%d/t%c/n", 1, 666, 'a');

}

在這個程序里,printf的第一個輸出參數是一個int(4字節),而我們告訴printf它是一個double(8字節以上),因此printf的輸出會錯誤,由于printf在讀取double的時候實際造成了越界,因此后面幾個參數的輸出也會失敗。該程序的實際輸出為(根據實際編譯器和環境可能不同):

0.000000????97? ?'

下面讓我們來看va_list等宏應該如何實現。

va_list實際是一個指針,用來指向各個不定參數。由于類型不明,因此這個va_list以void*或char*為最佳選擇。

va_start將va_list定義的指針指向函數的最后一個參數后面的位置,這個位置就是第一個不定參數。

va_arg獲取當前不定參數的值,并根據當前不定參數的大小將指針移向下一個參數。

va_end將指針清0。

按照以上思路,va系列宏的一個最簡單的實現就可以得到了,如下所示:

#define va_list char*

#define va_start(ap,arg)(ap=(va_list)&arg+sizeof(arg))

#define va_arg(ap,t)(*(t*)((ap+=sizeof(t))-sizeof(t)))

#define va_end(ap) (ap=(va_list)0)

【小提示】

變長參數宏

在很多時候我們希望在定義宏的時候也能夠像print一樣可以使用變長參數,即宏的參數可以是任意個,這個功能可以由編譯器的變長參數宏實現。在GCC編譯器下,變長參數宏可以使用“##”宏字符串連接操作實現,比如:

#define printf(args…) fprintf(stdout, ##args)

那么printf(“%d %s”, 123,“hello”)就會被展開成:

fprintf(stdout, “%d %s”, 123, “hello”)

而在MSVC下,我們可以使用__VA_ARGS__這個編譯器內置宏,比如:

#define printf(…) fprintf(stdout,__VA_ARGS__)

它的效果與前面的GCC下使用##的效果一樣。

2. 非局部跳轉

非局部跳轉即使在C語言里也是一個備受爭議的機制。使用非局部跳轉,可以實現從一個函數體內向另一個事先登記過的函數體內跳轉,而不用擔心堆棧混亂。下面讓我們來看一個示例:

#include<setjmp.h>

#include<stdio.h>

jmp_buf b;

void f()

{

??? longjmp(b,1);

}

int main()

{

??? if(setjmp(b))

???????printf("World!");

??? else

??? {

???????printf("Hello ");

???????f();

??? }

}

這段代碼按常理不論setjmp返回什么,也只會打印出“Hello ”和“World!”之一,然而事實上的輸出是:

Hello World!

實際上,當setjmp正常返回的時候,會返回0,因此會打印出“Hello”的字樣。而longjmp的作用,就是讓程序的執行流回到當初setjmp返回的時刻,并且返回由longjmp指定的返回值(longjmp的參數2),也就是1,自然接著會打印出“World!”并退出。換句話說,longjmp可以讓程序“時光倒流”回setjmp返回的時刻,并改變其行為,以至于改變了未來。

是的,這絕對不是結構化編程。K

11.2.3? glibc與MSVC CRT

運行庫是平臺相關的,因為它與操作系統結合得非常緊密。C語言的運行庫從某種程度上來講是C語言的程序和不同操作系統平臺之間的抽象層,它將不同的操作系統API抽象成相同的庫函數。比如我們可以在不同的操作系統平臺下使用fread來讀取文件,而事實上fread在不同的操作系統平臺下的實現是不同的,但作為運行庫的使用者我們不需要關心這一點。雖然各個平臺下的C語言運行庫提供了很多功能,但很多時候它們畢竟有限,比如用戶的權限控制、操作系統線程創建等都不是屬于標準的C語言運行庫。于是我們不得不通過其他的辦法,諸如繞過C語言運行庫直接調用操作系統API或使用其他的庫。Linux和Windows平臺下的兩個主要C語言運行庫分別為glibc(GNU CLibrary)和MSVCRT(Microsoft Visual C Run-time),我們在下面將會分別介紹它們。

值得注意的是,像線程操作這樣的功能并不是標準的C語言運行庫的一部分,但是glibc和MSVCRT都包含了線程操作的庫函數。比如glibc有一個可選的pthread庫中的pthread_create()函數可以用來創建線程;而MSVCRT中可以使用_beginthread()函數來創建線程。所以glibc和MSVCRT事實上是標準C語言運行庫的超集,它們各自對C標準庫進行了一些擴展。

glibc

glibc即GNU C Library,是GNU旗下的C標準庫。最初由自由軟件基金會FSF(FreeSoftwareFoundation)發起開發,目的是為GNU操作系統開發一個C標準庫。GNU操作系統的最初計劃的內核是Hurd,一個微內核的構架系統。Hurd因為種種原因開發進展緩慢,而Linux因為它的實用性而逐漸風靡,最后取代Hurd成了GNU操作系統的內核。于是glibc從最初開始支持Hurd到后來漸漸發展成同時支持Hurd和Linux,而且隨著Linux的越來越流行,glibc也主要關注Linux下的開發,成為了Linux平臺的C標準庫。

20世紀90年代初,在glibc成為Linux下的C運行庫之前,Linux的開發者們因為開發的需要,從Linux內核代碼里面分離出了一部分代碼,形成了早期Linux下的C運行庫。這個C運行庫又被稱為Linuxlibc。這個版本的C運行庫被維護了很多年,從版本2一直開發到版本5。如果你去看早期版本的Linux,會發現/lib目錄下面有libc.so.5這樣的文件,這個文件就是第五個版本的Linux libc。1996年FSF發布了glibc2.0,這個版本的glibc開始支持諸多特性,比如它完全支持POSIX標準、國際化、IPv6、64-位數據訪問、多線程及改進了代碼的可移植性。在此時Linuxlibc的開發者也認識到單獨地維護一份Linux下專用的C運行庫是沒有必要的,于是Linux開始采用glibc作為默認的C運行庫,并且將2.x版本的glibc看作是Linuxlibc的后繼版本。于是我們可以看到,glibc在/lib目錄下的.so文件為libc.so.6,即第六個libc版本,而且在各個Linux發行版中,glibc往往被稱為libc6。glibc在Linux平臺下占據了主導地位之后,它又被移植到了其他操作系統和其他硬件平臺,諸如FreeBSD、NetBSD等,而且它支持數十種CPU及嵌入式平臺。目前最新的glibc版本號是2.8(2008年4月)。

glibc的發布版本主要由兩部分組成,一部分是頭文件,比如stdio.h、stdlib.h等,它們往往位于/usr/include;另外一部分則是庫的二進制文件部分。二進制部分主要的就是C語言標準庫,它有靜態和動態兩個版本。動態的標準庫我們及在本書的前面章節中碰到過了,它位于/lib/libc.so.6;而靜態標準庫位于/usr/lib/libc.a。事實上glibc除了C標準庫之外,還有幾個輔助程序運行的運行庫,這幾個文件可以稱得上是真正的“運行庫”。它們就是/usr/lib/crt1.o、/usr/lib/crti.o和/usr/lib/crtn.o。是不是對這幾個文件還有點印象呢?我們在第2章講到靜態庫鏈接的時候已經碰到過它們了,雖然它們都很小,但這幾個文件都是程序運行的最關鍵的文件。

glibc啟動文件

crt1.o里面包含的就是程序的入口函數_start,由它負責調用__libc_start_main初始化libc并且調用main函數進入真正的程序主體。實際上最初開始的時候它并不叫做crt1.o,而是叫做crt.o,包含了基本的啟動、退出代碼。由于當時有些鏈接器對鏈接時目標文件和庫的順序有依賴性,crt.o這個文件必須被放在鏈接器命令行中的所有輸入文件中的第一個,為了強調這一點,crt.o被更名為crt0.o,表示它是鏈接時輸入的第一個文件。

后來由于C++的出現和ELF文件的改進,出現了必須在main()函數之前執行的全局/靜態對象構造和必須在main()函數之后執行的全局/靜態對象析構。為了滿足類似的需求,運行庫在每個目標文件中引入兩個與初始化相關的段“.init”和“.finit”。運行庫會保證所有位于這兩個段中的代碼會先于/后于main()函數執行,所以用它們來實現全局構造和析構就是很自然的事情了。鏈接器在進行鏈接時,會把所有輸入目標文件中的“.init”和“.finit”按照順序收集起來,然后將它們合并成輸出文件中的“.init”和“.finit”。但是這兩個輸出的段中所包含的指令還需要一些輔助的代碼來幫助它們啟動(比如計算GOT之類的),于是引入了兩個目標文件分別用來幫助實現初始化函數的crti.o和crtn.o。

與此同時,為了支持新的庫和可執行文件格式,crt0.o也進行了升級,變成了crt1.o。crt0.o和crt1.o之間的區別是crt0.o為原始的,不支持“.init”和“.finit”的啟動代碼,而crt1.o是改進過后,支持“.init”和“.finit”的版本。這一點我們從反匯編crt1.o可以看到,它向libc啟動函數__libc_start_main()傳遞了兩個函數指針“__libc_csu_init”和“__libc_csu_fini”,這兩個函數負責調用_init()和_finit(),我們在后面“C++全局構造和析構”的章節中還會詳細分析。

為了方便運行庫調用,最終輸出文件中的“.init”和“.finit”兩個段實際上分別包含的是_init()和_finit()這兩個函數,我們在關于運行庫初始化的部分也會看到這兩個函數,并且在C++全局構造和析構的章節中也會分析它們是如何實現全局構造和析構的。crti.o和crtn.o這兩個目標文件中包含的代碼實際上是_init()函數和_finit()函數的開始和結尾部分,當這兩個文件和其他目標文件安裝順序鏈接起來以后,剛好形成兩個完整的函數_init()和_finit()。我們用objdump可以查看這兩個文件的反匯編代碼:

$ objdump -dr/usr/lib/crti.o

crti.o:????file format elf32-i386

Disassembly of section .init:

00000000 <_init>:

??0:??55?????????????????????push?? �p

??1:?? 89e5??????????????????mov???%esp,�p

??3:??53?????????????????????push?? �x

??4:?? 83 ec04???????????????sub???$0x4,%esp

??7:?? e8 00 00 0000?????????call?? c<_init+0xc>

??c:??5b?????????????????????pop??? �x

??d:?? 81 c3 03 00 0000??????add???$0x3,�x

???????????????????????f: R_386_GOTPC? _GLOBAL_OFFSET_TABLE_

?13:?? 8b 93 00 00 0000??????mov 0x0(�x),�x

???????????????????????15: R_386_GOT32 __gmon_start__

?19:?? 85d2??????????????????test?? �x,�x

?1b:?? 7405??????????????????je????22 <_init+0x22>

?1d:?? e8 fc ff ffff?????????call?? 1e<_init+0x1e>

???????????????????????1e: R_386_PLT32 __gmon_start__

Disassembly of section .fini:

00000000 <_fini>:

??0:??55?????????????????????push?? �p

??1:?? 89e5??????????????????mov???%esp,�p

??3:??53?????????????????????push?? �x

??4:?? 83 ec04???????????????sub???$0x4,%esp

??7:?? e8 00 00 0000?????????call?? c<_fini+0xc>

??c:??5b?????????????????????pop??? �x

??d:?? 81 c3 03 00 0000??????add???$0x3,�x

???????????????????????f: R_386_GOTPC? _GLOBAL_OFFSET_TABLE_

$ objdump -dr/usr/lib/crtn.o

crtn.o:????file format elf32-i386

Disassembly of section .init:

00000000 <.init>:

??0:??58?????????????????????pop??? �x

??1:??5b?????????????????????pop??? �x

??2:??c9?????????????????????leave

??3:??c3?????????????????????ret

Disassembly of section .fini:

00000000 <.fini>:

??0:??59?????????????????????pop??? �x

??1:??5b?????????????????????pop??? �x

??2:??c9?????????????????????leave

??3:??c3?????????????????????ret

于是在最終鏈接完成之后,輸出的目標文件中的“.init”段只包含了一個函數_init(),這個函數的開始部分來自于crti.o的“.init”段,結束部分來自于crtn.o的“.init”段。為了保證最終輸出文件中“.init”和“.finit”的正確性,我們必須保證在鏈接時,crti.o必須在用戶目標文件和系統庫之前,而crtn.o必須在用戶目標文件和系統庫之后。鏈接器的輸入文件順序一般是:

ld crt1.o crti.o [user_objects] [system_libraries]crtn.o

由于crt1.o(crt0.o)不包含“.init”段和“.finit”段,所以不會影響最終生成“.init”和“.finit”段時的順序。輸出文件中的“.init”段看上去應該如圖11-8所示(對于“.finit”來說也一樣)。

?

圖11-8?.init段的組成

在默認情況下,ld鏈接器會將libc、crt1.o等這些CRT和啟動文件與程序的模塊鏈接起來,但是有些時候,我們可能不需要這些文件,或者希望使用自己的libc和crt1.o等啟動文件,以替代系統默認的文件,這種情況在嵌入式系統或操作系統內核編譯的時候很常見。GCC提高了兩個參數“-nostartfile”和“-nostdlib”,分別用來取消默認的啟動文件和C語言運行庫。

其實C++全局對象的構造函數和析構函數并不是直接放在.init和.finit段里面的,而是把一個執行所有構造/析構的函數的調用放在里面,由這個函數進行真正的構造和析構,我們在后面的章節還會再詳細分析ELF/Glib和PE/MSVC對全局對象構造和析構的過程。

除了全局對象構造和析構之外,.init和.finit還有其他的作用。由于它們的特殊性(在main之前/后執行),一些用戶監控程序性能、調試等工具經常利用它們進行一些初始化和反初始化的工作。當然我們也可以使用“__attribute__((section(“.init”)))”將函數放到.init段里面,但是要注意的是普通函數放在“.init”是會破壞它們的結構的,因為函數的返回指令使得_init()函數會提前返回,必須使用匯編指令,不能讓編譯器產生“ret”指令。

GCC平臺相關目標文件

就這樣,在第2章中我們在鏈接時碰到過的諸多輸入文件中,已經解決了crt1.o、crti.o和crtn.o,剩下的還有幾個crtbeginT.o、libgcc.a、libgcc_eh.a、crtend.o。嚴格來講,這幾個文件實際上不屬于glibc,它們是GCC的一部分,它們都位于GCC的安裝目錄下:

l??????????/usr/lib/gcc/i486-Linux-gnu/4.1.3/crtbeginT.o

l??????????/usr/lib/gcc/i486-Linux-gnu/4.1.3/libgcc.a

l??????????/usr/lib/gcc/i486-Linux-gnu/4.1.3/libgcc_eh.a

l??????????/usr/lib/gcc/i486-Linux-gnu/4.1.3/crtend.o

首先是crtbeginT.o及crtend.o,這兩個文件是真正用于實現C++全局構造和析構的目標文件。那么為什么已經有了crti.o和crtn.o之后,還需要這兩個文件呢?我們知道,C++這樣的語言的實現是跟編譯器密切相關的,而glibc只是一個C語言運行庫,它對C++的實現并不了解。而GCC是C++的真正實現者,它對C++的全局構造和析構了如指掌。于是它提供了兩個目標文件crtbeginT.o和crtend.o來配合glibc實現C++的全局構造和析構。事實上是crti.o和crtn.o中的“.init”和“.finit”提供一個在main()之前和之后運行代碼的機制,而真正全局構造和析構則由crtbeginT.o和crtend.o來實現。我們在后面的章節還會詳細分析它們的實現機制。

由于GCC支持諸多平臺,能夠正確處理不同平臺之間的差異性也是GCC的任務之一。比如有些32位平臺不支持64位的 longlong類型的運算,編譯器不能夠直接產生相應的CPU指令,而是需要一些輔助的例程來幫助實現計算。libgcc.a里面包含的就是這種類似的函數,這些函數主要包括整數運算、浮點數運算(不同的CPU對浮點數的運算方法很不相同)等,而libgcc_eh.a則包含了支持C++的異常處理(ExceptionHandling)的平臺相關函數。另外GCC的安裝目錄下往往還有一個動態鏈接版本的libgcc.a,為libgcc_s.so。

MSVC CRT

相比于相對自由分散的glibc,一直伴隨著不同版本的Visual C++發布的MSVCCRT(Microsoft Visual C++ C Runtime)倒看過去更加有序一些。從1992年最初的Visual C++1.0版開始,一直到現在的Visual C++ 9.0(又叫做Visual C++ 2008),MSVCCRT也從1.0版發展到了9.0版。

同一個版本的MSVCCRT根據不同的屬性提供了多種子版本,以供不同需求的開發者使用。按照靜態/動態鏈接,可以分為靜態版和動態版;按照單線程/多線程,可以分為單線程版和多線程版;按照調試/發布,可分為調試版和發布版;按照是否支持C++分為純C運行庫版和支持C++版;按照是否支持托管代碼分為支持本地代碼/托管代碼和純托管代碼版。這些屬性很多時候是相互正交的,也就是說它們之間可以相互組合。比如可以有靜態單線程純C純本地代碼調試版;也可以有動態的多線程純C純本地代碼發布版等。但有些組合是沒有的,比如動態鏈接版本的CRT是沒有單線程的,所有的動態鏈接CRT都是多線程安全的。

這樣的不同組合將會出現非常多的子版本,于是微軟提供了一套運行庫的命名方法。這個命名方法是這樣的,靜態版和動態版完全不同。靜態版的CRT位于MSVC安裝目錄下的lib/,比如Visual C++ 2008的靜態庫路徑為“ProgramFiles/Microsoft Visual Studio 9.0/VC/lib”,它們的命名規則為:

libc [p] [mt] [d] .lib

l??????????p 表示 C Plusplus,即C++標準庫。

l??????????mt表示 Multi-Thread,即表示支持多線程。

l??????????d 表示 Debug,即表示調試版本。

比如靜態的非C++的多線程版CRT的文件名為libcmtd.lib。動態版的CRT的每個版本一般有兩個相對應的文件,一個用于鏈接的.lib文件,一個用于運行時用的.dll動態鏈接庫。它們的命名方式與靜態版的CRT非常類似,稍微有所不同的是,CRT的動態鏈接庫DLL文件名中會包含版本號。比如Visual C++2005的多線程、動態鏈接版的DLL文件名為msvcr90.dll(Visual C++2005的內部版本號為8.0)。表11-1列舉了一些最常見的MSVC CRT版本(以Visual C++ 2005為例)。

表11-1

文件名

相關的DLL

屬性

編譯器選項

預編譯宏

libcmt.lib

多線程,靜態鏈接

/MT

_MT

msvcrt.lib

msvcr80.dll

多線程,動態鏈接

/MD

_MT, _DLL

libcmtd.lib

多線程,靜態鏈接,調試

/MTd

_DEBUG, _MT

msvcrtd.lib

msvcr90d.dll

多線程,動態鏈接,調試

/MDd

_DEBUG, _MT, _DLL

msvcmrt.lib

msvcm90.dll

托管/本地混合代碼

/clr

?

msvcurt.lib

msvcm90.dll

純托管代碼

/clr:pure

?

自從Visual C++ 2005(MSVC8.0)以后,MSVC不再提供靜態鏈接單線程版的運行庫(LIBC.lib、LIBCD.lib),因為據微軟聲稱,經過改進后的新的多線程版的C運行庫在單線程的模式下運行速度已經接近單線程版的運行庫,于是沒有必要再額外提供一個只支持單線程的CRT版本。

默認情況下,如果在編譯鏈接時不指定鏈接哪個CRT,編譯器會默認選擇LIBCMT.LIB,即靜態多線程CRT,Visual C++2005之前的版本會選擇LIBC.LIB,即靜態單線程版本。關于CRT的多線程和單線程的問題,我們在后面的章節還會再深入分析。

除了使用編譯命令行的選項之外,在VisualC++工程屬性中也可以設置相關選項。如圖11-9所示。

?

?

圖11-9?Visual C++ 2003 .NET工程屬性的截圖

我們可以從圖11-9中看到,除了多線程庫以外,還有單線程靜態/ML、單線程靜態調試/MLd的選項。

C++ CRT

表11-1中的所有CRT都是指C語言的標準庫,MSVC還提供了相應的C++標準庫。如果你的程序是使用C++編寫的,那么就需要額外鏈接相應的C++標準庫。這里“額外”的意思是,如表11-2所列的C++標準庫里面包含的僅僅是C++的內容,比如iostream、string、map等,不包含C的標準庫。

表11-2

文件名

相應DLL

屬性

編譯選項

宏定義

LIBCPMT.LIB

多線程,靜態鏈接

/MT

_MT

MSVCPRT.LIB

MSVCP90.dll

多線程,動態鏈接

/MD

_MT, _DLL

LIBCPMTD.LIB

多線程,靜態鏈接,調試

/MTd

_DEBUG, _MT

MSVCPRTD.LIB

MSVCP90D.dll

多線程,動態鏈接,調試

/MDd

_DEBUG, _MT, _DLL

當你在程序里包含了某個C++標準庫的頭文件時,MSVC編譯器就認為該源代碼文件是一個C++源代碼程序,它會在編譯時根據編譯選項,在目標文件的“.drectve”段(還記得第2章中的DIRECTIVE吧?)相應的C++標準庫鏈接信息。比如我們用C++寫一個“Hello World”程序:

// hello.cpp

#include<iostream>

int main()

{

??? std::cout<< "Hello world"<< std::endl;

??? return0;

}

然后將它編譯成目標文件,并查看它的“.drectve”段的信息:

cl /c hello.cpp

dumpbin /DIRECTIVEShello.obj

Microsoft (R) COFF/PE Dumper Version9.00.21022.08

Copyright (C) MicrosoftCorporation.? All rights reserved.

Dump of file msvcprt.obj

File Type: COFF OBJECT

?? LinkerDirectives

??-----------------

??/DEFAULTLIB:"libcpmt"

??/DEFAULTLIB:"LIBCMT"

??/DEFAULTLIB:"OLDNAMES"

cl /c /MDdhello.cpp

dumpbin /DIRECTIVEShello.obj

Microsoft (R) COFF/PE Dumper Version9.00.21022.08

Copyright (C) MicrosoftCorporation.? All rights reserved.

Dump of file msvcprt.obj

File Type: COFF OBJECT

?? LinkerDirectives

??-----------------

??/manifestdependency:"type='win32'

??name='Microsoft.VC90.DebugCRT'

??version='9.0.21022.8'

??processorArchitecture='x86'

??publicKeyToken='1fc8b3b9a1e18e3b'"

??/DEFAULTLIB:"msvcprtd"

??/manifestdependency:"type='win32'

??name='Microsoft.VC90.DebugCRT'

??version='9.0.21022.8'

??processorArchitecture='x86'

??publicKeyToken='1fc8b3b9a1e18e3b'"

??/DEFAULTLIB:"MSVCRTD"

??/DEFAULTLIB:"OLDNAMES"

可以看到,hello.obj須要鏈接libcpmt.lib、LIBCMT.lib和OLDNAMES.lib。當我們使用“/MDd”參數編譯時,hello.obj就需要msvcprtd.lib、MSVCRTD.lib和OLDNAMES.lib,除此之外,編譯器還給鏈接器傳遞了“/manifestdependency”參數,即manifest信息。

Q&A

Q:如果一個程序里面的不同obj文件或DLL文件使用了不同的CRT,會不會有問題?

A:這個問題實際上分很多種情況。如果程序沒有用到DLL,完全靜態鏈接,不同的obj在編譯時用到了不同版本的靜態CRT。由于目前靜態鏈接CRT只有多線程版,并且如果所有的目標文件都統一使用調試版或發布版,那么這種情況下一般是不會有問題的。因為我們知道,目標文件對靜態庫引用只是在目標文件的符號表中保留一個記號,并不進行實際的鏈接,也沒有靜態庫的版本信息。

??????但是,如果程序涉及動態鏈接CRT,這就比較復雜了。因為不同的目標文件如果依賴于不同版本的msvcrt.lib和msvcrt.dll,甚至有些目標文件是依賴于靜態CRT,而有些目標文件依賴于動態CRT,那么很有可能出現的問題就是無法通過鏈接。鏈接器對這種情況的具體反應依賴于輸入目標文件的順序,有些情況下它會報符號重復定義錯誤:

??????MSVCRTD.lib(MSVCR80D.dll) : error LNK2005: _printf already definedin LIBCMTD.lib (printf.obj)

??????但是有些情況下,它會使鏈接順利通過,只是給出一個警告:

??????LINK : warning LNK4098: defaultlib 'LIBCMTD' conflicts with use ofother libs; use /NODEFAULTLIB:library

??????如果碰到上面這種靜態/動態CRT混合的情況,我們可以使用鏈接器的/NODEFAULTLIB來禁止某個或某些版本的CRT,這樣一般就能使鏈接順利進行。

??????最麻煩的情況應該屬于一個程序所依賴的DLL分別使用不同的CRT,這會導致程序在運行時同時有多份CRT的副本。在一般情況下,這個程序應該能正常運行,但是值得注意的是,你不能夠在這些DLL之間相互傳遞使用一些資源。比如兩個DLLA和B分別使用不同的CRT,那么應該注意以下問題:

????l????不能在A中申請內存然后在B中釋放,因為它們分屬于不同的CRT,即擁有不同的堆,這包括C++里面所有對象的申請和釋放;

????l????在A中打開的文件不能在B中使用,比如FILE*之類的,因為它們依賴于CRT的文件操作部分。

??????還有類似的問題,比如不能相互共享locale等。如果不違反上述規則,可能會使程序發生莫名其妙的錯誤并且很難發現。

??????防止出現上述問題的最好方法就是保證一個工程里面所有的目標文件和DLL都使用同一個版本的CRT。當然有時候事實并不能盡如人意,比如很多時候當我們要用到第三方提供的.lib或DLL文件而對方又不提供源代碼時,就會比較難辦。

??????Windows系統的system32目錄下有個叫msvcrt.dll的文件,它跟msvcr90.dll這樣的DLL有什么區別?

Q:為什么我用Visual C++2005/2008編譯的程序無法在別人的機器上運行?

A:因為Visual C++2005/2008編譯的程序使用了manifest機制,這些程序必須依賴于相對應版本的運行庫。一個解決的方法就是使用靜態鏈接,這樣就不需要依賴于CRT的DLL。另外一個解決的方法就是將相應版本的運行庫與程序一起發布給最終用戶。

總結

以上是生活随笔為你收集整理的转:c/c++ 运行库的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 91免费毛片| 一本色道久久hezyo无码 | 久久精品在线 | 亚洲天堂男人 | 久久精品视频5 | 日韩黄色录像 | 精品久久久久久无码人妻 | 麻豆精品一区 | 中国免费黄色片 | 成年人av在线播放 | 日本大尺度做爰呻吟舌吻 | 国产黄色精品网站 | 99爱免费视频 | 欧美精品欧美精品系列 | 久久久av一区二区三区 | 国产femdom调教7777 | 国产3区| 亚洲精品www久久久 一级aaa毛片 | 欧美日韩国产一区二区 | 男人的天堂2018 | 国内偷拍一区二区 | 亚洲精品在线视频观看 | 亚洲福利一区二区三区 | 久久精品国产精品亚洲毛片 | 爽爽窝窝午夜精品一区二区 | 国产夫妻自拍小视频 | 国产精品久久久久久久一区二区 | 日韩在线视频免费播放 | 亚洲欧洲色图 | 国产67194 | 久久一区二区三区四区五区 | 91avcom| 亚洲视频综合网 | 不卡影院av| 热久久精品免费视频 | 亚洲天堂av电影 | 中文字幕一区二区久久人妻网站 | 精品国产乱码久久久久久影片 | 午夜视频在线观看网站 | 少妇系列在线观看 | 亚洲视频www| 少妇爽 | 天天夜夜久久 | 闷骚老干部cao个爽 av九九九 | 国产又粗又猛又爽又黄的视频小说 | 天堂网中文在线观看 | 久久久久久久久久久久久久久久久久久久 | 成年人一级黄色片 | 国产精品毛片久久久 | 亚洲国产一二三区 | 亚洲无卡视频 | 国产欧美在线观看 | 久久毛片 | 精品一区二区成人免费视频 | 久久久国产精品成人免费 | 麻豆影视在线播放 | 三上悠亚ssⅰn939无码播放 | 久久美女性网 | 处破女av一区二区 | 日韩人妻一区二区三区 | 久久国产经典视频 | 日韩视频在线观看一区二区三区 | 极品少妇av | 噜噜色网 | 欧美第一页浮力影院 | 少妇一级淫片免费放播放 | 欧美人与性动交α欧美精品 | 欧美一区二区三区成人精品 | 国产网红无码精品视频 | 性激烈视频在线观看 | 精品免费国产 | 99自拍| 蜜桃色av| youjizz.com日本| 国产91精品一区 | 亚洲区小说区图片区qvod | 古装做爰无遮挡三级 | 500福利视频导航 | 国产成人av电影 | 国产午夜在线观看 | 国产成人午夜精品 | 66亚洲一卡2卡新区成片发布 | 高清一区二区三区视频 | 中文字幕一区二区三区人妻在线视频 | 今天高清视频在线观看视频 | 亚洲国产无码久久 | 五月香蕉网 | 国产黄色片在线 | 国产av无码专区亚洲精品 | 国产精品羞羞答答 | 五月天堂婷婷 | 欧美黑人一区二区三区 | 在线看日本 | 午夜性激情 | 男男gay羞辱feet贱奴vk | 超碰在线人人草 | 成年人的免费视频 | 午夜之声l性8电台lx8电台 | 欧美极品一区二区三区 |