官术网_书友最值得收藏!

4.1 C++/C程序的基本概念

4.1.1 啟動函數main()

C++/C程序的可執行部分都是由函數組成的,main()就是所有程序中都應該提供的一個默認全局函數——主函數——所有的C++/C程序都應該從函數main()開始執行。但是語言實現本身并不提供main()的實現(它也不是一個庫函數),并且具體的語言實現環境可以決定是否用main()函數來作為用戶應用程序的啟動函數,這是標準賦予語言實現的權利(又是一個“實現定義的行為”?)。

雖然main不是C++/C的保留字(因此你可以在其他地方使用main這個名字,比如作為類、名字空間或者成員函數等的名字),但是你也不可以修改main()函數的名字。如果修改了main()的名字,比如改為mymain,連接器就會報告類似的連接時錯誤:“unresolved external symbol _main”。這是因為C++/C語言實現有一個啟動函數,例如,MS C++/C應用程序的啟動函數為mainCRTStartup()或者WinMainCRT- Startup(),同時在該函數的末尾調用了main()或者WinMain(),然后以它們的返回值為參數調用庫函數exit(),因此也就默認了main()應該作為它的連接對象,如果找不到這樣一個函數定義,自然會報錯了。如此看來,main()其實就是一個回調函數。main()由我們來實現,但是不需要我們提供它的原型,因為我們并不能在自己的程序中調用它,這又和普通的回調函數有所不同。

基于應用程序框架(Application Framework,如MFC)生成的源代碼中往往找不到main(),這并不是說這樣的程序中就不需要main(),而是應用程序框架把main()的實現隱藏起來了,并且它的實現具有固定的模式,所以不需要程序員來編寫。在應用程序的連接階段,框架會將包含main()實現的library加進來一起連接。

由于main()函數如此重要,C++標準特別規定了標準main()函數的原型(參見ISO/IEC 14882:1998 3.6.1節):

                  “…It shall have a return type of type int, but otherwise its type is
              implementation-defined. All implementations shall allow both of the following
              definitions of main() :
                    int main() { /* …… */ }
              and
                    int main( int argc, char *argv[] ) { /* …… */ }
              …It is recommended that any further(optional) parameters be added after argv.”
                  “…main()應該返回int,但是具體返回什么類型可以由實現來定義(注:
              即可由實現來擴展)。不過所有實現版本都應該至少允許下面兩種形式的main()
              函數:
                    int main() { /* …… */ }
              和
                    int main( int argc, char *argv[] ) { /* …… */ }
              ……并允許實現在參數argv后面增加任何需要的也是可選的參數(注:這也是
              可擴展的)。”

也就是說,上述兩種形式是最具有可移植性的正確寫法,其他形式都是特定實現的擴展形式,比如MS C++/C允許main()返回void,以及增加第三個參數char* env[]等。讀者可參考編譯器的幫助文檔,以了解當前的編譯器支持怎樣的擴展形式。

關于main()函數的返回值問題,C++標準如是說:

                  “…A return statement in main() has the effect of leaving the main function
              (destroying any objects with automatic storage duration) and calling exit() with the
              return value as the argument. If control reaches the end of main() without
              encountering a return statement, the effect is that of executing
                    return 0;”
                  “…main()中的return語句的作用是離開main()(返回到C運行時庫的啟動
              模塊,并啟動銷毀過程,銷毀任何具有自動存儲生命期的對象),就像其他函數
              一樣,并且用其返回值作為參數調用exit()返回操作系統。如果控制到達main()
              的結尾,卻沒有遇到任何return語句,則效果相當于執行一條return 0;語句。”

當main()返回int類型時,不同的返回值具有不同的含義。當返回0時,表示程序正常結束;返回任何非0值表示錯誤或者非正常退出。exit()用main()的返回值作為返回操作系統的代碼,以指示程序執行的結果(當然你也可以在main()或其他函數內直接調用exit()來結束程序)。

特別地,C++標準對main()有幾個不同于一般函數的限制:

(1)不能重載。

(2)不能內聯。

(3)不能定義為靜態的。

(4)不能取其地址。

(5)不能由用戶自己調用。

……

4.1.2 命令行參數

