你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> 寫給 iOS 程序員看的 C++(1)

寫給 iOS 程序員看的 C++(1)

編輯:IOS開發綜合

你是一個 Objective-C 方面的專家吧?你是否正在尋找下一個要學習的目標?如果是,那麼這篇文章就是專門為你准備的了,它教你如何在 iOS 開發中使用 C++。

就像我後面將會提到的一樣, Objective-C 可以和 C 和 C++ 代碼無縫集成。因此,對於 iOS 開發者來說,學習一些 C++ 是有好處的,具體理由如下:

有時候,你需要在你的 app 中調用 C++ 寫的庫。 可以將 app 中的一部分代碼用 C++ 來寫,這樣便於跨平台。 擁有使用其它語言的背景,有助於從本質上理解編程。

這篇文章是為具備 Objective-C 語言基礎的 iOS 程序員而寫的。本文假設你已經了解如何編寫 Objective-C 代碼並熟悉基本的 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++ 的語言特性!

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++ 的 new 和 delete

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 類中實現,這樣就只有一個實現了——不會有任何歧義。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved