這些小技巧之所以特別,是因為這些信息通常吧不能在C++書籍或者網站上找到。比如說,成員指針,即使對于高級程序員也是比較棘手,和易于產生bugs的,是應該盡量避免的問題之一。
<翻 by凌云健筆>
What makes these tips special is that the information they provide usually cannot be found in C++ books or Web sites. For example, pointers to members are one of the most evasive, tricky, and bug-prone issues for even advanced users.
by Danny Kalev
====================================
Page 1: Introduction 介紹
接下來的這幾條技巧主要集中于實用技術和一些晦澀知識上;它們與特殊的平臺、編程領域、或編譯器無關。因此,它們適用于所有的C++程序員。本人把這些技巧劃分為五大類:編碼風格、內存管理、性能提升、面向對象的設計,和標準模板庫(STL)五方面的一般準則。
The following tips are a collection of general hands-on techniques and recondite pieces of knowledge not associated with a specific platform, programming domain, or compiler. As such, they can be of use to all C++ programmers. I grouped the tips into five major categories: general guidelines for coding style, memory management, performance enhancement, object-oriented design, and the Standard Template Library (STL).
====================================
First Four: Guidelines for Better Coding Style 較好編程風格所要遵循的一些準則
在這個類別中,所涉及的技巧是各級C++的程序員均會經常提及的問題。舉個例子,我很驚訝的發現,有很多具有一定經驗的程序員仍舊不知道.h是一種過時的標準頭文件標識方式,不會正確的應用名空間,不了解在向臨時對象綁定引用時所要遵循的準則。這些問題以及一些其他問題將在這里進行討論。首先,我們先解釋過時的頭文件命名符號<xxx.h>與現代的符合標準的<xxx>頭文件命名符號之間的區別。接下來,我們探究一些由于編譯器限制以及相關的語言規則深奧性質所帶來的C++“陰暗角落”;這點往往有許多程序員混淆不清。例如,用逗號分隔的表達式,對右值綁定引用的規則等。最后,我們將學習如何在程序的啟動之前調用某個函數。
技巧1:用<iostream.h>還是<iostream>?這不是一個問題!
很多的C++程序員依舊使用<iostream.h>,而非最新的、標準編譯生成的<iostream>庫。這兩個庫之間有什么區別呢?首先,針對用.h作為標準頭文件的標識符這一問題,五年前就已經不被推薦使用了。在新的代碼中再使用這種不被認同的表示方式絕不是一個好主意。在功能方面,<iostream>包括模板化的IO類,它同時支持窄字符和寬字符;而<iostream.h>卻只支持以char為導向的流。第三,在C++的iostream接口標準規格在許多微妙的方面發生了變化。所以,<iostream>的接口與實現與<iostream.h>存在著一定得差異。最后,,<iostream>組件聲明于std命名空間中,而<iostream.h>組件是全局性的。
因為二者之間存在著這些重大分歧,你不能在同一程序中混合使用兩個庫。作為一條準則:使用<iostream>代替<iostream.h>,除非你處理了那些只與<iostream.h>兼容的遺留代碼。
Tip 1: <iostream.h> or <iostream>?
Many C++ programmers still use <iostream.h> instead of the newer, standard compliant <iostream> library. What are the differences between the two? First, the .h notation of standard header files was deprecated more than five years ago. Using deprecated features in new code is never a good idea. In terms of functionality, <iostream> contains a set of templatized I/O classes which support both narrow and wide characters, as opposed to <iostream.h> which only supports char-oriented streams. Third, the C++ standard specification of iostream's interface was changed in many subtle aspects. Consequently, the interfaces and implementation of <iostream> differ from those of <iostream.h>. Finally, <iostream> components are declared in namespace std whereas <iostream.h> components are global.
Because of these substantial differences, you cannot mix the two libraries in one program. As a rule, use <iostream> unless you're dealing with legacy code that is only compatible with <iostream.h>.
技巧2:左值的引用,要注意!
左值和右值是C++編程的基本概念。從本質上說,右值是一種不能出現在賦值表達式左側的表達式。相較而言,左值是一個你可以寫入數值的對象或者內存塊。引用可以指向左值,也可以是右值。但是,由于對右值的語言限制,所以你必須了解在向右值綁定引用時所要遵循的規則。
只要引用綁定的對象是一個const類型,那么就可以對右值綁定引用。這一規則背后的理由很簡單:你不能試圖去改變一個右值,常量的引用保證了程序不會通過引用去改變右值。在下面的例子中,函數f()輸入一個const int的引用:
void f(const int & i);
int main()
{
f(2); /* OK */
}
程序將右值2作為一個參數傳入函數f()。在實時運行中,C++會生成一個類型為int值為2的臨時對象,并將其與引用i綁定。臨時對象和他的引用存在與函數f()從觸發到返回整個過程;函數返回后,他們被立即銷毀。注意,如果我們聲明引用i時沒有使用const標識符,函數f()就可以修改它的參數,從而引起未定義的行為。所以,你只能向常量對象綁定引用。
同樣的準則適用于自定義對象類性。只有當臨時對象是常量時,你才能綁定引用。
Tip 2: Binding a Reference to an Rvalue
Rvalues and lvalues are a fundamental concept of C++ programming. In essence, an rvalue is an expression that cannot appear on the left-hand side of an assignment expression. By contrast, an lvalue refers to an object (in its wider sense), or a chunk of memory, to which you can write a value. References can be bound to both rvalues and lvalues. However, due to the language's restrictions regarding rvalues, you have to be aware of the restrictions on binding references to rvalues, too.
Binding a reference to an rvalue is allowed as long as the reference is bound to a const type. The rationale behind this rule is straightforward: you can't change an rvalue, and only a reference to const ensures that the program doesn't modify an rvalue through its reference. In the following example, the function f() takes a reference to const int:
void f(const int & i);
int main()
{
f(2); /* OK */
}
The program passes the rvalue 2 as an argument to f(). At runtime, C++ creates a temporary object of type int with the value 2 and binds it to the reference i. The temporary and its reference exist from the moment f() is invoked until it returns; they are destroyed immediately afterwards. Note that had we declared the reference i without the const qualifier, the function f() could have modified its argument, thereby causing undefined behavior. For this reason, you may only bind references to const objects.
技巧3:奇怪的逗號分割表達式
逗號分隔的表達式是從C繼承而來的。你很有可能會在使用for-循環和while-循環的時候經常使用這樣的表達式。然而,在這方面的語言規則還遠不直觀。首先,讓我們來看看什么是逗號分隔的表達式:這種表達式可能包含一個或多個用逗號分隔的子表達式。例如:
if(++x, --y, cin.good()) /*three expressions 三個表達式*/
IF條件包含由逗號分隔的三個表達式。C++確保每表達式都被執行,產生其副作用。然而,整個表達式的值僅是最右邊的表達式的結果。因此,只有cin.good()返回true時,上述條件才為真。再舉一個逗號表達式的例子:
int j=10;
int i=0;
while( ++i, --j)
{
/*只要j不為0,在循環執行*/
}
Tip 3: Comma-Separated Expressions
Comma-separated expressions were inherited from C. It's likely that you use such expressions in for- and while-loops rather often. Yet, the language rules in this regard are far from being intuitive. First, let's see what a comma separated expression is. An expression may consist of one or more sub-expressions separated by commas. For example:
if(++x, --y, cin.good()) /*three expressions */
The if condition contains three expressions separated by commas. C++ ensures that each of the expressions is evaluated and its side effects take place. However, the value of an entire comma-separated expression is only the result of the rightmost expression. Therefore, the if condition above evaluates as true only if cin.good() returns true. Here's another example of a comma expression:
int j=10;
int i=0;
while( ++i, --j)
{
/*if (j!=0) loop*/
}
技巧4:如何在程序啟動前調用函數?
某些應用程序需要在調用主要程序之前開始啟動功能。例如,polling(輪詢),billing(***),和logger(日志記錄)等函數必須在調用實際的程序之前開始。最簡單的實現這一目標的方式是調用一個全局對象的構造函數。因為從概念上說,全局對象是在程序開始之構造的,這個函數會在main()開始之前返回。例如:
class Logger
{
public:
Logger()
{
activate_log();
}
};
Logger log; /*global instance*/
int main()
{
record * prec=read_log();
//.. application code
}
全局對象log在main()開始之前完成構造。在構造過程中,log觸發了函數activate_log()。當main()開始后,它就可以從日志文件中讀取數據。
Tip 4: Calling a Function Before Program's Startup
Certain applications need to invoke startup functions that run before the main program starts. For example, polling, billing, and logger functions must be invoked before the actual program begins. The easiest way to achieve this is by calling these functions from a constructor of a global object. Because global objects are conceptually constructed before the program's outset, these functions will run before main() starts. For example:
class Logger
{
public:
Logger()
{
activate_log();
}
};
Logger log; /*global instance*/
int main()
{
record * prec=read_log();
//.. application code
}
The global object log is constructed before main() starts. During its construction, log invokes the function activate_log(). Thus, when main() starts, it can read data from the log file.
毫無疑問,內存管理是在C++編程中最復雜和最易出錯的問題之一。能夠直接地訪問原始內存,動態地分配內存空間,以及C++的高效性決定它必須有一些非常嚴格的規則;如果你不遵守將難以避免那些內存相關的錯誤或者程序運行時的崩潰。
指針是訪問內存的主要手段。 C++可以分為兩個主要類別:指向數據的指針和指向函數的指針。第二大類又可以分為兩個子類類:普通函數指針和成員函數指針。在下面的技巧中,我們將深入探討這些問題,并學習一些方法,簡化指針的使用,同時隱藏它們的笨拙語法。
指向函數的指針很可能是C++中一種最不具可讀性的語法結構。唯一的可讀性更差的似乎只有成員指針。第一個技巧會教你如何提高普通的函數指針的可讀性。這是你理解C++成員指針的前提。接下來,我們將學習如何避免內存碎片,并告訴你其可怕的后果。最后,我們討論delete和delete []的正確使用方法;它常常會是眾多錯誤和誤解的來源。
技巧5:函數指針的繁瑣語法?!見鬼去吧!!
你能告訴我下面定義的含義么?
void (*p[10]) (void (*)());
p是“一個包含10個函數指針的數組,這些函數返回為空,其參數為{【(另外一個無參數返回為空)的函數】的指針}。”如此繁瑣的語法幾乎難以辨認,難道不是嗎?解決之道在何方?你可以通過typedef來合理地大大地去簡化這些聲明。首先,聲明一個無參數、返回空的函數的指針的typedef,如下所示:
接下來 聲明另一個typedef,一個指向參數為pfv返回為空的函數的指針:
typedef void (*pf_taking_pfv) (pfv);
現在,再去聲明一個含有10個這樣指針的數組就變得輕而易舉,不費吹灰之力了:
pf_taking_pfv p[10]; /*等同于void (*p[10]) (void (*)()); 但更具可讀性*/
技巧6:函數指針的枝枝節節
類有兩類成員:函數成員和數據成員。同樣,也就有兩種類別的成員指針:成員函數指針和數據成員指針。后者不太常見,因為,一般來說,類是沒有公共數據成員的。當使用傳統C代碼的時候,數據成員指針才是有用的,因為傳統C代碼中包含的結構體或類是具有公開數據成員的。
在C++中,成員指針是最為復雜的語法結構之一;可是,這卻是一個非常強大而重要的特性。它們可以使您在不知道這個函數的名字的前提下調用一個對象的成員函數。這是非常方便的回調實現。同樣的,你可以使用一個數據成員指針來監測和修改數據成員的值,而不必知道它的名字。
指向數據成員的指針
雖然成員指針的語法可能會顯得有點混亂,但是它與普通指針的形式比較一致和類似,只需要在星號之前加上類名::即可。例如,如果一個普通的整形指針如下所示:
那么,你就可以按照下面的方式來定義一個指向類A的整形成員變量的指針:
int A::*pmi; /* pmi is a pointer to an int member of A*/
你需要按照這樣的方式初始化成員指針:
class A
{
public:
int num;
int x;
};
int A::*pmi = & A::num; /* 1 */
標號1的語句聲明了一個指向類A的整形成員的指針,它用A類對象中的成員變量num的地址實現了初始化。使用pmi和內置操作符.*,你可以監測和修改任何一個A類型的對象中的num的值。
A a1, a2;
int n=a1.*pmi; /* copy a1.num to n */
a1.*pmi=5; /* assign the value 5 to a1.num */
a2.*pmi=6; /* assign the value 6 to a2.num */
如果有一個指向A的指針,你必須使用->*操作符:
A * pa=new A;
int n=pa->*pmi;
pa->*pmi=5;
成員函數指針
它由成員函數的返回類型,類名加上::,指針名稱,函數的參數列表幾部分組成。例如,類型A的一個成員函數返回一個int,無參數,那么其函數指針應該定義如下(注意兩對括號是必不可少的):
class A
{
public:
int func ();
};
int (A::*pmf) ();
換句話說,pmf是一個指向類A的成員函數的指針,類A的成員函數返回int指針,無參數。事實上,一個成員函數指針看起來和普通的函數指針相似,除了它包含函數名加上::操作符。您可以使用.*操作符來調用pmf指向的成員函數:
pmf=&A::func;
A a;
(a.*pmf)(); /* invoke a.func() */
如果有的是一個對象的指針,你必須使用->*操作符:
A *pa=&a;
(pa->*pmf)(); /*calls pa->func() */
成員函數指針遵循多態性。因此,如果你通過這樣一個指針調用虛成員函數,則會實現動態調用。但是需要注意的是,你不能將成員函數指針指向一個類的構造函數或者析構函數的地址。
技巧7:內存碎片,No!!No!!No!!
通常而言,應用程序是不受內存泄露影響的;但如果應用程序運行很長一段時間,頻繁的分配和釋放內存則會導致其性能逐漸下降。最終,程序崩潰。這是為什么呢?因為經常性的動態內存分配和釋放會造成堆碎片,尤其是應用程序分配的是很小的內存塊。支離破碎的堆空間可能有許多空閑塊,但這些塊既小又不連續的。為了證明這一點,請參看一下下面的堆空間表示。0表示空閑內存塊,1表示使用中的內存塊:
100101010000101010110
上述堆是高度分散。如果分配一個包含五個單位(即五個0)內存塊,這將是不可能的,盡管系統總共中還有12個空閑空間單位。這是因為可用內存是不連續的。另一方面,下面的堆可用內存空間雖然少,但它卻不是支離破碎的:
1111111111000000
你能做些什么來避免這樣的堆碎片呢?首先,盡可能少的使用動態內存。在大多數情況下,可以使用靜態或自動儲存,或者使用STL容器。其次,盡量分配和重新分配大塊的內存塊而不是小的。例如,不要為一個單一的對象分配內存,而是一次分配一個對象數組。當然,你也可以將使用自定義的內存池作為最后的手段。
技巧8:對于Delete 和 Delete [],你要區分清楚
在程序員當中流傳有一個眾所周知的傳說:對于內置類型,使用delete 代替delete []來釋放內存是完全可以的。例如,
int *p=new int[10];
delete p; /*bad; should be: delete[] p*/
這是完全錯誤的方式。在C++標準明確指出,使用delete來釋放任何類型的動態分配的數組,將會導致未定義行為。事實上,在某些平臺上應用程序使用delete而非delete[]但是不死機可以歸結為純粹的運氣:例如,針對內置數據類型,Visual C++通過調用free()同時實現了delete[]和delete。但是,我們并不能保證的Visual C++未來版本中將仍然堅持這么做。此外,也不會保證這個代碼將適用于其他的編譯器。總括來說,使用delete來代替delete [],或者用delete []來代替delete都很危險,應當盡量去避免。
Nine to 11: Performance Enhancements
下面所列出的是三個相當簡單但又不是很常見的技術,在不犧牲程序可讀性、不修改程序設計的前提下,提高程序的性能。例如,程序員往往不太清楚,只是簡單的對數據成員進行重新排序就可以大大減少它的大小。這種優化可以提高性能,如果應用程序使用到了這些對象的數組,效果尤其明顯。此外,我們還將學習前綴和后綴操作符之間的差異;在重載操作符中,這是一個很重要的問題。最后,我們將學習一些消除臨時對象的創建的方法。
技巧9:類成員對齊方式的優化
只需改變類成員的聲明順序就可以改變這個類的大小:
struct A
{
bool a;
int b;
bool c;
}; /*sizeof (A) == 12*/
在我的機器上,sizeof (A) 等于12。結果看起來非常的出乎意料,因為A的成員大小之和僅僅是6個字節,多余的6個字節來自何方呢?編譯器在每個bool類型成員的后面插入了各插入三個填充字節,使得它四字節邊界對齊。你可以按照下面的方式重新組織數據成員減少A的大小:
struct B
{
bool a;
bool c;
int b;
}; // sizeof (B) == 8
這次編譯器只是在成員c的后面插入兩個填充字節。因為b占4字節,它自然就word邊界對齊,而不需要額外的填充字節。
技巧10:明確前綴后綴操作符之間的差異
內置的++操作符可以放在操作數的兩邊:
int n=0;
++n; /*前綴*/
n++; /*后綴*/
你一定知道前綴操作符首先改變操作數,然后再使用它的值。比如:
int n=0, m=0;
n = ++m; /*first increment m, then assign its value to n*/
cout << n << m; /* display 1 1*/
在這個例子中,在賦值之后,n等于1;因為它是在將m賦予n之前完成的自增操作。
int n=0, m=0;
n = m++; /*first assign m's value to n, then increment m*/
cout << n << m; /*display 0 1*/
在這個例子中,賦值之后,n等于0;因為它是先將m賦予n,之后m再加1。
為了更好的理解前綴操作符和后綴操作符之間的區別,我們可以查看這些操作的反匯編代碼。即使你不了解匯編語言,你也可以很清楚地看到二者之間的區別,注意inc指令出現的位置:
/*disassembly of the expression: m=n++;*/
mov ecx, [ebp-0x04] /*store n's value in ecx register*/
mov [ebp-0x08], ecx /*assign value in ecx to m*/
inc dword ptr [ebp-0x04] /*increment n*/
/*disassembly of the expression: m=++n;*/
inc dword ptr [ebp-0x04] /*increment n;*/
mov eax, [ebp-0x04] /*store n's value in eax register*/
mov [ebp-0x08], eax /*assign value in eax to m*/
注:前綴操作符、后綴操作符與性能之間的聯系是什么?原文作者沒有說明。所以有了 的疑問。在此做一下說明:當應用內置類型的操作時,前綴和后綴操作符的性能區別通常可以忽略。然而,對于用戶自定義類型,后綴操作符的效率要低于前綴操作符,這是因為在運行操作符之間編譯器需要建立一個臨時的對象
技巧11:盡量消除臨時對象
在一些情況下,C++會“背著你”創建一些臨時對象。一個臨時對象的開銷可能很大,因為它的構造和析構函數肯定會被調用。但是,在大多數情況下,您可以防止臨時對象的創建。在下面的例子,一個臨時對象被創建:
Complex x, y, z;
x=y+z; /* temporary created */
表達式y+z;將會導致一個Complex類型的臨時對象的產生,這個臨時對象保存著相加的結果。之后,這個臨時對象被賦予x,隨后銷毀。臨時對象的生成可以用兩種方式加以避免:
Complex y,z;
Complex x=y+z; /* initialization instead of assignment */
在上面的例子中,y和z相加的結果直接用于對象x的構造,所以就避免了起中介作用的臨時對象。或者你可以用+=代替+,同樣可以達到相同的效果:
/* instead of x = y+z; */
x=y;
x+=z;
雖然采用+=的這個版本不太優雅,它只有兩個成員函數調用:賦值操作和+=操作。相較而言,使用+操作符則產生三次成員函數調用:臨時對象的構造、對于x的拷貝構造,以及臨時對象的析構!
12 and 13: Object-oriented Design
雖然C++支持多種非常有用的編程范式,如procedural programming、functional programming、generic programming,以及object-oriented programming。其中object-oriented programming(面向對象編程)無疑是使用最為廣泛和重要的范例。下面的兩個小技巧將幫助你更好的應用面向對象的設計和實現。首先,我將解釋一下虛析構函數在類繼承中的重要性。另外一個小技巧將闡述如何去處理嵌套類:將嵌套類聲明為其包含類的友元。
技巧12:為什么沒有虛析構函數的類繼承是危險的?
如果一個類的析構函數是非虛的,那么就意味著它不會作為基類來使用(這種類就是我們所熟知的“實體類”)。std::string,std::complex,以及std::vector都是實體類。為什么不推薦繼承這些類呢?當你使用公共繼承時,你就會在基類與派生類之間創建一種is-a的關系。因此,基類的指針和引用實際上可以指向一個派生的對象。由于析構函數不是虛的,所以當您刪除這樣一個對象時,C++將不會調用整個析構鏈。例如:
class A
{
public:
~A() // non virtual
{
// ...
}
};
class B: public A /* 不好; A 沒有虛析構函數*/
{
public:
~B()
{
// ...
}
};
int main()
{
A * p = new B; /*貌似沒什么問題*/
delete p; /*問題出現, B的析構未被調用*/
}
沒有調用對象的析構所帶來的結果是不確定的。因此,你不應該公開繼承這樣的類。尤其是,不要繼承STL容器和std::string。
技巧13:將嵌套類聲明為其包含類的友元
當作為其包含類的友元來聲明一個嵌套類時,你應當將友元聲明放于嵌套類聲明的后面,而不是前面:
class A
{
private:
int i;
public:
class B /*先定義嵌套類*/
{
public:
B(A & a) { a.i=0;};
};
friend class B;/*友元聲明*/
};
如果你把友元聲明放置于嵌套類聲明之前,編譯器將丟棄友元聲明,因為編譯器還沒有見過它,不知道它是什么玩意兒。
注:關于嵌套類定義
A nested class is any class whose declaration occurs within the body of another class or interface. A top level class is a class that is not a nested class.
一個 class A 如果定義在了另一個 class B 或 interface B 里,那么這個 class A 就是 nested class,class B 或 interface B 則被稱為 enclosing class。至于 class A 是定義在了 class B 或 interface B 的什么地方,例如 method 和 constructor,則是沒有限制的。
標準模板庫和通用編程
標準模板庫(STL)給C++程序員編寫代碼的方式帶來了革命性的影響。這樣的代碼重用將生產力水平提升到了更高的水平,節省了大量的時間,避免了重復性的勞動。然而,STL是一個具有特殊術語和復雜規則的、比較全面的框架,如果你想更好的去應用它,那么你只能去掌握它,“知己知彼方能百戰不殆”嗎。為了更深入地了解STL某些方面的情況,這大類中將包含6個小技巧。
第一個技巧將介紹一下STL的基本組成和一些關鍵術語。接下來的小技巧則集中于模板定義這一方面。正如你所知,模板是STL容器和算法最基礎的“建筑材料”。接下來的三個小技巧將依次描述如何使用標準庫中應用最為廣泛的容器 - vector,學習如何在vector中存儲對象指針,避免常見陷阱,以及如何將vector當做一個內置的數組來使用。第五個提示將會告訴你如何使用vector來模仿多維數組。最后的提示將介紹一個非常重要的問題:auto_ptr和STL容器之間的一些問題。
技巧14:非常有用的STL術語
接下來所講述的是STL中一些非常關鍵的條款。也許在你閱讀標準模板庫(STL)文獻或文檔的時候,您遇到過它們。
Container 容器
容器是一個對象,它將對象作為元素來存儲。通常情況下,它是作為類模板來實現,其成員函數包括遍歷元素,存儲元素和刪除元素。std::list和std::vector就是兩種典型的容器類。
Genericity 泛型
泛型就是通用,或者說是類型獨立。上面對于容器類的定義是非常寬松的,因為它適用于字符串,數組,結構體,或者是對象。一個真正的容器是不局限于某一種或著某些特定的數據類型的。相反,它可以存儲任何內置類型或者用戶自定義類型。這樣的容器就被認為是通用的。請注意,string只能包含字符。泛型也許是STL的最重要的特征。第三個技巧將給出函數對象的標準基類。因為函數對象是通用編程重的一個重要部分。在設計實現過程中,堅持標準規范將會省去你的很多的困難。
Algorithm 算法
算法就是對一個對象序列所采取的某些操作。例如std::sort()排序,std::copy()復制,和std::remove()刪除。STL中的算法都是將其作為函數模板來實現的,這些函數的參數都是對象迭代器。
Adaptor 適配器
適配器是一個非常特殊的對象,它可以插入到一個現有的類或函數中來改變它的行為。例如,將一個特殊的適配器插入到std::sort()算法中,你就可以控制排序是降序還是升序。 STL中定義了多種類型的序列適配器,它可以將一個容器類變換成一個具有更嚴格接口的不同容器。例如,堆棧(stack)就可以由queue<>和適配器來組成,適配器提供了必要的push()和pop()操作。
O(h) Big Oh Notation
O(h)是一個表示算法性能的特殊符號,在STL規范當中用于表示標準庫算法和容器操作的最低性能極限。任何其他的實現可能會提供更好的性能,但絕不是更壞的。O(h)可以幫助您去評估一個算法或者某個特定類型容器的某個操作的效率。std::find()算法遍歷序列中的元素,在最壞的情況下,其性能可以表示為:
T(n) = O(n). /* 線性復雜度 */
Iterator 迭代器
迭代器是一種可以當做通用指針來使用的對象。迭代器可以用于元素遍歷,元素添加和元素刪除。 STL定義了五個種主要的迭代器:
輸入迭代器和輸出迭代器 input iterators and output iterators
前向迭代器 forward iterators
雙向迭代器 bidirectional iterators
隨機訪問迭代器 random access iterators
請注意,上述迭代器列表并不具有繼承關系,它只是描述了迭代器種類和接口。下面的迭代器類是上面類的超集。例如,雙向迭代器不僅提供了前向迭代器的所有功能,還包括一些附加功能。這里將對這些類別做簡要介紹:
輸入迭代器允許迭代器前行,并提供只讀訪問。
輸出迭代器允許迭代器前行,并提供只寫訪問。
前向迭代器支持讀取和寫入權限,但只允許一個方向上的遍歷。
雙向迭代器允許用戶在兩個方向遍歷序列。
隨機訪問迭代器支持迭代器的隨機跳躍,以及“指針算術”操作,例如:
string::iterator it = s.begin();
char c = *(it+5); /* assign sixth char to c*/
技巧15:模板定義的位置在哪里?是.cpp文件嗎?
通常情況下,你會在.h文件中聲明函數和類,而將它們的定義放置在一個單獨的.cpp文件中。但是在使用模板時,這種習慣性做法將變得不再有用,因為當實例化一個模板時,編譯器必須看到模板確切的定義,而不僅僅是它的聲明。因此,最好的辦法就是將模板的聲明和定義都放置在同一個.h文件中。這就是為什么所有的STL頭文件都包含模板定義的原因。
另外一個方法就是使用關鍵字“export”!你可以在.h文件中,聲明模板類和模板函數;在.cpp文件中,使用關鍵字export來定義具體的模板類對象和模板函數;然后在其他用戶代碼文件中,包含聲明頭文件后,就可以使用該這些對象和函數了。例如:
// output.h - 聲明頭文件
template<class T> void output (const T& t);
// out.cpp - 定義代碼文件
#include <****>
export template<class T> void output (const T& t) {std::cerr << t;}
//main.cpp:用戶代碼文件
#include "output.h"
void main() // 使用output()
{
output(4);
output("Hello");
}
某種程度上,這有點類似于為了訪問其他編譯單元(如另一代碼文件)中普通類型的變量或對象而采用的關鍵字extern。
但是,這里還有一個不得不說的問題:并非所有的編譯器都支持export關鍵字(我們最熟悉、最常用的兩款編譯器VS 和 GCC就是不支持export的典型代表)。對于這種不確定,最好的方法就是采用解決方案一:聲明定義放在一起,雖然這在某種程度上破壞了C++編程的優雅
性。
---------------版權分割線--------以下引自 飛諾網(www.firnow.com)-------------
分離編譯模式(Separate Compilation Model)允許在一處翻譯單元(Translation Unit)中定義(define)函數、類型、類對象等,在另一處翻譯單元引用它們。編譯器(Compiler)處理完所有翻譯單元后,鏈接器(Linker)接下來處理所有指向 extern 符號的引用,從而生成單一可執行文件。該模式使得 C++ 代碼編寫得稱心而優雅。
然而該模式卻馴不服模板(Template)。標準要求編譯器在實例化模板時必須在上下文中可以查看到其定義實體;而反過來,在看到實例化模板之前,編譯器對模板的定義體是不處理的——原因很簡單,編譯器怎么會預先知道 typename 實參是什么呢?因此模板的實例化與定義體必須放到同一翻譯單元中。
以優雅著稱的 C++ 是不能容忍有此“敗家玩意兒”好好活著的。標準 C++ 為此制定了“模板分離編譯模式(Separation Model)”及 export 關鍵字。然而由于 template 語義本身的特殊性使得 export 在表現的時候性能很次。編譯器不得不像 .net 和 java 所做的那樣,為模板實體生成一個“中間偽代碼(IPC,intermediate pseudo - code)”,使得其它翻譯單元在實例化時可找到定義體;而在遇到實例化時,根據指定的 typename 實參再將此 IPC 重新編譯一遍,從而達到“分離編譯”的目的。因此,該標準受到了幾乎所有知名編譯器供應商的強烈抵制。
誰支持 export 呢?Comeau C/C++ 和 Intel 7.x 編譯器支持。而以“百分百支持 ISO ”著稱的 VS 和 GCC 卻對此視而不見。真不知道這兩大編譯器“百分百支持”的是哪個版本的 ISO。在 VS 2008 中,export 關鍵字在 IDE 中被標藍,表示 VS IDE 認識它,而編譯時,會用警告友情提示你“不支持該關鍵字”,而配套的 MSDN 9 中的 C++ keywords 頁則根本查不到該關鍵字;而在 VS 2010 中,就沒那么客氣了,盡管 IDE 中仍然會將之標藍,但卻會直截了當地報錯。
---------------版權分割線--------以上引自 飛諾網(www.firnow.com)-------------
技巧16:函數對象的標準基類
為了簡化編寫函數對象的過程,標準庫提供了兩個類模板,作為用戶自定義函數對象的基類:std::unary_function和std::binary_function。兩者都聲明在頭文件<functional>中。正如名字所顯示的那樣,unary_function被用作是接受一個參數的函數的對象的基類,而binary_function是接受兩個參數的函數的對象的基類。這些基類定義如下:
template < class Arg, class Res > struct
unary_function
{
typedef Arg argument_type;
typedef Res result_type;
};
template < class Arg, class Arg2, class Res >
struct binary_function
{
typedef Arg first_argument_type;
typedef Arg2 second_argument_type;
typedef Res result_type;
};
這些模板并不提供任何實質性的功能。他們只是確保其派生函數對象的參數和返回值有統一的類型名稱。在下面的例子中,is_vowel繼承自unary_function,接受一個參數:
template < class T >
class is_vowel: public unary_function< T, bool >
{
public:
bool operator ()(T t) const
{
if ((t=='a')||(t=='e')||(t=='i')||(t=='o')||(t=='u'))
return true;
return false;
}
};
技巧17:如何在STL容器中存儲動態分配的對象?
假設你需要在同一容器中存儲不同類型的對象。通常情況下,您可以通過存儲儲動態分配對象的指針來達到這一點。然而,除了使用指針外,也可以按照下面的方式將元素插入到容器中:
class Base {};
class Derived : public Base{};
std::vector <Base *> v;
v.push_back(new Derived);
v.push_back(new Base);
如果按照這種方式,那么存儲的對象只能通過其容器來訪問。請記住,應按照下面的方式刪除分配的對象:
技巧18:將向量當作數組使用
假設你有一個整型向量vector<int> v,和一個以int*為參數的函數。為了獲得向量v內部數組的地址,并將它傳給函數,你必須使用表達式&v[0]或者是&*v.front()。舉個例子:
void func(const int arr[], size_t length );
int main()
{
vector <int> vi;
//.. fill vi
func(&vi[0], vi.size());
}
只要你遵守線面的幾條規則,你用&vi[0]和&*v.front()作為其內部數組地址就會很安全放心:
(1)fun()不應訪問超出數組范圍的元素。
(2)向量中的元素必須是連續的。雖然C++標準中并沒有做這樣的規定,但是據我所知,沒有一個vector的實現不是使用連續內存的。
技巧19:動態多維數組和向量的恩恩怨怨
你可以按照以下方式手動分配多維數組:
int (*ppi)[5] = new int[4][5]; /*parentheses required*/
/*fill array..*/
ppi[0][0] = 65;
ppi[0][1] = 66;
ppi[0][2] = 67;
//..
delete [] ppi;
然而,這種編碼風格是非常枯燥的,而且容易出錯。你必須用圓括號括起ppi,以確保這個聲明能被編譯器正確解析;同時你也必須手動地刪除你所分配的內存。更糟糕的是,你會在不經意間碰上令人頭疼的緩沖區溢出。而使用向量的向量來模擬多維數組則是一個更好的選擇:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector <vector <int> > v; /*two dimensions*/
v.push_back(vector <int>()); /*create v[0]*/
v.push_back(vector <int>()); /*create v[1]*/
v[0].push_back(15); /*assign v[0][0]*/
v[1].push_back(16); /*assign v[1][0]*/
}
因為vector重載了操作符[],你可以向使用內置的二維數組一樣使用[][]:
cout << v[0][0];
cout << v[1][0];
用向量的向量模擬多維數組主要有兩個優點:向量會自動的按照需要來分配內存。其次,它自己負責釋放分配的內存,而你則不必擔心潛在的內存泄漏。
技巧20:為什么你不應該在STL容器中存儲auto_ptr對象?
在C++標準中,一個STL元素必須是可以“拷貝構造”和“賦值”。這個條款意味著,對于一個給定類,“拷貝”和“賦值”是其非常便利順手的操作。特別是,當你將它復制到目標對象時,原始對象的狀態是不會改變的。
但是,這是不適用auto_ptr的。因為auto_ptr的從一個拷貝到另一個或賦值到另一個對象時會使得原始對象產生預期變動之外的變化。具體說來就是,會將原來的對象的指針轉移到目標對象上,從而使原來的指針變為空指針,試想一下,如果照下面代碼所示來做,會出現什么結果呢:
std::vector <auto_ptr <Foo> > vf;/*a vector of auto_ptr's*/
// ..fill vf
int g()
{
std::auto_ptr <Foo> temp=vf[0]; /*vf[0] becomes null*/
}
當temp被初始化時,vf[0]的指針變成空。任何對該元素的調用都將導致程序的運行崩潰。在您從容器中復制元素時,這種情況極有可能會發生。注意,即使您的代碼中沒有執行任何顯式的拷貝操作或賦值操作,許多算法(例如std::swap()和std::random_shuffle()等)都會創建一個或多個元素的臨時副本。此外,該容器的某些成員函數也會創建一個或多個元素的臨時副本,這就會抵消原有的元素指針。以致任何對容器中元素的后續操作都將帶來的不確定的后果。
可能Visual C++用戶會說,我在STL容器中使用auto_ptr時,從來沒有遇到過任何問題。這是因為在Visual C++中auto_ptr的實現已經過時,而且依賴在一個過時規范上。如果廠商決定趕上當前的最新ANSI/ISO C++標準,并相應地改變它的標準庫,在STL容器中使用auto_ptr將會導致嚴重故障。
總結來說,你是不應該在STL容器中使用auto_ptr的。你可以使用普通指針或其他智能指針類,但絕不是auto_ptr指針。
//------------------------------------------
<<任何時候都適用的20個C++技巧>>這個小系列到此就結束了,雖然關于C++的技巧還很多,還值得我們去總結。如果遇到,我會第一時間拿出來與大家分享! 謝謝各位博友!!
作者: 凌云健筆
出處:http://www.cnblogs.com/lijian2010/
版權:本文版權歸作者和博客園共有
轉載:歡迎轉載,為了保存作者的創作熱情,請按要求【轉載】
要求:未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則必究法律責任