写给 iOS 程序员看的 C++

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了写给 iOS 程序员看的 C++相关的知识,希望对你有一定的参考价值。

原文:Introduction to C++ for iOS Developers: Part 2
作者:Matt Galloway
译者:kmyhy

欢迎回到《写给 ios 程序员看的 C++ 教程系列》第二部分!

在第一部分,你学习了类和内存管理。

在第二部分,你将进一步深入类的学习,以及其他更有意思的特性。你会学习什么是模板以及标准模板库。

最后,你将大致了解 Objectiv-C++——一种将 C++ 混入 Ojective-C 的技术。

准备好了吗?让我们开始吧!

多态

这里的多态不是那只会变化的鹦鹉,尽管听起来很像!

技术分享

好了,我承认这个玩笑一点都不好笑!:]

简单说,多态是在子类中覆盖某个函数。在 O-C 中,你无数次用过多态,例如继承 UIViewController 并重写 viewDidLoad 方法。

C++ 中的多态比 O-C 强得太多了。所以在我介绍这个强大特性的时候,你最好不要开小差。

这是在一个类中覆盖一个成员函数的例子:

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 <typename T>
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 <typename T>
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<int> intTriplet(1, 2, 3);
Triplet<float> floatTriplet(3.141, 2.901, 10.5);
Triplet<Person> personTriplet(Person(“Matt”), Person(“Ray”), Person(“Bob”));

强吧?

别急!这还没完!

模板函数或类并不是只能使用一种未知类型。Triplet 类可以进一步增强到支持 3 种不同类型,而不是原来的 3 个值都是同一个类型。

要实现这个,只需要在 template 定义中指定更多的类型就行:

template <typename TA, typename TB, typename TC>
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<int, float, Person> 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 <vector>

std::vector<int> 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 <list>

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

当执行完这 3 句代码,3 个共享指针的引用计数都变成了 3。每当一个共享指针被销毁或者 reset 之后,引用计数就减一。一旦最后一个引用它的共享指针被销毁,背后的指针就会被删除。

由于共享指针自身属于栈对象,当它们的作用域结束它们会被删除。因此它们的行为就等同于 O-C 中 ARC 下面的对象指针。

这是一个创建共享指针和销毁共享指针的例子:

std::shared_ptr<int> p1(new int(1)); ///< Use count = 1

if (doSomething) {
    std::shared_ptr<int> 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<Person> 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> cppClass;
@end

@implementation ObjcClass
@end

// Using the two classes above
std::shared_ptr<CppClass> 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 <Foundation/Foundation.h>
#include <list>

@interface MyClass : NSObject

@property (nonatomic, assign) std::list<int> 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 有许多精彩资源。

如果你有任何问题或建议,请留言。



以上是关于写给 iOS 程序员看的 C++的主要内容,如果未能解决你的问题,请参考以下文章

写给大忙人看的操作系统

写给小白看的递归(硬核)

写给自己看的单链表:归并排序

后端程序员必备!写给大忙人看的分布式事务基础!

小白写给菜鸟看的MQ简介

写给新入大学的程序员:你不得不看的宝贵经验!