首页 > 代码库 > 写给 iOS 程序员看的 C++(2)
写给 iOS 程序员看的 C++(2)
原文: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++(2)