向iOS開發者介紹C++(二)


http://www.cocoachina.com/applenews/devnews/2014/0417/8182.html


歡迎回到向iOS開發者介紹C++系列的第二部分向iOS開發者介紹C++(一) !在第一部分,我們瞭解了類和內存管理。在第二部分部分我們將深入瞭解類以及其他有意思的特徵。你將會了解到什麼是“模板”以及標準模板庫。
 
多態性
簡單地說,多態性是一個重載子類中函數的概念。在Objective-C中,你可能已經做過很多次,例如,子類化UIViewController和重載viewDidLoad。

C++的多態性比Objective-C的多態性更進一層。因此,當我解釋這個強大的功能時要緊跟我的思路。
 
首先,以下爲在類中重載成員函數的例子:
  1. class Foo { 
  2.   public
  3.     int value() { return 5; } 
  4. }; 
  5.   
  6. class Bar : public Foo { 
  7.   public
  8.     int value() { return 10; } 
  9. }; 
 
但是,如果你這樣做會發生什麼呢:
  1. Bar *b = new Bar(); 
  2. Foo *f = (Foo*)b; 
  3. printf(“%i”, f->value()); 
  4. // Output = 5 
哇,這可不是你所期望的輸出!我猜你認爲輸出值應該是10,對麼?這就是C++和Objective-C最大的不同。
 
在Objective-C中,將子類指針轉換成基類指針是無關緊要的。如果你向對象發消息(如調用函數),是運行時找到對象的類並調用最先派生的方法。因此,Objective-C中這種情況下,子類Bar中的方法被調用。這裏凸顯出了我在第一部分提到的編譯時和運行時的不同。
 
在上面的例子中,編譯器調用value()時,編譯器的職責是計算出哪個函數需要被調用。由於f的類型是指向Foo類的指針,
它執行跳至Foo:value()的代碼。編譯器不知道f實際上是Bar類的指針。
 
在這個簡單的例子中,你可以認爲編譯器能推斷出f是Bar類的指針。但是想一想如果f確實是一個函數的輸入值的話將會發生什麼呢?這種情況下編譯器將不會知道它是一個繼承了Foo類的指針。
 
靜態綁定和動態綁定
上面的例子很好的證明了C++和Objective-C最主要的區別--靜態綁定和動態綁定。上面的例子是靜態綁定的例子。編譯器負責解決調用哪個函數,並且在編譯完成後這個過程將被存儲爲二進制。在運行時不能改變這個過程。
 
這與Objective-C中方法調用形成了對比,這就是動態綁定的一個例子。運行時本身負責決定調用哪個函數。
 
動態綁定會使Objective-C很強大。你可能已經意識到了在運行時可以爲類方法或者交換方法實現。這在靜態綁定語言中是不能實現的,靜態綁定是在編譯時調用方法的。
 
但是,在C++中還不止這樣!C++通常是靜態綁定,但是也可以使用動態綁定機制,即“虛函數”。
 
虛函數和虛表
虛函數提供動態綁定機制。通過使用table lookup(每個類定義一個表),虛函數推遲到runtime時選擇調用哪個函數。然而,跟靜態綁定相比,這確實引起了運行時輕微的間接成本。除了調用函數外,table lookup是必須的。靜態綁定時僅需要執行調用的函數。
 
使用虛函數很簡單,只需要將關鍵詞“virtual”添加到談及的函數。例如上面的例子用虛函數方式寫的話,如下:
  1. class Foo { 
  2.   public
  3.     virtual int value() { return 5; } 
  4. }; 
  5.   
  6. class Bar : public Foo { 
  7.   public
  8.     virtual int value() { return 10; } 
  9. }; 
 
現在想一想運行同樣的代碼會發生什麼:
  1. Bar *b = new Bar(); 
  2. Foo *f = (Foo*)b; 
  3. printf(“%i”, f->value()); 
  4. // Output = 10 
 
這正是前面所預期的輸出值,對吧?因此在C++中可以用動態綁定,但是你需要根據遇到的情況決定是用靜態綁定還是動態綁定。
 
在C++中這種類型的靈活性是司空見慣的,這使C++成爲一種多範型的語言。Objective-C很大程度上迫使你進入嚴格的模式,尤其是用Cocoa框架時。而C++中,很多都是由開發者決定的。
 
現在開始瞭解虛擬函數是如何發揮作用的吧!

