写给 iOS 程序员看的 C++(1)
你是一个 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 类中实现,这样就只有一个实现了——不会有任何歧义。