Android NDK開發之旅21 C++ 類

小楠總發表於2017-12-18

###前言

C++是物件導向的程式語言,因此有類的概念。

類的定義是在標頭檔案,實現在原始檔中,這裡為了方便,都寫在原始檔中。

###建構函式

  1. 系統會自動建立預設的無慘建構函式,但是一旦提供了有參的,預設的就會去掉了。

  2. 構造有兩種寫法。

    class Teacher { public: Teacher(){ cout << "無參建構函式" << endl; } Teacher(char* name, int age){ cout << "有參建構函式" << endl; this->name = name; this->age = age; }

    private: char* name; int age; };

    void main(){ Teacher t1 = Teacher("nan",20); Teacher t2("lu", 18); system("pause"); }

####建構函式的初始化屬性列表

現在有Student類,裡面有Teacher的私有成員兩個。Student構造的時候需要初始化Teacher,但是又不能直接訪問Teacher類的私有屬性。

因此只能通過初始化列表的方式來呼叫。相當於Java利用super(...)呼叫父類的建構函式初始化父類。

class Teacher
{
public:
	Teacher(char* name, int age){
		this->name = name;
		this->age = age;
	}
	Teacher(){
	}
private:
	char* name;
	int age;
};

class Student{
public:
	Student(int id, char* t1_name, int t1_age, char*t2_name, int t2_age)
		:t1(t1_name, t1_age), t2(t2_name, t2_age)
	{
		//t1.name = t1_name;不可以這樣賦值,因為Teacher的私有屬性不可以訪問
	}

private:
	int id;
	Teacher t1;
	Teacher t2;
};

void main(){
	Student s(1, "wu", 20, "li", 18);
	system("pause");
}
複製程式碼

###解構函式

解構函式:當物件要被系統釋放時(例如函式棧退出的時候),解構函式被呼叫 作用:善後處理,例如釋放動態分配的記憶體。

class Teacher
{
public:
	Teacher(){
		cout << "無參建構函式" << endl;
		name = (char*)malloc(sizeof(char)* 100);
		strcpy(name, "lu");
		age = 18;
	}
	~Teacher(){
		cout << "解構函式" << endl;
		free(name);
	}

private:
	char* name;
	int age;
};

void fun(){
	Teacher t;
}

void main(){
	fun();
	system("pause");
}
複製程式碼

###拷貝建構函式

  1. 系統預設的拷貝建構函式就是拷貝值的,跟下面的寫法一樣。

  2. 拷貝建構函式被呼叫的場景有(實質上都是第一種的變種),有印象即可:

     一、宣告時賦值(只有宣告的時候才會呼叫)
     二、作為引數傳入,實參給形參賦值(引用傳值不會呼叫,因為指向的都是同一片記憶體,不存在拷貝問題)
     三、作為函式返回值返回,給變數初始化賦值
    複製程式碼

例子如下:

class Teacher
{
public:
	Teacher(char* name, int age){
		cout << "有參建構函式" << endl;
		this->name = name;
		this->age = age;
	}
	Teacher(const Teacher &obj){
		cout << "拷貝建構函式" << endl;
		this->name = obj.name;
		this->age = obj.age;
	}

private:
	char* name;
	int age;
};

Teacher fun(Teacher t){
	return t;
}

void main(){
	Teacher t1 = Teacher("nan", 20);
	Teacher t2 = t1;
	Teacher t3 = fun(t1);

	system("pause");
}
複製程式碼

####淺拷貝

預設的拷貝建構函式實現是淺拷貝,即值拷貝,如下所示:

class Teacher
{
public:
	Teacher(){
		this->name = (char*)malloc(sizeof(char)* 100);
		strcpy(name, "lu");
		this->age = age;
	}
	~Teacher(){
		cout << "解構函式" << endl;
		free(name);
	}
	Teacher(const Teacher &obj){
		cout << "拷貝建構函式" << endl;
		this->name = obj.name;
		this->age = obj.age;
	}

private:
	char* name;
	int age;
};

void fun(){
	Teacher t1;
	Teacher t2 = t1;
}

void main(){
	fun();
	system("pause");
}
複製程式碼