我們可能希望可執行程序具有處理命令行參數的能力,如常用的“dir X:\document /p /w”等DOS或UNIX命令。標準C++/C規定,可以在main()函數中添加形式參數以接收程序在啟動時從命令行中輸入的各個參數。這里需注意,不要把程序啟動時的“命令行參數”與調用main()的“函數實參”的概念混淆了,命令行參數是由啟動程序截獲并打包成字符串數組后傳遞給main()的一個形參argv,而包括命令字(即可執行文件名稱)在內的所有參數的個數則被傳遞給形參argc。

以上所述,包括main()的連接規范(Linkage Specification)和調用約定(Calling Convention)在內,不同的語言實現很可能是不同的。具體可參考編譯器文檔或者C Runtime Library和類庫的幫助文檔,甚至類庫的源代碼也可拿來讀一讀(如Visual C++的crtexe.c和internal.h等)。

示例4-1是一個文件拷貝的程序,和DOS內部命令copy的功能一樣。

示例4-1

            // mycopy.c : copy file to a specified destination file.
            #include <stdio.h>
            int main(int argCount, char* argValue[])
            {
                  FILE *srcFile = 0, *destFile = 0;
                  int ch = 0;
                  if (argCount != 3) {
                    printf("Usage: %s src-file-name dest-file-name\n", argValue[0]);
                  } else {
                    if (( srcFile = fopen(argValue[1], "r")) == 0) {
                    printf("Can not open source file \"%s\" !", argValue[1]);
                  } else {
                    if ((destFile = fopen( argValue[2], "w")) == 0) {
                          printf("Can not open destination file \"%s\"!", argValue[2]);
                          fclose(srcFile);  /*!!!*/
                    } else {
                          while((ch = fgetc(srcFile)) != EOF) fputc(ch, destFile);
                          printf("Successful to copy a file!\n");
                          fclose(srcFile);   /*!!!*/
                          fclose(destFile);  /*!!!*/
                          return 0;       /*!!!*/
                    }
                  }
              }
              return 1;
            }
            // 用法示例:
            mycopy C:\file1.dat C:\newfile.dat

如果你還不了解文件操作,沒有關系,你不妨輸入這個程序,把源文件名改為mycopy.c(或.cpp),編譯連接。在DOS命令行方式下隨便找幾個文件測試一下,是不是很有成就感呢?

4.1.3 內部名稱

請注意4.1.1節的連接錯誤信息中的“_main”,這是編譯器為main生成的內部名稱。C和C++語言實現都會按照特定的規則把用戶(指程序員)定義的標識符(各種函數、變量、類型及名字空間等)轉換為相應的內部名稱。當然,這些內部名稱的命名方法還與用戶為它們指定的連接規范有關,比如使用C的連接規范,則main的內部名稱就是_main。

內部名稱是否多此一舉呢?非也!

在C語言中,所有函數不是局部于編譯單元(文件作用域)的static函數,就是具有extern連接類型和global作用域的全局函數,因此除了兩個分別位于不同編譯單元中的static函數可以同名外,全局函數是不能同名的;全局變量也是同樣的道理。其原因是C語言采用了一種極其簡單的函數名稱區分規則:僅在所有函數名的前面添加前綴“_”,從唯一識別函數的作用上來說,實際上和不添加前綴沒什么不同。

但是,C++語言允許用戶在不同的作用域中定義同名的函數、類型、變量等,這些作用域不僅限于編譯單元,還包括class、struct、union、namespace等,甚至在同一個作用域中也可定義同名的函數,即重載函數。那么編譯器和連接器如何區分這些同名且又都會在同一個編譯單元中被引用的程序元素呢?在示例4-2中,假設兩個類都定義在同一個作用域中,且都定義了名為foo的成員函數。

示例4-2

由于成員函數并不屬于某一個對象,那么編譯器如何區分下面這些語句分別調用的是哪個函數呢?

            Sample_1  a;
            Sample_2  b;
            a.foo(“aaa”);
            a.foo(100);
            b.foo(“bbb”);
            b.foo(false);

