歡迎回到《寫給 iOS 程序員看的 C++ 教程系列》第二部分!
在第一部分,你學習了類和內存管理。
在第二部分,你將進一步深入類的學習,以及其他更有意思的特性。你會學習什麼是模板以及標准模板庫。
最後,你將大致了解 Objectiv-C++——一種將 C++ 混入 Ojective-C 的技術。
准備好了嗎?讓我們開始吧!
這裡的多態不是那只會變化的鹦鹉,盡管聽起來很像!
好了,我承認這個玩笑一點都不好笑!:]
簡單說,多態是在子類中覆蓋某個函數。在 O-C 中,你無數次用過多態,例如繼承 UIViewController 並重寫 viewDidLoad 方法。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPkMrKyDW0LXEtuDMrLHIIE8tQyDHv7XDzKu24MHLoaPL+dLU1NrO0r3pydzV4rj2x7+088zY0NS1xMqxuvKjrMTj1+66w7K70qq/qtChsu6hozwvcD4NCjxwPtXiysfU2tK7uPbA4NbQuLK4x9K7uPazydSxuq/K/bXEwP3X06O6PC9wPg0KPHByZSBjbGFzcz0="brush:java;"> class Foo { public: int value() { return 5; } }; class Bar : public Foo { public: int value() { return 10; } };
想一下如下代碼會發生什麼:
Bar *b = new Bar(); Foo *f = (Foo*)b; printf(“%i”, f->value()); // Output = 5
噢——輸出結果決不會是你期望的!我猜你以為會輸出 10 的,是不是?這絕對是 C++ 和 O-C 的巨大不同。
在 O-C 中,將一個子類的指針轉換為基類指針不會有任何問題。當你向對象發送消息(比如調用方法)時,運行時會查找對象的類,並調用最後派生的方法。這種情況下,子類 Bar 的方法被調用。
在第一部分中,我已經提到過編譯時與運行時的這種明顯區別。
在上面的代碼中,當編譯器發現有對 value() 的調用時,編譯器會計算到底該調用哪個函數。因為指針的類型是 Foo,因此編譯器會讓代碼跳轉到 Foo::value()。編譯器不知道實際上 f 指向的是 Bar。
在這個簡單例子裡,你會認為編譯器應該可以推斷出 f 是一個 Bar 指針。設想一下如果 f 被傳遞給一個函數的情況。這時,編譯器根本無從判斷它實際上是一個繼承自 Foo 的類的指針。
上面的例子很好地說明了 C++ 和 O-C 之間的關鍵的不同,靜態綁定和動態綁定。上面的代碼是一個靜態綁定的例子。編譯器負責決定調用哪個函數,這種行為在編譯後的二進制裡就已經固定了,無法在運行時再作改變。
在 O-C 中則不同,它是動態綁定。運行時才決定調用哪個函數。
運行時綁定使 O-C 變得尤其強大。你可能知道在 O-C 中可以在運行時為某個類增加新的方法。對於靜態綁定的語言,這是做不到的,這些語言的調用行為在編譯時就已經確定。
別急——C++ 技決不僅於此!通常 C++ 是靜態綁定的,但它也有動態綁定的機制;即所謂的“虛函數”。
虛函數提供了動態綁定機制。它會在運行時通過查表的方式推斷需要調用哪個函數——每個類都有這麼一個表。當然與靜態綁定相比,這會帶來一定的性能開銷。動態綁定除了調用函數還需要查表。而靜態綁定,直接調用函數即可。
虛函數的使用很簡單,只需要在對應函數的前面加一個 virtual 關鍵字。前面的例子用虛函數實現是這個樣子:
class Foo { public: virtual int value() { return 5; } }; class Bar : public Foo { public: virtual int value() { return 10; } };
現在來執行同樣的語句:
Bar *b = new Bar(); Foo *f = (Foo*)b; printf(“%i”, f->value()); // Output = 10
干得不錯!輸出的結果和我們先前所期望的相一致了,不是嗎?我們可以在 C++ 裡面使用動態綁定了,但到底使用動態綁定還是靜態綁定,要根據你的實際情況而定。
這樣的靈活性在 C++ 中是很常見的,這也是 C++ 被當成是多重編程范式語言的原因。O-C 強制要求遵循嚴格的編程泛型,特別是使用 Cocoa 框架的時候。而 C++ 則將更多選擇交由程序員決定。
接下來討論下虛函數是如何工作的。
在討論這個問題之前,你需要理解非虛函數是如何工作的,看如下代碼:
MyClass a; a.foo();
如果 foo() 不是虛函數,編譯器會將代碼轉換成直接跳轉到 MyClass 類的 foo() 函數的指令。
但這恰恰就是非虛函數問題之所在。回想前面的例子,如果類是多態的,編譯器無法知道變量的完整類型,從而無法得知要跳到哪個函數。需要有一種機制在運行時查找正確的函數。
為了實現查找,虛函數使用了所謂虛函數表或者 v-table 的概念;它是一張速查表,將函數和它們的實現對應起來,每個類都可以訪問這張表。當編譯器發現某個虛函數被調用時,它會查找這個對象的 v-table 並定位到正確的函數。
再回到前面的例子,看看這一切是如何實現的:
class Foo { public: virtual int value() { return 5; } }; class Bar : public Foo { public: virtual int value() { return 10; } }; Bar *b = new Bar(); Foo *f = (Foo*)b; printf(“%i”, f->value()); // Output = 10
當你創建 b 這個 Bar 對象的時候,b 的 v-table 應該是 Bar 的 v-table。當 b 被轉換成 Foo 指針時,它並沒有改變對象的實際內容。b 的 v-table 仍然是 Bar 的 v-table,而非 Foo 的 v-table。因此當調用 value() 時,將調用 Bar::value() 並返回相應結果。
每個對象的生命周期中有兩個最重要的階段:構造和析構。C++ 允許你控制這兩者。它們等同於 O-C 的初始化方法(例如 init 或者 init開頭的方法)和 dealloc 方法。
C++ 中構造函數名和類名相同。可以有多個構造函數,就像 O-C 中你可以有多個初始化方法一樣。
例如,有一個類,擁有兩個不同的構造函數:
class Foo { private: int x; public: Foo() { x = 0; } Foo(int x) { this->x = x; } };
這裡出現了兩個構造函數。一個構造函數叫做默認構造函數:Foo()。另一個則使用一個參數來初始化成員變量的值。
如果你在構造函數中僅僅是設置內部狀態,就像上面的代碼一樣,則我們可以有一種更省代碼的辦法。替代自己設置成員變量,你可以使用下列語法:
class Foo { private: int x; public: Foo() : x(0) { } Foo(int x) : x(x) { } };
通常,只有在設置成員變量時可以使用這種辦法。當你需要執行某些邏輯或調用其他函數時,你就必須實現函數體了。當然你也可以同時使用這兩者。
在繼承的情況下,通常會調用父類的構造函數。在 O-C 中,我們經常看到第一句代碼就是調用父類的指定初始化函數。
在 C++ 中,你要這樣做:
class Foo { private: int x; public: Foo() : x(0) { } Foo(int x) : x(x) { } }; class Bar : public Foo { private: int y; public: Bar() : Foo(), y(0) { } Bar(int x) : Foo(x), y(0) { } Bar(int x, int y) : Foo(x), y(y) { } };
繼承父類的構造函數需要寫在函數簽名後列表中第一個元素的位置。你可以繼承任何父類的構造函數。
C++ 沒有指定初始化函數的概念。到目前為止,還是無法在全體構造函數中調用這個類的某個構造函數。在 O-C 中,經常可以看到指定初始化函數,其它初始化方法都會調用它,僅有指定初始化方法繼承父類的指定初始化方法。例如:
@interface Foo : NSObject @end @implementation Foo - (id)init { if (self = [super init]) { ///< Call to super’s designated initialiser } return self; } - (id)initWithFoo:(id)foo { if (self = [self init]) { ///< Call to self’s designated initialiser // … } return self; } - (id)initWithBar:(id)bar { if (self = [self init]) { ///< Call to self’s designated initialiser // … } return self; } @end
在 C++ 中,你可以調用父類的構造函數,但在最近之前調用自己的構造函數一直是不被允許的。下面的代碼也很常見:
class Bar : public Foo { private: int y; void commonInit() { // Perform common initialisation } public: Bar() : Foo() { this->commonInit(); } Bar(int y) : Foo(), y(y) { this->commonInit(); } };
當然,這看起來很蠢。為什麼不用 Bar(int y) 繼承 Bar() 然後在 Bar() 中使用 Bar::commonInit() 一句?在 O-C 中這是可以的。
在 2011 年,最新的 C++ 標准實現:C++11。在這個版本中終於允許我們這樣做了。仍然有許多 C++ 代碼沒有升級到 C++11 標准,因此兩種方法都需要了解。2011 以後的 C++ 代碼會這樣寫:
class Bar : public Foo { private: int y; public: Bar() : Foo() { // Perform common initialisation } Bar(int y) : Bar() { this->y = y; } };
這種方法有一點小小的不足,就是在調用同一類的構造函數的時候你無法對成員變量賦值。如上所示,y 變量必須在構造函數的函數體中進行初始化。
注意:C++11 在 2011 年成為了完整標准。在開發的時候,一開始叫做 C++0x。這是因為它原准備在 2000-2009 年完成,x 應當被年份的最後一位數字所替代。但它並沒有按期完成,所以最終被叫做 C++11。包括 clang 在內的所有編譯器,都完整支持 C++11。
這是構造,那麼析構又是怎樣的呢?當一個堆對象被 delete ,或者一個棧對象超出了作用域,這時對象會被析構。在析構函數裡你需要進行必要的清理。
一個析構函數沒有參數,你可以想一下,那完全沒有意義。基於同樣的理由 O-C 的 dealloc 也沒有任何參數。一個類只能有一個析構函數。
析構函數名由一個波折號 ~ 加上類名構成。這是一個析構函數的例子:
class Foo { public: ~Foo() { printf(“Foo destructor\n”); } };
來看一下,當類有繼承的時候會發生什麼:
class Bar : public Foo { public: ~Bar() { printf(“Bar destructor\n”); } };
假設你寫了類似的代碼,則當你通過一個 Foo 指針刪除一個 Bar 實例時,會發生一些奇怪的事情:
Bar *b = new Bar(); Foo *f = (Foo*)b; delete f; // Output: // Foo destructor
呃,不對吧?明明刪除的是 Bar 對象,為什麼調用的是 Foo 的構造方法。
回想前面講到的問題,你可以在這裡使用虛函數來解決它。這其實是同樣的問題。編譯器看見的是有一個 Foo 對象需要 delete,而且 Foo 的析構函數又不是虛函數,因此就調用了 Foo 的析構函數。
將函數標記為虛函數能夠解決這個問題:
class Foo { public: virtual ~Foo() { printf(“Foo destructor\n”); } }; class Bar : public Foo { public: virtual ~Bar() { printf(“Bar destructor\n”); } }; Bar *b = new Bar(); Foo *f = (Foo*)b; delete f; // Output: // Bar destructor // Foo destructor
這個結果是我們需要的——但和我們之前講到的虛函數的使用又有所不同。這次兩個函數都被調用了。首先是 Bar 的,然後是 Foo 的?怎麼回事?
因為析構函數的特殊性。Bar 的析構函數會自動調用父類即 Foo 的析構函數。
這是有必要的;在 O-C 中,在 ARC 出現之前,你也會調用父類的 dealloc。
我猜你會想到這個:
難道編譯器不能幫我們做這些事情嗎?是的,編譯器確實有這個能力,但這樣並不能保證所有情況下都適用。
例如,如果你從來不繼承某個類呢?如果析構函數是虛函數,則當對象被 delete 時,都會通過 v-table 來間接調用,而你根本不想這種間接調用發生。C++ 讓你自己選擇——這也是 C++ 非常強大的一個例子——但程序員需要知道究竟發生了什麼。
給你一條忠告。總是讓析構函數成為虛函數,除非你明確知道你不會從某個類繼承。
接下來這個主題在 O-C 中是完全不存在的,因此你首先需要補充一點概念。不用擔心,這些概念都不復雜!
操作符是一種符號比如大家所知道的 +、-、*、/。例如你可以在標量上使用 + 操作符:
int x = 5; int y = x + 5; ///< y = 10
在這裡,+ 號的作用名副其實:將 x 加上 5 並返回結果。如果還是不明白,我們將它寫成函數:
int x = 5; int y = add(x, 5);
我們可以想到,add(…) 函數將兩個參數加在一起,然後返回結果。
在 C++ 中,你可以用在任何自定義類上使用操作符。這個功能相當強大。當然有時候會有點奇怪。例如將一個 Person 和一個 Person 相加是什麼結果?難道是兩個人結婚?:]
但不管這麼說,這個功能還是蠻強大的。看下面例子:
class DoubleInt { private: int x; int y; public: DoubleInt(int x, int y) : x(x), y(y) {} };
你可能會寫出這樣的代碼:
DoubleInt a(1, 2); DoubleInt b(3, 4); DoubleInt c = a + b;
在這裡,我們希望 c 等於 DoubleInt(4,6),分別將兩個 DoubleInt 的 x 和 y 進行相加。其實很簡單!你只需為 DoubleInt 編寫一個這樣的方法:
DoubleInt operator+(const DoubleInt &rhs) { return DoubleInt(x + rhs.x, y + rhs.y); }
函數名有點特殊,叫做 operator+ i;當編譯器看到一個加號外帶兩邊各有一個 DoubleInt 時,就會調用這個函數。這個函數會在 + 號左邊的對象上調用,而右邊的對象是作為參數傳遞給函數。這就是我們通常會把這個參數命名為 rhs 的原因,因為 rhs 是 right hand side(右邊)的意思。
函數參數是引用類型,因為沒有必要使用拷貝類型,如果用拷貝類型的話,則表明我們會改變這個值,所以需要重構造一個對象。此外,參數用 const 修飾,表明在執行加法時不允許對 rhs 進行修改。
C++ 還不僅僅能做這些。你也許不想僅僅做 DoubleInt 和 DoubleInt 的加法,還想做 DoubleInt 和 int 的加法。這完全是可以的。
要實現這個,請實現下列成員函數:
DoubleInt operator+(const int &rhs) { return DoubleInt(x + rhs, y + rhs); }
然後你就可以這樣:
DoubleInt a(1, 2); DoubleInt b = a + 10; // b = DoubleInt(11, 12);
強!真強!這下誰敢不服?
並不僅僅是加法。還有任意操作符。你可以重載 ++、–、+=、-=、*、-> 等等。實在是數不勝數。我建議你去 learncpp.com 好好看一下關於操作符重載的內容,那裡有整整的一篇都是討論運算符重載。
現在,卷起你的手袖。C++ 中非常好玩的戲肉來了。
你經常在編寫完一個函數或類的以後,發現以前已經寫過了同樣的東西——僅僅是類型有區別。例如,看一個交換兩個數的例子。你可能會這樣寫:
void swap(int &a, int &b) { int temp = a; a = b; b = temp; }
注意:這裡參數是引用類型,以便這個變量自身能夠被真正傳入並進行互換。如果是值類型,則僅僅是與參數值相同的兩個對象拷貝進行了交換。這個函數很好滴演示了 C++ 中引用特性所帶來的好處。
這個函數只能交換整數。如果你想交換浮點數,你需要再寫一個函數:
void swap(float &a, float &b) { float temp = a; a = b; b = temp; }
你不得不又在方法體中書寫了重復的代碼,有夠笨的。C++ 有一種語法,讓你能夠忽略掉數據的類型。你可以利用所謂的模板實現這一點。在 C++ 中,你可以這樣做而不用像上面一樣寫兩個方法:
templatevoid swap(T a, T b) { T temp = a; a = b; b = temp; }
這樣,你可以對任何數據進行互換了!你可以在任意類型上調用這個函數:
int ix = 1, iy = 2; swap(ix, iy); float fx = 3.141, iy = 2.901; swap(fx, fy); Person px(“Matt Galloway”), py(“Ray Wenderlich”); swap(px, py);
但使用模板時有一點要注意,模板函數的實現只能在頭文件裡。這是由於只有這樣模板才能編譯。編譯器看到模板函數被調用時,如果這種類型的函數不存在,則編譯一個該類型的版本。
在編譯器需要看到模板函數實現的前提下,我們必須將實現放到頭文件裡,然後在使用時包含它。
同樣的原因,如果你修改了模板函數的實現,那麼每個用到這個函數的文件都需要重新編譯。這和修改實現文件中的函數和類成員函數是不同,那種情況下只需要重新編譯一個文件。
因此,大范圍使用模板可能會導致一些使用上的問題。但它們有時又非常有用,因此和 C++ 中的許多東西一樣,你需要在強大和簡單之間尋找平衡點。
模板不僅能在函數中使用。它也能在類中使用!
假設你有一個類,需要存放 3 個值——這 3 個值分別用於保存某些數據。首先你想讓它們存放整數,你可以這樣寫:
class IntTriplet { private: int a, b, c; public: IntTriplet(int a, int b, int c) : a(a), b(b), c(c) {} int getA() { return a; } int getB() { return b; } int getC() { return c; } };
但在開發過程中,你有發現需要存放 3 個浮點數。這回你創建了新的類:
class FloatTriplet { private: float a, b, c; public: FloatTriplet(float a, float b, float c) : a(a), b(b), c(c) {} float getA() { return a; } float getB() { return b; } float getC() { return c; } };
看起來我們可以用模板解決這個問題——沒錯,就是模板!和可以在函數中使用模板一樣,我們可以在整個類中使用。語法是一樣的。這兩個類可以替換成:
templateclass Triplet { private: T a, b, c; public: Triplet(T a, T b, T c) : a(a), b(b), c(c) {} T getA() { return a; } T getB() { return b; } T getC() { return c; } };
但是,模板類在使用上有些變化。模板函數的代碼不需要動,因為參數類型由編譯器推斷。但你得告訴編譯器,你准備讓模板類使用哪個類型。
幸好這也非常簡單。模板類的使用是這也的:
TripletintTriplet(1, 2, 3); Triplet floatTriplet(3.141, 2.901, 10.5); Triplet personTriplet(Person(“Matt”), Person(“Ray”), Person(“Bob”));
強吧?
別急!這還沒完!
模板函數或類並不是只能使用一種未知類型。Triplet 類可以進一步增強到支持 3 種不同類型,而不是原來的 3 個值都是同一個類型。
要實現這個,只需要在 template 定義中指定更多的類型就行:
templateclass Triplet { private: TA a; TB b; TC c; public: Triplet(TA a, TB b, TC c) : a(a), b(b), c(c) {} TA getA() { return a; } TB getB() { return b; } TC getC() { return c; } };
現在的模板由 3 種不同的類型構成,每一種都在各自對應的地方使用。
這個模板類的使用非常簡單:
TripletmixedTriplet(1, 3.141, Person(“Matt”));
這就是模板!現在我們來看一下有些庫使用這個特性有多頻繁——STL 標准模板庫。
每個典型的編程語言都會有一個標准庫,用於包含常用的數據結構、算法和功能。在 O-C 中是 Fundation 庫,包括 NSArray、NSDictionary 以及其它大家見過或沒見過的成員。在 C++ 中,則是標准模板庫,或者 STL,包含了標准的代碼。
被叫做標准模板庫的原因是它大量使用了模板。有意思吧? :]
在 STL 中有許多有用的東西;細述起來就太多了,所以只能提幾個最重要的地方。
數組、字典和集合:全都是其他對象的容器。在 O-C 中,Foundation 庫就幫我們實現了最常見的容器。在 C++ 中,由 STL 實現這些容器。事實上,STL 中包含的容器類要比 Foundation 中的多。
在 STL 中,有兩個不同的 NSArray 的兄弟。第一個是 vector 第二個是 list。二者都表示一系列對象,但各有各的優缺點。C++ 再次將選擇權交給了你。
首先看 vector:
#includestd::vector v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); v.push_back(5);
注意 std::,因為大部分 STL 都位於 std 命名空間下。STL 將它所有的類放在它的命名空間 std 中以防止命名沖突。
在上面的代碼中,首先創建了一個存儲 int 的 vector,然後依次添加 5 個整數到 vector 的後端。最終,vector 會順序包含 1-5。
有一點值得注意,所有的容器都是可變的,它不像 O-C,C++ 中沒有可變和不可變的區別。
訪問 vector 的元素:
int first = v[1]; int outOfBounds = v.at(100);
有兩種方法可以訪問 vector 的元素。第一種方法使用方括號,即 C 語言的數組風格。在 O-C 加入了下標語法之後,你也可以在 NSArray 上這樣做了。
第二行使用 at 成員函數,它和方括號的作用是一樣的,不過它會檢查索引是否越界。如果越界,這個方法會拋出一個異常。
一個 vector 是一塊單獨的、連續的內存塊。它的大小等於要存儲的對象類型的大小(比如整型為 4 或 8 個字節,取決於架構的類型是32位還是64位)乘以數組中的元素個數。
向 vector 中添加新元素的代價是昂貴的,因為需要重新計算內存大小、重新分配內存。不過要訪問某個索引上的對象是很快的,因為只需要從數組某個偏移位置讀取固定字節數的數據到內存。
std::list 類似於 std::vector;但是數組的實現稍有不同。它不是連續的內存塊,而是一個雙向鏈表。也就是說數組中每個元素都包含數據本身和分別指向前一元素、後一元素的指針。
由於雙向鏈表的緣故,插入和刪除很快,但訪問第n個元素需要從 0-n 逐一遍歷。
list 的使用和 vector 非常像:
#includestd::list
l; l.push_back(1); l.push_back(2); l.push_back(3); l.push_back(4); l.push_back(5);
和前面 vector 的例子相似,這裡也依序創建了 1-5 個數的數組。但這次不能使用方括號或 at 函數來訪問數組中的元素。你必須用迭代器的來逐一遍歷數組。
比如這樣來遍歷數組中的元素:
std::list::iterator i; for (i = l.begin(); i != l.end(); i++) { int thisInt = *i; // Do something with thisInt }
絕大多數容器類都有迭代器。一個迭代器是一個對象,能夠通過向前或向後移動來訪問集合中的某個元素。迭代器 +1,指針向前移動一個元素,迭代器 -1 指針向後移動一個元素。
要獲取迭代器當前位置的數據,使用解除引用運算符(*)。
注意:上面的代碼中,用到了兩個運算符重載。i++ 使用了迭代器對 ++ 操作符的重載。i 使用了對解除引用運算符 的蟲子啊。像類似的運算符重載在 STL 中非常常見。
除了 vector 和 list,C++ 還有許多容器類。它們有著完全不同的特性。比如 O-C 中的 set,在 C++ 中是 std::set,字典則是 std::map。還有一個常用的容器類 std::pair 用於存放一對值。
重溫一下內存管理:當你你在 C++ 中使用堆對象時,你必須自己處理內存管理;沒有引用計數可用。在整個語言來說確實是這樣的。但從 從 C++11 開始,STL 中增加了一個新的類用於支持引用計數。它就是 shared_ptr,即“Shared Pointer”,共享指針。
共享指針包裝了一個普通的指針以及指針底層的引用計數。它的使用方式非常類似於 O-C 中 ARC,可以引用一個對象。
例如,下面的例子演示了如何用共享指針引用一個整數:
std::shared_ptrp1(new int(1)); std::shared_ptr p2 = p1; std::shared_ptr p3 = p1;
當執行完這 3 句代碼,3 個共享指針的引用計數都變成了 3。每當一個共享指針被銷毀或者 reset 之後,引用計數就減一。一旦最後一個引用它的共享指針被銷毀,背後的指針就會被刪除。
由於共享指針自身屬於棧對象,當它們的作用域結束它們會被刪除。因此它們的行為就等同於 O-C 中 ARC 下面的對象指針。
這是一個創建共享指針和銷毀共享指針的例子:
std::shared_ptrp1(new int(1)); ///< Use count = 1 if (doSomething) { std::shared_ptr p2 = p1; ///< Use count = 2; // Do something with p2 } // p2 has gone out of scope and destroyed, so use count = 1 p1.reset(); // p1 reset, so use count = 0 // The underlying int* is deleted
將 p1 賦給 p2 會生成一份 p1 的拷貝。還記得函數參數是值傳遞的嗎?當向函數傳遞參數時,實際上傳遞的是這個值的拷貝。因此,如果你傳遞一個共享指針給函數,實際上傳遞了一個新的共享指針過去。當函數結束,作用域結束,指針被銷毀。
因此在函數的生命周期中,對應指針的計數值被 +1。實際上在 O-C 的 ARC 中就是這樣干的!
當然,如果你想讀取或者使用共享指針中所包含的指針時,有兩種方式。解除引用運算符(*)或者箭頭操作符(->),這兩者都被重載了,以便共享指針能夠像普通指針那樣工作,比如:
std::shared_ptrp1(new Person(“Matt Galloway”)); Person *underlyingPointer = *p1; ///< Grab the underlying pointer p1->doADance(); ///< Make Matt dance
共享指針是一種很好的方法,它讓 C++ 實現了引用計數。當然它們會帶來一些代價,但與它所帶來的好處相比這種代價是值得的。
但你也許會問:C++ 就行了,為什麼還要用 O-C ?沒錯,通過 Obejctive-C++ 我們能夠混合 O-C 和 C++。它的名字就已經說明這一點了,它不是一種嶄新的語言,而是兩種語言的聯合。
通過混合 O-C 和 C++,你可以使用兩種語言特性。你可以將 C++ 對象作為 O-C 類的實例數據,反之亦然。如果你想在 app 中調用一個 C++ 庫時,這非常有用。
讓編譯器將一個文件視作 Objectdive-C++ 文件很簡單。你只需要將文件名從 .m 改成 .mm,編譯器就會將它特別對待,從而允許你使用 Objective-C++。
你可以像這樣來使用一個對象:
// Forward declare so that everything works below @class ObjcClass; class CppClass; // C++ class with an Objective-C member variable class CppClass { public: ObjcClass *objcClass; }; // Objective-C class with a C++ object as a property @interface ObjcClass : NSObject @property (nonatomic, assign) std::shared_ptrcppClass; @end @implementation ObjcClass @end // Using the two classes above std::shared_ptr cppClass(new CppClass()); ObjcClass *objcClass = [[ObjcClass alloc] init]; cppClass->objcClass = objcClass; objcClass.cppClass = cppClass;
就是這樣簡單!注意屬性被聲明稱 assign,而不是強引用或弱引用,因為它是一個非 O-C 對象。編譯器不會 retain 或者 release C++ 對象,因為它不是 O-C 對象。
雖然使用了 assign,但內存管理仍然不會出錯,因為你使用了共享指針。你可以使用裸指針,但這樣你就必須自己實現 setter 方法,以刪除舊的對象然後再設置新值。
注意:有一些限制。C++ 類不能繼承 O-C 類,反之亦然。異常處理也需要注意。當前的編譯器和運行時允許 C++ 異常和 O-C 異常同時存在,但仍然需要小心。如果你使用了異常,請閱讀文檔。
Objective-C++ 是非常有用的,因為有些時候,能夠適用於某個任務的最好的庫都是用 C++ 寫的。能夠在 iOS 或 Mac app 上無痛地調用這些庫將讓我們受益無窮。
注意在 Objective-C++ 時有一些注意事項。一個是內存管理。記住 O-C 對象總是在堆中,但 C++ 對象既可以在堆中也可以在棧中。如果把棧對象使用在 O-C 類的某個成員上會有意向不到的結果。它實際上仍然放在堆內存中,因為整個 O-C 對象都是在堆上的。
對於 C++ 棧對象,編譯器會自動添加alloc 和 dealloc 代碼用於構造和析構對象。這是通過創建兩個名為 .cxx_construct 和 .cxx_destruct 方法來實現的,前者負責 alloc,後者負責 dealloc。在這些方法中,根據需要進行和 C++ 有關的處理。
注意: ARC 實際上扮演了 .cxx_destruct 的角色,它為所有的 O-C 類創建了一個類似的方法來放入所有的自動清理代碼。
這種過程在所有的 C++ 棧對象上發生,但你需要記住,應當對所有的 C++ 堆對象進行正確的創建和銷毀。你應當在你的指定初始化函數中創建它們,然後在 dealloc 方法中 delete.
另外一個使用 Objective-C++ 的注意事項是 C++ 依賴洩漏。你應當盡量避免它。要明白為什麼,請看下面的例子:
// MyClass.h #import#include @interface MyClass : NSObject @property (nonatomic, assign) std::list
listOfIntegers; @end // MyClass.mm #import “MyClass.h” @implementation MyClass // … @end
由於使用了 C++,這個類的實現文件肯定是一個 .mm 文件。但想像一下,當你使用 MyClass 時會發生什麼?你需要導入 MyClass.h。但你導入的這個文件中使用了 C++。因此其他文件也會需要被當成 Objective-C++ 來編譯,哪怕是它根本不想使用 C++。
如果可能的話,盡量在你的公共頭文件中減少對 C++ 的使用。你可以在實現中用私有屬性或實例變量來替代。
C++ 是一門值得學習的偉大語言。它有著和 O-C 一樣的血統,但被用於做不同的事情。通過學習 C++,能夠更好地理解面向對象編程。進而幫助你在 O-C 中做出更好的設計方案。
我鼓勵你閱讀更多的 C++ 代碼並親自動手測試它們。如果你想進一步學習這門語言,這裡 learncpp.com 有許多精彩資源。
如果你有任何問題或建議,請留言。