這些小技巧之因此特別,是由於這些信息一般吧不能在C++書籍或者網站上找到。好比說,成員指針,即便對於高級程序員也是比較棘手,和易於產生bugs的,是應該儘可能避免的問題之一。html
<翻 by凌雲健筆>java
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.ios
by Danny Kalev程序員
====================================
Page 1: Introduction 介紹web
接下來的這幾條技巧主要集中於實用技術和一些晦澀知識上;它們與特殊的平臺、編程領域、或編譯器無關。所以,它們適用於全部的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 較好編程風格所要遵循的一些準則express
在這個類別中,所涉及的技巧是各級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>組件是全局性的。sass
由於兩者之間存在着這些重大分歧,你不能在同一程序中混合使用兩個庫。做爲一條準則:使用<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.
// 續 任什麼時候候都適用的20個C++技巧 <5-8> 內存管理
===================================================
===================================================
===================================================
毫無疑問,內存管理是在C++編程中最複雜和最易出錯的問題之一。可以直接地訪問原始內存,動態地分配內存空間,以及C++的高效性決定它必須有一些很是嚴格的規則;若是你不遵照將難以免那些內存相關的錯誤或者程序運行時的崩潰。
指針是訪問內存的主要手段。 C++能夠分爲兩個主要類別:指向數據的指針和指向函數的指針。第二大類又能夠分爲兩個子類類:普通函數指針和成員函數指針。在下面的技巧中,咱們將深刻探討這些問題,並學習一些方法,簡化指針的使用,同時隱藏它們的笨拙語法。
指向函數的指針極可能是C++中一種最不具可讀性的語法結構。惟一的可讀性更差的彷佛只有成員指針。第一個技巧會教你如何提升普通的函數指針的可讀性。這是你理解C++成員指針的前提。接下來,咱們將學習如何避免內存碎片,並告訴你其可怕的後果。最後,咱們討論delete和delete []的正確使用方法;它經常會是衆多錯誤和誤解的來源。
===================================================
===================================================
技巧5:函數指針的繁瑣語法?!見鬼去吧!!
你能告訴我下面定義的含義麼?
void (*p[10]) (void (*)());
p是「一個包含10個函數指針的數組,這些函數返回爲空,其參數爲{【(另一個無參數返回爲空)的函數】的指針}。」如此繁瑣的語法幾乎難以辨認,難道不是嗎?解決之道在何方?你能夠經過typedef來合理地大大地去簡化這些聲明。首先,聲明一個無參數、返回空的函數的指針的typedef,以下所示:
typedef void (*pfv)();
接下來 聲明另外一個typedef,一個指向參數爲pfv返回爲空的函數的指針:
typedef void (*pf_taking_pfv) (pfv);
如今,再去聲明一個含有10個這樣指針的數組就變得垂手可得,不費吹灰之力了:
pf_taking_pfv p[10]; /*等同於void (*p[10]) (void (*)()); 但更具可讀性*/
===================================================
===================================================
技巧6:函數指針的枝枝節節
類有兩類成員:函數成員和數據成員。一樣,也就有兩種類別的成員指針:成員函數指針和數據成員指針。後者不太常見,由於,通常來講,類是沒有公共數據成員的。當使用傳統C代碼的時候,數據成員指針纔是有用的,由於傳統C代碼中包含的結構體或類是具備公開數據成員的。
在C++中,成員指針是最爲複雜的語法結構之一;但是,這倒是一個很是強大而重要的特性。它們可使您在不知道這個函數的名字的前提下調用一個對象的成員函數。這是很是方便的回調實現。一樣的,你可使用一個數據成員指針來監測和修改數據成員的值,而沒必要知道它的名字。
指向數據成員的指針
雖然成員指針的語法可能會顯得有點混亂,可是它與普通指針的形式比較一致和相似,只須要在星號以前加上類名::便可。例如,若是一個普通的整形指針以下所示:
int * pi;
那麼,你就能夠按照下面的方式來定義一個指向類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都很危險,應當儘可能去避免。
// 續 任什麼時候候都適用的20個C++技巧 <9-11> 性能的提升
===================================================
===================================================
===================================================
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的拷貝構造,以及臨時對象的析構!
// 續 任什麼時候候都適用的20個C++技巧 <12-13> Object-oriented ===================================================
===================================================
===================================================
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,則是沒有限制的。
// 續 任什麼時候候都適用的20個C++技巧 <14-20> STL and Generic Programming
===================================================
===================================================
===================================================
標準模板庫和通用編程
標準模板庫(STL)給C++程序員編寫代碼的方式帶來了革命性的影響。這樣的代碼重用將生產力水平提高到了更高的水平,節省了大量的時間,避免了重複性的勞動。然而,STL是一個具備特殊術語和複雜規則的、比較全面的框架,若是你想更好的去應用它,那麼你只能去掌握它,「知己知彼方能百戰不殆」嗎。爲了更深刻地瞭解STL某些方面的狀況,這大類中將包含6個小技巧。
第一個技巧將介紹一下STL的基本組成和一些關鍵術語。接下來的小技巧則集中於模板定義這一方面。正如你所知,模板是STL容器和算法最基礎的「建築材料」。接下來的三個小技巧將依次描述如何使用標準庫中應用最爲普遍的容器 - vector,學習如何在vector中存儲對象指針,避免常見陷阱,以及如何將vector當作一個內置的數組來使用。第五個提示將會告訴你如何使用vector來模仿多維數組。最後的提示將介紹一個很是重要的問題:auto_ptr和STL容器之間的一些問題。
技巧14:很是有用的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來定義具體的模板類對象和模板函數;而後在其餘用戶代碼文件中,包含聲明頭文件後,就可使用該這些對象和函數了。例如:
某種程度上,這有點相似於爲了訪問其餘編譯單元(如另外一代碼文件)中普通類型的變量或對象而採用的關鍵字extern。
可是,這裏還有一個不得不說的問題:並不是全部的編譯器都支持export關鍵字(咱們最熟悉、最經常使用的兩款編譯器VS 和 GCC就是不支持export的典型表明)。對於這種不肯定,最好的方法就是採用解決方案一:聲明定義放在一塊兒,雖然這在某種程度上破壞了C++編程的優雅性。
分離編譯模式(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 中仍然會將之標藍,但卻會直截了當地報錯。
===================================================
===================================================
技巧16:函數對象的標準基
爲了簡化編寫函數對象的過程,標準庫提供了兩個類模板,做爲用戶自定義函數對象的基類:std::unary_function和std::binary_function。二者都聲明在頭文件<functional>中。正如名字所顯示的那樣,unary_function被用做是接受一個參數的函數的對象的基類,而binary_function是接受兩個參數的函數的對象的基類。這些基類定義以下:
這些模板並不提供任何實質性的功能。他們只是確保其派生函數對象的參數和返回值有統一的類型名稱。在下面的例子中,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容器中存儲動態分配的對象?
假設你須要在同一容器中存儲不一樣類型的對象。一般狀況下,您能夠經過存儲儲動態分配對象的指針來達到這一點。然而,除了使用指針外,也能夠按照下面的方式將元素插入到容器中:
若是按照這種方式,那麼存儲的對象只能經過其容器來訪問。請記住,應按照下面的方式刪除分配的對象:
delete v[0];
delete v[1];
===================================================
===================================================
假設你有一個整型向量vector<int> v,和一個以int*爲參數的函數。爲了得到向量v內部數組的地址,並將它傳給函數,你必須使用表達式&v[0]或者是&*v.front()。舉個例子:
只要你遵照線面的幾條規則,你用&vi[0]和&*v.front()做爲其內部數組地址就會很安全放心:
(1)fun()不該訪問超出數組範圍的元素。
(2)向量中的元素必須是連續的。雖然C++標準中並無作這樣的規定,可是據我所知,沒有一個vector的實現不是使用連續內存的。
===================================================
===================================================
技巧19:動態多維數組和向量的恩恩怨怨
你能夠按照如下方式手動分配多維數組:
然而,這種編碼風格是很是枯燥的,並且容易出錯。你必須用圓括號括起ppi,以確保這個聲明能被編譯器正確解析;同時你也必須手動地刪除你所分配的內存。更糟糕的是,你會在不經意間碰上使人頭疼的緩衝區溢出。而使用向量的向量來模擬多維數組則是一個更好的選擇:
由於vector重載了操做符[],你能夠向使用內置的二維數組同樣使用[][]:
用向量的向量模擬多維數組主要有兩個優勢:向量會自動的按照須要來分配內存。其次,它本身負責釋放分配的內存,而你則沒必要擔憂潛在的內存泄漏。
===================================================
===================================================
技巧20:爲何你不該該在STL容器中存儲auto_ptr對象?
在C++標準中,一個STL元素必須是能夠「拷貝構造」和「賦值」。這個條款意味着,對於一個給定類,「拷貝」和「賦值」是其很是便利順手的操做。特別是,當你將它複製到目標對象時,原始對象的狀態是不會改變的。
可是,這是不適用auto_ptr的。由於auto_ptr的從一個拷貝到另外一個或賦值到另外一個對象時會使得原始對象產生預期變更以外的變化。具體說來就是,會將原來的對象的指針轉移到目標對象上,從而使原來的指針變爲空指針,試想一下,若是照下面代碼所示來作,會出現什麼結果呢:
當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++的技巧還不少,還值得咱們去總結。若是遇到,我會第一時間拿出來與你們分享! 謝謝各位博友!!