值拷貝帶來的問題:

如果有動態記憶體分配的時候,如果單純是值拷貝,析構的時候就會析構兩次。

例如程式碼中的char* name,t1、t2都指向了同一個地址,那麼析構的時候就會free兩次,因為一個記憶體地址不能釋放兩次,因此會觸發異常中斷。
複製程式碼

如下圖所示:

淺拷貝的問題.png

####深拷貝

重寫覆蓋預設的淺拷貝,自己寫一個深拷貝:

t2的name是新分配的記憶體,因此t1、t2指向的是兩片不同的記憶體,因此析構的時候分別釋放,互不干擾,解決了淺拷貝的問題。

class Teacher
{
public:
	Teacher(){
		this->name = (char*)malloc(sizeof(char)* 100);
		strcpy(name, "lu");
		this->age = age;
	}
	~Teacher(){
		cout << "解構函式" << endl;
		free(name);
	}
	Teacher(const Teacher &obj){
		cout << "拷貝建構函式" << endl;
		int len = strlen(obj.name);
		this->name = (char*)malloc(sizeof(char)* (len + 1));//+1是因為結束符
		strcpy(this->name, obj.name);
		this->age = obj.age;
	}

private:
	char* name;
	int age;
};

void fun(){
	Teacher t1;
	Teacher t2 = t1;
}

void main(){
	fun();
	system("pause");
}
複製程式碼

如下圖所示:

深淺拷貝的區別.png

###動態記憶體分配

  1. C++中使用new和delete進行動態記憶體分配,這時候構造和析構會呼叫。
  2. C中使用malloc和free進行動態記憶體分配,構造和析構不會呼叫。

例子:

void main(){
	cout << "C++中使用new和delete進行動態記憶體分配" << endl;
	Teacher* t1 = new Teacher("wu", 20);
	delete t1;

	cout << "C中使用malloc和free進行動態記憶體分配" << endl;
	Teacher* t2 = (Teacher*)malloc(sizeof(Teacher));
	free(t2);
	system("pause");
}
複製程式碼

陣列的話,delete的時候需要加上[]:

int* arr1 = (int*)malloc(sizeof(int)* 10);
arr1[0] = 1;
free(arr1);

int* arr2 = new int[10];
arr2[0] = 1;
delete[] arr2;
複製程式碼

###類的靜態屬性和函式

  1. 類的靜態屬性必須放在全域性的地方初始化。
  2. 非靜態、靜態函式可以訪問靜態屬性,但是靜態函式只能訪問靜態屬性。(跟Java一樣)
  3. 靜態的屬性和方法可以直接通過 類名:: 來訪問(如果是public的話)。也可以通過 物件:: 來訪問。

例子:

class Test
{
public:
	void m1(){
		count++;
	}
	static void m2(){
		count++;
	}
private:
	static int count;
};

//靜態屬性初始化
int Test::count = 0;

void main(){
	//Test::count
	Test::m2();

	system("pause");
}
複製程式碼

###類的大小(sizeof)

C++的記憶體分割槽:

棧
堆
全域性(靜態、全域性)
常量區(字串)
程式程式碼區
複製程式碼

結構體有位元組對齊的概念,那麼類的普通屬性與結構體相同的記憶體佈局。

例如下面的結構體大小是32 = 4 x 8:

struct C{
public:
	int i;//8個位元組(由於位元組對齊,與double一樣為8個位元組)
	int j;//8個位元組
	int k;//8個位元組
	double x;//8個位元組
	void(*test)();//指標是4個位元組,但是存放在程式碼區
};
複製程式碼

例如下面的類的大小是24(這裡沒搞懂,先放一放,以後回來再補)

class A{
public:
	int i;//8個位元組(由於位元組對齊,與double一樣為8個位元組)
	int j;//8個位元組
	int k;//8個位元組
	double t;//8個位元組

	static int m;//4個位元組,但是存在於全域性靜態區
	void test(){//4個位元組,相當於函式指標的寫法,但是存放在程式碼區
		cout << "列印" << endl;
	}
};
複製程式碼

