你是一個 Objective-C 方面的專家吧?你是否正在尋找下一個要學習的目標?如果是,那麼這篇文章就是專門為你准備的了,它教你如何在 iOS 開發中使用 C++。
就像我後面將會提到的一樣, Objective-C 可以和 C 和 C++ 代碼無縫集成。因此,對於 iOS 開發者來說,學習一些 C++ 是有好處的,具體理由如下:
有時候,你需要在你的 app 中調用 C++ 寫的庫。 可以將 app 中的一部分代碼用 C++ 來寫,這樣便於跨平台。 擁有使用其它語言的背景,有助於從本質上理解編程。這篇文章是為具備 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”,這說明讓 C++ 面向對象具是一件順理成章的事情。
你也許會問“它們有什麼不一樣嗎?”。主要的區別是,二者實現面向對象的方式不同。在 C++ 中,許多實現依賴於編譯時,而 O-C 更多的依賴於運行時。你也許用過了 O-C 的運行時特性比如方法混合。在 C++ 中這顯然是不可能的。
O-C 中大量存在的自省和反射在 C++ 也不存在。在 O-C 中你可以調用實例的 class 方法,但在 C++ 中你無法獲得一個 C++ 對象的 class。同樣,在 C++ 也沒有類似 isMemberOfClass 或者 isKindOfClass 的方法。
這裡簡單介紹了 C++ 的歷史和它與 O-C 的重要區別。歷史課就上到這裡——接下來介紹 C++ 的語言特性!
對於面向對象的語言來說,你需要知道的第一件事就是如何定義類。在 O-C 中,你需要分別創建類的頭文件和實現文件。在 C++ 中也是同樣的,它們的語法也非常接近。
舉個例子。這是一個 O-C 類:
// MyClass.h #import@interface MyClass : NSObject @end // MyClass.m #import “MyClass.h” @implementation MyClass @end
如果你是一個熟練的 iOS 程序員,這些代碼實在是太簡單不過了。但如果用 C++ 來寫這個類,則應當是:
// MyClass.h class MyClass { }; // MyClass.cpp #include “MyClass.h” /* Nothing else in here */
有幾個地方不同。首先在 C++ 實現文件中是空的。因為你沒有為這個類定義任何方法。對於一個空類,O-C 中需要寫一個空的 @implemenation 和 @end 塊,但 C++ 中卻不需要。
在 O-C 中,每個類都需要從 NSObject 繼承(直接或間接)。你可能想將你的類創建成一個根類,也就是不繼承任何父類。但是除非你在運行時中這樣做(僅僅是為了好玩),否則你不可能這樣做。相反在 C++ 中,創建一個沒有父類的類是非常普遍的做法,就如上面的代碼所示。
另一個細微的差別是 #include 和 #import。O-C 添加了一個 #import 的預處理指令。在 C++ 中沒有這個,因此只能使用標准的 C 語言中的 #include。O-C 的 #import 能夠確保一個文件只會包含一次,但是在 C++ 中,你只能自己去手動檢查了。
當然,除了僅僅是聲明類本身外,我們還可以做更多的事情。就像在 O-C 中一樣,在 C++ 中你可以為類實例成員變量和方法。在 C++ 中它們有另外一種叫法,分別叫做成員變量和成員函數。
注意:C++ 中並沒有 “方法”一詞的說法。注意兩者的卻別,在 O-C 中的方法是以發送消息的方式進行調用的,而函數是以靜態 C 語言函數的方式進行調用。稍後,我會解釋關於靜態與動態的區別。
如何聲明成員變量和成員函數?請看例子:
class MyClass { int x; int y; float z; void foo(); void bar(); };
這裡聲明了 3 個成員變量和 2 個成員函數。但在 C++ 中還有更多講究,你可以限制成員變量和成員函數的作用域,將它們聲明稱公共的或者是私有的。這樣就可以限制哪些代碼能夠訪問變量或函數。
例如:
class MyClass { public: int x; int y; void foo(); private: float z; void bar(); }
這裡,x、y 和 foo 是公共的。也就是說這些變量可以在 MyClass 類之外訪問。而 z 和 bar 是私有的。也就是說它們只能被 MyClass 自身所使用。成員變量默認就是私有的。
這二者的區別在 O-C 中實例變量上也存在,但很少用到。此外,在 O-C 中不可能限制方法的作用域。哪怕你只在類的實現中定義了一個方法,不將它暴露在接口中,你仍然可以通過某種技術從外部訪問這個方法。
在 O-C 中方法是共有還是私有只是一種約定。這就是為什麼許多開發者在私有方法前加一個 p_ 前綴以示區別。而 C++ 不同,當你試圖在類的外部訪問私有方法時,編譯器會報錯。
那麼類是如何使用的呢?和 O-C 非常類似。你可以創建一個實例:
MyClass m; m.x = 10; m.y = 20; m.foo();
就是這樣簡單!這裡我們創建了一個 MyClass 實例,設置 x 為 10,y 為 20,然後調用 foo 方法。
你已經知道如何定義類的接口了,但如何實現它的函數?實際上也很簡單。有幾種方法。
第一種方法是在類文件——.cpp 文件裡實現這個方法。例如:
// MyClass.h class MyClass { int x; int y; void foo(); }; // MyClass.cpp #include “MyClass.h” MyClass::foo() { // Do something }
這是第一種方法。和 O-C 中非常類似。注意 MyClass:: 的使用;這表明你將 foo() 函數做為 MyClass 類的一個部分來實現。
第二種則是 O-C 無法做到的方法。在 C++ 中,你可以直接在頭文件中實現方法:
// MyClass.h class MyClass { int x; int y; void foo() { // Do something } };
如果你只會 O-C,這種方法看起來有點別扭。它確實有點別扭,但也很有用。當用這種方式聲明函數時,編譯器能夠進行 inlining 優化。也就是說當函數被調用時,不用跳到新的代碼塊,函數的完整代碼會被編譯到調用地址。
在使代碼變得更快的同時,inlining 還會導致編譯後的代碼膨大,因為函數調用的次數越多,同樣的二進制代碼重復的次數也就越多。如果這個函數很大,或者調用的次數非常多,這會導致二進制的尺寸明顯增加。也會導致性能下降,因為能夠放進緩存中的代碼更少,意味著緩存命中率下降。
這裡的僅僅是為了演示 C++ 擁有更大的靈活性。作為開發者,你應該理解每種做法的優劣並做出決定。當然,要使用哪一種方法,唯一的標准應根據 instrument 的結果而定!
上面的代碼介紹了幾個你從來沒見過的語法——比如雙冒號 ::。它表示 C++ 中作用域的概念,上面代碼告訴編譯器應該在哪裡查找到 foo 函數。
另一個使用雙冒號的地方是命名空間。命名空間是一種分離代碼的方式,它減少了命名沖突的出現。
例如,你實現了一個類叫做 Person,但有一個第三方的庫可能也實現了一個同名的類。但是,在你編寫 C++ 代碼時,你一般會將自己的代碼放在一個命名空間,這樣命名沖突就不會出現。
命名空間的使用很容易,只需要將每樣東西都用命名空間包裹起來,例如:
namespace MyNamespace { class Person { … }; } namespace LibraryNamespace { class Person { … }; }
現在,當使用到 Person 類的時候,你可以用雙冒號來區分,例如:
MyNamespace::Person pOne; LibraryNamespace::Person pTwo;
很簡單吧!?
在 O-C 中沒有命名空間的概念,你只能在類前面加上一個前綴…你已經在你的類中使用了前綴了?:] 如果你還沒有這樣做,那麼最好現在去做!
注意:關於 O-C 有人提過增加命名空間的建議。其中一個在這裡可以看到。我不知道 O-C 最終會不會支持命名空間,但我真的希望有這麼一天!
噢,不… 沒那麼可怕,內存管理在任何語言中都是必須學習的重中之重。Java 完全依靠垃圾回收器完成這個工作。在 O-C 中你必須學習關於引用計數和 ARC 規則。在 C++ 中… 好吧,C++ 就是一個怪胎。
首先,要理解 C++ 中的內存管理必須先理解棧和堆。如果你已經知道這兩個概念,我建議你再看一下;你可能會重新學到點什麼。
棧是一塊 app 運行時能夠使用的內存。它有固定大小,能被應用程序的代碼用來存放數據。棧通過出棧/入棧進行工作,當一個函數執行時,它將數據壓入棧中,當函數執行完畢,它必須彈出同樣的數據。因此,棧不會隨時間運行增長。
堆也是程序運行中使用的內存塊。它的大小不是固定的,並隨著程序的運行而增長。程序用堆存儲函數以外的數據。大數據通常會用堆來存儲,因為放到棧中會導致堆棧溢出——記住,棧是固定大小。
上面是關於棧和堆的最基本的概念;讓我們看幾個使用二者的 C 語言例子:
int stackInt = 5; int *heapInt = malloc(sizeof(int)); *heapInt = 5; free(heapInt);
其中,stackInt 使用棧空間,當函數返回之後,這塊存放有值 “5” 的內存自動被釋放。
但是,heapInt 使用堆空間。malloc 方法負責分配空間,以便能夠存下一個 int 值。因為堆必須由你自己管理,因此在使用完數據之後必須調用 free 函數釋放它,以防止內存洩漏。
在 O-C 中,你只能在堆上創建對象,如果你試圖在棧上創建對象會導致一個編譯錯誤。這是不允許的。
例如:
NSString stackString; // Untitled 32.m:5:18: error: interface type cannot be statically allocated // NSString stackString; // ^ // * // 1 error generated.
這就是為什麼在 O-C 代碼中到處都是星號的緣故;所有的對象都是創建在堆裡,你通過指針來引用這些對象。O-C 通過這種方法進行內存的管理。引用計數和 O-C 綁定得非常緊密;對象必須放在堆中,這樣它們的生命周期才能被嚴格控制。
在 C++ 中,由你來決定數據放在堆中還是棧中;這個選擇權賦給了開發者。因此,在 C++ 中你必須自己管理好內存。如果將數據放到棧中,內存是自動管理的,但如果你使用了堆,你必須自己管理內存——否則到處都會有內存洩漏的風險。
C++ 中有幾個用於堆中對象的內存管理的關鍵字,它們用於在堆上創建和摧毀對象。
創建對象:
Person *person = new Person();
當對象不再需要時,你可以這樣摧毀它:
delete person;
實際在 C++ 中,它們甚至能夠在標量類型上使用:
int *x = new int(); *x = 5; delete x;
你可以把它們等同於 O-C 中的對象初始化和釋放。在 C++ 中的 new Person() 就等於 O-C 中的 [[Person alloc]init]。
在 O-C 中沒有和 delete 相同的功能。相信你知道,在 O-C 中有 dealloc 的概念,當一個 O-C 對象的引用計數等於 0 時,運行時會自動將對象 dealloc。但是,C++ 不會為你進行引用計數。你有義務在使用完對象之後 delete 這個對象。
現在你對 C++ 的內存管理有點概念了;C++ 的內存管理要比 O-C 復雜得多。你真的需要考慮到底發生了什麼,以及跟蹤你創建的所有對象。
如你所見,在 C++ 中,對象既可以在棧中創建也可以在堆中創建。但二者有一個細微和重要的區別:每一種方式創建的對象,在訪問成員變量和成員函數的方法上有些許的不同。
當使用棧對象時,你需要使用點 . 操作符。而使用堆對象時,你需要使用箭頭 -> 操作符,例如:
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
這種區別很微妙,但非常重要。
在指針上使用了箭頭操作符,這和 O-C 中的 self 指針是一回事,在類成員函數訪問當前對象時會用到箭頭操作符。
下面是箭頭操作符的 C++ 例子:
Person::doSomething() { this->doSomethingElse(); }
在 C++ 中這會有些問題。在 O-C 中,如果你在空指針上調用一個方法,不會有任何問題:
myPerson = nil; [myPerson doSomething]; // does nothing
但是在 C++ 中,如果試圖在空指針上調用方法或者訪問實例變量,app 會崩潰:
myPerson = NULL; myPerson->doSomething(); // crash!
因此,在 C++ 中,你必須非常小心,千萬不要在 NULL 指針上進行任何操作。
當你將一個對象傳遞給函數時,你傳遞的是這個對象的拷貝,而不是對象自身。例如:
void changeValue(int x) { x = 5; } // … int x = 1; changeValue(x); // x still equals 1
這很簡單,不值一提。但當你用一個對象作為參數傳遞給這個函數時,會發生什麼呢?
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 對象時,生成了一個它的拷貝。
但某些情況下,你真的想將對象自身傳遞給函數。一種方法是修改函數,用一個指針指向這個對象,而不是使用對象自身。這需要在函數調用時書寫額外的代碼。
C++ 有一個新概念,允許你以“引用方式”傳遞變量。也就是說不會拷貝值,與此相反,上面的這種做法就是以“值拷貝方式”傳遞參數。
以引用方式傳遞非常簡單。在函數簽名中,在變量前增加一個地址操作符 &:
void changeValue(Foo &foo) { foo.x = 5; } // … Foo foo; foo.x = 1; changeValue(foo); // foo.x equals 5
對於非類的變量也是可以的:
void changeValue(int &x) { x = 5; } // … int x = 1; changeValue(x); // x equals 5
引用方式傳遞很有用,而且能顯著提升性能。當對象的復制代價非常高的時候,這種方式尤其有用。比如巨大的列表,要復制這樣的對象需要操作一個對對象的深層拷貝。
一門面向對象的語言沒有繼承是不完整的,C++ 也不例外。下面這兩個 O-C 類的例子中,一個繼承了另一個:
@interface Person : NSObject @end @interface Employee : Person @end
同樣的也可以用 C++ 來寫,方式非常接近:
class Person { }; class Employee : public Person { };
唯一的區別是 public 關鍵字。這裡,Emplyee 以 public 方式從 Person 繼承。這表示 Person 的所有公共成員仍然在 Employee 中保持 public。
如果將 public 換為 private,則 Person 的公共成員在 Emplyee 中會變為 private。關於這個問題,這篇繼承和訪問修飾符寫的非常好,推薦閱讀。
這是繼承中低難度的內容——現在來點高難度的。C++ 和 O-C 不同的地方是,C++ 允許多重繼承。多重繼承允許一個類繼承兩個以上的類。如果你從來沒有用過 O-C 以外的語言,這點確實難於理解。
這是 C++ 多重繼承的例子:
class Player { void play(); }; class Manager { void manage(); }; class PlayerManager : public Player, public Manager { };
在這個例子裡,有兩個基類,還有一個類則從這兩個基類繼承。也就是說 PlayerManager 能夠訪問兩個基類的成員變量和函數。簡單吧?但在 O-C 中這根本做不到,你是不是會覺得很不舒服呢?
好吧… 嚴格來說也不完全正確。
比較較真的讀者會說 O-C 中有類似的東西啊:協議。雖然這和多重繼承不是一回事,但兩者都解決了同一個問題:提供某種將功能接近的類連接在一起的機制。
協議的概念稍有不同。協議沒有實現,它只是簡單地描述了某個類必須實現的接口。
在 O-C 中,上述例子變成:
@protocol Player - (void)play; @end @protocol Manager - (void)manage; @end @interface Player : NSObject@end @interface Manager : NSObject @end @interface PlayerManager : NSObject @end
當然,這多少有些勉強,但也說明了一些問題。在 O-C 中你必須在 PlayerManager 類中實現 Play 和 Manager 協議,但在 C++ 中你只需要在基類中實現對應方法,然後在 PlayerManager 類中自動會繼承這些方法。
但實際上,多重繼承有時候也會帶來麻煩和問題。對於 C++ 開發者來說,多重繼承是一個危險的工具,除非必要,否則盡量不用。
為什麼?想像以下,如果兩個基類都實現了一個接受同樣參數的同名函數——即函數原型相同時會發生什麼?在這種情況下,你需要一種方法去將二者的歧義消除。例如,假設 Player 和 Manager 類都有一個 foo 函數。
你需要這樣來消除二者的歧義:
PlayerManager p; p.foo(); ///< Error! Which foo? p.Player::foo(); ///< Call foo from Player p.Manager::foo(); ///< Call foo from Manager
這樣當然可行,但它增加了歧義和問題的復雜性,最好避免它。這由 PlayerManager 的使用者決定。而使用協議的話,則 foo 函數會留到 PlayerManager 類中實現,這樣就只有一個實現了——不會有任何歧義。