你也許會說“通過它們各自的對象和成員標識符就可以區分”。是的,你說得沒錯,但這只是源代碼級或者說是形式上的區分。在連接器看來,所有函數都是全局函數,能夠用來區分不同函數調用的除了作用域外就是函數名稱了。但是,上面的調用顯然都是合理合法的。因此,如果不對它們進行重命名,就會導致連接二義性。在C++中,重命名稱為“Name-Mangling”(名字修飾或名字改編)。例如,在它們的前面分別添加所屬各級作用域的名稱(class、namespace等)及重載函數的經過編碼的參數信息(參數類型和個數等)作為前綴或者后綴,產生全局名字Sample_1_foo@pch@1、Sample_1_foo@int@1、Sample_2_foo@pch@1和Sample_2_foo@int@1,這樣就可以區分了。關于這方面更詳細的信息請參考Lippman的《Inside The C++ Object Model》相關章節,你也可以從MS C++/C編譯器輸出的MAP文件了解一下它所Mangling出來的函數的內部名稱。

另外,標準C++的不同實現會采取不同的Name-Mangling方案(標準沒有強制規定),這正是導致不同語言實現之間的連接器不能兼容的原因之一。

4.1.4 連接規范

在使用不同編程語言進行軟件聯合開發的時候,需要統一全局函數、全局變量、全局常量、數據類型等的連接規范(Linkage Specification),特別是在不同模塊之間共享的接口定義部分。為什么呢?因為連接規范關系到編譯器采用什么樣的Name-Mangling方案來重命名這些標識符的問題,而如果同一個標識符在不同的編譯單元或模塊中具有不一致的連接規范,就會產生不一致的內部名稱,這肯定會導致程序連接失敗。

同樣道理,在開發程序庫的時候,明確連接規范也是必須要遵循的一條規則。通用的連接規范則屬C連接規范:extern“C”,其使用方法如下。

(1)如果是僅對一個類型、函數、變量或常量等指定連接規范:

            extern "C" void WinMainCRTStartup();
            extern "C" const CLSID CLSID_DataConverter;
            extern "C" struct Student{……};
            extern "C" Student g_Student;

(2)如果是對一段代碼限定連接規范:

            #ifdef  __cplusplus
            extern "C" {
            #endif
            const int MAX_AGE = 200;
            #pragma pack(push, 4)
            typedef struct _Person
            {
                char *m_Name;
                int   m_Age;
            } Person, *PersonPtr;
            #pragma pack(pop)
            Person g_Me;
            int    __cdecl  memcmp(const void*,const void*,size_t);
            void*  __cdecl  memcpy(void*,const void*,size_t);
            void*  __cdecl  memset(void*,int,size_t);
            #ifdef  __cplusplus
            }
            #endif

(3)如果當前使用的是C++編譯器,并且使用了extern“C”來限定一段代碼的連接規范,但是又想令其中某行或某段代碼保持C++的連接規范,則可以編寫如下代碼(具體要看你的編譯器是否支持extern“C++”):

            #ifdef  __cplusplus
            extern "C" {
            #endif
            const int MAX_AGE = 200;
            #pragma pack(push, 4)
            typedef struct _Person
            {
                char *m_Name;
                int   m_Age;
            } Person, *PersonPtr;
            #pragma pack(pop)
            Person g_Me;
            #if  _SUPPORT_EXTERN_CPP_
            extern “C++” {
            #endif
            int    __cdecl  memcmp(const void*,const void*,size_t);
            void*  __cdecl  memcpy(void*,const void*,size_t);
            #if  _SUPPORT_EXTERN_CPP_
            }
            #endif
            void*  __cdecl  memset(void*,int,size_t);
            #ifdef  __cplusplus
            }
            #endif

(4)如果在某個聲明中指定了某個標識符的連接規范為extern“C”,那么也要為其對應的定義指定extern“C”連接規范,如下所示:

            #ifdef  __cplusplus
            extern "C" {
            #endif
            int  __cdecl  memcmp(const void*,const void*,size_t);  // 聲明
            #ifdef  __cplusplus
            }
            #endif
            #ifdef  __cplusplus
            extern "C" {
            #endif
            int  __cdecl  memcmp(const void*p,const void*a,size_t len)
            {
              ……  // 功能實現
            }
            #ifdef  __cplusplus
            }
            #endif

