0、其實常規的邏輯判斷結構、工具類、文件讀寫、控制台讀寫這些的關系都不大,熟悉之后,這些都是靈活運用的問題。
學習c/c++需要預先知道的一個前提就是,雖然有ANSI C標准,但是每個c/c++編譯器的實現在不少實用特性(除了標准庫外)上存在着很大的差異,所以最好的方法是先針對某種實現(可參考標准)去,而不是針對ANSI C或者C99或者C11標准去學。比如,vc對c99的標准支持就比較奇葩。比如說,在vc++中,內置的布爾類型或者別名就包括BOOL、bool、_Bool,如下所示:
BOOL boptVal = FALSE; bool cppbool; _Bool cBool;
這三都是合法的,而在標准的c99中,只定義了_Bool。
VC++的C++可參見https://msdn.microsoft.com/zh-cn/library/ty9hx077(v=vs.100).aspx
VC++的C可參考https://msdn.microsoft.com/zh-cn/library/fw5abdx6(v=vs.110).aspx
VC++的主要類庫可參考https://msdn.microsoft.com/zh-cn/library/52cs05fz(v=vs.100).aspx,其中大部分在https://msdn.microsoft.com/zh-cn/library/59ey50w6(v=vs.100).aspx
VC++ 2010不支持C99下面的幾個特性:
- 支持不定長的數組
- 變量聲明不必放在語句塊的開頭
- 除了已有的 __line__、__file__ 以外,增加了 __func__ 得到當前的函數名。
- 不支持_Bool類型。不過在vc++中,從vc++ 5.0開始,內置支持bool類型,占用1個字節,_Bool通過typedef定義的,而不是定義在stdbool.h頭文件中。
1、在java中,不支持無符號型基本數據,也就是都是有符號的。如果要得到無符號的值,需要使用更大范圍的值做位與,比如byte a = (byte) 234; int i = a; i = a&0xff; 來得到。
在c++中,默認有沒有符號跟c++編譯器實現有關,不同的編譯器可能有不同的行為,同時標准因為制定的時候,目標C就是短小精悍,因此僅僅規定了最小值,而不是明確值,導致不同平台可以自己決定如何實現。如下所示:
尤其是最常用的int和long。
通常應該顯示進行指定確保平台間的通用。還有一點比較注意的是,c++沒有byte類型,也可以認為c/c++中的char其實就是java中的byte。c/c++中的char/wchar_t對應java中的char,取決於他們是單字節字符還是多字節字符。為此,標准定義了一個inttypes.h頭文件,其中定義了平台無關的數據類型別名,比如int32_t。VC在basetsd.h定義了類似的類型別名,msinttypes.h提供了兼容的別名。
2、在c++中,char占一個字節,這樣的話很多字符比如中文就沒法存儲在一個char中,這樣就需要使用wchar_t。在java中,字符天然就是unicode表示,所以char就天然的可以保持人類理解的char。
所以,對於非西方國家來說,wchar_t是個使用極為頻繁的類型,如果將中文存儲在char中,會導致溢出而出現不可預期的字符如亂碼。
其次,因為在內部,任何的字符都是存儲為數字編碼,所以要在c++中輸出wchar_t類型的值,還必須顯示告訴它要怎么顯示,比如:
char sc = '測'; wchar_t wsc = L'測';
wchar_t wsca[] = L"大中華區"; cout << sc << "\n"; wcout.imbue(locale("chs")); wcout << wsc << "\n";
在java中,因為原生就是當做unicode表示,所以就沒有了這個必要。cout用於處理單字節字符,wcout對應於寬字節字符。
3、在java中,string的使用是如此的頻繁,以至於對於需要字符串的地方,幾乎沒有人會去使用char[]數組。而在c++中,有大量的程序其實使用標准c而非c++的語法,以至於不得不在這兩者間來回,以'\0'結尾的char[]等價於string,反之就不是(在判斷string長度的時候,這一點必須考慮到)。比如cout就是以'\0'作為結束的標志,這對於新手來說,char[]和string的關系和轉換必須足夠的重視。雖然可以用char str[] = "abc";進行初始化。但后續要修改還是要一個個下表進行修改,比如如下:
char st[] = "abc"; st[0] = '1';
而不能st = "bcd"; 也不能st = another_st; --因為在c++中,數組(array是標准庫里面的類型)不是對象,而在java中,數組也是對象,這是允許的。
如果確定在c++編譯器下運行的話,應該用string類型的字符串(但是得注意,畢竟string等屬於stl的東西,性能上可能會有一定的下降,這估計也是個重要的原因,所以實際主流仍然是char[]),string str = "abc"; str = "abcd"; str = str + L"測試"; 這樣可以省去不必要繁瑣的細節。但是用到string還得注意,c++有string和wstring之分,它分別對應了char和wchar_t的差別。總之,如果說在java中,絕大部分開發人員不用關心編碼的話,在c++下,絕大部分開發人員必須關心編碼以及對應的SDK。
此外,判斷長度的時候,對於以字符串初始化的數組,strlen僅考慮實際的字符長度,而sizeof則數組的長度。比如:
char st[] = "aaaaaaabc"; st[0] = '1'; cout << st << "\n"; cout << strlen(st) << "\n"; cout << sizeof st << "\n";
輸出:
9
10
就字符串來說,通常情況下,無論對於一些臨時需要的字符串上下文變量亦或是結構體中的成員變量,其長度都是根據不同的請求而不同,所以實際中真正的使用固定長度的數組寫死是很少的,不少情況下都是根據實際的大小動態申請內存,賦值給臨時變化或者成員變量,然后用完的時候進行free,並不是教科書中使用的很大一部分固定長度的數組。
PS:從IDE的角度來說,使用原生類型和OO的差別在於前者沒有上下文信息,后者具有上下文信息,意味着使用前者開發完全取決於對標准庫的熟悉,而OO的話,IDE有足夠的上下文進行代碼提示從而一定程度上降低了開發要求,從開發的角度來說,這又像是apache提供的各種工具類或者數據庫提供的函數亦或是運維管理員天天打交道的命令,你必須去熟悉API。通常之所以覺得c++難,一種原因通常是隔行如隔山,習慣了java的各種ide后,c++的ide不怎么熟悉,要是ue打開,無法各種智能跳轉和hint,那要想熟悉可想而知。。。如果有好的IDE幫助,在任何代碼位置,可以快速跳轉到定義,確定對象來自於標准類庫、三方類庫或者應用公用庫,進一步得到完整的上下文信息,再通過API注釋,很快能夠猜測的八九不離十。就vs而言,如果要查看一個頭文件中的所有定義,可如下:
查看函數原型:
4、名稱空間,簡單的理解就是java的包,你可以在import之后直接調用某個類的方法,也可以通過全路徑的引用。只不過c++除了include外,還需要加一個using namespace XXX而已,純c則沒有必要。還需要注意的是,在java中,無論class/還是interface都可以import,在c/c++中,一般只能import .h頭文件。一般來說,每個c編譯器或者IDE都有自己搜索頭文件的約定,ansi c無明文規定。
5、在java中,除了創建pojo和在某些地方使用幫助變量會用原生類型外,大部分情況下打交道的就是object了,因為pojo通常代表着領域中的實體。雖然C++是oo語言,但是前文提過,絕大部分的c++程序其實很大一部分代碼都是標准c代碼(所以,在生產系統中,可以發現通常是絕大部分c代碼夾雜着偶小小部分c++ oo代碼。比如redis用c寫的,mysql用c++寫的,postgresql用c寫的。)所以,在c/c++中,struct可能是使用最頻繁的類型之一了,自然也是極為重要。
java定義類用的是class ObjName。在c++中,一樣要先定義struct。和class一樣,你可以在方法內部定義結構,也可以在文件級別獨立定義結構,唯一的差別只是可見范圍,相當於內部類的性質。
struct NodeInfo { int id; char name1[20]; }; int main() { NodeInfo nodeInfo = { 1, "name1" }; cout << nodeInfo.id << "\n"; struct NodeInfo { int id; char name[20]; }; struct NodeInfo cNodeInfo; //c風格 cNodeInfo.id = 1; cNodeInfo.name[0] = '1'; cout << nodeInfo.id << "\n"; NodeInfo cppNodeInfo; //c++風格,強調的是這是一種新的類型
不同於對象的構造器,如果在聲明變量時沒有初始化結構,后面只能一個個成員賦值,這和不使用getter/setter,直接用public字段性質類似。
既然定義了結構體,通常是常用的領域對象,應該跟oo一樣的思路,聲明在文件級別,這樣可以全局公用,變量以及函數同理。
結構體變量可以直接賦值,而且是值拷貝,比較接近於java中Object.clone(),試具體實現而定。
在定義結構體的時候甚至可以直接聲明變量,如下:
struct NodeInfo { int id; char name1[20]; } g_nodeInfo1,g_nodeInfo2; //典型語法糖
在c++的實現中,結構體還可以有成員函數,這通常來說屬於OO的范疇了。通常,生產中用的並不多。
6、基本類型、結構、數組之后,從數據結構的角度來說,重要的就是他們的組合了。畢竟,在實際的系統中,集合的處理占了很大一部分比例。他們中,又以結構數組為主。
NodeInfo nodeInfoA[10]; nodeInfoA[0].id = 9999; nodeInfoA[9].id = 123;
實際上,這種用法在生產系統中並不多,因為在絕大部分情況下,無法提前預知會有幾個元素。所以,通常需要動態確定,而標准c里面沒有提供類似於java list的集合類。
7、上面說到數組其實並不是那么的實用,這個時候就要講到c/c++編程中最重要的一個部分,內存的動態分配、釋放和操作這些內存的指針了。我們應該說,很多c++教科書里面那基本抄寫c參考手冊的組織真是不合理,先從簡單變量的地址開始講解指針絕對是個沒腦子的想出來的,很明顯,應該從分配和釋放內存以及引用傳遞、值傳遞、函數指針開始(至於動態分配還是靜態分配無所謂,但必須找到足夠合適的上下文闡明不得不使用指針的場景),講解指針才是合乎邏輯的。因為實際的系統任何時候內存需求都是動態變化的,這才是核心,否則都靜態確定,能用得着指針?
在c/c++中,動態分配內存有兩種方式,malloc/free庫函數或new/delete操作符。其實用哪個不重要的,重要的是不要讓指針指向不確定的地址,也就是:
int *bad_pt; -- 默認這個時候指針指向哪里是不確定的,取決於bad_pt中當前的值指向哪里,確保動態指針通過malloc初始化是非常重要的 int *good_pt = new int;
以及及時釋放指向的內存塊:
delete bad_pt;
malloc與free是C++/C語言的標准庫函數,new/delete是C++的運算符。它們都可用於申請動態內存和釋放內存。
對於非內部數據類型的對象而言,光用maloc/free無法滿足動態對象的要求。對象在創建的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。由於malloc/free是庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加於malloc/free。
大部分的現行c/c++代碼都使用malloc和free。
操作普通類型的指針變量並不復雜,重點是數組指針(主要是涉及遍歷),結構體指針的操作。
動態創建數組:
int *bad_pt; int *good_pt = new int; delete good_pt; int *arrayPt = new int[10]; // 指向第一個元素的地址 arrayPt[0] = 1; // 可以把指向數組的指針當數組使用,在內部,c/c++就是把數組當指針處理的 arrayPt[1] = 2; arrayPt = arrayPt + 1; //現在指向第二個元素,一般不建議這么用,不然delete的時候還得回去 arrayPt[1] = 3; //指向第三個元素 arrayPt = arrayPt - 1; delete [] arrayPt;
需要注意的一點是對於我們如此常用的arraylist,在c99標准中是定義了可變長度數組的,而實際中,在支持的c99 的編譯器下運行如gcc vc不支持c99標准中的c語言變長數組,windows有時候對於開發者來說就是個奇葩。
動態創建結構體:
NodeInfo * node_info_pt = new NodeInfo; node_info_pt->id = 1; node_info_pt->name[0] = '1'; (*node_info_pt).name[1] = '1'; NodeInfo *node_info = &cNodeInfo; //通常調用方法的時候都是這種用法,因為結構體按值傳遞 node_info->id = 1111; //通過結構指針訪問屬性 node_info->name[1] = 's'; delete node_info_pt; NodeInfo *node_info_arr = new NodeInfo[3]; //指向結構體數組的指針 node_info_arr[0].id = 1; (node_info_arr + 1)->id = 1;
malloc:
int* p = (int *) malloc ( sizeof(int) * 100 );
free:
free (p);
詳細可參考:http://www.cplusplus.com/reference/cstdlib/malloc/。
c函數調用中,經常不通過返回值的方式進行,而是通過指針變量進行交互,常用的方式中,包括:傳遞指針變量,傳遞變量的地址。
&用於獲取變量的地址(在OO的C++中,傳對象引用會非常的普遍,在面向過程的c中,則沒有那么的頻繁),*用於獲取指針指向的變量值,至於什么時候應該定義變量,傳遞變量的地址,什么時候又應該定義指針,並傳遞指針,這跟函數的實現方式有關系,從技術本身上而言,兩者可以互換,比如對於getsockopt獲取socket選項的函數,我們可以使用下列兩種方式得到其值:
int optlen = 4; int optval = 0; sock = WSASocket(iFamily, iType, iProtocol, NULL, 0, dwFlags); getsockopt(sock,SOL_SOCKET,SO_RCVBUF,(char*)&optval,&optlen);
或者:
int *optptr = new int; int *valptr = new int; memset(valptr,0,sizeof(int)); *optptr = sizeof(int); getsockopt(sock,SOL_SOCKET,SO_RCVBUF,(char*)valptr,optptr);
通常來說,內存的使用是誰是主動者誰負責分配和釋放,庫函數通常要求傳遞一個包含了最小長度的內存的指針,然后進行填充,順便有可能返回或者通過指針告訴調用者實際的長度。
就函數調用而言,數組和指針可以說是等價的,下面的原型是等價的。
但是在函數內部,聲明是數組時,pt++是不合法的,而聲明是指針時,這是常用的。至於調用的時候是數組傳遞給指針聲明還是反過來,無所謂,當前的馬甲決定了可以執行什么操作。
講到指針,就不得不講指向指針的指針,指針數組。
7、函數和函數指針。c因為沒有原生的層級組織方式,所以各種函數列表的管理更顯得重要,比如可以使用功能號的概念。
函數分為兩塊,函數定義以及函數原型,簡單地說,函數原型就相當於java接口,至於運行時有沒有實現那是另外一回事。頭文件就是典型的例子,其中包含了ANSI標准庫的原型(他只是開發、編譯時需要)。對於全局性的函數,應該首先在頭文件中定義函數原型。
定義原型和java類似,包含聲明不含實現。比如:
int calc();
int calc1_bad(int arr[]);
int calc1_bad(const int arr[]); --不可變數組
c++建議將程序的整體結構包含在main中,並放在最前面。需要注意的是,因為數組名邏輯上等價於指針,所以默認情況下被調用函數可以修改傳入的數組。因為通過指針通常無法知道數組的元素數即無法使用sizeof,所以通常的用法是指定數組的長度:
int calc1(const int arr[],int len);
傳遞結構以及結構指針,就實際可用性而言,這是使用指針的第二個很普遍的場景。因為結構是傳值,所以一般都是傳遞結構指針,如下:
int transfer_strc(NodeInfo *nodeInfo);
傳遞指向數組的指針而不是數組的原因在於,數組可能會很大,而是用指針可以避免不必要的拷貝。
函數指針說得簡單點就是個回調函數的入口。在jquery/事件編程中這是很常見的場景,對於系統靈活性而言,這是非常重要的。
在java中,函數指針就相當於傳遞接口。在jquery中可以傳遞函數或者匿名函數塊。同理,在c++中,函數指針的步驟是:
1、聲明函數指針;
簡單地說,就是使用函數指針名代替函數聲明即可:
int transfer_strc(NodeInfo *nodeInfo); int (*pf) (NodeInfo*);
就常用而言,難度一般在於復雜的簽名,如:
int call_func_func(int (*pf) (NodeInfo*));
維護現有代碼時,有時候需要知道(*pf)到底是誰???
2、定義指向的地址(獲取函數的地址);
只要函數名,不要括號和參數即可,否則就是函數調用了。比如:test_callback_func(be_called_func)
int (*pf) (NodeInfo*);
pf = be_called_func;
3、使用函數指針調用函數。
int abc = (*pf)(參數);
8、#defined、typedef。
預處理器在c/c++中是非常常用的工具,幾乎任何的程序都會很大程度的時候用它。
使用極為廣泛的另一點宏定義和別名定義,這在java中並不存在直接的API,你會發現幾乎任何類庫或者API里面都大量的使用了各種宏定義,比如:
#defined用於定義宏,簡單地說就是快捷方式或者別名。
在大規模的開發過程中,特別是跨平台和系統的軟件里,define最重要的功能是條件編譯。
就是:
#ifdef WINDOWS
......
......
#endif
#ifdef Linux
......
......
#endif
跟java不同,c++中不能重復導入,所以在頭文件中使用#ifdef和#ifndef是非常重要的,可以防止雙重定義的錯誤。
在C/C++語言中,typedef常用來定義一個標識符及關鍵字的別名,它是語言編譯過程的一部分。
#defined常用於定義類型別名之外的用途,一般來說,宏定義純粹就是替換。typedef用於定義類型別名,並且typedef並不是純粹的替換,特別是在涉及到指針相關的概念時,這也是很多代碼繞來繞去的原因,直接替換就不正確了。
9、static,extern。static聲明文件內部全局,非應用程序全局,默認是全局變量,類似於private類變量。extern聲明變量定義在其他地方,通常聲明應用全局,應定義在頭文件中。如果說java文件之間的協作通過接口來組裝的話,那c/c++文件間就是通過頭文件進行黏合。對於標准庫的頭文件也是一樣的,設計者認為相關的一組函數會放在同一個頭文件中。
10、c/c++的另一點難處在於,不知出於什么原因,大量的頭文件中中並沒有說明這個struct以及函數原型的含義,以至於每次都要參考手冊。如下:
只有簽名,沒有說明。
11、c/c++中常見的宏定義含義及以及參考:
__stdcall:Windows API默認的函數調用協議,vc下基本上都是這了。 Windows上使用dumpbin工具查看函數名字修飾。
__cdecl:C/C++默認的函數調用協議。
__fastcall:適用於對性能要求較高的場合。
extern "C" {}
extern "C" { #endif #ifndef _NLSCMP_DEFINED #define _NLSCMPERROR 2147483647 /* currently == INT_MAX */ #define _NLSCMP_DEFINED #endif
C++保留了一部分過程式語言的特點,因而它可以定義不屬於任何類的全局變量和函數。但是,C++畢竟是一種面向對象的程序設計語言,為了支持函數的重載,C++對全局函數的處理方式與C有明顯的不同。
extern "C"的主要作用就是為了能夠正確實現C++代碼調用其他C語言代碼。加上extern "C"后,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。由於C++支持函數重載,因此編譯器編譯函數的過程中會將函數的參數類型也加到編譯后的代碼中,而不僅僅是函數名;而C語言並不支持函數重載,因此編譯C語言代碼的函數時不會帶上函數的參數類型,一般之包括函數名。
比如說你用C 開發了一個DLL 庫,為了能夠讓C ++語言也能夠調用你的DLL輸出(Export)的函數,你需要用extern "C"來強制編譯器不要修改你的函數名。
而在C語言的頭文件中,對其外部函數只能指定為extern類型,C語言中不支持extern "C"聲明,在.c文件中包含了extern "C"時會出現編譯語法錯誤。
在windows c++中,很多的函數原型可以看到聲明了 FAR PASCAL,其含義可參見http://blog.csdn.net/gucas2008/article/details/2187992,當代處理器和OS基本可以忽略,其定義在很多頭文件中,比如:
12、基本上c++代碼中,有些是C++的駝峰式風格,有些是c的下划線風格,必須習慣。
13、很重要、也是導致c/c++學習難度大的一點在於各種數據類型之間的轉換,初始化,傳各種指針變量、變量的指針地址等等。
比如,經常代碼中會有如下聲明:
typeName *pt = NULL;
NULL是C++從C語言繼承下來的預處理器變量,在cstdlib頭文件中定義,值為0,。代表一個空指針,由系統保證空指針不指向任何實際的對象或者函數。 反過來說,任何對象或者函數的地址都不可能是空指針。”
就面向對象的特性來說,java中對象的傳遞(string除外),傳遞的是對象的引用,在c++中,實現上則是傳遞的對象的深拷貝,如果要實現引用傳遞,需要函數原型transferObjRef(Obj&),使用的時候跟普通對象沒有區別,同java不同,c++接口定義中參數名可以省去。
14、c/c++中,通常用的類似log4j/log4net一樣的方式不多,大都還是printf的方式,它和sprintf/fsprintf一樣,都是為了用於格式化,跟java中MessageFormat類似,因為沒有自成的toString()概念,所以printf的格式必須很了解。
15、現實的應用通常都是由大量的源碼文件+頭文件組成的,因此好的IDE是很有必要的,就如現在開發java,幾乎沒有人會直接用javac去編譯,用記事本寫代碼,但是如同eclipse/idea均有不同的風格,vs和codeblock也不同。
16、常用頭文件。
ctype.h:其中包含常用的判斷比如字符類型的函數,類似apache commons相關的類庫。
17、編譯器的種類,常用的編譯器有unix c編譯器(主要在solaris、freebsd下),linux gcc編譯器,visual c++編譯器。vs通常以工程單位進行編譯,而不是文件,意味着所有.c文件必須包含在工程中,而不包括頭文件。在java中,雖有oracle jdk和ibm/hp各自的jdk,但基本上都是跟着JLS走的,而JLS還是很規范的,沒那么多自由度。c這有點跟HTML/JS/CSS規范之於各種瀏覽器的實現。還有一點,c直接編譯成了機器碼,以至於通過接口,我們無法直接看到內部的實現,這一點不同於java和解釋性語言,調用不正確的時候,我們可以看一下源碼就知道問題可能是什么了。
18、預定義宏。
__DATE__
__FILE__
__LINE__
__STDC__
__TIME__
19、編譯與鏈接。同java一樣,在命令行進行編譯的時候,可能需要指定各種javac編譯選項,比如指定各種classpath以及jar的目錄等等。只不過java開發來說,只需要編譯成字節碼就可以了,c/c++還需要將.o/obj鏈接成可執行程序,此時還會增加一個步驟,通常用IDE編譯的話,熟悉之后可以自動鏈接,對於開源的項目,通常我們要download源碼,自行編譯configure,make,make install。
20、類庫,同java一樣,通常會引用第三方的類庫和jvm的類庫,只不過在java中,通常三方庫打包成jar,運行時由classloader加載,相當於只有動態加載的概念。在c/c++中,分為動態庫和靜態庫兩種。windows下分別是dll和lib結尾,linux下則是so和a結尾。具體的用法可以參考http://www.cnblogs.com/skynet/p/3372855.html。
21、最后,最好的方式不是寫個DEMO或者教科書中的例子,而是實現比較實際的功能,比如實現現有java/python的某個功能點,這么一折騰下來,很多問題就都能理解了。
最最最最最最最需要轉換思路的一點是,作為使用最為頻繁的字符串,在java中,除了socket和框架編程外,幾乎所有人都會使用String類型,而在c++中,幾乎所有的人都傾向於使用各種char變種指針,這一點導致的cpp代碼與純OO代碼的差別估計占據了很大一部分的比例。