虛函數的內部功能
在你明白虛函數是怎樣工作之前,你需要知道非虛函數是如何工作的。
 
想一想下面的代碼:
  1. MyClass a; 
  2. a.foo(); 
 
如果foo()是個非虛函數,那麼編譯器將會把它轉換成代碼,直接跳到MyClass類的foo()函數。
 
但是記住,這就是非虛函數的問題所在。回想之前的例子,如果這個類是多態的,那麼編譯器由於不知道變量的全部類型,也就不知道應該跳到哪個函數。這就需要一種方法在運行時查找到正確的函數。

要完成這種查找,虛函數要使用“virtual table”(也稱“v-table”,虛表)。虛表是一個查找表來將函數映射到其實現上,並且每個類都訪問一個表。當一個虛函數被調用時,編譯器發出代碼來檢索對象的虛表從而查找到正確的函數。
 
回顧上面的例子來看看這是如何工作的:
  1. class Foo { 
  2.   public
  3.     virtual int value() { return 5; } 
  4. }; 
  5.   
  6. class Bar : public Foo { 
  7.   public
  8.     virtual int value() { return 10; } 
  9. }; 
  10.   
  11. Bar *b = new Bar(); 
  12. Foo *f = (Foo*)b; 
  13. printf(“%i”, f->value()); 
  14. // Output = 10 
 
當你創建一個類指針b和一個Bar類的實例,那麼它的虛表將是Bar類的虛表。當b指針轉換爲Foo類的一個指針時,它並沒有改變對象的內容,虛表仍然是Bar類的虛表而不是Foo類的。因此當查找v-table以調用value()時,結果是將調用Bar::value()。
 
構造函數和析構函數
每個對象在其生命週期中都要經歷兩個重要階段:構造函數和析構函數。C++允許你同時控制這兩個階段。在Objective-C中與這兩階段相同的是初始化方法(例如,init或者以init開頭的其他方法)和dealloc(釋放內存)。
 
C++中定義構造函數時與類同名。正如在Objective-C中有多個初始化方法,你也可以定義多個構造函數。
 
例如,下面這個類中有兩個不同的構造函數:
  1. class Foo { 
  2.   private
  3.     int x; 
  4.   
  5.   public
  6.     Foo() { 
  7.         x = 0; 
  8.     } 
  9.   
  10.     Foo(int x) { 
  11.         this->x = x; 
  12.     } 
  13. }; 
 
這就是兩個構造函數,一個是默認構造函數Foo(),另一個構造函數含有一個參數來設置成員變量。
 
如上例中,如果在構造函數中給成員變量初始化,有用少量代碼實現的方法。不需要自己去設置成員變量的值,你可以用下面的語法:
  1. class Foo { 
  2.   private
  3.     int x; 
  4.   
  5.   public
  6.     Foo() : x(0) { 
  7.     } 
  8.   
  9.     Foo(int x) : x(x) { 
  10.     } 
  11. }; 
通常來講,如果僅僅是給成員變量賦值的話可以用上面這種方式。但是如果你需要用到邏輯或者調用其他函數的話,那麼你就要實現函數主體。你也可以結合以上兩種方式。
 
當用繼承時,你需要調用父類的構造函數。在Objective-C中,你通常採用先調用父類指定的初始化程序的方法。
 
在C++中,你可以這樣做:
  1. class Foo { 
  2.   private
  3.     int x; 
  4.   
  5.   public
  6.     Foo() : x(0) { 
  7.     } 
  8.   
  9.     Foo(int x) : x(x) { 
  10.     } 
  11. }; 
  12.   
  13. class Bar : public Foo { 
  14.   private
  15.     int y; 
  16.   
  17.   public
  18.     Bar() : Foo(), y(0) { 
  19.     } 
  20.   
  21.     Bar(int x) : Foo(x), y(0) { 
  22.     } 
  23.   
  24.     Bar(int x, int y) : Foo(x), y(y) { 
  25.     } 
  26. }; 
函數簽名後,列表中的第一個元素表示對父類構造函數的調用。你可以調用任何一個你想要的超類構造函數。
 