但是對COM接口方法(Interface Methods,Interface中的pure virtual functions)使用的C復合數據類型來說(它們也是COM對象接口的組成部分),是否采用統一的連接規范,對COM對象及組件的二進制數據兼容性和可移植性都沒有影響。因為即使接口兩端(COM接口實現端和接口調用端)對接口數據類型的內部命名不同,只要它們使用了一致的成員對齊和排列方式、一致的調用規范、一致的virtual function實現方式,總之就是一致的C++對象模型,并且保證COM組件升級時不改變原來的接口和數據類型定義,則所有方法的運行時綁定和參數傳遞都不會存在問題(所有方法的調用都被轉換為通過對象指針對vptr和vtable以及函數指針的訪問和調用,這種間接性不再需要任何方法名即函數名的參與,而接口名和方法名只是為了讓客戶端的代碼能夠順利通過編譯,但是連接時就全部不再需要了)。

4.1.5 變量及其初始化

變量就是用來保存數據的程序元素,它是內存單元的別名,取一個變量的值就是讀取其內存單元中存放的值,而寫一個變量就是把值寫入到它代表的內存單元中。在C++/C中,全局變量(extern或static的)存放在程序的靜態數據區中,在程序進入main()之前創建,在main()結束之后銷毀,因此在我們的代碼中根本沒有機會初始化它們,于是語言及其實現就提供了一個默認的全局初始化器0。如果你沒有明確地給全局變量提供初值,編譯器會自動地將0轉換為所需要的類型來初始化它們。函數內的static局部變量和類的static數據成員都具有static存儲類型,因此最終被移到程序的靜態數據區中,也會默認初始化為0,除非你明確地提供了初值。但是自動變量的初始化則是程序員的責任,因為它們是運行時在堆棧上創建的并且可以在運行時由程序員來初始化的,不要指望編譯器會給它一個默認的初值。

全局變量的聲明和定義應當放在源文件的開頭位置。

【提示4-1】: 要區分初始化和賦值的不同。前者發生在對象(變量)創建的同時,而后者是在對象創建后進行的。要區分什么是編譯器的責任,什么是程序員的責任,不可錯把程序員的責任推給編譯器,否則結果可能出乎意料!例如,全局變量的初始化、數據類型的隱式轉換、類的隱含成員的初始化等都是編譯器的責任,而局部變量的初始化、強制類型轉換、類的非靜態數據成員的初始化等都是程序員的責任。

在一個編譯單元中定義的全局變量的初始值不要依賴定義于另一個編譯單元中的全局變量的初始值。這是因為:雖然編譯器和連接器可以決定同一個編譯單元中定義的全局變量的初始化順序保持與它們定義的先后順序一致,但是卻無法決定當兩個編譯單元連接在一起時哪一個的全局變量的初始化先于另一個編譯單元的全局變量的初始化。也就是說,這一次編譯連接和下一次編譯連接很可能使不同編譯單元之間的全局變量的初始化順序發生改變。例如:

當這兩個文件編譯完成并連接時,在最后的可執行程序啟動時,到底是先初始化g_x還是先初始化g_d呢?答案是:我們無法預料,連接器也不會給你保證一個順序!所以下面的做法是不當的:

如果g_x初始化被排在g_d的前面,那么g_d就會被初始化為110;但是如果反過來,那么g_d的初始值就無法預料了。

4.1.6 C Runtime Library

一般來說,一個C++/C程序不可能不使用C運行時庫,即使你沒有顯式地調用其中的函數也可能間接地調用,只是我們平時沒有在意罷了。例如,啟動函數、I/O系統函數、存儲管理、RTTI、動態決議、動態鏈接庫(DLL)等都會調用C運行時庫中的函數。我們在每一個程序開頭包含的stdio.h頭文件中的許多I/O函數就是它的一部分。C運行時庫有多線程版和單線程版,開發多線程應用程序時應該使用多線程版本的庫,僅在開發單線程程序時才使用單線程版本。另外,同一軟件的不同模塊最好使用一致的運行時庫,否則會出現連接問題。

4.1.7 編譯時和運行時的不同

