本文算做是 《Android 音視頻開發打怪升級》系列文章的「番外」篇,本來打算將本文的內容寫在 《Android FFmpeg視頻解碼播放》 這篇文章中,由於要想學習 FFmpeg
相關知識,C++
的基礎知識是必不可少的。html
可是寫着寫着發現,篇幅仍是太長了,加上有部分小夥伴對 C++
可能也比較熟悉,因此把此節獨立成篇,更有利於不熟悉 C++
的小夥伴學習查看,熟悉的小夥伴也能夠直接跳過。java
C++
相對於 Java
仍是有許多的不一樣之處,對於沒有使用過 C++
的人來講,若是要學習 NDK
開發,C++
是第一道坎,必需要掌握。程序員
本文經過對比的方式,把 C++
和 Java
之間最基礎,也是最常使用知識的異同標記出來,方便你們學習。bash
固然了,本文只是重點對 C++
中最經常使用的,也是重點的知識進行講解,若有時間,最好仍是系統地學一下相關的基礎知識。函數
本文使用對比的方式,將
C++
與咱們很是熟悉的Java
進行對比學習,介紹C++
與Java
使用的異同,幫助你們快速入門C++
。post
C++ 提供了一下幾種基礎數據類型學習
類型 | 關鍵字 |
---|---|
布爾型 | bool |
字符型 | char |
整型 | int |
浮點型 | float |
雙浮點型 | double |
無類型 | void |
同時,這些類型還能夠被類型修飾符修飾,拓展出更多的數據類型:網站
類型修飾符 | 關鍵字 |
---|---|
有符號類型 | signed |
無符號類型 | unsigned |
短類型 | short |
長類型 | long |
其中 signed
和 unsigned
指定了數據是否有正負; short
和 long
主要指定了數據的內存大小。ui
因爲不一樣的系統,同個數據類型所佔用的內存大小也不必定是同樣的,如下是典型值:this
類型 | 內存大小 | 範圍 |
---|---|---|
char | 1 個字節 | -128到127 或 0到255 |
unsigned char | 1 個字節 | 0 到 255 |
signed char | 1 個字節 | -128 到 127 |
int | 4 個字節 | -2147483648 到 2147483647 |
unsigned int | 4 個字節 | 0 到 4294967295 |
signed int | 4 個字節 | -2147483648 到 2147483647 |
short int | 2 個字節 | -32768 到 32767 |
unsigned short int | 2 個字節 | 0 到 65,535 |
signed short int | 2 個字節 | -32768 到 32767 |
long int | 8 個字節 | -xxx 到 xxx |
signed long int | 8 個字節 | -xxx 到 xxx |
unsigned long int | 8 個字節 | -xxx 到 xxx |
float | 4 個字節 | -xxx 到 xxx |
double | 8 個字節 | -xxx 到 xxx |
long double | 16 個字節 | -xxx 到 xxx |
能夠看到,
short
修飾符將原類型內存大小減少一半;
long
修飾符將原數據類型內存大小擴大一倍。
C++
是一門面向對象的語言,類是必不可少的。其類的定義與 Java
大同小異。
Java
類一般聲明和定義一般都是在同一個文件 xxx.java
中。
而 C++
類的聲明和定義一般是分開在兩個不一樣的文件中,分別是 .h 頭文件
和 .cpp 文件
一個 類的頭文件
一般以下:
// A.h
class A {
private: //私有屬性
int a;
void f1();
protected: //子類可見
int b;
void f2(int i);
public: //公開屬性
int c = 2;
int f3(int j);
A(int a, int b); // 構造函數
~A(); //析構函數
};
複製代碼
對應的類實現文件 A.cpp
以下:
// A.cpp
/** * 實現構造函數 */
A::A(int a, int b):
a(a),
b(b) {
}
// 等價於
/* A::A(int a, int b) { this.a = a; this.b = b; } */
/** * 實現析構函數 */
A::~A() {
}
/** * 實現 f1 方法 */
void A::f1() {
}
/** * 實現 f2 方法 */
void A::f2(int j) {
this.b = j
}
/** * 實現 f3 方法 */
int A::f3(int j) {
this.c = j
}
複製代碼
能夠看到,.h
文件主要負責類成員變量和方法的聲明; .cpp
文件主要負責成員變量和方法的定義。
可是,並不是必定要按照這樣的結構去實現類,你也能夠在 .h
頭文件中直接定義變量和方法。
好比:
// A.h
class A {
private:
int a = 1;
public:
void f1(int i) {
this.a = i;
}
}
複製代碼
1) 可見性 private、protected、public
這幾個關鍵字和 Java
是同樣的,只不過在 C++
中,一般不會對每一個成員變量和方法進行可見性聲明,而是將不一樣的可見性的變量和方法集中在一塊兒,統一聲明,具體見上面定義的類A。
2) 構造函數和析構函數
C++
中類的構造函數和 Java
基本一致,只不過,在實現構造函數時,對成員變量的初始化方式比較特別。以下:
A::A(int a, int b):
a(a),
b(b) {
}
// 等價於
A::A(int a, int b) {
this.a = a;
this.b = b;
}
複製代碼
以上兩種方式均可以,一般使用第一種方式。
析構函數
則是 Java
中沒有的。經過波浪符號 ~
進行標記。
它和構造函數同樣,都是由系統自動調用,只不過,構造函數
在類建立的時候調用,析構函數
在類被刪除的時候調用,主要用於釋放內部變量和內存。
析構函數的聲明形式爲 ~類名();
實現的形式爲 類名::~類名() { }
具體見上面類 A 的寫法。
3) ::
雙冒號
看了上面類的定義,確定會對 ::
這個符號感到很神奇。這是 C++
中的 域做用符
,用於標示變量和方法是屬於哪一個域的,好比上面的
void A::a() { }
複製代碼
說明 方法a
是屬於 類A
的。
也能夠用於調用類的靜態成員變量,如
//A.h
class A {
private:
static int a = 1;
int b;
void a();
}
//A.cpp
void A::a() {
b = A::a;
}
複製代碼
C++
類的繼承和 Java
也是大同小異,其格式以下:
class B: access-specifier A
,其中 access-specifier
是訪問修飾符, 是 public
、protected
或 private
其中的一個。
訪問修飾符的做用以下:
公有繼承(public):當一個類派生自公有基類時,基類的公有成員也是派生類的公有成員,基類的保護成員也是派生類的保護成員,基類的私有成員不能直接被派生類訪問,可是能夠經過調用基類的公有和保護成員來訪問。
保護繼承(protected): 當一個類派生自保護基類時,基類的公有和保護成員將成爲派生類的保護成員。
私有繼承(private):當一個類派生自私有基類時,基類的公有和保護成員將成爲派生類的私有成員。
一般狀況下,咱們都是使用
公有繼承(public)
,也就是和Java
是同樣的。
Java
中,子類只能繼承一個父類,可是 C++
能夠繼承自多個父類,使用逗號 ,
隔開:
class <派生類名>:<繼承方式1><基類名1>,<繼承方式2><基類名2>,…
{
<派生類類體>
};
複製代碼
Java
中,是沒有指針的概念的,可是其實 Java
中除了基本數據類,大部分狀況下使用都是 指針
。
好比下面這段 Java
代碼:
People p1 = new People("David","0001");
People p2 = p1;
p2.setName("Denny");
System.out.println(p1.getName());
// 輸出結果爲:Denny
複製代碼
緣由就是 p1 和 p2 都是對對象的引用,在完成賦值語句 People p2 = p1; 後, p2 和 p1 指向同一個存儲空間,因此對於p2的修改也影響到了p1。
那麼,爲何在 Java
中不多去關注指針呢?
由於 Java
已經將指針封裝了,也不容許顯式地去操做指針,而且 Java
中的內存都由虛擬機進行管理,無需咱們去釋放申請的內存。
1) 指針的聲明和定義
與 Java
不一樣的是,C++
中的指針概念很是重要,而且無處不在。
指針
:是一個變量,這個變量的值是另外一個變量的內存地址。也就是說,指針是一個指向內存地址的變量。
指針的聲明和定義方法以下:
int a = 1; // 實際變量的聲明
int *p; // 指針變量的聲明
p = &a; // 指針指向 a 的內存地址
printf("p 指向的地址: %d, p指向的地址存儲的內容: %d\n", p, *p);
// 輸出以下:
// p 指向的地址: -1730170860, p指向的地址存儲的內容: 1
複製代碼
這個例子中有兩個很重要的符號: *
、&
。其中:
*
:有兩個做用:
i. 用於定義一個指針: type *var_name;
,var_name
是一個指針變量,如 int *p;
ii. 用於對一個指針取內容: *var_name
, 如 *p
的值是 1
。
&
:是一個取址符號
其用於獲取一個變量所在的內存地址。如 &a;
的值是 a
所在內存的位置,即 a
的地址。
經過上面的例子,可能沒法很好的理解指針的用處,來看另外一個例子。
class A {
public:
int i;
};
int main() {
//-----1-------
A a = A(); // 定變量 a
a.i = 1; // 修改 a 中的變量
A b = a; // 定義變量 b ,賦值爲 a
A *c = &a; // 定義指針 c,指向 a
printf("%d, %d, %d\n", a.i, b.i, c->i);
// 輸出:1, 1, 1
//-----2-------
b.i = 2; //修改 b 中的變量
printf("%d, %d, %d\n", a.i, b.i, c->i);
// 輸出:1, 2, 1
//-----3-------
c->i = 3; //修改 c 中的變量
printf("%d, %d, %d\n", a.i, b.i, c->i);
// 輸出:3, 2, 3
//-----4-------
// 打印地址
printf("%d, %d, %d\n", &a, &b, c);
// 輸出:-1861360224, -1861360208, -1861360224
return 0;
}
複製代碼
上面的例子,定義了一個變量 a
,而後將 a
分別賦值給普通變量 b
和指針變量 c
。
第一次,打印三個變量中的成員變量的 i
的值都爲 1
;
第二次,修改了 b
中的 i
,結果只修改了 b
的值,對 a
和 c
都沒有影響;
第三次,修改了 c
中的 i
,結果修改了 a
和 c
的值,對 b
都沒有影響;
最後,打印了三個變量的地址,能夠發現 a
和 c
的值是同樣的,b
的地址不同。
從這個例子就能夠看出端倪了:
經過
普通變量
賦值的時候,系統建立了一個新的獨立的內存塊,如b
,對b
的修改,隻影響其自己;
經過
指針變量
賦值時,系統沒有建立新的內存塊,而是將指針指向了已存在的內存塊,如c
, 任何對c
的修改,都將影響原來的變量,如a
。
還有一點須要注意的是,指針變量
對成員變量的引用,使用的是箭頭符號 ->
,如 c->i
;普通變量對成員變量的引用,使用的是點符號 .
,如 b.i
。
2) new 和 delete
在上面的例子中,是經過建立了一個變量 a
,而後將 指針變量 c
指向了 a
的方式定義了 c
。還有另一種方法,能夠聲明和定義一個指針變量,那就是經過 new
動態建立。
class A {
public:
int i;
}
int main() {
A *a = new A();
a->i = 0;
printf("%d\n", a->i);
// 輸出: 0
// 刪除指針變量,回收內存
delete a;
return 0;
}
複製代碼
這就是動態建立指針變量的方式,這是 C++
經常使用的方式。
重要提醒:
要注意的是,經過
new
的方式建立的指針變量和不經過new
建立的變量最大的區別在於:經過new
建立的指針須要咱們本身手動回收內存,不然將會致使內存泄漏。回收內存則是經過delete
關鍵字進行的。
也就是說,
new
和delete
必需要成對調用。
int main() {
A a = A(); // 無new,main 函數結束後,系統會自動回收內存
A *b = new A(); // new 方式建立,系統不會自動回收內存,要手動 delete
delete b; // 手動刪除,回收內存
return 0;
}
複製代碼
能夠看到,C++
的指針變量其實更接近與 Java
中普通變量的使用方式。
引用
是除了指針外,另外一個很是重要的概念。在 C++
也是常用的。
引用指的是:爲一個變量起一個別名,也就是說,它是某個已存在變量的另外一個名字。
引用和指針很是的類似,初學者很是容易把這二者混淆了。
首先來看下如何聲明一個引用變量。
// 聲明一個普通變量 i
int i = 0;
// 聲明定義一個引用 j
int &j = i;
j = 1;
printf("%d, %d\n", i, j)
// 輸出:1, 1
複製代碼
是否是有點熟悉,又是與符號 &
,可是這裏並不是表示取址,這裏只是做爲一個標示符號。
請記住,千萬不要和取址符號混淆,取址表示方式是:A *p = &a;
在上面的例子中,修改了 j
的值,i
的值也發生了變化。這和指針是否是很是像?
那麼,引用和指針有什麼不同呢?
i. 不存在空引用。引用必須鏈接到一塊合法的內存。
ii. 一旦引用被初始化爲一個對象,就不能被指向到另外一個對象。指針能夠在任什麼時候候指向到另外一個對象。
iii. 引用必須在建立時被初始化。指針能夠在任什麼時候間被初始化。
i 和 iii 都很好理解,就是聲明引用的時候,必需要初始化好,而且不能初始化爲空 NULL
。
ii 是最讓人不理解的,什麼叫作 「不能被指向到另外一個對象」 ?
看如下的例子:
int i = 0;
// 定義引用 j ,指向 i
int &j = i;
int k = 1;
// 這個操做是指向另一個對象嗎?
j = k;
printf("%d, %d, %d\n", i, j, k);
// 輸出:1, 1, 1
// 打印地址
printf("%d, %d, %d\n", &i, &j, &k);
// 輸出:-977299952, -977299952, -977299948
複製代碼
能夠看到,i
j
k
三個的值都變成了 1
,這看起來和指針是同樣的效果,但卻有質的區別。
看最後一個打印輸出,i
和 j
的地址始終是同樣的,和 k
是不同的。也就是說, j
始終指向 i
,不可改變。 j = k
只是把 k
的值給到了 j
,同時也改變了 i
。
若是還不懂,再來看一下指針的例子,你就明白了。
int i = 0;
// 定義指針 j ,指向 i
int *j = &i;
int k = 1;
// 指向另外一個對象
j = &k;
printf("%d, %d, %d\n", i, *j, k);
// 輸出:0, 1, 1
// 打印地址
printf("%d, %d, %d\n", &i, j, &k);
// 輸出:-1790365184, -1790365180, -1790365180
複製代碼
看到了嗎? j
在賦值了 &k
之後,地址就變成和 k
同樣了,也就是說,指針 j
能夠指向不一樣的對象。這時候, j
和 i
就沒有任何關係了,i
的值也不會隨着 j
改變而改變。
引用最常出現的地方是做爲函數的參數使用。
void change(int &i, int &j) {
int temp = i;
i = j;
j = temp;
}
int main() {
int i = 0;
int j = 1;
// 打印地址
printf("[before: %d, %d]\n", &i, &j);
//輸出:[before: -224237816, -224237812]
change(i, j);
printf("[i: %d, j: %d]\n", i, j);
// 輸出:i: 1, j: 0
// 打印地址
printf("[after: %d, %d]\n", &i, &j);
// 輸出:after: -224237816, -224237812
return 0;
}
複製代碼
在上面的例子中,change
方法的兩個參數都是引用,和普通的參數有如下兩個區別:
i. 引用參數不會建立新的內存塊,參數只是對外部傳進來的變量的一個引用。
ii. 引用參數能夠改變外部變量的值。
這是普通變量的狀況:
void change(int i, int j) {
int temp = i;
i = j;
j = temp;
// 打印地址
pritf("[change: %d, %d]\n", &i, &j);
// 輸出[change: -1136723044, -1136723048]
}
int main() {
int i = 0;
int j = 1;
// 打印地址
printf("[before: %d, %d]\n", &i, &j);
//輸出:[before: -224237816, -224237812]
change(i, j);
printf("[i: %d, j: %d]\n", i, j);
// 輸出:i: 0, j: 1
// 打印地址
printf("[after: %d, %d]\n", &i, &j);
// 輸出:after: -224237816, -224237812
return 0;
}
複製代碼
能夠看到,i
j
的值不會被改不變,緣由是 change
方法建立了兩個臨時的局部變量,都有本身的內存塊,這個變量的地址和外部傳進來的變量是沒有關係的,因此沒法改變外部變量的值。
到這裏,就能夠看到參數引用的好處了:引用參數爲咱們節省了內存,執行效率也更快。
一樣的,指針參數也有相似的效果,可是其仍然和引用有着本質的區別。引用爲咱們提供另外一個種很好的傳參選擇。
有時候,咱們並不想讓函數內部改變外部變量的值,能夠給參數加上常量的標誌。
void change(const int &i, const int &j) {
int temp = i;
i = j; // 不容許修改i,編譯出錯
j = temp; // 不容許修改j,編譯出錯
}
複製代碼
多態
是面向對象的三大特色之一。
C++
的多態和 Java
很是類似,可是也有着明顯的不一樣。
看下面一個例子:
class A {
public:
void f() {
printf("a\n");
};
};
class B : public A {
public:
void f() {
printf("b\n");
};
};
int main() {
A *a = new B();
a->f();
// 輸出:a
return 0;
}
複製代碼
這裏 B
繼承了 A
,並重寫了方法 f
。
在 main
函數中,定義了一個基類變量指針 a
,並指向子類 B
。接着調用了 a
的方法 f
。
若是是 Java
中相似的操做的話,那麼毫無疑問,此處會輸出 b
,但是這裏卻輸出了 a
。也就是說,這裏方法 f
其實是基類 A
的 f
方法。
這就是 C++
和 Java
其中一個很大的不一樣。
緣由是,調用函數 f() 被編譯器設置爲基類中的版本,這就是所謂的靜態多態,或靜態連接。
函數調用在程序執行前就準備好了。有時候這也被稱爲早綁定,由於 f() 函數在程序編譯期間就已經設置好了。
那麼若是想實現相似 Java
中的多態重載呢?
virtual
是 C++
中的一個關鍵字,用於聲明函數,表示虛函數。用於告訴編譯器不要靜態連接到該函數,改成動態連接。
依然是上面的例子,在 A
的 f
函數上加上 virtual
,將獲得相似 Java
的效果:
class A {
public:
virtual void f() {
printf("a\n");
};
};
class B : public A {
public:
void f() {
printf("b\n");
};
};
int main() {
A *a = new B();
a->f();
// 輸出:b
return 0;
}
複製代碼
在 Java
中,咱們常常會使用 interface
或 abstract
來定義一些接口,方便代碼規範和拓展,可是在 C++
沒有這樣的方法,可是能夠有相似的實現,那就是:純虛函數。
class A {
public:
// 聲明一個純虛函數
virtual void f() = 0;
}
class B : public A {
public:
// 子類必須實現 f ,不然編譯不經過
void f() {
printf("b\n");
};
};
int main() {
A *a = new B();
a->f();
// 輸出:b
return 0;
}
複製代碼
A
中的 virtual void f() = 0;
就是一個純虛函數。若是繼承 A
,子類必須實現 f
這個接口,不然編譯不經過。
A
則是一個抽象類。不能被直接定義使用。
在 C++
中有一個方法,可讓咱們在程序編譯前,對代碼作一些處理,稱爲預處理。這是 Java
中沒有的,在 C++
中卻常用到。
預處理是一些指令,可是這些指令並非 C++
語句,因此不須要以分號 ;
結束。
全部的預處理語句都是以井號 #
開始的。
好比 #include
就是一個預處理,用於將其餘文件導入到一個另外一個文件中,相似 Java
的 import
。
例如導入頭文件:
// A.h
class A{
public:
A();
~A();
}
複製代碼
#include "A.h"
A::A() {
}
A::~A() {
}
複製代碼
在 C++
中經常使用的預處理有如下幾個 #include
、 #define
、#if
、#else
、 #ifdef
、 #endif
等。
最經常使用的一個預處理語句 #define
,一般稱爲宏定義。
其形式爲:
#define name replacement-text
複製代碼
#define PI 3.14159
printf("PI = %f", PI);
// 在編譯以前,上面的語句被展開爲:
// printf("PI = %f", 3.14159);
複製代碼
#define SUM(a,b) (a + b)
printf("a + b = %d", SUM(1, 2));
// 在編譯以前,上面的語句被展開爲:
// printf("a + b = %d", 1 + 2);
// 輸出:a + b = 3
複製代碼
#
和 ##
運算符在宏定義中,#
用於將參數 字符串化
。
#define MKSTR( x ) #x
printf(MKSTR(Hello C++));
// 在編譯以前,上面的語句被展開爲:
// printf("Hello C++");
// 輸出: Hello C++
複製代碼
在宏定義中,##
用於將參數 鏈接起來
。
#define CONCAT(a, b) a ## b
int xy = 100;
printf("xy = %d", CONCAT(x, y));
// 在編譯以前,上面的語句被展開爲:
// printf("xy = %d", xy);
// 輸出:xy = 100
複製代碼
注意:#
、 ##
在多個宏定義嵌套使用的時候,會致使不展開的問題
例如:
#define CONCAT(x, y) x ## y
#define A a
#define B b
void mian() {
char *ab = "ab";
char *AB = "AB";
printf("AB = %s", CONCAT(A, B));
// 在編譯以前,上面的語句被展開爲:
// printf("AB = %s", AB);
}
複製代碼
雖然定義了 A
B
兩個宏定義,可是在 CONCAT
中遇到 ##
的時候,A
B
這兩個宏定義是不會開展的,而是直接看成兩個參數被鏈接起來了。
那麼要如何解決這個問題呢?那就是再轉接一層。
#define _CONCAT(x, y) x ## y
#define CONCAT(x, y) _CONCAT(x, y)
#define A a
#define B b
void mian() {
char *ab = "ab";
char *AB = "AB";
printf("AB = %s", CONCAT(A, B));
// 在編譯以前,上面的語句被展開爲:
// printf("AB = %s", _CONCAT(a, b));
// printf("AB = %s", ab);
// 輸出:AB = ab
}
複製代碼
#if
、#else
、 #ifdef
、 #endif
這幾個的組合主要用條件編譯。
在 C++
中條件編譯也是常用到的,能夠用來控制哪些代碼參與編譯,哪些不參與編譯。
#define DEBUG
int main() {
#ifdef DEBUG
// 參與編譯
printf("I am DEBUG\n");
#else
// 不參與編譯
printf("No DEBUG\n");
#endif
return 0;
}
// 輸出:I am DEBUG
複製代碼
以上代碼,因爲先前已經定義了 #define DEBUG
因此 #ifdef DEBUG
爲 true
,編譯 printf("I am DEBUG\n");
。
若是去掉 #define DEBUG
,則編譯 printf("No DEBUG\n");
。
int main() {
#if 0
// 這裏面的代碼都被註釋掉,不參與編譯
printf("I am not compiled\n");
#endif
return 0;
}
複製代碼
以上,基本就是在 C++
常用到的,與 Java
類似,又存在差別的一些基礎知識,因爲面嚮對象語言都存在必定的類似性,相信有了以上的基礎以後,你就能夠比較通暢地閱讀一些 C++
代碼了。
若是你是一個 Java
程序員,可能對其中的一些知識仍是會感到迷惑,這時候須要你拋棄 Java
中的一些慣有思惟,從新細細品嚐一下 C++
的味道,能夠實際的去敲一下代碼來消化這些知識,只有實踐才能出真知。