###類的this指標常函式

  1. const寫在函式後面。
  2. 此時的const修飾的是this指標,this指標本類就是指標常量,不能修改值。
  3. 此處增加了const的修飾之後,tihs指標更加是一個常量指標,指標指向的值不能修改。

例子:

class Teacher{
private:
	char* name;
	int age;
public:
	Teacher(char* name, int age){
		this->name = name;
		this->age = age;
	}
	//常函式,修飾的是this
	//既不能改變指標的值,又不能改變指標指向的內容
	//const Teacher* const this
	void myprint() const{
		printf("%#x\n", this);
		//改變屬性的值
		//this->name = "yuehang";
		//改變this指標的值
		//this = (Teacher*)0x00009;
		cout << this->name << "," << this->age << endl;
	}
};
複製程式碼

this,當前物件的指標

C++函式是共享的(多個物件共享程式碼區中的一個函式),必須要有能夠標識當前物件是誰的辦法,那就是this指標。

函式共享.png

例如我們用結構體去模擬類的時候,就需要自定義this指標了。所以說,在JNI開發的時候,JniEnv在C語音裡面是二級指標(結構體實現,C語言中結構體沒有tihs指標),在C++中是一級指標(類實現,類有this指標)

C++編譯器對普通成員函式的內部處理.png

擴充:

Java記憶體分割槽:

  1. JVM Stack(基本資料型別、物件引用)
  2. Native Method Stack(本地方法棧)
  3. 方法區
  4. 程式計數區
  5. 直接記憶體

熱修復的基本概念:拿到錯誤的函式的指標,指向正確的函式的指標。(兩個函式都已經載入的情況下)

###友元函式、友元函式

主要作用:類、類與函之間的資料共享。比如類已經寫好了,不想去改動類的結構(比如訪問許可權,那麼最好的辦法就是使用友元)

在友元函式中可以訪問類的私有的屬性。

例如,fun是類A的友元函式,因此fun中可以訪問A的私有屬性。

class Test{
	friend void fun(Test &t, int i);//友元函式宣告
public:
	Test(int i){
		this->i = i;
	}
private:
	int i;
};

//友元函式實現,可以是全域性的,也可以是其他類的
void fun(Test &t, int i){
	t.i = i;
}

void main(){
	Test t(10);
	fun(t, 20);
	system("pause");
}
複製程式碼

同理,B是A的友元類,因此B可以訪問A的私有屬性:

class A{
	friend class B;
private:
	int i;
};

class B
{
public:
	void test(){
		a.i = 20;
	}
private:
	A a;
	int i;
};
複製程式碼

擴充:在Java(Java是C++寫的嘛)中Class就可以理解為Object的“友元類”,Class類就可以訪問Object的任何成員。

###運算子過載

運算子過載的本質還是函式的呼叫。

現在有一個類:

class Point
{
public:
	Point(int x, int y){
		this->x = x;
		this->y = y;
	}

public:
	int x;
	int y;
};
複製程式碼

需要過載“+”:

Point operator+(Point &p1, Point &p2){
	return Point(p1.x + p2.x, p2.y + p2.y);
}
複製程式碼

注意,如果Point的x、y屬性都是私有的話,就需要用到友元函式了,例如類已經寫好了,不想破壞類的結構:

class Point
{
	friend Point operator+(Point &p1, Point &p2);
public:
	Point(int x, int y){
		this->x = x;
		this->y = y;
	}

private:
	int x;
	int y;
};

Point operator+(Point &p1, Point &p2){
	return Point(p1.x + p2.x, p2.y + p2.y);
}
複製程式碼

如果是在類內部的運算子過載,因為有this指標,過載的時候可以省略一個引數,類可以直接訪問自己的私有成員:

class Point
{
public:
	Point(int x, int y){
		this->x = x;
		this->y = y;
	}

	Point operator+(Point &p){
		return Point(this->x + p.x, this->y + p.y);
	}

private:
	int x;
	int y;
};
複製程式碼

如果覺得我的文字對你有所幫助的話,歡迎關注我的公眾號:

公眾號:Android開發進階

我的群歡迎大家進來探討各種技術與非技術的話題,有興趣的朋友們加我私人微信huannan88,我拉你進群交(♂)流(♀)

相關文章