
一、C++ 基礎知識
1.1 函式
- 函式是一組一起執行一個任務的陳述句,每個 C 程式都至少有一個函式,即主函式
main(),所有簡單的程式都可以定義其他額外的函式, .h頭檔案 ,- 指標函式:指帶指標的函式,即本質是一個函式,函式回傳型別是某一型別的指標,
int *func(int x, int y), - 函式指標:指向函式的指標變數,即本質是一個指標變數,
int (*funcp)(int x),
int i;
int *a = &i; //這里a是一個指標,它指向變數i
int &b = i; //這里b是一個參考,它是變數i的參考(別名)
int * &c = a; //這里c是一個參考,它是指標a的參考
int & *d; //這里d是一個指標,它指向參考,但參考不是物體,所以這是錯誤的
復制代碼
在分析上面代碼時,可以從變數識別符號開始從右往左看,最靠近識別符號的是變數的本質型別,而再往左即為對變數型別的進一步修飾,
例如:int * & a 識別符號a的左邊緊鄰的是 &,證明 a 是一個參考變數,而再往左是 * ,可見 a 是一個指標的參考,再往左是 int,可見 a 是一個指向int型別的指標的參考,
.和->
struct Data
{
int a,b,c;
}; /*定義結構體型別*/
struct Data * p; /* 定義結構體指標 */
struct Data A = {1,2,3}; / * 宣告結構體變數A,A即結構體名 */
int x; /* 宣告一個變數x */
p = &A ; /* 地址賦值,讓p指向A */
x = p->a; /* 取出p所指向的結構體中包含的資料項a賦值給x */
/* 此時由于p指向A,因而 p->a == A.a,也就是1 */
復制代碼
因為此處
p是一個指標,所以不能使用.號訪問內部成員(即不能p.a),而要使用->,但是A.a是可以的,因為A不是指標,是結構體名,
一般情況下用 “.” 只需要宣告一個結構體,格式是:結構體型別名+結構體名,然后用 結構體名加“.”加成員名 就可以參考成員了,因為自動分配了結構體的記憶體,如同 int a; 一樣, 用 “->” ,則要宣告一個結構體指標,還要手動開辟一個該結構體的記憶體(上面的代碼則是建了一個結構體實體,自動分配了記憶體,下面的例子則會講到手動動態開辟記憶體),然后把回傳的地址賦給宣告的結構體指標,才能用“->” 正確參考,否則記憶體中只分配了指標的記憶體,沒有分配結構體的記憶體,導致想要的結構體實際上是不存在,這時候用 “->” 參考自然出錯了,因為沒有結構體,自然沒有結構體的域了, 此外,(*p).a 等價于 p->a,
::
::是作用域符,是運算子中等級最高的,它分為三種:
- 全域作用域符,用法
::name - 類作用域符,用法
class::name - 命名空間作用域符,用法
namespace::name
他們都是左關聯,他們的作用都是為了更明確的呼叫你想要的變數:
- 如在程式中的某一處你想呼叫全域變數
a,那么就寫成::a;(也可以是全域函式) - 如果想呼叫
class A中的成員變數a,那么就寫成A::a; - 另外一個如果想呼叫
namespace std中的cout成員,你就寫成std::cout(相當于using namespace std;cout)意思是在這里我想用cout物件是命名空間std中的cout(即就是標準庫里邊的cout);
- 表示“域運算子”:宣告了一個類
A,類A里宣告了一個成員函式void f(),但沒有在類的宣告里給出f的定義,那么在類外定義f時, 就要寫成void A::f(),表示這個f()函式是類A的成員函式,- 直接用在全域函式前,表示是全域函式:在 VC 里,你可以在呼叫 API 函式里,在 API 函式名前加
::- 表示參考成員函式及變數,作用域成員運算子:
System::Math::Sqrt()相當于System.Math.Sqrt();
1.2 linux記憶體布局
![[圖片上傳失敗...(image-365f2d-1632398115969)]](https://img.uj5u.com/2021/09/24/267310241106002.png)
1.3 指標陣列
- 陣列:
int arr[] = {1,2,3}; - 指標:
int* p = arr,指標 p 指向陣列 arr 的首地址;*p = 6;將 arr 陣列的第一個元素賦值為 6;*(p+1) = 10;將 arr 陣列第二個元素賦值為 10; - 指標陣列:陣列里面每一個元素都是指標
int* p[3];
for(int i = 0; i<3; i++){
p[i] = &arr[i];
}
復制代碼
- 陣列指標:也稱為行指標;
int (*p)[n]
優先級高,首先說明 p 是一個指標,指向一個整型的一維陣列,這個一維陣列的長度是 n,也可以說是 p 的步長,執行 p+1 時,p 要跨過 n 個整型資料的長度,
int a[3][4]; int (*p)[4]; //該陳述句是定義一個陣列指標,指向含 4 個元素的一維陣列 p = a; //將該二維陣列的首地址賦給 p,也就是 a[0] 或 &a[0][0] p++; //該陳述句執行后,也就是 p = p+1; p 跨過行 a[0][] 指向了行 a[1][]
1.4 結構體
struct Person
{
char c;
int i;
char ch;
};
int main()
{
struct Person person;
person.c = 8;
person.i = 9;
}
復制代碼
存盤變數時地址要求對齊,編譯器在編譯程式時會遵循兩個原則:
(1)結構體變數中成員的偏移量必須是成員大小的整數倍 (2)結構體大小必須是所有成員大小的整數倍,也即所有成員大小的公倍數
![[圖片上傳失敗...(image-2ef9b7-1632398115969)]](https://img.uj5u.com/2021/09/24/267310241106003.png)

1.5 共用體
- 共用體是一種特殊的資料型別,允許你在相同的記憶體位置存盤不同的資料型別,
- 你可以定義一個帶有多成員的共用體,但是任何時候只能有一個成員帶有值,
- 共用體提供了一種使用相同的記憶體位置的有效方式,
- 共用體占用的記憶體應足夠存盤共用體中最大的成員,
union Data
{
int i;
float f;
char str[20];
}data;
int main()
{
union Data data;
data.i = 11;
}
復制代碼
1.6 typedef
- 定義一種型別的別名,而不只是簡單的宏替換,可以用作同時宣告指標型的多個物件:
char *pa, *pb;//傳統寫法
復制代碼
typedef char* PCHAR; // 使用typedef 寫法 一般用大寫
PCHAR pa, pb; // 可行,同時宣告了兩個指向字符變數的指標
復制代碼
- 用在舊的C的代碼中,幫助
struct,以前的代碼中,宣告struct新物件時,必須要帶上struct,即形式為:struct 結構名 物件名:
struct tagPOINT1
{
int x;
int y;
};
struct tagPOINT1 p1;
復制代碼
//使用 typedef
typedef struct tagPOINT
{
int x;
int y;
}POINT;
POINT p1; // 這樣就比原來的方式少寫了一個struct,比較省事,尤其在大量使用的時候
復制代碼
- 用
typedef來定義與平臺無關的型別:
#if __ANDROID__
typedef double SUM;
#else
typedef float SUM ;
#endif
int test() {
SUM a;
return 0;
}
復制代碼
- 為復雜的宣告定義一個新的簡單的別名:
//原宣告:
int *(*a[5])(int, char*);
//變數名為a,直接用一個新別名pFun替換a就可以了:
typedef int *(*pFun)(int, char*);
//原宣告的最簡化版:
pFun a[5];
復制代碼
1.7 類的構造和決議、友元函式
1.7.1 C++ 中頭檔案(.h)和源檔案(.cpp)
.h這里一般寫類的宣告(包括類里面的成員和方法的宣告)、函式原型、#define常數等,但一般來說不寫出具體的實作,寫頭檔案時,為了防止重復編譯,我們在開頭和結尾處必須按照如下樣式加上預編譯陳述句:
#ifndef CIRCLE_H
#define CIRCLE_H
class Circle
{
private:
double r;
public:
Circle();//建構式
Circle(double R);//建構式
double Area();
};
#endif
復制代碼
至于
CIRCLE_H這個名字實際上是無所謂的,你叫什么都行,只要符合規范都行,原則上來說,非常建議把它寫成這種形式,因為比較容易和頭檔案的名字對應,
.cpp源檔案主要寫實作頭檔案中已經宣告的那些函式的具體代碼,需要注意的是,開頭必須#include一下實作的頭檔案,以及要用到的頭檔案,
#include "Circle.h"
Circle::Circle()
{
this->r=5.0;
}
Circle::Circle(double R)
{
this->r=R;
}
double Circle:: Area()
{
return 3.14*r*r;
}
復制代碼
- 最后,我們建一個
main.cpp來測驗我們寫的 Circle 類
#include <iostream>
#include "Circle.h"
using namespace std;
int main()
{
Circle c(3);
cout<<"Area="<<c.Area()<<endl;
return 1;
}
復制代碼
1.7.2 建構式和解構式
- 類的建構式是類的一種特殊的成員函式,它會在每次創建類的新物件時執行,建構式的名稱與類的名稱是完全相同的,并且不會回傳任何型別,也不會回傳 void,建構式可用于為某些成員變數設定初始值,
- 類的解構式是類的一種特殊的成員函式,它會在每次洗掉所創建的物件時執行,解構式的名稱與類的名稱是完全相同的,只是在前面加了個波浪號(~)作為前綴,它不會回傳任何值,也不能帶有任何引數,解構式有助于在跳出程式(比如關閉檔案、釋放記憶體等)前釋放資源,
1.7.3 友元函式、友元類
- 友元函式是一種定義在類外部的普通函式,它不屬于任何類,但它需要在類體內進行說明,為了與該類的成員函式加以區別,在說明時前面加以關鍵字
friend, - 友元函式不是成員函式,但是它可以訪問類中的私有成員,
- 一個函式可以是多個類的友元函式,只需要在各個類中分別宣告,
- 友元的作用在于提高程式的運行效率(即減少了型別檢查和安全性檢查等都需要的時間開銷),但是,它破壞了類的封裝性和隱藏性,使得非成員函式可以訪問類的私有成員,
- 友元類的所有成員函式都是另一個類的友元函式,都可以訪問另一個類中的隱藏資訊(包括私有成員和保護成員),
- 當希望一個類可以存取另一個類的私有成員時,可以將該類宣告為另一類的友元類,定義友元類的陳述句格式如下:
friend class 類名(friend和class是關鍵字,類名必須是程式中的一個已定義過的類),
class INTEGER
{
private:
int num;
public:
friend void Print(const INTEGER& obj);//宣告友元函式
};
void Print(const INTEGER& obj) //不使用friend和類::
{
//函式體
}
void main()
{
INTEGER obj;
Print(obj);//直接呼叫
}
復制代碼
#include <iostream>
using namespace std;
class girl
{
private:
char *name;
int age;
friend class boy; //宣告類boy是類girl的友元
public:
girl(char *n,int age):name(n),age(age){};
};
class boy
{
private:
char *name;
int age;
public:
boy(char *n,int age):name(n),age(age){};
void disp(girl &);
};
void boy::disp(girl &x) // 該函式必須在girl類定義的后面定義,否則girl類中的私有變數還是未知的
{
cout<<"boy's name is:"<<name<<",age:"<<age<<endl;
cout<<"girl's name is:"<<x.name<<",age:"<<x.age<<endl;
//借助友元,在boy的成員函式disp中,借助girl的物件,直接訪問girl的私有變數
//正常情況下,只允許在girl的成員函式中訪問girl的私有變數
}
void main()
{
boy b("aaa",8);
girl g("bbb",99);
b.disp(g);
}
復制代碼
1.8 單例物件、運算子多載
- 我們可以重定義或多載大部分 C++ 內置的運算子,這樣就能使用自定義型別的運算子,多載的運算子是帶有特殊名稱的函式,函式名是由關鍵字
operator和其后要多載的運算子符號構成的,與其他函式一樣,多載運算子有一個回傳型別和一個引數串列,
#include <iostream>
using namespace std;
class Box
{
public:
double getVolume(void)
{
return length * breadth * height;
}
void setLength( double len )
{
length = len;
}
void setBreadth( double bre )
{
breadth = bre;
}
void setHeight( double hei )
{
height = hei;
}
// 多載 + 運算子,用于把兩個 Box 物件相加
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
private:
double length; // 長度
double breadth; // 寬度
double height; // 高度
};
// 程式的主函式
int main( )
{
Box Box1; // 宣告 Box1,型別為 Box
Box Box2; // 宣告 Box2,型別為 Box
Box Box3; // 宣告 Box3,型別為 Box
double volume = 0.0; // 把體積存盤在該變數中
// Box1 詳述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);
// Box2 詳述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);
// Box1 的體積
volume = Box1.getVolume();
cout << "Volume of Box1 : " << volume <<endl;
// Box2 的體積
volume = Box2.getVolume();
cout << "Volume of Box2 : " << volume <<endl;
// 把兩個物件相加,得到 Box3
Box3 = Box1 + Box2;
// Box3 的體積
volume = Box3.getVolume();
cout << "Volume of Box3 : " << volume <<endl;
return 0;
}
復制代碼
列印結果:
Volume of Box1 : 210 Volume of Box2 : 1560 Volume of Box3 : 5400
1.9 繼承多型、虛函式
1.9.1 繼承
- 一個類可以派生自多個類,這意味著,它可以從多個基類繼承資料和函式,定義一個派生類,我們使用一個類派生串列來指定基類,類派生串列以一個或多個基類命名:
class derived-class: access-specifier base-class; - 其中,訪問修飾符
access-specifier是public、protected或private其中的一個,base-class是之前定義過的某個類的名稱,如果未使用訪問修飾符access-specifier,則默認為private, - 派生類可以訪問基類中所有的非私有成員,因此基類成員如果不想被派生類的成員函式訪問,則應在基類中宣告為
private, - 一個派生類繼承了所有的基類方法,但下列情況除外:
基類的建構式、解構式和拷貝建構式,
基類的多載運算子, 基類的友元函式,
-
當一個類派生自基類,該基類可以被繼承為
public、protected或private幾種型別,繼承型別是通過上面講解的訪問修飾符access-specifier來指定的, -
我們幾乎不使用
protected或private繼承,通常使用public繼承,當使用不同型別的繼承時,遵循以下幾個規則:
公有繼承(public):當一個類派生自公有基類時,基類的公有成員也是派生類的公有成員,基類的保護成員也是派生類的保護成員,基類的私有成員不能直接被派生類訪問,但是可以通過呼叫基類的公有和保護成員來訪問,
保護繼承(protected): 當一個類派生自保護基類時,基類的公有和保護成員將成為派生類的保護成員, 私有繼承(private):當一個類派生自私有基類時,基類的公有和保護成員將成為派生類的私有成員,
#include <iostream>
using namespace std;
// 基類
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生類
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
Rect.setWidth(5);
Rect.setHeight(7);
// 輸出物件的面積
cout << "Total area: " << Rect.getArea() << endl;
return 0;
}
復制代碼
列印結果:
Total area: 35
1.9.2 虛函式
定義一個函式為虛函式,不代表函式為不被實作的函式,
定義他為虛函式是為了允許用基類的指標來呼叫子類的這個函式, 定義一個函式為純虛函式,才代表函式沒有被實作, 定義純虛函式是為了實作一個介面,起到一個規范的作用,規范繼承這個類的程式員必須實作這個函式,
class A
{
public:
virtual void foo()
{
cout<<"A::foo() is called"<<endl;
}
};
class B:public A
{
public:
void foo()
{
cout<<"B::foo() is called"<<endl;
}
};
int main(void)
{
A *a = new B();
a->foo(); // 在這里,a雖然是指向A的指標,但是被呼叫的函式(foo)卻是B的!
return 0;
}
復制代碼
- 一個類函式的呼叫并不是在編譯時刻被確定的,而是在運行時刻被確定的,由于撰寫代碼的時候并不能確定被呼叫的是基類的函式還是哪個派生類的函式,所以被稱為“虛”函式,
- 純虛函式是在基類中宣告的虛函式,它在基類中沒有定義,但要求任何派生類都要定義自己的實作方法,在基類中實作純虛函式的方法是在函式原型后加 “=0” :
virtual void funtion()=0 - 將函式定義為純虛函式,則編譯器要求在派生類中必須予以重寫以實作多型性,宣告了純虛函式的類是一個抽象類,所以,用戶不能創建類的實體,只能創建它的派生類的實體,
1.10 類模板、函式模板
- 模板是泛型編程的基礎,泛型編程即以一種獨立于任何特定型別的方式撰寫代碼,
- 模板是創建泛型類或函式的藍圖或公式,
模板函式定義的一般形式如下所示:
template <typename type> ret-type func-name(parameter list)
{
// 函式的主體
}
復制代碼
#include <iostream>
#include <string>
using namespace std;
template <typename T>
inline T const& Max (T const& a, T const& b)
{
return a < b ? b:a;
}
int main ()
{
int i = 39;
int j = 20;
cout << "Max(i, j): " << Max(i, j) << endl;
double f1 = 13.5;
double f2 = 20.7;
cout << "Max(f1, f2): " << Max(f1, f2) << endl;
string s1 = "Hello";
string s2 = "World";
cout << "Max(s1, s2): " << Max(s1, s2) << endl;
return 0;
}
復制代碼
列印結果:
Max(i, j): 39 Max(f1, f2): 20.7 Max(s1, s2): World
類模板,泛型類宣告的一般形式如下所示:
template <class type> class class-name {
.
.
.
}
復制代碼
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
using namespace std;
template <class T>
class Stack {
private:
vector<T> elems; // 元素
public:
void push(T const&); // 入堆疊
void pop(); // 出堆疊
T top() const; // 回傳堆疊頂元素
bool empty() const{ // 如果為空則回傳真,
return elems.empty();
}
};
template <class T>
void Stack<T>::push (T const& elem)
{
// 追加傳入元素的副本
elems.push_back(elem);
}
template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
// 洗掉最后一個元素
elems.pop_back();
}
template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
// 回傳最后一個元素的副本
return elems.back();
}
int main()
{
try {
Stack<int> intStack; // int 型別的堆疊
Stack<string> stringStack; // string 型別的堆疊
// 操作 int 型別的堆疊
intStack.push(7);
cout << intStack.top() <<endl;
// 操作 string 型別的堆疊
stringStack.push("hello");
cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch (exception const& ex) {
cerr << "Exception: " << ex.what() <<endl;
return -1;
}
}
復制代碼
列印結果:
7 hello Exception: Stack<>::pop(): empty stack
1.11 容器
- 序列式容器(Sequence containers),此為可序群集,其中每個元素均有固定位置—取決于插入時機和地點,和元素值無關,如果你以追加方式對一個群集插入六個元素,它們的排列次序將和插入次序一致,STL提供了三個序列式容器:向量(vector)、雙端佇列(deque)、串列(list),此外你也可以把 string 和 array 當做一種序列式容器,
- 關聯式容器(Associative containers),此為已序群集,元素位置取決于特定的排序準則以及元素值,和插入次序無關,如果你將六個元素置入這樣的群集中,它們的位置取決于元素值,和插入次序無關,STL提供了四個關聯式容器:集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap),
- 容器配接器:根據上面七種基本容器類別實作而成,stack,元素采取 LIFO(后進先出)的管理策略、queue,元素采取 FIFO(先進先出)的管理策略,也就是說,它是個普通的緩沖區(buffer)、priority_queue,元素可以擁有不同的優先權,所謂優先權,乃是基于程式員提供的排序準則(預設使用 operators)而定義,Priority queue 的效果相當于這樣一個 buffer:“下一元素永遠是queue中優先級最高的元素”,如果同時有多個元素具備最髙優先權,則其次序無明確定義,
特點:
vector頭部與中間插入和洗掉效率較低,在尾部插入和洗掉效率高,支持隨機訪問,deque是在頭部和尾部插入和洗掉效率較高,支持隨機訪問,但效率沒有vector高,list在任意位置的插入和洗掉效率都較高,但不支持隨機訪問,set由紅黑樹實作,其內部元素依據其值自動排序,每個元素值只能出現一次,不允許重復,且插入和洗掉效率比用其他序列容器高,map可以自動建立 Key - value 的對應,key 和 value 可以是任意你需要的型別,根據 key 快速查找記錄,
選擇:
- 如果需要高效的隨機存取,不在乎插入和洗掉的效率,使用
vector, - 如果需要大量的插入和洗掉元素,不關心隨機存取的效率,使用
list, - 如果需要隨機存取,并且關心兩端資料的插入和洗掉效率,使用
deque, - 如果打算存盤資料字典,并且要求方便地根據 key 找到 value,一對一的情況使用
map,一對多的情況使用multimap, - 如果打算查找一個元素是否存在于某集合中,唯一存在的情況使用
set,不唯一存在的情況使用multiset,
時間復雜度:
vector在頭部和中間位置插入和洗掉的時間復雜度為 O(N),在尾部插入和洗掉的時間復雜度為 O(1),查找的時間復雜度為 O(1);deque在中間位置插入和洗掉的時間復雜度為 O(N),在頭部和尾部插入和洗掉的時間復雜度為 O(1),查找的時間復雜度為 O(1);list在任意位置插入和洗掉的時間復雜度都為 O(1),查找的時間復雜度為 O(N);set和map都是通過紅黑樹實作,因此插入、洗掉和查找操作的時間復雜度都是 O(log N),
1.12 命名空間
1.12.1 namespace
- 命名空間是一種描述邏輯分組的機制,可以將按某些標準在邏輯上屬于同一個集團的宣告放在同一個命名空間中,用于區分不同庫中相同名稱的函式、類、變數,
namespace namespace_name {
// 代碼宣告
}
復制代碼
- 無名命名空間,你可以在當前編譯單元中(無名命名空間之外),直接使用無名命名空間中的成員名稱,但是在當前編譯單元之外,它又是不可見的,它可以使代碼保持區域性,從而保護代碼不被他人非法使用,
namespace {
// 代碼宣告
}
復制代碼
- 不能在命名空間的定義中宣告(另一個嵌套的)子命名空間,只能在命名空間的定義中定義子命名空間,
- 不能直接使用
命名空間名::成員名 ……定義方式,為命名空間添加新成員,而必須先在命名空間的定義中添加新成員的宣告, - 命名空間是開放的,即可以隨時把新的成員名稱加入到已有的命名空間之中去,方法是,多次宣告和定義同一命名空間,每次添加自己的新成員和名稱,
1.12.2 using
- 可以使用
using namespace指令,這樣在使用命名空間時就可以不用在前面加上命名空間的名稱,這個指令會告訴編譯器,后續的代碼將使用指定的命名空間中的名稱,
#include <iostream>
using namespace std;
// 第一個命名空間
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二個命名空間
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
using namespace first_space;
int main ()
{
// 呼叫第一個命名空間中的函式
func();
return 0;
}
復制代碼
- 除了可以使用 using編譯指令(組合關鍵字
using namespace)外,還可以使用using宣告來簡化對命名空間中的名稱的使用:using 命名空間名::[命名空間名::……]成員名;,注意,關鍵字using后面并沒有跟關鍵字namespace,而且最后必須為命名空間的成員名(而在using編譯指令的最后,必須為命名空間名),
using指令使用后,可以一勞永逸,對整個命名空間的所有成員都有效,非常方便,而using宣告,則必須對命名空間的不同成員名稱,一個一個地去宣告,但是,一般來說,使用using宣告會更安全,因為,using宣告只匯入指定的名稱,如果該名稱與區域名稱發生沖突,編譯器會報錯,而using指令匯入整個命名空間中的所有成員的名稱,包括那些可能根本用不到的名稱,如果其中有名稱與區域名稱發生沖突,則編譯器并不會發出任何警告資訊,而只是用區域名去自動覆寫命名空間中的同名成員,特別是命名空間的開放性,使得一個命名空間的成員,可能分散在多個地方,程式員難以準確知道,別人到底為該命名空間添加了哪些名稱,
二、java 呼叫 C/C++
- 加載
.so庫;
//MainActivity.java
static {
System.loadLibrary("native-lib");
}
復制代碼
- 撰寫 java 函式;
//MainActivity.java
public native String stringFromJNI();
復制代碼
- 撰寫 C/C++ 函式;
//native-lib.cpp
#include <jni.h>
#include <string>
//函式名的構成:Java 加上包名、方法名并用下劃線連接(Java_packageName_methodName)
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_cppdemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
復制代碼
- ndk/cmake 配置(下面只列出cmake配置);
# CMakeLists.txt
# 設定構建本地庫所需的最小版本的cbuild,
cmake_minimum_required(VERSION 3.4.1)
# 創建并命名一個庫,將其設定為靜態
# 或者共享,并提供其源代碼的相對路徑,
# 您可以定義多個庫,而cbuild為您構建它們,
# Gradle自動將共享庫與你的APK打包,
add_library( native-lib #設定庫的名稱,即SO檔案的名稱,生產的so檔案為“libnative-lib.so”, 在加載的時候“System.loadLibrary("native-lib");”
SHARED # 將庫設定為共享庫,
native-lib.cpp # 提供一個源檔案的相對路徑
helloJni.cpp # 提供同一個SO檔案中的另一個源檔案的相對路徑
)
# 搜索指定的預構建庫,并將該路徑存盤為一個變數,因為cbuild默認包含了搜索路徑中的系統庫,所以您只需要指定您想要添加的公共NDK庫的名稱,cbuild在完成構建之前驗證這個庫是否存在,
find_library(log-lib # 設定path變數的名稱,
log # 指定NDK庫的名稱 你想讓CMake來定位,
)
#指定庫的庫應該鏈接到你的目標庫,您可以鏈接多個庫,比如在這個構建腳本中定義的庫、預構建的第三方庫或系統庫,
target_link_libraries( native-lib # 指定目標庫中,與 add_library的庫名稱一定要相同
${log-lib} # 將目標庫鏈接到日志庫包含在NDK,
)
#如果需要生產多個SO檔案的話,寫法如下
add_library( natave-lib # 設定庫的名稱,另一個so檔案的名稱
SHARED # 將庫設定為共享庫,
nataveJni.cpp # 提供一個源檔案的相對路徑
)
target_link_libraries( natave-lib #指定目標庫中,與 add_library的庫名稱一定要相同
${log-lib} # 將目標庫鏈接到日志庫包含在NDK,
)
復制代碼
// build.gradle(:app)
android {
compileSdkVersion 29
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "com.example.cppdemo"
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
復制代碼
三、JNI 基礎
3.1 JNIEnv 、JavaVM
JavaVM是 Java 虛擬機在 JNI 層的代表, JNI 全域只有一個;- 從上面的代碼中我們可以發現,雖然 Java 函式不帶引數,但是 native 函式卻帶了兩個引數,第一個引數
JNIEnv是指向可用 JNI 函式表的介面指標,第二個引數jobject是 Java 函式所在類的實體的 Java 物件參考; JNIEnv是JavaVM在執行緒中的代表, 每個執行緒都有一個, JNI 中可能有很多個JNIEnv,同時JNIEnv具有執行緒相關性,也就是 B 執行緒無法使用 A 執行緒的JNIEnv;JNIEnv型別實際上代表了 Java 環境,通過這個JNIEnv*指標,就可以對 Java 端的代碼進行操作:
呼叫 Java 函式; 操作 Java 物件;
JNIEnv的本質是一個與執行緒相關的代表 JNI 環境的結構體,里面存放了大量的 JNI 函式指標;JNIEnv內部結構如下:
![[圖片上傳失敗...(image-d1b2a4-1632398115966)]](https://img.uj5u.com/2021/09/24/267310241106005.png)
JavaVM的結構如下:
![[圖片上傳失敗...(image-5ffe41-1632398115966)]](https://img.uj5u.com/2021/09/24/267310241106006.png)

3.2 資料型別
3.2.1 基礎資料型別
| Signature格式 | Java | Native | Description |
|---|---|---|---|
| B | byte | jbyte | signed 8 bits |
| C | char | jchar | unsigned 16 bits |
| D | double | jdouble | 64 bits |
| F | float | jfloat | 32 bits |
| I | int | jint | signed 32 bits |
| S | short | jshort | signed 16 bits |
| J | long | jlong | signed 64 bits |
| Z | boolean | jboolean | unsigned 8 bits |
| V | void | void | N/A |
3.2.2 陣列資料型別
陣列簡稱:在前面添加 [
| Signature格式 | Java | Native |
|---|---|---|
| [B | byte[] | jbyteArray |
| [C | char[] | jcharArray |
| [D | double[] | jdoubleArray |
| [F | float[] | jfloatArray |
| [I | int[] | jintArray |
| [S | short[] | jshortArray |
| [J | long[] | jlongArray |
| [Z | boolean[] | jbooleanArray |
3.2.3 復雜資料型別
物件型別簡稱:L+classname +;
| Signature格式 | Java | Native |
|---|---|---|
| Ljava/lang/String; | String | jstring |
| L+classname +; | 所有物件 | jobject |
| [L+classname +; | Object[] | jobjectArray |
| Ljava.lang.Class; | Class | jclass |
| Ljava.lang.Throwable; | Throwable | jthrowable |
3.2.4 函式簽名
(輸入引數...)回傳值引數
| Signature格式 | Java函式 |
|---|---|
| ()V | void func() |
| (I)F | float func(int i) |
| ([I)J | long func(int[] i) |
| (Ljava/lang/Class;)D | double func(Class c) |
| ([ILjava/lang/String;)Z | boolean func(int[] i,String s) |
| (I)Ljava/lang/String; | String func(int i) |
3.3 JNI 操作 JAVA 物件、類
- 獲取你需要訪問的 Java 物件的類:
jclass thisclazz = env->GetObjectClass(thiz);//使用GetObjectClass方法獲取thiz對應的jclass,
jclass thisclazz = env->FindClass("com/xxx/xxx/abc");//直接搜索類名
復制代碼
- 獲取 MethodID:
/**
* thisclazz -->上一步獲取的 jclass
* "onCallback"-->要呼叫的方法名
* "(I)Ljava/lang/String;"-->方法的 Signature, 簽名參照前面的第 3.2 小節表格,
*/
jmethodID mid_callback = env->GetMethodID(thisclazz , "onCallback", "(Ljava/lang/String;)I");
jmethodID mid_callback = env->GetStaticMethodID(thisclazz , "onCallback", "(Ljava/lang/String;)I");//獲取靜態方法的ID
復制代碼
- 呼叫方法:
jint result = env->CallIntMethod(thisclazz , mid_callback , jstrParams);
jint result = env->CallStaticIntMethod(thisclazz , mid_callback , jstrParams);//呼叫靜態方法
復制代碼
貼一下JNI 常用介面檔案,有需要可以在這里查詢,
3.4 JNI 參考
3.4.1 區域參考
- 通過
NewLocalRef和各種JNI介面創建(FindClass、NewObject、GetObjectClass和NewCharArray等), - 會阻止 GC 回收所參考的物件,不在本地函式中跨函式使用,不能跨線前使用,
- 函式回傳后區域參考所參考的物件會被 JVM 自動釋放,或手動釋放,
- 手動釋放的方式:GetXXX 就必須呼叫 ReleaseXXX,呼叫完
GetStringUTFChars之后,呼叫ReleaseStringUTFChars釋放;對于手動創建的jclass,jobject等物件使用DeleteLocalRef方法進行釋放,
3.4.2 全域參考
- 呼叫
NewGlobalRef基于區域參考創建, - 會阻 GC 回收所參考的物件,可以跨方法、跨執行緒使用,
- JVM 不會自動釋放,必須手動釋放,
- 全域參考在顯式釋放之前保持有效,必須通過
DeleteGlobalRef來手動洗掉全域參考呼叫,
3.4.3 弱全域參考
- 呼叫
NewWeakGlobalRef基于區域參考或全域參考創建, - 不會阻止 GC 回收所參考的物件,可以跨方法、跨執行緒使用,
- 參考不會自動釋放,在 JVM 認為應該回收它的時候(比如記憶體緊張的時候)進行回收而被釋放,或呼叫
DeleteWeakGlobalRef手動釋放,
后續
本文參考了部分視頻、書籍、博客的內容,這里就不列出來了,想要了解更多NDK開發專案與學習資料,點擊下方二維碼,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/302538.html
標籤:其他
