当前位置: 移动技术网 > 移动技术>移动开发>IOS > 写给 iOS 程序员看的 C++(1)

写给 iOS 程序员看的 C++(1)

2018年02月14日  | 移动技术网移动技术  | 我要评论

你是一个 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 类中实现,这样就只有一个实现了——不会有任何歧义。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网