本文翻译自Introduction to C++ for iOS Developers: Part 1
为了促使自己更认真的看文章,看完之后随手翻译了一把,里面有翻译不准的欢迎指正,看完之后,墙裂推荐,看一下原文!
如果你已经掌握了Objective-C了,正在寻找下一个很酷的东西去学习,尝试本篇文章吧,本篇文章为iOS开发者介绍C++。
正如我稍后解释,Objective-C、C和C ++代码能无缝协调工作。 因此,对于iOS开发人员来说,学习理解C ++有以下几个理由:
- 有时你可能想要在APP中调用C ++编写的library。
- 你可能希望在C ++中编写应用程序的一部分代码,从而更容易跨平台移植。
- 掌握其他语言的良好基础,可以帮助你更好地了解编程。
本文是针对已经了解Objective-C的iOS开发人员编写的。 假设你已经了解如何编写Objective-C代码,并熟悉C语言的基本概念,如类型,指针和函数。
准备好了么?接下来我们开始学习吧!
1. Getting Started: A Brief History of Languages
C++和Objective-C同源:他们都来源于古老的C。这意味着它们都是C语言的“超集”(supersets)。 因此,在两种语言中,你可以使用C语言语法以及他们各自的特性。
如果你熟悉Objective-C,你可能对C ++代码有一个粗略的了解。 例如,诸如int,float和char之类标量类型的存在,并且在两种语言中的行为和表现方式完全相同。
Objective-C和C ++都将面向对象的功能添加到C。如果你还不熟悉“面向对象”这个术语,你需要了解的是,“面向对象”意味着数据通过对象表示,而对象又是类的实例。 事实上,C ++最初被称为“C with Classes”,它显示了使C++面向对象的基本愿望。
那么,你会问:“他们的差异在哪呢?”。 那么主要的区别就在面向对象特征的方式上(the approach to the object-oriented features)。 在C++中,很多操作在编译时发生,而在Objective-C中,运行时会发生更多的事情。 你可能已经使用Objective-C运行时来执行诸如方法转换(method swizzling)。 在C ++中,这根本就不可能。
C ++也没有Objective-C有过多的内省和反思方法( introspection and reflection methods)。 没有办法像Objective-C那样在实例中简单地调用“类”方法来获取C++对象的类。 类似地,在C ++中没有等价于isMemberOfClass
或isKindOfClass
。
这是对C ++的粗略的介绍,显示了它与Obejctive-C之间的历史和主要区别。 下面开始继续介绍C++类。 #2.C++ Classes 在任何面向对象语言中需要知道的第一件事是,如何定义一个类。
在Objective-C中,创建一个头文件和一个实现文件来定义一个类。 完全相同的事情发生在C ++中; 语法也很熟悉。 这是一个Objective-C类的例子:
MyClass.h
#import <Foundation/Foundation.h>
@interface MyClass : NSObject
@end
// MyClass.m
#import “MyClass.h”
@implementation MyClass
@end
对于iOS作为经验丰富的iOS开发人员来说,上述代码你是清楚的。 接下来看看C ++中的类:
// MyClass.h
class MyClass {
};
// MyClass.cpp
#include “MyClass.h”
/* Nothing else in here */
这里有一些明显的区别。 首先是C++中的实现文件没有任何内容,那是因为你没有在类上声明任何方法。 同样,空类也不需要像Objective-C那样的@implemenation
/ @end
块。
在Objective-C中,每个类几乎总是从NSObject
继承。 你可以创建自己的根类,这意味着你的类不会有superclass,但是,除非你使用运行时(runtime),否则你不可能执行此操作。 这与C ++不同,在C++里,创建一个没有superclass的类是很常见的,如上例所示。
另一个微小的区别是#include
vs. #import
。 Objective-C将#import预处理程序指令添加到C。在C ++中没有等价物,因此使用标准C风格的#include。** Objective-C的#import确保文件只包含一次,但在C++中,你必须自己执行此检查。** #3.Class Member Variables and Functions 当然,一个类还有不仅仅只是声明它。 就像Objective-C一样,在C ++中,你可以向实例类添加实例变量( instance variables)和方法( methods)。你可能会听到他们在C ++中的不同定义; 它们通常被称为成员变量(member variables)和成员函数(member functions)。
注意:术语“方法(methods)”并不真正用于C ++。 区别仅在Objective-C中,方法通过消息派发(message dispatch)调用。 另一方面,函数(function)是通过静态C风格的函数调用来调用的。 本文稍后将介绍静态与动态的更多内容。
那么,你如何声明成员变量和成员函数呢? 那么这里有一个例子:
class MyClass {
int x;
int y;
float z;
void foo();
void bar();
};
这里有三个成员变量和两个成员函数。
但是,在C++实际开发中你还可以进行更多操作,因为你可以限制C ++中的成员变量和成员函数的范围(scope),并将其声明为公开或私有可访问的。 这可以用来限制哪些代码可以访问每个变量或函数,示例代码如下:
class MyClass {
public:
int x;
int y;
void foo();
private:
float z;
void bar();
}
这里,x,y和foo是可公开访问的。这意味着它们可以在MyClass类之外使用。 但是,z和bar是私有的。 这意味着它们只能在MyClass内使用。 成员变量默认为私有。
虽然Objective-C中存在这种变量区分,但很少使用它。 另一方面,Objective-C不可能限制方法的范围。 即使你只在一个类的实现中声明一个方法,并且不将它暴露在接口中,你在技术上仍然可以从外部调用该方法。
Objective-C中的方法只有公开或仅仅按照惯例的私有(没有真正的私有)。 这就是为什么很多开发人员选择使用诸如“p_”之类的私有方法来表示区别。 这与C ++不同,如果您尝试从类外部调用私有方法,编译器将抛出错误。
那么你如何使用一个类? 和Objective-C相似。 你创建一个这样的实例:
MyClass m;
m.x = 10;
m.y = 20;
m.foo();
就那么简单! 这将创建一个MyClass的实例,将x和y分别设置为10和20,然后调用foo。 #4.Implementing Class Member Functions 你已经看到了如何定义一个类接口,但是如何实现函数呢?事实证明,这很简单。 有几种方法可以做到这一点。 的第一种实现方法是通过在类的实现文件 ---- .cpp文件中定义它。 你可以这样做:
//MyClass.h
classMyClass{
intx;
inty;
voidfoo();
};
//MyClass.cpp
#include“MyClass.h”
MyClass::foo(){
//Dosomething
}
这是第一种方式 它与Objective-C中如何做到非常相似。 注意,使用MyClass ::
; 这是你如何表示foo() 函数被实现为MyClass类的一部分。
实现方法的第二种方法是在Objective-C中不能做的事情。 在C ++中,你可以直接在头文件中实现一个方法,如下所示:
//MyClass.h
classMyClass{
intx;
inty;
voidfoo(){
//Dosomething
}
};
如果你仅使用过Objective-C,这可能对你来说看起来很奇怪。 这是相当奇怪的,但它也可以是非常有用的。 当以这种方式声明函数时,编译器可以执行称为“内联(inlining)”的优化。** 这意味着当调用此函数时,而不是跳转到新的代码块,整个功能代码在调用处内联编译。**
虽然内联可以使代码更快,但它也会增加编译代码的大小,因为如果函数多次被调用,代码将在整个二进制文件中复制。 如果函数相当大或被调用了很多次,那么这可能会对二进制文件的大小产生重大影响。 这可能导致性能下降,因为较少的代码可以适应缓存,这意味着可能存在更多的高速缓存未命中。
我的目标是说明C ++允许很多的灵活性。 作为开发人员,你需要了解权衡并作出决定。 当然,唯一的方法是真正知道哪个选择适合你,就是调整你的代码! #5.Namespaces 上面看到的示例介绍了一些你以前没有遇到的新语法 , 双冒号 ---- ::
。 这是你如何在C ++中引用范围的; 以上使用它来告诉编译器应该在哪里查找foo函数。
另一次你会看到双冒号是使用命名空间。 命名空间是一种分离代码的方式,因此命名冲突不太可能发生。
例如,你可以在自己的代码中实现一个名为Person的类,但第三方库也可以实现一个名为Person的类。 因此,在编写C ++时,通常将所有代码放入命名空间中,以避免这些类型的命名冲突。
这很容易做到这一点 你只需用以下命名空间声明来包装所有内容:
namespaceMyNamespace{
classPerson{…};
}
namespaceLibraryNamespace{
classPerson{…};
}
现在,当使用Person类的任一实现时,可以使用双冒号来消除歧义,像这样:
MyNamespace::PersonpOne;
LibraryNamespace::PersonpTwo;
很简单,不是吗? 在Objective-C中没有等价的命名空间,只能在类的前面加前缀。
注意:Objective-C中有几个命名空间的建议。 一个这样的建议可以在这里找到。 我不知道我们是否会在Objective-C中看到他们,但我确实希望如此!
#6.Memory Management Oh no…不是那个可怕的短语! 内存管理是任何语言中最重要的事情之一。 Java基本上让垃圾收集器做它的工作。 Objective-C要求你了解引用计数( reference counting)和ARC所扮演的角色。 在C ++中...好吧,C ++又是一个不同的野兽。
首先,要了解C ++中的内存管理,您真的需要了解栈(stack)和堆(heap)。 即使你认为你知道这一点,我建议你继续阅读; 你可能会学习以下两两点:
- stack是供正在运行的应用程序是用的内存块。 它是一个固定的大小,被应用程序的代码用于存储数据。 stack以push/pull为基础; 当给定的函数运行时,它将数据push到堆栈上,当函数完成时,它必须pull相同数量的数据。 因此,随着时间的推移,堆栈使用率将不会增长。
- heap也是运行应用程序可用的内存块。 它的大小不固定,并随着应用程序的运行而增长。 应用程存储将“在函数范围之外使用的数据”存储在heap中。 而且,大型数据通常会存储在堆中,因为将其存储在堆栈中可能会溢出堆栈 。请记住,堆栈是固定的大小。
这是堆栈和堆理论的一个很简单的概述; 这里有一些C代码显示了实践中的栈和堆:
intstackInt=5;
int*heapInt=malloc(sizeof(int));
*heapInt=5;
free(heapInt);
这里,stackInt正在使用栈空间; 函数返回后,用于存储值“5”的内存将自动释放。
然而,heapInt正在使用堆空间。 对malloc的调用在堆上分配足够的空间来存储int变量。 但是,由于堆必须由开发人员进行管理,因此,在完成数据后,需要进行释放这一片堆空间,以确保不会泄漏内存。
在Objective-C中,只能在堆上创建对象; 如果您尝试在栈上创建一个对象,则会收到编译器错误。
NSStringstackString;
//Untitled32.m:5:18:error:interfacetypecannotbestaticallyallocated
//NSStringstackString;
//^
//*
//1errorgenerated
这就是为什么你会看到,全部Objective-C代码创建的对象,都带有星号; 所有对象都在堆上创建,并且返回指向这些对象的指针。 这主要归结于Objective-C处理内存管理的方式。 对象需要在堆上,通过引用计数,使其生命周期得到严格的控制。
在C ++中,您可以决定将数据存储在堆栈或堆上; 选择取决于开发人员。 但是,在C ++中,必须自己管理内存。 存放在栈中的数据会自动进行处理,但是当你开始使用堆时,必须自己处理内存管理,处理不好有内存泄露的风险。 #7.C++ new and delete
C ++引入了几个关键字来帮助堆对象的内存管理; 它们用于创建和销毁堆上的对象。 创建对象是这样完成的:
Person *person = new Person();
当你完成对象后,你可以像这样释放它:
delete person;
实际上,这甚至适用于C ++中的标量类型:
int *x = new int();
*x = 5;
delete x;
您可以将这些操作看作与Objective-C中对象初始化和销毁相同的操作。 在C ++中使用新的Person()
初始化,相当于Objective-C中的[[Person alloc] init]
。
不过Objective-C中没有和delete
等价的关键字。 我确定你知道,一个Objective-C对象的释放由运行时处理,当它的引用计数降到零时。 记住,C++不会为你引用计数。 完成后,您将负责删除该对象。 #8.Accessing Members of Stack and Heap Objects 你已经看到可以在堆或C ++中的栈上创建对象。 但是,在使用每种类型时,有一个微妙但重要的区别:访问成员变量和成员函数的方式略有不同。
使用栈对象(stack objects)时,需要使用点运算符(.
), 使用堆对象( heap objects),你需要使用箭头运算符( ->
)解引用,如下所示:
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
虽然是微小差异,但还是应该引起你的注意!
你还将看到,与this
指针一起使用的箭头运算符; 它与Objective-C中的self
指针是一样的,并且在类成员函数中用于访问当前对象。
以下C ++示例显示了arrow运算符的用法:
Person::doSomething() {
this->doSomethingElse();
}
在Objective-C中,如果你在一个nil指针上调用一个方法,你的应用程序仍然运行正常:
myPerson = nil;
[myPerson doSomething]; // does nothing
但是,在C ++中,如果您尝试调用方法或访问NULL指针上的实例变量,则应用程序将崩溃:
myPerson = NULL;
myPerson->doSomething(); // crash!
因此,你在使用C++中必须非常小心,以确保不要尝试对NULL指针进行操作。
#9.References 当你将一个obejct传递给一个函数时,你传递一个obejct的副本,而不是obejct本身。 例如,考虑下面的C ++代码块:
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情况没有什么不同。
有时候,你确实想传递实际的对象。 一种方法是更改函数以获取对象的指针,而不是对象本身。 但是,当您调用该函数时,它会添加额外的代码。
C++添加了一个新的概念,允许你“通过引用”(by reference)传递变量。 这意味着没有复制; 这与上述“通过值”(by value)传递的例子形成对比。
通过引用改变你的调用是很简单的; 你只需使用地址运算符,即在函数签名中的变量前添加一个&符号(&),如下所示:
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
通过引用传参非常有用,可以显着提高性能,有时候,获取对象的副本成本非常高。
#10.Inheritance 继承是面向对象语言的特性, 考如下两个Objective-C类,其中一个继承自另一个:
@interface Person : NSObject
@end
@interface Employee : Person
@end
C++中对继承也有相似的表示:
class Person {
};
class Employee : public Person {
};
C ++的唯一区别是添加public
关键字。 在这里,Employee
从Person
继承,这意味着Person
中的public属性的成员变量在Employee
中保持public。
如果你用private替换public,那么Person的公共成员将在Employee中变为私有的。 有关此主题的更多信息,建议阅读inheritance and access specifiers here.。
这是继承的基础部分,C ++与Objective-C不同之处在于它允许多重继承(multiple inheritance)。 这允许类从两个或更多个基类继承。 这可能对你来说似乎是陌生的,特别是如果你没有使用Objective-C以外的语言。这儿有一个多继承的例子:
class Player {
void play();
};
class Manager {
void manage();
};
class PlayerManager : public Player, public Manager {
};
在这个例子中,有两个基类和一个继承自它们的类。 这意味着PlayerManager
可以访问每个基类的所有成员变量和函数。呃,我确定你很痛苦地意识到,Objective-C无法做到这一点。
细心的读者会注意到Objective-C中有类似的东西:protocol
。 尽管与多重继承不完全相同,但是这两种技术都旨在解决相同的问题:提供一种将几个类链接在一起的机制。
protocol
略有不同,因为它没有实现; 相反,它只是描述类的接口应该遵循的规范。
在Objective-C中,上面的例子可以写成如下:
@protocol Player
- (void)play;
@end
@protocol Manager
- (void)manage;
@end
@interface Player : NSObject <Player>
@end
@interface Manager : NSObject <Manager>
@end
@interface PlayerManager : NSObject <Player, Manager>
@end
当然,这是非常轻微的设计,但你得到的照片。 在Objective-C中,您必须在PlayerManager类中实现播放和管理,而在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的用户。 使用协议留给PlayerManager类实现foo,所以只有一个实现 ,不会产生歧义。
#11.Where to Go From Here? 在本系列的第一部分,您学习了C ++的简史,如何声明一个类以及内存管理如何在C ++中运行。 当然还有比这更多的语言!
在本系列的第二部分,您将在了解标准库和Objective-C++之前,先了解更多高级类的功能和模板。
在此期间,如果您对C ++的冒险有任何问题或意见,请加入下面的讨论!