欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2022-04-27 10:21:56
你是一个 Objective-C 方面的专家吧?你是否正在寻找下一个要学习的目标?如果是,那么这篇文章就是专门为你准备的了,它教你如何在 iOS 开发中使用 C++。 就像我后面...

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