我們把編譯預處理器、編譯器和連接器工作的階段合稱“編譯時”。語言中有些構造僅在編譯時起作用,而有些構造則是在“運行時”起作用的,分清楚這些構造對于程序設計很重要。例如,預編譯偽指令、類(型)定義、外部對象聲明、函數原型、標識符、各種修飾符號(const、static等)及類成員的訪問說明符(public、private、protected)和連接規范、調用規范等,僅在編譯器進行語法檢查、語義檢查和生成目標文件(.obj或.o文件)及連接的時候起作用的,在可執行程序中不存在這些東西。容器越界訪問、虛函數動態決議、函數動態連接、動態內存分配、異常處理和RTTI等則是在運行時才會出現和發揮作用的,因此運行時出現的程序問題大多與這些構造有關。

我們舉兩個例子來說明編譯時和運行時的不同,見示例4-3和示例4-4。

示例4-3

            int *pInt = new int[10];
            pInt+=100;          // 越界,但是還沒有形成越界訪問
            cout<<*pInt<<endl;   // 越界訪問!可能行,也可能不行!
            *pInt=1000;         // 越界訪問!即使偶爾不出問題,但不能確保永遠不出問題!

上述代碼在編譯時絕對沒有問題,但是運行時會出現錯誤!千萬不要寫出這樣的代碼來!

示例4-4

            class Base {
            public:
              virtual void Say(){ cout<< "Base::Say() was invoked!\n"; }
            };
            class Derived : public Base {
            private:      // 改變訪問權限,合法但不是好風格!
              virtual  void Say(){cout<<“Derived::Say()was invoked!\n”;}
            };
            // 測試
            Base *p = new Derived;
            p->Say();    // 輸出:Derived::Say()was invoked!
                         // 出乎意料地綁定到了一個private函數身上!

示例4-4在編譯時沒有問題,在運行時也不會出現錯誤,但是違背了private的用意。

我們在程序設計時就要對運行時的行為有所預見,通過編譯連接的程序在運行時不見得就是正確的。雖然你能夠一時“欺騙”編譯器(因為編譯器還不夠聰明),但是由此造成的后果要你自己來承擔。這里我們引用Bjarne Stroustrup的一段話來說明這一問題:“C++的訪問控制策略是為了防止意外事件而不是防止對編譯器的故意欺騙。任何程序設計語言,只要它支持對原始存儲器的直接訪問(如C++的指針),就會使數據處于一種開放的狀態,使所有有意按照某種違反數據項原有類型安全規則所描述的方式去觸動它的企圖都能夠實現,除非該數據項受到操作系統的直接保護。”

4.1.8 編譯單元和獨立編譯技術

語言實現和開發環境支持的獨立編譯技術并非語言本身所規定的。每一個源代碼文件(源文件及其遞歸包含的所有頭文件展開)就是一個最小的編譯單元,每一個編譯單元可以獨立編譯而不需要知道其他編譯單元的存在及其編譯結果。例如,一個編譯單元在單獨編譯的時候根本無法知道另一個編譯單元在編譯的時候是否已經定義了一個同名的extern全局變量或全局函數,所以每個編譯單元都能夠通過編譯,但是如果另一個編譯單元也定義了同名的extern全局變量或全局函數,那么當把兩個目標文件連接到一起的時候就會出錯。

獨立編譯技術最大的好處就是公開接口而隱藏實現,并可以創建預定義的二進制可重用程序庫(函數庫、類庫、組件庫等),在需要的時候用連接器把用戶代碼與庫代碼連接成可執行程序。另一方面,獨立編譯技術可以大大減少代碼修改后重新編譯的時間。

主站蜘蛛池模板: 贵港市| 新巴尔虎右旗| 福泉市| 江源县| 沾化县| 峡江县| 兴城市| 龙井市| 松原市| 临西县| 林州市| 肥乡县| 新晃| 信阳市| 靖江市| 盘锦市| 平度市| 霍城县| 台东市| 额敏县| 平度市| 乐陵市| 昌黎县| 达日县| 壤塘县| 潮州市| 福建省| 金湖县| 威宁| 论坛| 渝中区| 安乡县| 临西县| 遂平县| 工布江达县| 灌阳县| 穆棱市| 墨脱县| 大悟县| 循化| 白水县|