iOS 開發者應該掌握些 C++ 知識
作為一名 iOS 開發者,最近由於開發音訊播放器需要一些 C++ 的知識,便開始學習 C++ 知識。這裡分享給有需要的同學,如果寫的有不妥的地方,還望指出。本文主要圍繞 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)
如果定義了私有函式,其他的類是不能訪問這個私有函式的,如果是友元函式,則外部可以訪問該私有函式。
注意,友元函式只是一個普通函式,並不是該類的類成員函式,它可以在任何地方呼叫。