文章目录
作为一名 iOS 开发者,最近由于开发音频播放器需要一些 C++ 的知识,便开始学习 C++ 知识。这里分享给有需要的同学,如果写的有不妥的地方,还望指出。后续会利用学到的 C++ 知识对 FreeStreamer 这个音频播放器写一篇分析其实现原理的文章。本文主要围绕 FreeStreamer 这个库用到的 C++ 语法来讲解。
iOS 开发当中,偶尔会使用到 C++ 的知识,然而大多数同学每遇到这个问题时,选择逃避。如果从头开始学习 C++ 语法,会花费很多时间,如同笔者一样,花费很多时间了解基础的知识。
Objective-C 和 C++ 都是基于 C 语言而设计的,它们都具有面向对象的能力。在学习 C++ 知识之前,我们先了解下 Objective-C++,它可以把 C++ 和 Objective-C 结合起来使用,如果把两门语言的优点结合起来使用是不是会更好?腾讯开源的数据库 WCDB 是一个很好的例子,它不仅有 C++ 而且还有 Objective-C++。
本文主要内容:
- 类(Class)的定义与使用
- 命名空间
- 内存管理
- 继承
- 构造函数和析构函数
- virtual 关键字
- 静态成员与静态函数
- 运算符重载
- 打印日志
- 实例
- Array,map 使用
- 模版(Templates)
- 异常(Exceptions)
- 友元函数(Friend)
类(Class)
类在面向对象中非常重要,在 Objective-C 中,所有的 Class 必需继承自 NSObject 类,当创建一个类的时候,它会包含一个 .h 和 一个 .m 文件,我们以定义一个 Person 类为例:
/* Person.h */
#import <Foundation/Foundation.h>
@interface Person : NSObject
@end
/* Person.m */
#import "Person.h"
@implementation Person
@end
而 C++ 中,创建一个类也会有一个头文件 Person.hpp 和实现文件 Person.cpp 。
/* Person.hpp */
#include <stdio.h>
class Person {
};
/* Person.cpp */
#include "Person.hpp"
从上面的例子我们可以看到,OC 和 C++ 定义类主要有以下不同:
-
在实现文件中,C++ 中没有 @implementation Person @end;
-
OC 中每个类需要继承自 NSObject;
-
C++ 中使用 #include 导入其它文件中的代码,而 OC 使用 #import 导入其它文件代码,使用 #import
保证每个文件中不会重复导入,而使用 #include 需要开发者保证不要重复导入。class Person {
public:
int age;
bool isChild();
private:
float height;
bool isNomalHeight();
};
Person 类中定义了一个公有的成员变量 age 和 一个成员函数 isChild。一个私有的成员变量 height 和一个成员函数 isNomalHeight 。在 C++ 中可以定义某个变量或函数的作用范围,如果使用时超出作用范围编译器将会报错。而在 OC 中既使在 .h 文件中没有定义某个函数,我们任然可以调用,所以在 OC 中经常会出现以 _ 或某个前缀开头的私有函数。
Person 类的实现:
bool Person::isChild() {
return age >= 18;
}
bool Person::isNomalHeight(){
return height >= 170;
}
其中 :: 表示 isChild 函数属于 Person 类,它告诉编译器从哪里找到函数 isChild。
定义了一个 Person 类,使用的时候和 OC 会有一些不同。
OC中只能创建堆上的对象
Person *aPerson = [[Person alloc] init];
aPerson.age = 18;
在栈上创建一个 Person
Person aPerson;
aPerson.age = 18;
NSLog(@"age == %@", @(aPerson.age));
2018-02-19 12:12:20.252108+0800 C++Demo[2480:84138] age == 18
在堆上创建一个 Person
Person *aPerson = new Person();
aPerson->age = 18;
NSLog(@"aPerson age == %@", @(aPerson->age));
delete aPerson;
2018-02-19 12:16:25.432998+0800 C++Demo[2525:88221] aPerson age == 18
命名空间
在 C++ 中有命名空间的概念,可以帮助开发者在开发新的软件组件时不会与已有的软件组件产生命名冲突,而在 OC 中却没有命名空间的概念,我们常以前缀来与第三方库区分。它的定义为:
namespace 命名空间的名字 { }
我们将上面定义的类加上命名空间:
namespace Lefex {
class Person {
public:
int age;
bool isChild();
private:
float height;
bool isNomalHeight();
};
}
namespace Lefex {
bool Person::isChild(){
return age >= 18;
}
bool Person::isNomalHeight(){
return height >= 170;
}
}
那么使用时必须加上命名空间:
Lefex::Person aPerson;
aPerson.age = 18;
NSLog(@"age == %@", @(aPerson.age));
内存管理
在 OC 中使用引用计数来管理内存,当引用计数为 0 时,内存空间将被释放。而 C++ 中需要开发者自己管理内存。理解 C++ 的内存管理,我们有必要先了解一下栈内存和堆内存。
-
栈内存:它分配的大小是固定的,当一个函数执行时,将为某些变量分配存储空间,当函数执行完成后将释放其对应的存储空间。
-
堆内存:它会随着应用的运行,使用的空间逐步增加,分配的存储空间需要开发者自己释放。
void Person::ageMemory(){
int stackAge = 20;int *heapAge = (int *)malloc(sizeof(int));
*heapAge = 20;
free(heapAge);
}
stackAge 为栈内存,不需要开发者自己释放内存,当 ageMemory 函数执行完成后 stackAge 将被释放。heapAge 为堆空间,当函数 ageMemory 执行完成后,它不会释放,需要开发者手动释放。
下面这个例子是创建了一个 Person 对象,它使用的是堆内存,需要使用 delete 释放其内存空间。这里需要注意访问堆对象时使用 -> 访问它的成员或者方法,而访问栈对象时使用 . 访问它的成员或者方法。
在 OC 中,当一个对象为 nil 时调用一个方法时 [nil doSomeThing] , 程序并不会执行 doSomeThing 方法,而在 C++ 中,NULL-> doSomeThing ,程序将 crash。
Lefex::Person *aPerson = new Lefex::Person();
aPerson->age = 18;
NSLog(@"age == %@", @(aPerson->age));
NSLog(@"is a child: %@", @(aPerson->isChild()));
delete aPerson;
继承
C++ 中支持多继承,也就是说一个类可以继承多个类。而在 OC 中只能使用单继承。与 OC 中不同的一点就是增加了修饰符(比如:public),这样用来限制继承的属性和方法的范围。
// 男人
class Man: public Lefex::Person {
};
// 女人
class Woman: public Lefex::Person {
};
// 人妖
class Freak: public Man, public Woman {
};
构造函数和析构函数
构造函数通常在 OC 中使用的是 init,而在 C++ 中默认的构造函数是于类名相同的函数。比如类 Person 的默认构造函数是 Person(),自定义一个构造函数 Person(int age, int height) 。在 OC 中析构函数如 dealloc,而在 C++ 中是函数 ~Person(),当一个类被释放后,析构函数会自动执行。
// 默认构造函数
Person::Person(){
printf("Init person");
}
// 初始化列表构造函数
Person::Person()
:age(0),
height(0),
m_delegate(0){
printf("Init person\n");
}
// 自定义构造函数
Person::Person(int age, int height){
this->age = age;
this->height = height;
}
// 析构函数
Person::~Person(){
printf("Dealloc person");
}
虚析构函数:为了保证析构函数可以正常的被执行,引入了虚析构函数,一般基类中的析构函数都是虚析构函数。定义方式如下。
virtual ~Person();
Person::~Person(){
printf("person dealloc called \n");
}
virtual 关键字
虚函数是一种非静态的成员函数,定义格式:
virtual <类型说明符> <函数名> { 函数体 }
纯虚函数:是一种特殊的虚函数,这是一种没有具体实现的虚函数,定义格式:
virtual <类型说明符> <函数名> (<参数表>)=0;
抽象类:它是一个不能有实例的类,这样的类唯一用途在于被继承。一个抽象类中至少具有一个虚函数,它主要的作用来组织一个继承的层次结构,并由它提供一个公共的根。
有了抽象类和纯虚函数,就可以实现类似于 OC 中的 delegate。
// 相当于 OC 中的代理
class Person_delegate {
public:
// =0 表示无实现
virtual void ageDidChange()=0;
};
// 继承了 Person_delegate,Woman 类真正实现了 Person_delegate 的纯虚函数
class Woman: public Lefex::Person, public Lefex::Person_delegate {
public:
Woman();
void ageDidChange();
};
综上可以看到它于 OC 中实现的思路一致。
静态成员与静态函数
静态成员使用 static 修饰,只对其进行一次赋值,将一直保留这个值,直到下次被修改。
在 Person 类中定义一个静态变量 weight。
static int weight;
使用时直接:Person::weight; 即可访问静态变量。
静态成员函数与静态成员的定义一致。
static float currentWeight();
需要注意的是,在静态成员函数中,不可以使用非静态成员。
调用:
Person::currentWeight();
也可以:
aPerson->currentWeight();
运算符重载
有时候利用系统的运算符作自定义对象之间的比较的时候,不得不对运算符进行重载。
定义:
类型 operator op(参数列表) {}
“类型”为函数的返回类型,函数名是 “operator op”,由关键字 operator 和被重载的运算符op组成。“参数列表”列出该运算符所需要的操作数。
例子:
// Person 类中定义 Person 是否相等。
bool operator==(Person &){
if (this->age == person.age) {
return true;
}
return false;
};
+ (void)operatorOverload
{
Lefex::Person aPerson;
aPerson.age = 18;
Lefex::Person aPerson2;
aPerson2.age = 18;
if (aPerson == aPerson2) {
NSLog(@"aPerson is equal to aPerson2");
} else {
NSLog(@"aPerson is not equal to aPerson2");
}
}
打印日志
C++ 中打印日志需要导入库 #include 。
-
\n 和 endl 效果一样都是换行;
-
打印多个使用 << 拼接;
void Woman::consoleLog(){
std::cout<< “Hello Lefe_x\n”;
std::cout<< "Hello " << “Lefe_x\n”;
int age = 20;
std::cout<< "Lefe_x age is " << age << std::endl;
}
打印结果:
Hello Lefe_x
Hello Lefe_x
Lefe_x age is 20
实例
下面这段代码摘自 FreeStreamer 中的 audio_queue.h,有部分重复的知识点有删减。建议读者仔细看看下面的代码,看看有没有看不懂的地方。
namespace astreamer {
class Audio_Queue_Delegate;
class Audio_Queue {
public:
Audio_Queue_Delegate *m_delegate;
Audio_Queue();
virtual ~Audio_Queue();
void start();
private:
void cleanup();
static void audioQueueOutputCallback(void *inClientData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer);
};
class Audio_Queue_Delegate {
public:
virtual void audioQueueStateChanged(Audio_Queue::State state) = 0;
virtual void audioQueueFinishedPlayingPacket() = 0;
};
} // namespace astreamer
Array,map 使用
- Array (数组)
数组可以看作是一个存放相同类型变量的一个集合。在 C++ 数组大小是不可变的,相当于 iOS 中的 NSArray,没有 NSMutableArray 这种可变数组。
使用数组,需要注意:
-
指定数组大小
-
数组大小不可变
-
数组中元素类型相同
int a[5] = {1, 4, 5, 6, 9};
void Collection::printArray(){
// 获取数组的长度
int length = sizeof(a) / sizeof(a[0]);
for (int i = 0; i < length; i++) {
if (i == 0) {
// 修改数组中的值
a[i] = 100;
}
cout << a[i] << endl;
}// 未指定数组长度,自动推断 string b[] = {"Hello ", "Lefe_x"}; cout << b[0] << b[1] << endl;
}
- map
map 相当于 iOS 中的 NSDictionary,使用前需要导入 #include
模版(Templates)
模版相当于 Swift 中的范型。
函数模版:
通常如果定一个一个 sum 函数,而参数不同的时候,需要定义多个,比如:
int sum(int a, int b);
float sum(float a, float b);
而使用模版后,写一个函数就行,编译器会自动匹配类型,比如:
template <class T>
T sum(T a, T b){
return a+b;
};
类模版:
定义类模版后,允许成员使用定义的模版,比如:
template <class T>
class Dog {
public:
T age;
};
定义一个 Dog 类,并定义了一个模版 T,那么在 Dog 类中即可定义 T 类型的成员。
使用:
Dog<int> dog;
dog.age = 10;
NSLog(@"dog age: %@", @(dog.age));
异常(Exceptions)
捕获异常使用关键字:try, catch 和 throw, 其中 throw 用来抛出异常,而 catch 用来捕获异常。需要注意的是不是所有的异常都是可以捕获的,比如内存泄漏等问题。捕获只能捕获到 throw 抛出的异常。
void Person::updateAge(int age){
if (age < 0) {
// 抛出异常
throw "Age is invalid";
return;
}
this->age = age;
if (this->m_delegate) {
this->m_delegate->ageDidChange();
}
}
// 使用的时候后需要注意,如果不捕获抛出的异常,程序将挂掉,如果遇到异常
// // libc++abi.dylib: terminating with uncaught exception of type char const*
try {
aPerson.updateAge(-1);
} catch (char const* error) {
NSLog(@"%s", error);
}
友元函数(Friend Functions)
如果定义了私有函数,其他的类是不能访问这个私有函数的,如果是友元函数,则外部可以访问该私有函数。
注意,友元函数只是一个普通函数,并不是该类的类成员函数,它可以在任何地方调用。