C++沒有一個指定的初始化程序。目前,沒有辦法調用同一個類的構造函數。在Objective-C中,有一個指定的初始化程序可以被其他初始化程序調用,並且只有這個指定的初始化程序去調用超類的指定初始化程序,例如:
  1. @interface Foo : NSObject 
  2. @end 
  3.   
  4. @implementation Foo 
  5.   
  6. - (id)init { 
  7.     if (self = [super init]) { ///< Call to super’s designated initialiser 
  8.     } 
  9.     return self; 
  10.   
  11. - (id)initWithFoo:(id)foo { 
  12.     if (self = [self init]) { ///< Call to self’s designated initialiser 
  13.         // … 
  14.     } 
  15.     return self; 
  16.   
  17. - (id)initWithBar:(id)bar { 
  18.     if (self = [self init]) { ///< Call to self’s designated initialiser 
  19.         // … 
  20.     } 
  21.     return self; 
  22.   
  23. @end 
在C++中,雖然你可以調用父類的構造函數,但是目前調用自己的構造函數仍是不合法的。因此,下面的解決方案很常見:
  1. class Bar : public Foo { 
  2.   private
  3.     int y; 
  4.     void commonInit() { 
  5.         // Perform common initialisation 
  6.     } 
  7.   
  8.   public
  9.     Bar() : Foo() { 
  10.         this->commonInit(); 
  11.     } 
  12.   
  13.     Bar(int y) : Foo(), y(y) { 
  14.         this->commonInit(); 
  15.     } 
  16. }; 
然而,這十分麻煩。爲什麼你不能用Bar(int y)調用Bar(),然後在Bar()中這樣寫“Bar::commonInit()”呢?畢竟,Objective-C中恰恰是這樣寫的。
 
2011年發佈了最新版的C++標準:C++11。在這個更新的標準中確實可以這樣做。目前仍有許多C++代碼還沒有按C++11標準來更新,所以知道這兩種方法很重要。任何2011年前標準的C++代碼都按以下這種方式:
  1. class Bar : public Foo { 
  2.   private
  3.     int y; 
  4.   
  5.   public
  6.     Bar() : Foo() { 
  7.         // Perform common initialisation 
  8.     } 
  9.   
  10.     Bar(int y) : Bar() { 
  11.         this->y = y; 
  12.     } 
  13. }; 
這種方法唯一一個不足的地方是,你不能在同一個類中調用構造函數的同時設置一個成員變量。上面的例子中,成員變量y在構造函數主體中設置。
 
注意:在2011年C++11標準成爲一個完整的標準,起初稱爲C++ 0x。意思是在2000年至2009年之間這項標準成熟的話,x可以替換爲這一年的最後一個數字。然而比預期的時間要晚,因此以11爲結尾!所有的現代編譯器,包括clang,現在都支持C++11標準。
 
以上爲構造函數,那麼析構函數呢?當一個堆對象被刪除或者一個棧函數溢出時會調用析構函數。在析構函數中你需要做的事情就是清理對象。
 
析構函數中不能有任何參數。同樣,在Objective-C中dealloc也不需要任何參數。因此每個類中只有一個析構函數。
 
在類中定義析構函數時在函數名字前要加前綴--波浪號(~)。如下:
  1. class Foo { 
  2.   public
  3.     ~Foo() { 
  4.         printf(“Foo destructor\n”); 
  5.     } 
  6. }; 
 
看一下當你的類被繼承時,會發生什麼:
  1. class Bar : public Foo { 
  2.   public
  3.     ~Bar() { 
  4.         printf(“Bar destructor\n”); 
  5.     } 
  6. }; 
 
如果你不這樣寫的話,當通過Foo指針刪除Bar類的一個實例的時候將會發生異常,如下:
  1. Bar *b = new Bar(); 
  2. Foo *f = (Foo*)b; 
  3. delete f; 
  4. // Output: 
  5. // Foo destructor 
 
這樣是錯誤的。刪除的應該是Bar類的實例,但是爲什麼是去調用Foo類的析構函數呢?
 
回想一下,之前發生的同樣的問題,你是使用虛函數解決的。這個正是同樣的問題。編譯器看到是一個Foo需要被刪除,因爲Foo的析構函數並不是虛函數,所以編譯器認爲要調用的是Foo的析構函數。
 
解決這個問題的辦法就是將析構函數定義爲虛函數,如下:
  1. class Foo { 
  2.   public
  3.     virtual ~Foo() { 
  4.         printf(“Foo destructor\n”); 
  5.     } 
  6. }; 
  7.   
  8. class Bar : public Foo { 
  9.   public
  10.     virtual ~Bar() { 
  11.         printf(“Bar destructor\n”); 
  12.     } 
  13. }; 
  14.   
  15. Bar *b = new Bar(); 
  16. Foo *f = (Foo*)b; 
  17. delete f; 
  18. // Output: 
  19. // Bar destructor 
  20. // Foo destructor 
 
這就接近了期望的結果,但最終結果不同於之前使用虛函數得到的結果。在這裏,兩個函數都被調用了。首先Bar的析構函數被調用,然後Foo的析構函數被調用。爲什麼呢?
 
這是因爲析構函數比較特殊。由於Foo的析構函數是父類的析構函數,所以Bar的析構函數自動調用Foo的析構函數。
 
這正是所需要的,正如Objective-c中的ARC方法中,你調用的是父類的dealloc。

你可能在想這個:你認爲編譯器會爲你做這個事情,但是並不是在所有類中都是最佳方法。
 
例如,如果你沒有從某個類繼承呢?如果析構函數是虛函數,那麼每次都要通過虛表來刪除一個實例,或許這種間接方法並不是你需要的。C++中你可以自己做決定,另一個方法很強大,但是開發者必須清楚發生了什麼。
 
 注意:除非你確定你不需要繼承一個類,否則一定要定義析構函數爲虛函數。
 
運算符重載
在Objective-C中沒有運算符重載的概念,但是這並不複雜。
 
操作符是實體,如我們熟悉的+,-,*,/。例如,你可以用“+”運算符與標準常量(操作數)做如下運算:
  1. int x = 5; 
  2. int y = x + 5; ///< y = 10 
 
運算符“+”在這裏的作用顯而易見,將x加上5然後返回一個值。或許這個還不夠明顯,如果以函數的形式就很清楚了:
  1. int x = 5; 
  2. int y = add(x, 5); 
 
在函數add()中,兩個參數相加並返回一個值。
 
在C++中,在類中使用操作符時是可以定義功能函數的。這一功能很強大。當然,這也不是總能行得通的。例如,將兩個Person類相加就無任何實際意義。
 
然而,這一特性很有用處。考慮下面的類:
  1. class DoubleInt { 
  2.   private
  3.     int x; 
  4.     int y; 
  5.   
  6.   public
  7.     DoubleInt(int x, int y) : x(x), y(y) {} 
  8. }; 
 
這樣做可能更好一些:
  1. DoubleInt a(1, 2); 
  2. DoubleInt b(3, 4); 
  3. DoubleInt c = a + b; 
 
我們想要將DoubleInt(4, 6)的值賦值給變量c,即將兩個DoubleInt的實例x和y相加,然後賦值給c。事實證明這很簡單。你需要做的就是給DoubleInt類添加一個方法,即:
  1. DoubleInt operator+(const DoubleInt &rhs) { 
  2.     return DoubleInt(x + rhs.x, y + rhs.y); 
 
函數operator+很特別。編譯器將使用這個函數,當它看到“+”運算符任一側的DoubleInt時。“+”運算符左邊的對象將調用這個函數,將“+”運算符右邊的對象作爲參數進行傳遞。因此,經常命名參數爲“rhs”,意思是“右邊”。
 
由於使用實參的副本不僅沒必要還可能會改變值,函數的參數將作爲引用,可能會創建一個新的對象。此外,這個對象將是常量,這是因爲在相加的過程中,對於“+”運算符的右邊來講這是非法的。
 
C++能做的不僅是這些。你可能不僅僅想把DoubleInt添加至DoubleInt。你可能想要給DoubleInt添加一個整數。這些都是可能實現的!
 
爲實現此操作,你需要實現如下成員函數:
  1. DoubleInt operator+(const int &rhs) { 
  2.     return DoubleInt(x + rhs, y + rhs); 
 
然後你可以這樣做:
  1. DoubleInt a(1, 2); 
  2. DoubleInt b = a + 10; 
  3. // b = DoubleInt(11, 12); 
 
很有用吧!
 
除了加法運算,其他運算也可以這樣做。你可以重載++, --, +=, -=, *, ->等等。這裏就不一一列舉了。如果想要對運算符重載做更多瞭解,你可以訪問learncpp.com,這裏有整個章節在介紹運算符重載。
 
模板
在C++中,模板很有意思。
 
你是否發現你經常會重複寫相同的函數或者類,但只是函數或者類的類型不同呢?例如,交換兩個值的函數:
  1. void swap(int &a, int &b) { 
  2.     int temp = a; 
  3.     a = b; 
  4.     b = temp; 
 
 注:這裏是對參數做引用傳遞,以確保是對函數的實參作交換。如果兩個參數是用值傳遞,那麼所交換的值只是實參的副本。這個例子很好的說明了C++中引用好處。
 
上面的例子只適用於整數類型。如果是浮點數類型,那麼你需要寫另一個函數:
  1. void swap(float &a, float &b) { 
  2.     float temp = a; 
  3.     a = b; 
  4.     b = temp; 
 
如果你重複寫函數的主體,這樣很不明智。C++介紹一種語法可以有效的忽略變量的類型。你可以通過模板這個特性來實現這一功能。取代上面的兩種方法,在C++中,你可以這樣寫:
  1. template <typename T> 
  2. void swap(T a, T b) { 
  3.     T temp = a; 
  4.     a = b; 
  5.     b = temp; 
 
因此,你的函數可以交換任何類型的參數。你可以用以下任一種方式來調用函數:
  1. int ix = 1, iy = 2; 
  2. swap(ix, iy); 
  3.   
  4. float fx = 3.141, iy = 2.901; 
  5. swap(fx, fy); 
  6.   
  7. Person px(“Matt Galloway”), py(“Ray Wenderlich”); 
  8. swap(px, py); 
 
但是,你在用模板的時候仍需仔細。只有在頭文件中實現模板函數,這種方法才能起作用。這是由模板的編譯方式決定的。使用模板函數時,如果函數類型不存在,編譯器會根據類型實例化一個函數模板。
 
考慮到編譯器需要知道模板函數的實現,你需要在頭文件中定義一個實現,並且在使用的時候必須要包含這個頭文件。
 
同理,如果要修改模板函數中的實現,所有用到這個函數的文件都需要重編譯。相比之下,如果在實現文件中修改函數或者實現類成員函數,那麼只有這個實現文件需要重編譯。
 
因此,過度地使用模板會使應用程序很繁瑣。但是正如C++中很多方法,模板的作用很大。
 
模板類
不僅僅有模板函數,還可以在整個類中使用模板。
 
假設你的類中有三個值,這三個值用來存儲一些數據。首先,你想用整數類型,因此你要這樣寫:
  1. class IntTriplet { 
  2.   private
  3.     int a, b, c; 
  4.   
  5.   public
  6.     IntTriplet(int a, int b, int c) : a(a), b(b), c(c) {} 
  7.   
  8.     int getA() { return a; } 
  9.     int getB() { return b; } 
  10.     int getC() { return c; } 
  11. }; 
 
但是,你繼續寫程序時發現你需要三個浮點型數據。這是你又要寫一個類,如下:
  1. class FloatTriplet { 
  2.   private
  3.     float a, b, c; 
  4.   
  5.   public
  6.     FloatTriplet(float a, float b, float c) : a(a), b(b), c(c) {} 
  7.   
  8.     float getA() { return a; } 
  9.     float getB() { return b; } 
  10.     float getC() { return c; } 
  11. }; 
 
這裏,模板就會很有用。與模板函數相同,可以在類中使用模板。語法是一樣的。上面的兩個類可以寫成這樣:
  1. template <typename T> 
  2. class Triplet { 
  3.   private
  4.     T a, b, c; 
  5.   
  6.   public
  7.     Triplet(T a, T b, T c) : a(a), b(b), c(c) {} 
  8.   
  9.     T getA() { return a; } 
  10.     T getB() { return b; } 
  11.     T getC() { return c; } 
  12. }; 
但是,用模板類需要做一些細微的改動。使用模板函數不會改變代碼,這是因爲參數類型允許模板推斷需要做什麼。然而,使用模板類時,你要告訴編譯器你需要模板類使用什麼類型。
 
幸運的是,這個很簡單。用上面的模板類也很簡單:
  1. Triplet<int> intTriplet(1, 2, 3); 
  2. Triplet<float> floatTriplet(3.141, 2.901, 10.5); 
  3. Triplet<Person> personTriplet(Person(“Matt”), Person(“Ray”), Person(“Bob”)); 
很強大,對吧?

此外,模板函數和模板類並不侷限於單個未知類型。三重態的類可以被擴展以支持任何三種類型,而不是每個值必須是同樣的類型。
 
要做到這一點,只需要擴展提供更多類型的模板定義,如下:
  1. template <typename TA, typename TB, typename TC> 
  2. class Triplet { 
  3.   private
  4.     TA a; 
  5.     TB b; 
  6.     TC c; 
  7.   
  8.   public
  9.     Triplet(TA a, TB b, TC c) : a(a), b(b), c(c) {} 
  10.   
  11.     TA getA() { return a; } 
  12.     TB getB() { return b; } 
  13.     TC getC() { return c; } 
  14. }; 
 
以上模板中有三個不同類型,每個類型都在代碼中的適當位置被使用。
 
使用這樣的模板也很簡單,如下所示:
  1. Triplet<intfloat, Person> mixedTriplet(1, 3.141, Person(“Matt”)); 
以上爲模板的間接。接下來看看大量使用其特性的一個庫--標準模板庫
 
標準模板庫(STL)
每個規範的編程語言都有一個標準庫,這個標準庫包含通用的數據結構、算法以及函數。在Objective-C中你有Foundation。其中,包含NSArray、NSDictionary等熟悉或者不熟悉的成員函數。在C++中,標準模板庫(簡稱STL)包含這些標準代碼。
 
之所以成爲標準模板庫,是因爲在這個庫中使用了大量的模板。
 
STL中有很多內容,要介紹所有需要很長時間,所以在這裏我只介紹一些重要的。
 
容器
數組、字典和集合都是對象的容器。在Objective-C中,Foundation框架包含了大部分常用容器的實現。在C++中,STL包含了這些實現。實際上,STL所包含的的容器要比Foundation多一些。
 
在STL中有兩點與NSArray不同。分別是vector(列表)和list(列表)。兩個都可以存儲對象的序列,但是每個容器都有自己的優點和缺點。在C++中,從所給的容器中選擇你需要的很重要。
 
首先,看一看vector的用法:
  1. #include <vector> 
  2.   
  3. std::vector<int> v; 
  4. v.push_back(1); 
  5. v.push_back(2); 
  6. v.push_back(3); 
  7. v.push_back(4); 
  8. v.push_back(5); 
 
 
 注意std::的用法,這是因爲大部分STL位於命名空間內。STL將其所有的類放在自己的名爲"std"的命名空間中以避免潛在的命名衝突。
 
上面的代碼中,首先你創建一個vector來存放整型數據(int),然後五個整數被依次壓入vector的棧頂。操作完成後,vector中將是從1到5的有序序列。
 
這裏需要注意的是,正如Objective-C中,所有的容器都是可變的,沒有可變或者不可變的變量。
 
訪問一個vector的元素是這樣完成的:
  1. int first = v[1]; 
  2. int outOfBounds = v.at(100); 
 
這兩種方法都能有效地訪問vector中的元素。第一種使用方括號的方法,這便是索引C語言數組的方法。Objective-C中的下標取值方法也是用這種方法索引NSArray。
 
上面例子中的第二行使用at()成員函數,和方括號功能相同,只是at()函數需要檢查是否在vector範圍內索引,超出範圍的話會拋出異常。
 
vector被實現爲一個單一的或連續的內存塊。vector的空間大小等於所存儲的對象的大小乘以vector中對象數(存儲4字節或者8字節的整數取決於你使用的體系結構是32位還是64位的)。
 
向vector中添加元素是很昂貴的,因爲一個新的內存塊需要被分配給這個新的vector。然而,訪問一個確定的索引很快,因爲這僅僅是訪問內存中的一個字
 
std::list與std::vector很相似。但是,list的實現方式稍稍有些不同。不是作爲一個連續的內存塊被實現而是作爲一個雙向鏈表被實現。這意味着,list中每個的元素都包含一個數據,一個指向前一個元素的指針和一個指向後一個元素的指針。
 
由於是雙向鏈表,插入和刪除操作很簡單。然而,如果要訪問list中的第n個元素,就需要從0到n去遍歷。
 
綜上,list和vector的用法很相似:
  1. #include <list> 
  2.   
  3. std::list<int> l; 
  4. l.push_back(1); 
  5. l.push_back(2); 
  6. l.push_back(3); 
  7. l.push_back(4); 
  8. l.push_back(5); 
 
正如上面的vector例子,這將創建一個從1到5的有序序列。但是,在list中你不能使用方括號或者at()成員函數去訪問一個指定元素。你需要用一個迭代器(iterators)去遍歷list。
 
你可以這樣遍歷list中的每個元素:
  1. std::list<int>::iterator i; 
  2. for (i = l.begin(); i != l.end(); i++) { 
  3.     int thisInt = *i; 
  4.     // Do something with thisInt 
 
大多數容器類有迭代器(iterator)的概念。迭代器是一個對象,可以遍歷並指向一個特定的元素。你可以通過增量或減量來控制迭代前移或者後移。
 
用迭代器在當前位置獲得元素的值與使用解引用運算符(*)一樣簡單。
 
 注:在上面的代碼中,有兩個運算符重載的實例。i++是迭代器重載增量運算符(++),*i是重載解引用操作符(*)。STL中大量使用了這樣的運算符重載。
 
除了vector(向量)和list(列表),C++中還有很多容器。都有不同的特徵。例如Objective-C中的集合,C++中爲std::set;Objective-C中的字典,C++中爲std::map。C++中,另一個常用的容器是std::pair,其中只存儲兩個值。
 
Shared Pointer
回想一下內存管理,當在C++中使用堆對象是,你需要自己處理內存。沒有引用計數。在C++中確實是這樣。但是在C++ 11標準中,STL中添加了一個新類,這個類中添加了引用計數,稱之爲shared_ptr,意思是“shared pointer”。
 
Shared Pointer是一個對象,這個對象定義一個指針以便在underlying pointer中實現引用計數。這與在Objective-C中在ARC下使用指針是相同的。例如,以下例子展示瞭如何用智能指針來定義一個指針去指向一個整數:
  1. std::shared_ptr<int> p1(new int(1)); 
  2. std::shared_ptr<int> p2 = p1; 
  3. std::shared_ptr<int> p3 = p1; 
 
運行這三行代碼後,每個shared pointer的引用計數爲3。當每個shared pointer被刪除或者被釋放後,引用指數減少。直到最後一個包含underlying pointer的shared pointer被刪除後,底層指針被刪除。
 
由於shared pointer本身就是棧對象,溢出時就會被刪除。因此,shared pointer與Objective-C中的自動引用計數(ARC)下的對象指針的約束規則相同。
 
下面的例子爲shared pointer創建和刪除的全過程:
 
  1. std::shared_ptr<int> p1(new int(1)); ///< Use count = 1 
  2.   
  3. if (doSomething) { 
  4.     std::shared_ptr<int> p2 = p1; ///< Use count = 2; 
  5.     // Do something with p2 
  6.   
  7. // p2 has gone out of scope and destroyed, so use count = 1 
  8.   
  9. p1.reset(); 
  10.   
  11. // p1 reset, so use count = 0 
  12. // The underlying int* is deleted 
 
把p1分配給p2是將p1的副本分配給p2。記住當一個函數參數是按值傳遞的話,是將參數的副本傳給了函數。這一點是很有用處的,因爲如果你將一個shared pointer傳給一個函數,傳遞給這個函數的是一個新的shared pointer。當然,在函數結束時就會發生越界,從而被刪除。所以在函數週期中,underlying pointer的使用數量將會增加。這與在Objective-C中的自動引用計數(ARC)下的引用計數功能相同。
 
當然,你需要能夠獲得或者使用underlying pointer,有兩種方式可以實現這一操作。重載解引用操作符(*)和箭頭操作符(->)以使shared pointer的工作方式本質上與一個正常的指針相同。如下:
  1. std::shared_ptr<Person> p1(new Person(“Matt Galloway”)); 
  2.   
  3. Person *underlyingPointer = *p1; ///< Grab the underlying pointer 
  4.   
  5. p1->doADance(); ///< Make Matt dance 
 
Shared Pointer很好地給C++引入了引用計數的技術。當然,shared pointer也添加了一些少量的開銷,但是這個開銷帶來了很明顯的好處,所以也是值得的。
 
Objective-C++
C++很好,但是跟Objective-C有什麼關係呢?
 
通過用Objective-C++可以將Objective-C和C++結合起來。它並不是一個全新的語言,而是Objective-C和C++兩者的結合。
 
通過兩者的結合,你可以使用兩者的語言特徵。可以將C++的對象作爲Objective-C的實例,反之亦然。如果在應用程序中使用C++庫的話這將會很有用處。
 
要使編譯器理解一個Objective-C++文件是很容易的。你需要做的只是將文件名從.m改爲.mm。當你這樣做的時候,編譯器會考慮到這個文件的不同,並將允許你使用Objective-C++。
 
以下爲如何在兩者間使用對象的例子:
  1. // Forward declare so that everything works below 
  2. @class ObjcClass; 
  3. class CppClass; 
  4.   
  5. // C++ class with an Objective-C member variable 
  6. class CppClass { 
  7.   public
  8.     ObjcClass *objcClass; 
  9. }; 
  10.   
  11. // Objective-C class with a C++ object as a property 
  12. @interface ObjcClass : NSObject 
  13. @property (nonatomic, assign) std::shared_ptr<CppClass> cppClass; 
  14. @end 
  15.   
  16. @implementation ObjcClass 
  17. @end 
  18.   
  19. // Using the two classes above 
  20. std::shared_ptr<CppClass> cppClass(new CppClass()); 
  21. ObjcClass *objcClass = [[ObjcClass alloc] init]; 
  22.   
  23. cppClass->objcClass = objcClass; 
  24. objcClass.cppClass = cppClass; 
 
簡單吧!注意這個屬性被定義爲assign,然而你不能用strong或者weak,因爲這些對非OBjective-C對象類型沒有意義。編譯器不能“保留”或者“釋放”一個C++對象類型,因爲它並不是一個Objective-C對象。
 
assign的內存管理仍然是正確的,因爲你使用了shared pointer。你可以使用raw pointer,但是你需要自己寫一個setter來刪除原來的實例並根據情況設置一個新的值。
 
 注:Objective-C++是有侷限性的。C++的類不能繼承Objective-C的類,反之亦然。異常處理也是需要注意的地方。現代編譯器和運行時確實允許C++異常和Objective-C異常共存,但是仍需要注意。使用異常處理之前一定要閱讀相關文檔。
 
Objective-C++很有用處,因爲很多好的庫都是用C++寫的。能夠在iOS和Mac的應用程序上使用它是很有價值的。
 
需要注意的是,Objective-C++確實有它需要注意的地方。第一個需要注意的地方是內存管理。記住Objective-C的對象都是建立在堆上的,而C++的對象可以建立在棧上也可以是在堆上。如果Objective-C類的對象是建立在棧上的話會很奇怪。必須是在堆上,因爲整個Objective-C對象都是建立在堆上的。
 
編譯器通過自動在代碼中添加alloc和dealloc來構造和析構C++棧對象以確保這種情況。在此過程中,編譯器需要創建兩個函數“.cxx_construct”和“.cxx_destruct”,這兩個函數分別被alloc和delloc調用。在這寫方法中,執行所有相關的C++處理是必要的。
 
 注:ARC實際上依託於“.cxx_destruct”,現在它爲所有的Objective-C類創建了一個函數來寫所有的自動消除代碼。
 
這個處理所有基於棧的C++對象,但是你要記住任何基於堆的對象都需要在適當的情況下創建和銷燬。你可以在指定的初始化程序中創建對象然後再dealloc中刪除。
 
另一個在Objective-C++中需要注意的地方是減少對C++的依賴。這一點要儘量避免。要想明白這是爲什麼,看看下面這個使用Objective-C++的類。
  1. // MyClass.h 
  2. #import <Foundation/Foundation.h> 
  3. #include <list> 
  4.   
  5. @interface MyClass : NSObject 
  6.   
  7. @property (nonatomic, assign) std::list<int> listOfIntegers; 
  8.   
  9. @end 
  10.   
  11. // MyClass.mm 
  12. #import “MyClass.h” 
  13.   
  14. @implementation MyClass 
  15. // … 
  16. @end 
MyClass類的實現文件必須是.mm文件,因爲它是使用C++編寫的。這沒有錯,但是想一想如果你想要使用MyCLass類的話會發生什麼呢。你需要import MyClass.h,但是這樣做你引入了一個使用C++編寫的文件。所以即使其他的文件不需要用C++編寫,也需要使用Objective-C++來進行編譯。
 
因此,最好是在公共頭文件中減少使用C++。你可以使用在實現文件中聲明的私有屬性或者實體變量實現這一目的。
 
下一步
C++是一個偉大的語言。它與Objective-C有相似的根源,但是它選擇一種很不同的方式去編寫程序。總之,學習C++可以很好的理解面向對象程序。而且C++能幫助你在objective - c代碼做出更好的設計決策。我鼓勵你去學習更多的C++知識並自己寫程序。你可以在learncpp.com中找到很多好的資源。如果你有任何評論或者疑問或者C++問題,請留言。
 

 相關閱讀: 向iOS開發者介紹C++(一)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章