C/C++程序設計的基本概念詳解
概述
學C語言有很長一段時間了,想做做筆記,把C和C++相關的比較容易忽視的地方記下來,也希望可以給需要的同學一些幫助。
我的這些文章不想對C和C++的語法進行講解和羅列,這些東西隨便找一本書就講的比我清楚,我只是想把一般人忽視的地方盡自己所能描述一下。權當班門弄斧,貽笑大方了。
首先我想先從C和C++的一些基本概念入手。
main()函數(shù)
稍微學過C和C++的人都知道m(xù)ain()函數(shù)市所有C和C++程序必不可少的東西。叫做主函數(shù)。所有的程序都應該從main()函數(shù)開始執(zhí)行。但是你們又對這個函數(shù)了解多少呢?
我們都知道C和C++是一種函數(shù)語言,幾乎絕大多數(shù)的功能都是通過各種函數(shù)的調用來實現(xiàn)的,C和C++也提供了豐富的函數(shù)庫供編程人員調用??呻m然main()函數(shù)每個C程序都必須有的函數(shù),在C或者C++的函數(shù)庫里卻沒有叫做main()的函數(shù),它是需要程序設計人員實現(xiàn)的函數(shù)。
而且,你們發(fā)現(xiàn)了沒有,main并不是C和C++的保留字。因此理論上,你可以在其他地方使用main這個名字,比如變量名、類名字、名字空間的名字甚至成員函數(shù)的名字。但是,即使這樣,你也不能修改main()函數(shù)本身的函數(shù)名,否則連接器就會報告錯誤。
main()函數(shù)是C和C++程序的入口,這是因為C和C++語言實現(xiàn)會有一個啟動函數(shù),比如MS-C++的啟動函數(shù)就叫做
mainCRTStartup()或者WinMainCRT-Startup()。在這個啟動函數(shù)的最后會調用main()函數(shù),然后再調用exit()函數(shù)結束程序。如果沒有main()函數(shù),當然會報錯了。所以再C和C++開發(fā)環(huán)境中main()函數(shù)其實是一個回調函數(shù)。它是需要我們來實現(xiàn)的。
有些同學可能學過一些應用程序框架,比如MFC什么的。這些程序代碼中往往找不到main()函數(shù),這是因為那些應用程序框架把main()函數(shù)的實現(xiàn)給隱藏起來了,main()函數(shù)在它們這里有固定的實現(xiàn)模式,所以不需要我們編寫。在連接階段,框架會自動將包含main()實現(xiàn)的庫加進來連接。
main()函數(shù)也是有原型的。這個原型已經(jīng)是一種標準了,在ISO/IEC14882中對main()的原型進行了定義。
int main(){/*......*/} 和 int main(int argc, char *argv[]){/*......*/}
上面這兩種形式是最具有可移植性的正確寫法。當然不同的編譯器可能會允許出現(xiàn)一些擴展。比如允許main()返回void,或者有第三個參數(shù)char *env[]什么的。這個就要看具體的編譯器文檔了。
關于返回值,我們知道m(xù)ain()返回的是int類型的。到底返回什么是有不同含義的。一般情況下,返回0,表示程序正常結束,返回任何非0表示錯誤或非正常退出。前面講到了,啟動函數(shù)最后還會調用exit()函數(shù)。那么main()函數(shù)的返回值就會作為exit()函數(shù)的操作數(shù)來返回操作系統(tǒng)。
在C++當中對main()函數(shù)還有一些特殊的限制。比如:
- 不能重載
- 不能內聯(lián)
- 不能定義為靜態(tài)的
- 不能取其地址
- 不能由用戶自己調用
關于main()函數(shù)的參數(shù),它可以讓編譯好的執(zhí)行程序具有處理命令行參數(shù)的能力。這里需要注意,不要把“命令行參數(shù)”和main()函數(shù)的“函數(shù)實參”混淆,這是兩個不同的概念。命令行參數(shù)由啟動程序截獲并打包成字符串數(shù)組傳遞給main()的形參argv[],而包括命令字(也就是執(zhí)行文件自己的名字)在內的所有參數(shù)的個數(shù)則被傳遞給形參argc。試一下吧,咱們來模擬copy命令寫個簡單的文件拷貝程序。
//mycopy.c:文件拷貝程序。 #include <stdio.h> int main(int argCount, char* argValue[]) { FILE *srcFile = 0; FILE *destFile = 0; int ch = 0; if(argCount != 3) { printf("使用方法:%s 原文件 目標文件\n",argValue[0]); } else { if((srcFile = fopen(argValue[1],"r")) == 0) { printf("無法打開原文件!\"%s\"!",argValue[1]); } else { if((destFile = fopen(argValue[2],"w")) == 0) { printf("無法打開目標文件!\"%s\"!",argValue[2]); fclose(srcFile); } else { while((ch = fgetc(srcFile)) != EOF) { fputc(ch,destFile); } fclose(srcFile); fclose(destFile); return 0; } } } return 1; } //用法:mycopy C:\file1.dat D:\newfile.dat
內部名稱
在編寫C程序的時候如果沒有main()函數(shù),連接器會報錯。一般報錯信息會提示“unresolved external symbol_main”。這里的"_main"其實就是編譯器為main生成的內部名稱。其實C和C++語言在編譯過程中都會按照特定的規(guī)則把用戶定義的標識符(函數(shù)、變量、類型、名字空間什么的)轉換為相應的內部名稱。而這些規(guī)則還跟指定的連接規(guī)范有關。比如,在C語言中,main的內部名稱就叫做_main。
C語言這么做,是告訴連接器,這個東西是個函數(shù)。實際上,C語言在所有函數(shù)的函數(shù)名前其實都是加了前綴“_”的,以此來區(qū)別函數(shù)名和其他標識符名稱。
這種規(guī)范在C++又是另一種樣子。這是因為在C中,所有函數(shù)只要不是局部于編譯單元(文件作用域)的static函數(shù),就會是具有extern連接類型的和global作用域的全局函數(shù)。全局函數(shù)是不可以有同名的。但是在C++里面,可以在不同的作用域,比如class,struct,union,namespace中定義同名的函數(shù),甚至在同一個作用域也可以定義同名函數(shù),也就是函數(shù)重載。那么轉換為內部名稱的連接規(guī)范就要復雜一些了。比如:
class Sample_1 { char m_name[16]; public: void foo(char *newName); void foo(int age); }; class Sample_2 { char m_name[16]; public: void foo(char *newName); void foo(bool sex); };
在其他地方根據(jù)這兩個類生成兩個實例,并進行操作:
Sample_1 a; Sample_2 b; a.foo("aaa"); a.foo(100); b.foo("bbb"); b.foo(false);
這里有四個函數(shù),但是確是同一個名稱。編譯器應該怎么區(qū)分呢?通過各自對象的成員標識符區(qū)分?那是在代碼中區(qū)分的,但是在連接器看來,所有函數(shù)其實都是全局函數(shù),而全局函數(shù)是不能重名的。所以為了避免二義,在C++中有一個名字修飾規(guī)則。也就是在函數(shù)名前面添加各級作用域的名稱以及重載函數(shù)經(jīng)過編碼的參數(shù)信息。比如上面四次調用foo函數(shù),其實它們會調用四個具有全局名稱的函數(shù),分別是Sample_1_foo@pch@1,Sample_1_foo@int@1,Sample_2_foo@pch@1,Sample_2_foo@int@1。
然而,這種標準并不是強制的,所以不同廠家開發(fā)的C++編譯器有可能會有些許不同,而這也正是導致不同廠家的C++編譯器和連接器不能兼容的原因。
那么好了,當使用不同編程語言聯(lián)合開發(fā)時候,就要定義一個統(tǒng)一的規(guī)范,這個規(guī)范叫做連接規(guī)范。這個很好理解了吧,因為如果同一個標識符在不同編譯單元中用不同的連接規(guī)范,就會產生不一致的內部名稱,連接肯定會失敗。
所以,在開發(fā)程序庫的時候就一定要明確你要用那條連接規(guī)范。比如,編寫C程序是就要規(guī)定C連接規(guī)范:extern “C”。大約有這么幾種情況:
- 僅對一個類型、函數(shù)、變量或常量指定連接規(guī)范;
extern "C" void WinMainCRTStartup(); extern "C" const CLSID CLSID_DataConverter; extern "C" struct Student{/*....*/}; extern "C" Student g_Student;
- 對一段代碼限定連接規(guī)范
#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
- 當前使用的是C++編譯器,并且使用了extern "C"限定了一段代碼的連接規(guī)范,但又想在其中某行或某段代碼保持C++的連接規(guī)范。
#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
- 某個聲明中指定了某個標識符的連接規(guī)范為extern “C”,那么對應的定義也要指定extern “C”:
#ifdef __cplusplus extern "C" { #endif memcmp(const void*, const void*, size_t); #ifdef __cplusplus } #endif #ifdef __cplusplus extern "C" { #endif memcmp(const void *p, const void *a, size_t len) { //功能實現(xiàn) } #ifdef __cplusplus } #endif
其實如果是面向接口的編程,就不用考慮這么多了。因為即使接口兩端的內部名稱不同,只要使用了一致的成員對其和排列方式,并遵守一致的調用規(guī)范,一致的函數(shù)實現(xiàn)方式。也就是C++一致的對象模型,那么基本不會有什么問題的。
變量和它的初始化
在C和C++中,全局變量(extern或static的)存放在程序的靜態(tài)數(shù)據(jù)區(qū)里面。這些變量在程序進入main()之前就被創(chuàng)建了,并在main()結束后銷毀,C和C++提供了一個默認的全局初始化器0。也就是編譯器會默認的用0來初始化它們。函數(shù)內部的static變量和類的static成員也是在靜態(tài)存儲區(qū),因此也會默認初始化為0。除非你在創(chuàng)建的時候就提供了初值。這是編譯器對靜態(tài)變量的待遇。而對于其他的自動變量,就需要我們給他初始化了。不要指望編譯器自動對它們初始化。
所以,全局變量的聲明和定義應當放在源文件的開頭部位。
變量的初始化和變量的賦值是有區(qū)別的。初始化是發(fā)生在變量創(chuàng)建的同時,而賦值是在程序中變量創(chuàng)建后干的。
前面說了,對靜態(tài)存儲區(qū)的變量(比如全局變量,靜態(tài)變量什么)的進行初始化是編譯器自動進行的,但是局部變量的初始化確實需要編程人員手動進行。
還有,在一個編程單元中,全局變量的初始值不要依賴另一個編譯單元的全局變量。什么意思?比如:
//file1.c int g_x = 100;
//file2.c extern int g_x; double g_d = g_x + 10;
這兩個編譯單元編譯完成后進行連接,兩個全局變量到底先初始化哪個并不確定,連接器也不能保證這一點。先初始化g_x,那g_d也就能順利初始化,而反之,g_d就不一定是多少了。
另外,C和C++都會有現(xiàn)成的庫。就是文件開頭包含的那些*.h文件。注意哦,C的庫可是有多線程版和單線程版,開發(fā)多線程程序應該使用多線程版本的庫。另外,在多人開發(fā)軟件是,庫的版本一定要統(tǒng)一。
編譯時和運行時
源代碼文件編寫的功能有些時運行時起作用,有些編譯時就起作用的。這件事需要區(qū)分的。比如預編譯偽指令、類定義、外部對象聲明、函數(shù)原型、修飾符號(const,static那些)、類成員訪問說明符號(public、private那些)以及連接規(guī)范是在編譯階段發(fā)揮作用的,可執(zhí)行程序里是不存在這些東西的。而容器越界訪問、虛函數(shù)動態(tài)決議、動態(tài)連接、動態(tài)內存分配、異常處理、RTTI這些則是在運行時發(fā)揮作用的。比如:
int* pInt = new int[10]; pInt += 100; cout << *pInt << endl; *pInt = 1000;
這段代碼一般在編譯階段沒什么問題,但運行時會出錯。所以,我們在程序設計時就要對運行的行為有所預見,通過編譯連接的程序在運行時不見得正確。
總結
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關注腳本之家的更多內容!