你已經精通了Objective-C,並且一直想學更酷的東西?看看這篇文章吧!本文將向iOS開發者介紹C++。稍後我會介紹,Objective-C能夠無縫地使用C和C++程式碼。因此,基於以下幾點原因,iOS開發者理解C++將會很有幫助:
- 1.有時候你想在應用中使用一個用C++編寫的庫。
- 2.你可能用C++寫一部分應用程式的程式碼,以便更容易跨平臺移植。
- 3. 瞭解其他語言通常能幫助你更好地理解程式設計。
這篇文章針對那些已經理解Objective-C的iOS開發者。前提是假定你已明白怎麼寫Objective-C程式碼,並熟悉基本的C概念,比如型別、指標、函式等。
準備好學C++了麼?那麼就馬上開始吧!
開始:語言簡史
C++和Objective-C有一些共源:它們都根植於老式的好用的C語言,都是C語言的“超集”。因此,你可以在這兩種語言中使用C語言的一些功能,和每種語言的附加特性。
如果你熟悉Objective-C,那麼你將能粗略地理解你所遇到的C++程式碼。例如,兩種語言中的數值型別(int型、float型和char型)的表現方式和使用規則都是完全一樣的。
Objective-C和C++都在C語言基礎上新增了物件導向的特徵。如果你不熟悉“物件導向”,那麼你真正需要明白的是物件導向指資料是由物件表示的,而物件是類的例項。事實上,C++最初稱為“C with Classes”,內在的涵義是使C++物件導向。
“那麼有什麼區別麼?”我聽到了你的疑問。最大的區別是物件導向特性的方法。在C++中,很多行為是發生在編譯時,而在Objective-C中,大多數是發生在執行時。你可能已經修改了Objective-C的執行時間來實現了一個類似method swizzling的詭計,而在C++中這是不可能的。
C++也不像Objective-C一樣有大量內省以及對映方法。在C++中,沒有辦法獲得C++物件的類,而在Objective-C中你可以在一個例項中呼叫“類”方法。同樣的,在C++中也沒有相當於isMemberOfClass或者isKindOfClass的類。
以上對C++的粗略介紹顯示了C++和Objective-C的歷史和主要不同點。歷史部分已經完成了,到我們繼續學習一些C++特徵的時間了。
C++ 類
在任何面嚮物件語言中,首先你要知道的是如何定義一個類。在Objective-C中,你通過建立一個標頭檔案和一個執行檔案來定義一個類,在C++中同樣如此,語法也十分相似。
如下,是一個Objective-C類的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// MyClass.h #import <Foundation/Foundation.h> @interface MyClass : NSObject @end // MyClass.m #import “MyClass.h” @implementation MyClass @end |
作為一個經驗豐富的iOS開發者你應該很明白,但是看看同樣用C++寫的例子:
1 2 3 4 5 6 7 8 9 10 |
// MyClass.h class MyClass { }; // MyClass.cpp #include “MyClass.h” /* Nothing else in here */ |
這裡有一些本質的區別。首先,C++中的實現檔案中什麼都沒有,這是因為你並沒有在類中宣告任何的方法。同理,就像Objective-C,一個空類不需要@implemenation/@end模組。
在Objective-C中,幾乎每個類都繼承自NSObject。你可以建立自己的根類,這意味著你的類將沒有任何superclass。但是,你可能從來沒有這麼做過,除非你只是為了執行時好玩兒。對比C++,正如上面的例子一樣,建立一個沒有超類的類是很普遍的。
另外一個微小的區別是#include和#import。Objective-C將#import前處理器指令新增到C。在C++中沒有相同的,標準的C-style是使用#include。Objective-C中的#import是確保一個檔案只被包含一次,但在C++中你必須自己檢查。
類成員變數和成員函式
當然,有比宣告一個類多得多的事情。正如,在Objective-C和C++中,你可以在類中新增例項變數和方法。或許,你知道在C++中這兩個不是這樣命名的,C++中通常稱為成員變數和成員函式。
注意:“method(實體方法)”這個術語通常不用於C++中,這個特性只用在Objective-C中。在Objective-C中,通過訊息分派帶呼叫“method(實體方法)”。另外,function(函式)通過一個靜態的C-style函式被呼叫。稍後在這篇文章中我將更多的解釋靜態和動態。 |
那麼接下來你要如何宣告成員變數和成員函式呢?如下:
1 2 3 4 5 6 7 8 |
class MyClass { int x; int y; float z; void foo(); void bar(); }; |
這裡有三個成員變數和兩個成員函式。但是在C++中這裡要有更多,在C++中,你可以限定成員變數和成員函式的範圍,並且可以宣告它們是公開訪問的還是私有訪問的。這個可以用於限制什麼程式碼可以訪問每個變數或者函式。
思考下面這個例子:
1 2 3 4 5 6 7 8 9 10 |
class MyClass { public: int x; int y; void foo(); private: float z; void bar(); } |
這裡,x,y和foo函式是公開訪問。意思是可以在MyClass類的外部被呼叫。然而,z和bar函式是私有的。意味著只能在MyClass內部呼叫被呼叫。成員變數預設是私有的。
雖然這種區別確實存在於Objective-C中的例項變數中,但是很少使用。另外,在Objective-C中不太可能限制方法的呼叫範圍。即使你只是在實現類內部宣告一個方法而沒有在介面中顯示,技術上你還是可以外部呼叫這個方法。
Objective-C中的方法只約定為公開或私有。這就是為什麼很多開發者選擇給私有方法加字首(例如“p_”字首)來定義這個區別。這是為了和C++作比較,在C++中如果你試圖從類的外部呼叫一個私有方法,編譯器會丟擲一個錯誤。
那麼你要怎麼使用類呢?和Objective-C非常相似,真的!你可以像下面這樣建立一個例項:
1 2 3 4 |
MyClass m; m.x = 10; m.y = 20; m.foo(); |
簡單吧!這裡建立了一個MyClass的例項,分別設x=10,y=20,然後呼叫foo函式。
實現類的成員函式
你已經看到了如何定義一個類介面,但是函式呢?事實證明,這個十分簡單。有如下兩種方法你可以定義。
第一個實現函式的方法是在類的實現檔案中定義–.cpp檔案。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// MyClass.h class MyClass { int x; int y; void foo(); }; // MyClass.cpp #include “MyClass.h” MyClass::foo() { // Do something } |
以上是第一個方法。在Objective-C中定義十分簡單。注意MyClass::的用法,這就是你如何表明foo()函式已經作為MyClass類的一部分被實現了。
第二個實現函式的方法是你在Objective-C中不能做到的。在C++中,你可以直接在標頭檔案中定義一個函式,如下:
1 2 3 4 5 6 7 8 |
// MyClass.h class MyClass { int x; int y; void foo() { // Do something } }; |
如果你只用過Objective-C,這看上去會很奇怪。確實奇怪,但是這種方法會十分有用。當一個函式以這種方式被宣告時,編譯器可以執行“內聯”優化。這意味著當函式被呼叫時,整個函式程式碼在呼叫站點被內聯編譯而不是跳到一個新的程式碼塊。
雖然內聯可以使程式碼更快,但會增加編輯器程式碼的大小,因為如果函式被多次呼叫,程式碼將通過二進位制複製。如果函式很大,或者被呼叫很多次,那麼這可能會對二進位制檔案的大小產生重大的影響。由於很少的程式碼會在快取中,這將會導致效能下降,這就意味著可能會有潛在的更多的快取丟失。
我的目標是舉例證明C++允許更多的靈活性。作為一個開發者,你需要去理解權衡並做決定。當然,唯一能真正明白哪種選擇對你是正確的方法就是測試你的程式碼!
名稱空間
上面的例子介紹了一些你之前沒有遇到過的新的語法–雙冒號::,即指在C++中如何指代範圍。雙冒號用來告訴編譯器應該在哪裡可以找到foo函式。
下一次你會在使用名稱空間的時候遇到雙冒號。名稱空間是分離程式碼的一種方式,以便減少命名衝突。
例如,你可能會在程式碼中定義一個叫Person的類,但是一個第三方庫也可能命名一個叫Person的類。因此,在寫C++程式碼時,你通常會將你的程式碼放到一個名稱空間中來避免這些型別的命名衝突。
很容易做這個,套用以下名稱空間宣告即可:
1 2 3 4 5 6 7 |
namespace MyNamespace { class Person { … }; } namespace LibraryNamespace { class Person { … }; } |
現在,當使用任何一個Person類的實現時,你可以使用兩個冒號消除歧義,如下:
1 2 |
MyNamespace::Person pOne; LibraryNamespace::Person pTwo; |
簡單吧?
除了在類前加一個字首來約定,在Objective-C中沒有類似的名稱空間。你確實這樣命名類,對吧?如果不是這樣命名的話,那就馬上這樣做吧!
注意:在Objective-C中已經有很多名稱空間的建議了。這樣的方案可以在這裡(連結)找到。我不知道在Objective-C中是否還能用到它們,但是我希望如此。 |
記憶體管理
哦,不……不是那個可怕的詞吧!在任何語言中,記憶體管理都是需要理解的最重要的概念之一。Java基本上是用記憶體回收器來管理記憶體。Objective-C需要你明白引用計數以及ARC所扮演的角色。在C++中,嗯。。。C++又不同了。
首先,在C++中,要理解記憶體管理,你需要先了解堆和棧。即使你認為你知道這一點,我建議你繼續往下閱讀,或許你能略有收穫。
棧是指用於執行應用程式的一個記憶體塊。棧大小固定,並用於儲存應用程式的程式碼的資料。棧基於puch/pop工作,當一個給定函式將資料壓入棧中,當函式執行結束時,出棧的必須是等量的資料。因此,隨著時間的推移,棧使用率不會增長。
堆同樣也是執行應用程式的一個記憶體塊。堆大小不固定,並且隨著程式的執行而增長。應用程式傾向於使用堆來儲存在函式範圍外使用的資料。此外,大的資料單元通常會儲存到堆中,因為存到棧中有可能會溢位。–記住,棧的大小是固定的。
以上是一個堆和棧原理的簡述,以下為兩者的C語言示例:
1 2 3 4 |
int stackInt = 5; int *heapInt = malloc(sizeof(int)); *heapInt = 5; free(heapInt); |
這裡,stackInt使用棧空間。程式返回後,用來儲存“5”的這塊記憶體就會自動釋放。
然而,heapInt使用堆空間,在堆上呼叫malloc分配足夠的空間來儲存一個整數(int)。但是由於堆必須是由你分配,在用完資料後,開發者需要呼叫一個free函式來確保你沒有記憶體洩露。
在Objective-C中,你只能在堆上建立物件。如果你試著在棧上建立物件,那麼編譯器就會報錯。根本行不通。
思考下面的例子:
1 2 3 4 5 6 |
NSString stackString; // Untitled 32.m:5:18: error: interface type cannot be statically allocated // NSString stackString; // ^ // * // 1 error generated. |
這就是為什麼在Objective-C程式碼上會看到星號,所有的物件都在堆上建立,並且所有物件都有指標。這在很大程度上歸結為Objective-C處理記憶體管理。引用計數廣泛應用於Objective-C中,物件需要在堆中以便它們的生命週期能被嚴格控制。
在C++中你既可以把資料存到棧中也可存到堆中。由開發者自己決定。然而,在C++中你也必須自己管理記憶體。資料放入棧中時記憶體將自動被處理;但用堆時,你必須自己管理記憶體,否則要面臨記憶體洩露的風險。
C++中new和delete運算子
C++中引入一組關鍵詞以幫助堆物件進行記憶體管理;他們分別用來建立和撤銷堆中的物件。
建立物件:
1 |
Person *person = new Person(); |
當你不用這個物件時,你就要撤銷它:
1 |
delete person; |
事實上,這同樣適用於C++中標量型別:
1 2 3 |
int *x = new int(); *x = 5; delete x; |
你可以認為這些運算相當於Objective-C中的初始化和刪除物件。在C++中初始化用的new Person()等同於Objective-C中的[[Person alloc] init]。
但是,在Objective-C中沒有等同於delete的運算子。但是我想你已經意識到了,當引用計數歸零時,執行時Objective-C物件的儲存單元就會被釋放。記住,C++不會自動處理引用計數,開發者呼叫物件完成後負責釋放物件。
現在你對C++的記憶體管理有了大致瞭解,簡言之,在C++中的記憶體管理要比Objective-C中的要複雜得多。你真的需要考慮下一步是怎樣,並且要跟蹤物件。
訪問棧和堆物件成員
你已經瞭解到,C++中既可以在棧上也可以在堆上建立物件。然而,這兩種方法還有一點微妙但是很重要的區別,即訪問成員變數和成員函式的方式稍有不同。
使用棧物件時,你需要點運算子(.);使用堆物件時,你需要使用箭頭操作符(–>)。如下:
1 2 3 4 5 6 7 |
Person stackPerson; stackPerson.name = “Bob Smith”; ///< Setting a member variable stackPerson.doSomething(); ///< Calling a member function Person *heapPerson = new Person(); heapPerson->name = “Bob Smith”; ///< Setting a member variable heapPerson->doSomething(); ///< Calling a member function |
區別很微妙,但是值得注意。
你還看到箭頭操作符與this指標一起用,就像在Objective-C中的self指標一樣,它用於類內部函式去訪問當前的物件。
下面的C++例子展示了箭頭操作符的用法:
1 2 3 |
Person::doSomething() { this->doSomethingElse(); } |
這會引起一個常見的C++陷阱。在Objective-C中,你可以用空指標呼叫一個方法,你的應用程式仍會執行的很好:
1 2 |
myPerson = nil; [myPerson doSomething]; // does nothing |
然而,在C++中,如果你要用一個NULL指標呼叫一個方法或者訪問一個例項,你的應用程式會崩潰:
1 2 |
myPerson = NULL; myPerson->doSomething(); // crash! |
因此,你必須確保在C++中不要試圖使用空指標。
引用
向函式傳遞物件時,你傳遞的是一個物件副本,而不是物件本身。例如,思考下面的C++程式碼:
1 2 3 4 5 6 7 8 9 |
void changeValue(int x) { x = 5; } // … int x = 1; changeValue(x); // x still equals 1 |
很簡單,沒什麼特別的。但是想一想當用一個函式做同樣的事情,並且這個函式可以把一個物件作為一個引數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Foo { public: int x; }; void changeValue(Foo foo) { foo.x = 5; } // … Foo foo; foo.x = 1; changeValue(foo); // foo.x still equals 1 |
這或許令你有些驚訝。仔細想想的話,和簡單的int型例子沒有不同。在將物件傳遞給函式之前,建立一個Foo object副本會發生什麼情況?不過有時候確實需要傳遞一個實際物件。一種方法是改變函式指向物件的指標,而不是物件本身。但是無論什麼時候呼叫函式都會產生附加程式碼。
對比上面列舉的值傳遞的例子, C++定義了一個新的概念來允許通過引用來傳遞變數。這就意味著不需要建立物件副本。
利用引用傳遞可以很簡單的改變你的呼叫,你可以在函式簽名前簡單地在變數前使用ampersand (&)即可,如下:
1 2 3 4 5 6 7 8 9 10 |
void changeValue(Foo &foo) { foo.x = 5; } // … Foo foo; foo.x = 1; changeValue(foo); // foo.x equals 5 |
它也適用於non-class變數:
1 2 3 4 5 6 7 8 9 |
void changeValue(int &x) { x = 5; } // … int x = 1; changeValue(x); // x equals 5 |
引用傳遞很有用,並能顯著提高效能。當建立一個物件副本成本相當高的時候引用傳遞更加有用,例如使用一個大型連結串列,建立副本意味著要對物件執行深度複製。
繼承
一個物件導向的語言沒有繼承就不完整。C++當然不會違反這一趨勢。思考下面的兩個Objective-C類,其中一個類從另一個類繼承:
1 2 3 4 5 |
@interface Person : NSObject @end @interface Employee : Person @end |
同樣的事情可以用C++以很相似的方式表達:
1 2 3 4 5 |
class Person { }; class Employee : public Person { }; |
唯一的區別是在C++中要加一個public關鍵詞。這裡Employee類公共的繼承Person類。這就意味著person類中的公共成員在Employee類中也是公共型別的。如果用private代替public,那麼Person類中的公共成員在Employee類中就將變為私有的。關於這個話題的更多資訊,我建議讀一篇很棒的關於繼承和儲存說明符的文章。
以上是關於“繼承“的簡單部分,下面我們開始複雜的部分。與Objective-C不同的是,C++中允許多重繼承,即一個類可以繼承兩個或以上基類。如果你除了Objective-C沒有用過其他語言,那麼這對你來說一定很陌生。
下面是C++中多重繼承的例子:
1 2 3 4 5 6 7 8 9 10 |
class Player { void play(); }; class Manager { void manage(); }; class PlayerManager : public Player, public Manager { }; |
在這個例子中,有兩個基類,一個類繼承這兩個基類。意思是PlayerManager類可以訪問每個基類的所有成員變數和函式。簡單吧?我確定你已經意識到了,在Objective-C中沒有這種方法。
然而,這並不完全正確,對吧?
精明的讀者一定注意到在Objective-C中有類似的方法,即protocols(協議)。雖然跟多重繼承不太相似,但是兩種技術都為了解決同樣的問題:提供一個機制來連線兩個有相似用途的類。
Protocols(協議)有一個微小的區別,那就是協議沒有實現,只是描述類必須遵循哪個介面。
在Objective-C中,上面的例子可被寫成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@protocol Player - (void)play; @end @protocol Manager - (void)manage; @end @interface Player : NSObject <Player> @end @interface Manager : NSObject <Manager> @end @interface PlayerManager : NSObject <Player, Manager> @end |
當然,這個細小的差別你是能想象的到的。在Objective-C中你要在PlayerManager類中執行play和manager。在C++中你只要在每個基類中實現該方法,然後PlayerManager類會自動的繼承每個實現。
雖然,在實踐中,多重繼承有時會另人混淆和複雜化。對C++開發者來說,多重繼承是一個危險的方法,除非絕對必要,開發者會盡量避免使用。
為什麼呢?想一想如果兩個基類用同樣的名字去實現一個函式,並接受同樣的引數的話,那麼這兩個基類就會有同樣的原型。在這種情況下,你就需要消除歧義。例如,假設Player和Manager兩個類都有一個命名為foo的函式。
你需要這樣消除歧義:
1 2 3 4 |
PlayerManager p; p.foo(); ///< Error! Which foo? p.Player::foo(); ///< Call foo from Player p.Manager::foo(); ///< Call foo from Manager |
這絕對是可行的,但是這增加了混淆,而且最好避免複雜性。這由PlayerManager類的使用者決定。使用協議直接使PlayerManager類實現函式foo,因此這裡只有一次實現,沒有混淆。
下一步
第一部分中我們瞭解了C++的簡史,如何宣告類以及C++中記憶體管理是如何工作的。當然,關於C++還有很多需要學習的!
第二部分中,在查閱Objective-C和C++標準庫之前,學到了更多的高階類的特徵和模板。
與此同時,如果你在學C++的過程中有任何問題或者觀點,請加入下面的討論!