你好,歡迎來到IOS教程網

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

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

編輯:IOS開發綜合

歡迎回到《寫給 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++ 中,你可以這樣做而不用像上面一樣寫兩個方法:

template 
void 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; }
};

看起來我們可以用模板解決這個問題——沒錯,就是模板!和可以在函數中使用模板一樣,我們可以在整個類中使用。語法是一樣的。這兩個類可以替換成:

template 
class 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; }
};

但是,模板類在使用上有些變化。模板函數的代碼不需要動,因為參數類型由編譯器推斷。但你得告訴編譯器,你准備讓模板類使用哪個類型。

幸好這也非常簡單。模板類的使用是這也的:

Triplet intTriplet(1, 2, 3);
Triplet floatTriplet(3.141, 2.901, 10.5);
Triplet personTriplet(Person(“Matt”), Person(“Ray”), Person(“Bob”));

強吧?

別急!這還沒完!

模板函數或類並不是只能使用一種未知類型。Triplet 類可以進一步增強到支持 3 種不同類型,而不是原來的 3 個值都是同一個類型。

要實現這個,只需要在 template 定義中指定更多的類型就行:

template 
class 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 種不同的類型構成,每一種都在各自對應的地方使用。

這個模板類的使用非常簡單:

Triplet mixedTriplet(1, 3.141, Person(“Matt”));

這就是模板!現在我們來看一下有些庫使用這個特性有多頻繁——STL 標准模板庫。

標准模板庫 STL

每個典型的編程語言都會有一個標准庫,用於包含常用的數據結構、算法和功能。在 O-C 中是 Fundation 庫,包括 NSArray、NSDictionary 以及其它大家見過或沒見過的成員。在 C++ 中,則是標准模板庫,或者 STL,包含了標准的代碼。

被叫做標准模板庫的原因是它大量使用了模板。有意思吧? :]

在 STL 中有許多有用的東西;細述起來就太多了,所以只能提幾個最重要的地方。

1. 容器

數組、字典和集合:全都是其他對象的容器。在 O-C 中,Foundation 庫就幫我們實現了最常見的容器。在 C++ 中,由 STL 實現這些容器。事實上,STL 中包含的容器類要比 Foundation 中的多。

在 STL 中,有兩個不同的 NSArray 的兄弟。第一個是 vector 第二個是 list。二者都表示一系列對象,但各有各的優缺點。C++ 再次將選擇權交給了你。

首先看 vector:

#include 

std::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 非常像:

#include 

std::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_ptr p1(new int(1));
std::shared_ptr p2 = p1;
std::shared_ptr p3 = p1;

當執行完這 3 句代碼,3 個共享指針的引用計數都變成了 3。每當一個共享指針被銷毀或者 reset 之後,引用計數就減一。一旦最後一個引用它的共享指針被銷毀,背後的指針就會被刪除。

由於共享指針自身屬於棧對象,當它們的作用域結束它們會被刪除。因此它們的行為就等同於 O-C 中 ARC 下面的對象指針。

這是一個創建共享指針和銷毀共享指針的例子:

std::shared_ptr p1(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_ptr p1(new Person(“Matt Galloway”));

Person *underlyingPointer = *p1; ///< Grab the underlying pointer

p1->doADance(); ///< Make Matt dance

共享指針是一種很好的方法,它讓 C++ 實現了引用計數。當然它們會帶來一些代價,但與它所帶來的好處相比這種代價是值得的。

Objective-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_ptr cppClass;
@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 有許多精彩資源。

如果你有任何問題或建議,請留言。

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