C++核心程式設計

Kun發表於2022-02-14


物件導向程式設計思想

一、 記憶體分割槽模型

C++程式在執行時,將記憶體大方向劃分為4個區域

  • 程式碼區:存放函式體的二進位制程式碼,由作業系統進行管理
  • 全域性區:存放全域性變數和靜態變數以及常量
  • 棧區:由編譯器自動分配釋放,存放函式的引數值、區域性變數等
  • 堆區:由程式設計師分配和釋放,若程式設計師不釋放,程式結束時由作業系統回收

記憶體四區的意義:不同區域存放的資料,賦予不同的生命週期,給我們更大的靈活程式設計

1、 程式執行前

在程式編譯後,生成了.exe課執行程式,未執行該程式前分為兩個區域

程式碼區

  • 存放CPU執行的機械命令
  • 程式碼區是共享的,共享的目的是對於頻繁被執行的程式,只需要在記憶體中有一份程式碼即可
  • 程式碼區是隻讀的,使其只讀的原因是防止程式意外地修改了它的指令

全域性區

  • 全域性變數和靜態變數存放在此
  • 全域性區還包括了常量區、字串常量和其他常量也存放在此
  • 該區域的數量在程式結束後由作業系統釋放
#include <iostream>
using namespace std;

// 全域性變數
int global_a = 10;
int global_b = 10;

// 全域性常量
const int c_g_a = 10;
const int c_g_b = 10;

int main() {
	// 全域性區
	// 全域性變數、靜態變數、常量

	// 靜態變數
	static int static_a = 10;
	static int static_b = 10;

	// 常量
	// 字串常量
	cout << "字串常量的地址為 " << (int)&"hello" << endl;
	// const修飾的變數
	// const修飾的全域性變數
	// const修飾的區域性變數
	const int c_l_a = 10;
	const int c_l_b = 10;


	// 建立普通區域性變數
	int a = 10;
	int b = 10;
	cout << "區域性變數a的地址為 " << (int)&a << endl;
	cout << "區域性變數b的地址為 " << (int)&b << endl;
	cout << "區域性常量c_l_a的地址為 " << (int)&c_l_a << endl;
	cout << "區域性常量c_l_b的地址為 " << (int)&c_l_b << endl;
	cout << "全域性變數global_a的地址為 " << (int)&global_a << endl;
	cout << "全域性變數global_b的地址為 " << (int)&global_b << endl;
	cout << "靜態變數static_a的地址為 " << (int)&static_a << endl;
	cout << "靜態變數static_b的地址為 " << (int)&static_b << endl;
	cout << "全域性常量c_g_a的地址為 " << (int)&c_g_a << endl;
	cout << "全域性常量c_g_b的地址為 " << (int)&c_g_b << endl;
}

2、 程式執行後

棧區

  • 由編譯器自動分配釋放,存放函式的引數值,全域性變數等
  • 注意事項:不要返回區域性變數的地址,棧區開闢的資料由編譯器自動釋放
#include <iostream>
using namespace std;

int* func(int b) {  // 形引數據也會放在棧區
   	 b = 100;
	int a = 10;  // 區域性變數在棧區,在函式執行完成之後就被清除
	return &a;  // 返回區域性變數的地址,非法函式
}

int main() {
	cout << *func() << endl;  // 第一次可以列印正確的數字,是因為編譯器做了留
	cout << *func() << endl;  // 第二次的資料可能不會保留
}

堆區

  • 由程式設計師分配釋放,若程式設計師不釋放,程式結束時由作業系統回收
  • 在C++中主要利用new在堆區開闢記憶體
#include <iostream>
using namespace std;


int* func() {
	// 利用new關鍵字 可以將資料開闢到堆區
	int* p = new int(10);  // 指標本質也是區域性變數,放在棧上,指標儲存的資料放在堆區
	return p;
}

int main() {
	// 在堆區開闢資料
	int* p = func();
	cout << *p << endl;
}

3、 new操作符

C++中利用new操作符在堆區開闢資料

堆區開闢的額資料,由程式設計師手動開闢,手動釋放,釋放利用操作符delete

語法:new 資料型別

利用new建立的資料,會返回該資料對應的型別的指標

#include <iostream>
using namespace std;


int* func() {
	// new方法開闢一個陣列
	int* p = new int(10);  // 返回的是該資料型別的指標
	return p;
}

void test() {
 //建立10整型資料的陣列,在堆區 
	int* arr = new int[10];  // 10代表陣列有10個元素
	for (int i = 0; i < 10; i++) {
		arr[i] = i;  // 給陣列賦值
	}
	// 釋放堆區陣列,釋放陣列的時候,要加[]才可以
	delete[] arr;
}

int main() {
	int* p = func();
	cout << *p << endl;  // 堆區的資料由程式設計師管理資料
	delete p;  // 資料已經釋放,再次訪問就是非法操作
	 cout << *p << endl;
	test();
}

二、 引用

1、 基本使用

作用:給變數起別名

語法:資料型別 &別名 = 變數名;

#include <iostream>
using namespace std;


int main() {
	// 引用基本語法
	// 資料型別 &別名 = 變數名
	int a = 10;
	int& b = a;
	cout << b << endl;
	b = 100;
	cout << a << endl;
}

2、 注意事項

  • 引用必須初始化
  • 引用在初始化後,不可以改變
#include <iostream>
using namespace std;


int main() {
	int a = 10;
	// 引用必須要初始化
	int& b = a;
	// int &b;  // 非法操作
	// 引用一旦初始化後就不可以更改其地址,但是可以賦值
}

3、 引用做函式引數

作用:函式傳參時,可以利用引用的技術讓形參修飾實參

優點:可以簡化指標修改實參

#include <iostream>
using namespace std;


// 交換函式
// 1、值傳遞
void swap(int a,int b) {
	int temp = a;
	a = b;
	b = temp;
}

// 2、 地址傳遞
void swap1(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

// 3、 引用傳遞
void swap3(int &a, int &b) {  // &a即為別名,別名可以和變數名字一樣
	int temp = a;
	a = b;
	b = temp;
}


int main() {
	int a = 10;
	int b = 20;

	cout << a << b << endl;
	cout << "執行函式" << endl;
	swap(a, b);
	cout << a << b << endl;
	// 實參並沒有改變

	cout << a << b << endl;
	cout << "執行函式" << endl;
	swap1(&a, &b);
	cout << a << b << endl;
	// 實參發生改變


	cout << a << b << endl;
	cout << "執行函式" << endl;
	swap3(a, b);
	cout << a << b << endl;
	// 實參發生改變
}

通過引用引數產生的效果同按地址傳遞是一樣的,引用的語法更加清楚簡單

4、 引用做函式的返回值

作用:引用是可以作為函式的返回值存在的

注意:不要返回區域性變數的引用

用法:函式的呼叫作為左值

#include <iostream>
using namespace std;

// 引用做函式的返回值
// 1、 不要返回區域性變數的引用
int& test1() {  // 加&相當於以引用的方式返回
	int a = 10;  // 區域性變數存放在四區中的 棧區
	return a;
}

// 2、 函式的呼叫可以作為左值
int& test2() {
	static int a = 10;  // 靜態變數,存放在全域性區,全域性區上的資料在程式結束後系統釋放
	return a;			
}

int main() {
	int& b = test1();
	cout << b << endl;
	cout << b << endl;
	int& c = test2();
	cout << c << endl;
	test2() = 1000;  // 記憶體地址修改,同時函式的呼叫可以作為左值
	cout << c << endl;
}

5、 本質

本質:引用的本質在C++內部的實現是一個指標常量

6、 常量引用

作用:常量引用主要用來修飾形參,防止誤操作

在函式形參列表中使用

#include <iostream>
using namespace std;


// 列印資料
void showValue(int &a) {  // 可以加入const防止誤操作
	a = 1100;
	cout << a << endl;
}

int main() {
	// 常量引用:用來修飾形參,防止誤操作
	int a = 10;
	const int& ref = 10;  // 加const之後,編譯器將程式碼修改 int temp = 10; const int & ref = temp;
	// ref = 20;  // 加入const之後變為只讀
	showValue(a);
	cout << a << endl;
}

三、 函式提高

1、 預設引數

在C++中,函式的形參列表中的形參是可以有預設值的

語法:返回值型別 函式名 (引數= 預設值) {}

#include <iostream>
using namespace std;


// 函式的預設引數
void func(int a , int b = 10) {  // 自己也可以傳入引數,如果傳入引數,就使用自定義引數,否則使用預設引數
	cout << a << "\t" << b << endl;
}

// 注意事項
// 如果莫個位置已經有了預設引數,那麼從這個位置以後都必須要有預設值
// 如果函式宣告有預設引數,那麼函式的實現就不能有預設引數,不能重定義引數,即宣告和實現只能有一個有預設引數
void func(int a, int b);

int main() {
	int a = 20;
	// int b = 30;
	func(a);
}

2、 佔位引數

C++中函式的形參列表裡可以有佔位引數,用來做佔位,呼叫函式是必須填補這個位置

語法:返回值型別 函式名(資料型別){}

#include <iostream>
using namespace std;


// 佔位引數:可以有預設引數
// 返回值型別 函式名(資料型別){}
void func(int a, int) {
	cout << "this is func" << endl;
}

void func(int a, int);

int main() {
	func(10, 20);
}

3、 函式過載

3.1 概述

作用:函式名可以相同,提高函式名的複用性

函式過載的條件

  • 同一作用域下
  • 函式名相同
  • 函式引數型別不同 或者 個數不同 或者 順序不同

注意:函式的返回值不可以作為函式過載的條件

#include <iostream>
using namespace std;


// 函式過載
// 在同一作用域下
// 函式名相同
// 函式引數型別不同,或者個數不同,或者順序不同
void func(double a, int b) {
	cout << "10" << endl;
}
void func(string name) {
	cout << name << endl;
}
void func(int a, double b) {
	cout << "1010" << endl;
}


int main() {
	func("hello");
	func(1.0, 20);
	func(20, 1.0);
}

3.2 注意事項

  • 引用作為過載條件
  • 函式過載碰到函式預設引數
#include <iostream>
using namespace std;


// 函式過載的注意事項
// 1、 引用作為過載的條件
void func(int& a) {
	cout << "func(int &a)呼叫" << endl;
}
void func(const int& a) {
	cout << "func(const int &a)呼叫" << endl;
}

// 2、 函式過載碰到的預設引數,會出現二義性
void func2(int a, int b = 10) {
	cout << "func(int a預設引數)呼叫" << endl;
}
void func2(int a) {
	cout << "func(int a)呼叫" << endl;
}


int main() {
	const int a = 10;
	int b = 20;
	func(a);
	func(b);
	func2(a, b);
}

四、 類與物件

C++物件導向的三大特徵:封裝、繼承、多型

C++認為萬事萬物都皆為物件,物件上有其屬性和行為

1、 封裝

1.1 封裝的意義

封裝是C++物件導向的三大特性之一

封裝的意義

  • 將屬性和行為作為一個整體,表現生活中的事物

    在設計類的時候,屬性和行為寫在一起,表現事物

    語法:class 類名 { 訪問許可權: 屬性 / 行為 };

    // 設定一個圓類,求其周長
    #include <iostream>
    using namespace std;
    
    // 圓周率
    const double PI = 3.14;
    
    class Circle {
    	// 訪問許可權:公共許可權
    public:  
    
    	// 屬性:半徑
    	int m_r;  
    
    	// 行為:獲取圓的周長
    	double calcculate() {
    		return 2 * PI * m_r;
    	}
    };
    
    
    int main() {
    
    	// 通過圓類來建立物件(例項化物件)
    	Circle c1;
        
    	// 給圓物件的屬性賦值
    	c1.m_r = 10;
    
    	cout << "圓的周長為" << c1.calcculate() << endl;
    }
    

    案例:設計一個學生類,屬性有姓名和學號,可以給姓名和學號賦值,可以顯示學生的姓名和學號

    #include <iostream>
    using namespace std;
    #include <string>;
    
    
    class Student
    {
    	// 訪問許可權
    public:
    
    	// 屬性
    	string stu_name;
    	int stu_id;
    
    	// 行為
    	void showInfo() {
    		cout << "姓名:" << stu_name << " 學號:" << stu_id << endl;
    	}
    
    	// 給屬性賦值
    	void setInfo(string name, int id) {
    		stu_name = name;
    		stu_id = id;
    	}
    };
    
    
    int main() {
    	Student stu;  // 例項化
    	stu.setInfo("張三", 1);  
    	stu.showInfo();
    }
    

    類中的屬性和行為,我們同一稱為成員

    成員屬性,成員變數,成員方法/成員函式

  • 將屬性和行為加以許可權控制

    類在設計時,可以把屬性和行為放在不同的許可權下,加以控制

    訪問許可權有

    • public 公共許可權
    • protected 保護許可權
    • private 私有許可權
    #include <iostream>
    using namespace std;
    #include <string>;
    
    
    // public     成員類內可以訪問,類外也可以訪問 
    // protected  成員類內可以訪問,類外不可以訪問,兒子也可以訪問父親保護的內容
    // private    成員類內可以訪問,類內不可以訪問,兒子不可以訪問父親的私有內容
    
    class Person {
    	// 公共許可權
    public:
    	string name;
    	// 保護許可權
    protected:
    	string car;
    	// 私有許可權
    private:
    	int money;
    
    public:
    	void func() {
    		name = "zhansan";
    		car = "tuolaji";
    		money = 20;
    	}
    
    };
    int main() {
    	// 例項化具體物件
    	Person p1;
    	p1.name = "lishi";
    	// p1.car = "benc";  // 保護許可權在類外不能訪問
    	// p1.money = 30;  // 私有許可權內容,類外也不能訪問
    }
    

1.2 struct 和 class 的區別

在C++中,struct 和 class 唯一區別就在於 預設訪問的許可權不同

  • struct 預設許可權為公共
  • class 預設許可權為私有
#include <iostream>
using namespace std;
#include <string>;


class C1 {
	int A;  // 預設許可權是私有
};
struct C2
{
	int A2;  // 預設公有
};
int main() {
	// struct 預設公有
	C2 c2;
	c2.A2 = 100;  // 可以訪問
	// class 預設私有
	C1 c;
	// c.A = 100;  // 不能修改
}

1.3 成員屬性私有化

優點

  • 將所有成員屬性設為私有,可以自己控制讀寫許可權
  • 對於寫許可權,我們可以檢測資料的有效性
#include <iostream>
using namespace std;
#include <string>;


// 成員屬性私有化
class Person {
public:
	// 設定姓名、獲取姓名 提供許可權
	void setName(string pname) {
		name = pname;
	}
	string getName() {
		return name;
	}

	/* // 獲取年齡
	int getAge() {
		age = 18;  // 初始化為18歲
		return age;
	} */
	// 可讀可寫年齡,如果想修改(年齡範圍必須是0到18歲)
	int getAge() {
		return age;
	}
	// 設定年齡
	void setAge(int num) {
		if (num < 0 || num > 18) {
			age = 15;
			cout << "您輸入的年齡有問題" << endl;
			return;
		}
		cout << num << endl;
		age = num;
	}


	// 只寫財產
	void setMoney(int num) {
		money = num;
	}



private:  // 全部設定為私有
	// 姓名  可讀可寫
	string name;
	// 年齡  只讀
	int age; 
	// 財產 只寫
	int money;
};


int main() {
	Person p;
	p.setName("張三");
	cout << "名字:" << p.getName() << endl;

	p.setAge(150);  // 如果輸入的值不符合要求,直接強制賦值
	cout << "年齡:" << p.getAge() << endl;
}

1.4 案例

設計一個立方體類(Cube),求出立方體的面積和體積,分別用全域性函式和成員函式判斷兩個立方體是否相等

#include <iostream>
using namespace std;


// 建立一個立法體的類
class Cube {
	// 設計屬性
private:
	int c_l;  // 長
	int c_h;  // 高
	int c_w;  // 寬

	// 行為 獲取立方體的面積和體積
public:
	// 設定屬性
	void setInfo(int l, int w, int h) {
		c_l = l;
		c_w = w;
		c_h = h;
	}
	// 獲取屬性
	int getL() {
		return c_l;
	}
	int getH() {
		return c_h;
	}
	int getW() {
		return c_w;
	}
	// 獲取面積
	int getS() {
		return c_l * c_h * 2 + c_l * c_w * 2 + c_h * c_w * 2;
	}
	// 獲取體積
	int getV() {
		return c_l * c_h * c_w;
	}
	// 成員函式判斷是否相等
	bool isSameByClass(Cube& c) {
		if (getH() == c.getH() && getW() == c.getW() && getL() == c.getL()) {
			return true;
		}
		return false;
	}

};

// 利用全域性函式和成員函式判斷兩個立方體是否相等

// 全域性函式
bool isSame(Cube &c1, Cube &c2) {  // 引用傳遞
	if (c1.getH() == c2.getH() && c1.getW() == c2.getW() && c1.getL() == c2.getL()) {
		return true;
	}
	return false;
}

int main() {
	// 第一個正方體
	Cube c1;
	c1.setInfo(1, 1, 1);
	int s1 = c1.getS();  // 體積
	int v1 = c1.getV();  // 面積

	// 第二個正方體
	Cube c2;
	c2.setInfo(1, 1, 2);
	int s2 = c2.getS();
	int v2 = c2.getV();
	
	if (isSame(c1, c2)) {
		cout << "相同" << endl;
	}
	else
	{
		cout << "不相同" << endl;
	}

	if (c1.isSameByClass(c2)) {
		cout << "成員判斷,相同" << endl;
	}
	else
	{
		cout << "成員判斷,不相同" << endl;
	}
}

判斷點在圓上的位置

#include <iostream>
using namespace std;

// 判斷點和圓的關係
// 點類
class Point {
private:
	int x;
	int y;
public:
	void setPoint(int x, int y) {
		x = x;
		y = y;
	}
	int getx() {
		return x;
	}
	int gety() {
		return y;
	}
};


// 圓類
class Circle {
private:
	int r; // 半徑
	Point circlePoint;  // 代表圓心
public:
	void setR(int r) {
		r = r;
	}
	int getR() {
		return r;
	}
	void setCenter(Point center) {
		circlePoint = center;
	}
	Point getCenter() {
		return circlePoint;
	}
};

// 判斷點和圓的關係
void isInCircle(Circle &c, Point &p) {
	// 計算距離的平方
	if (pow(c.getCenter().getx() - p.getx(), 2) - pow(c.getCenter().gety() - p.gety(), 2) == pow(c.getR(), 2)) {
		cout << "點在圓上" << endl;
		return;
	}
	cout << "點不在圓上" << endl;
}


int main() {
	// 建立圓
	Circle c;
	Point ct;
	ct.setPoint(10, 0);
	c.setCenter(ct);
	c.setR(10);
	Point p;
	p.setPoint(10, 10);
	isInCircle(c, p);
}

一個類可以例項化另一個類,作為其屬性

2、 物件的初始化和清理

  • 生活中我們買的電子產品都基本會有出廠設定,在某一天我們不用的時候刪除一些自己資訊資料保證安全
  • C++中的物件導向來源於生活,每個物件都會有初始化設定以及物件銷燬前清理資料的設定

2.1 建構函式和解析函式

物件的初始化和清理也是兩個非常重要的安全問題

  • 一個物件或者變數沒有初始狀態,對其使用後果是未知的
  • 同樣使用完一個物件或變數,沒有及時清理,也會造成一定安全問題

C++利用了建構函式和解構函式解決上述問題,這兩個函式將會被編譯器自動呼叫,完成物件初始化操作和清理工作

物件的初始化和清理工作是編譯器強制要我們做的事情,因此如果我們不提供建構函式和析構,編譯器會提供

編譯器提供的建構函式和解構函式是空實現的

  • 建構函式:主要作用在於建立物件時為物件的成員屬性賦值,建構函式由編譯器自動呼叫,無須手動呼叫
  • 解構函式:主要作用在於物件銷燬前系統自動呼叫,執行一些清理工作

建構函式語法:類名() {}

  1. 建構函式,沒有返回值也不寫void
  2. 函式名字與類名相同
  3. 建構函式可以有引數,也可以發生過載
  4. 程式在呼叫物件時候會自動呼叫構造,無須手動呼叫,而且只會呼叫一次

解構函式語法:~類名() {}

  1. 解構函式,沒有返回值也不寫void
  2. 函式名與類名相同,在名稱前面加上符號~
  3. 解構函式不可以有引數,因此不可以發生過載
  4. 程式在物件銷燬前會自動呼叫析構,無須手動呼叫,而且只會呼叫一次
#include <iostream>
using namespace std;

class Person {
public:
	// 建構函式
	Person() {
		cout << "Person 建構函式的呼叫" << endl;
	}

	// 解構函式
	~Person() {
		cout << "Person 解構函式的呼叫" << endl;
	}

};
// 構造和析構都是必須有的實現,如果我們自己不提供,編譯器會提供一個空實現的構造和析構
void test() {
	Person p;  // 棧上的資料,test執行完畢之後,會自動釋放這個物件
}
int main() {
	// 物件的初始化和清理
	//test();
	Person p;

	system("pause");
	return 0;
}

2.2 建構函式的分類及呼叫

兩種分類方式

  • 按引數分類:有參構造和無參構造
  • 按型別分類:普通構造和拷貝構造

三種呼叫方式

  • 括號法
  • 顯示法
  • 隱式轉換法
#include <iostream>
using namespace std;

// 建構函式的分類和呼叫
class Person {
public:
	int age;
	// 建構函式
	// 無參構造
	Person() {
		cout << "Person 建構函式的呼叫" << endl;
		age = 1;
	}
	// 有參構造
	Person(int a) {
		age = a;
		cout << "Person 建構函式的呼叫有參" << a << endl;
	}
	// 拷貝建構函式
	Person(const Person &p) {
		// 將傳入的人身上的所有屬性,拷貝到我身上
		age = p.age;
		cout << "Person 建構函式的呼叫拷貝" << age << endl;
	}
};
void test() {
	// 呼叫
	// 括號法
	Person p;  // 預設建構函式的呼叫
	Person p02(10);  // 呼叫有參建構函式
	Person p03(p02);  //  拷貝建構函式的呼叫 
	//cout << "p2的年齡為:" << p2.age << endl;
	//cout << "p3的年齡為:" << p3.age << endl;
	
	// 顯示法
	Person p1;
	Person p2 = Person(10);  // 有參構造
	Person p3 = Person(p2);  // 拷貝構造
	//Person(10);  // 匿名物件,特點:當前行執行結束後,系統會立即回收掉匿名物件

	// 隱式轉換法
	Person p4 = 10;  // 相當於 Person p4 = Person(10);
	Person p5 = p4;
}
int main() {
	test();
}

注意事項

  • 呼叫預設建構函式的時候,不要加()

    • 因為下面這行程式碼,編譯器會認為是一個函式的宣告

      Person p1();

  • 不要利用拷貝建構函式,初始化匿名物件

    • 因為編譯器會認為Person p4(p3) === Person p3;,認為其為物件的宣告

      Person p4(p3);

2.3 拷貝函式的呼叫時機

C++中拷貝函式呼叫時機通常由三種情況

  • 使用一個已經建立完畢的物件來初始化一個新物件
  • 值傳遞的方式給函式引數傳值
  • 以值方式返回區域性物件
#include <iostream>
using namespace std;


// 拷貝建構函式的呼叫時機

class Person {
public:
	Person() {
		cout << "預設函式" << endl;
	}
	Person(int age) {
		age = age;
		cout << "有參函式" << endl;
	}
	Person(const Person& p) {
		age = p.age;
		cout << "拷貝函式" << endl;
	}
	~Person() {
		cout << "類被釋放" << endl;
	}
	int age;
};
void test01() {
// 使用一個已經建立完畢的物件來初始化一個新物件
	Person p1(20);
	Person p2(p1);
	cout << p2.age << endl;
}
void doWork( Person p ) {
	cout << "doWork" << endl;
}
void test02() {
// 值傳遞的方式給函式引數傳值
	Person p;
	doWork(p);
}
Person doWork02() {
	Person p1;
	return p1;
}
void test03() {
// 以值方式返回區域性物件
	Person p = doWork02();
}
int main() {
	//test01();
	//test02();
	test03();
	system("pause");
	return 0;
}

2.4 建構函式呼叫規則

預設情況下,C++編譯器至少給一個類新增3個函式

  • 預設建構函式(無參,函式體為空)
  • 預設解構函式(無參,函式體為空)
  • 預設拷貝函式,對屬性進行值拷貝

建構函式呼叫規則

  • 如果使用者定義有參建構函式,C++下不會提供預設無參構造,但是會提供預設拷貝構造
  • 如果使用者定義拷貝建構函式,C++不會再提供其他建構函式
#include <iostream>
using namespace std;


// 建構函式的呼叫規則

// 1. 建立一個類,C++編譯器會給每個類新增至少三個函式,解構函式,預設函式,拷貝函式

// 如果使用者定義有參建構函式,C++下不會提供預設無參構造,但是會提供預設拷貝構造
class Person {
public:
	//Person() {
	//	cout << "預設函式呼叫" << endl;
	//}
	~Person() {
		cout << "解構函式呼叫" << endl;
	}
	int age;
	Person(int age) {
		age = age;
		cout << age << endl;
	}
	//Person(const Person &p) {
	//	age = p.age;
	//	cout << "拷貝建構函式" << endl;
	// }
};
//void test1() {
//	Person p;
//	p.age = 18;
//	Person p2(p);
//	cout << p2.age << "拷貝" << endl;  // 自動呼叫拷貝函式
//}
void test2() {
	Person p(10);
	Person p2(p);
	cout << p2.age << "拷貝" << endl;
}
int main() {
	//test1();
	test2();
}

2.5 深拷貝與淺拷貝

深拷貝:簡單的賦值拷貝操作

淺拷貝:在堆區重新申請空間,進行拷貝操作

#include <iostream>
using namespace std;


// 深拷貝、淺拷貝
class Person {
public:
	int age;
	int* height;
	Person() {
		cout << "Person的預設建構函式呼叫" << endl;
	}
	Person(int age_, int height_) {
		cout << "Person的有參建構函式呼叫" << endl;
		age = age_;
		height = new int(height_);  // 將身高的資料建立到堆區
	}
	Person(const Person& p) {
		cout << "Person的拷貝建構函式呼叫" << endl;
		age = p.age;
		// height = p.height;  // 編譯器預設實現的就是這行程式碼——淺拷貝
		// 深拷貝操作
		height = new int(*p.height);
	}
	~Person() {
		// 析構程式碼,將堆區開闢的資料做釋放操作
		if (height != NULL) {
			delete height;
			height = NULL;  // 避免野指標的出現
		}
		cout << "Person的解構函式呼叫" << endl;
	}
};
void test() {
	Person p1(18, 160);
	cout << "p1的年齡為" << p1.age << "  p1的身高為" << *p1.height <<endl;
	Person p2(p1);  // 如果利用編譯器提供的拷貝建構函式,會做淺拷貝操作;淺拷貝帶來的問題是堆區的記憶體會重複釋放
	// 這個問題要用深拷貝來解決
	cout << "p2的年齡為" << p2.age << "  p2的身高為" << *p2.height <<endl;
}
int main() {
	test();
	system("pause");
	return 0;
}

2.6 初始化列表

作用

  • C++提供了初始化列表語法,用來初始化屬性

語法:建構函式():屬性1(值1)屬性2(值2)...{}

#include <iostream>
using namespace std;


// 初始化列表
class Person {
public:
	// 傳統初始化操作:利用傳參建構函式進行操作

	// 初始化列表初始化屬性
	// Person() :a(10), b(20), c(30) {
	// }
	// 更靈活的初始化
	Person(int a, int b, int c) :a(a), b(b), c(c) {
	}
	int a;
	int b;
	int c;
};
void test() {
	Person p(20, 30, 50);
	cout << "建立P" << endl;
	cout << p.a << p.b << p.c << endl;
}
int main() {
	test();
	system("pause");
	return 0;
}

2.7 類物件作為類成員

C++類中的成員可以是另一個類的物件,我們稱該成員為物件成員

class A {}
class B {
    A a;  // B類中有物件A作為成員,A為物件成員
}

那麼,當建立B物件時,A與B的構造和析構順序是誰先誰後呢?

#include <iostream>
using namespace std;


// 類物件作為類成員
class Phone {
public:
	string c_Pname;
	Phone(string pName) {
		c_Pname = pName;
		cout << "phone建構函式" << endl;
	}
	~Phone() {
		cout << "Phone解構函式" << endl;
	}
};
class Person {
public:
	string c_Name;
	Phone c_Phone;
	Person(string name, string pName): c_Name(name), c_Phone(pName) {
		cout << "person建構函式" << endl;
	}
	~Person() {
		cout << "person解構函式" << endl;
	}
};
void test() {
	Person p("張三", "水果13ProMax");
	cout << "c_Name:" << p.c_Name << "  c_Phone:" << p.c_Phone.c_Pname << endl;
}
int main() {
	test();
	system("pause");
	return 0;
}

構造的順序是:先呼叫物件成員的構造,在呼叫本類的構造

析構的順序相反

2.8 靜態成員

靜態成員就是在成員變數和成員函式前加上關鍵字static,稱為靜態成員

靜態成員分為

  • 靜態成員變數
    • 所有物件共享同一份資料
    • 在編譯階段分配記憶體
    • 類內宣告,類外初始化
  • 靜態成員函式
    • 所有物件共享同一個函式
    • 靜態成員函式只能訪問靜態成員變數
#include <iostream>
using namespace std;

// 靜態成員函式
class Person {
public:
	// 靜態成員函式
	static void func() {
		c_A = 100;
		// c_B = 100;  // 靜態成員函式不可以訪問非靜態成員變數
		cout << "static void func" << c_A << endl;
	}
	static int c_A;  // 靜態成員變數,類內申明
	int c_B;  // 非靜態成員變數
};
int Person::c_A = 10;  // 類外的初始化
void test() {
	// 訪問
	Person p;
	p.func();  // 通過物件訪問
	
	Person::func();  // 通過類名訪問  :: 為域操符
}
int main() {
	test();
	system("pause");
	return 0;
}

靜態函式也有訪問許可權

3、 C++ 物件模型和 this 指標

3.1 成員變數和成員函式分開儲存

在 C++ 中,類內成員變數和成員函式分開儲存

只有非靜態成員變數才屬於類的物件

#include <iostream>
using namespace std;

// 成員變數和成員函式分開儲存
class Person {
	int c_A;  // 非靜態成員變數,屬於類的物件
	static int c_B;  // 靜態成員變數,不屬於類物件上面
	void fn() {};  // 非靜態成員函式,不屬於類物件上
	static void  fn() {};  // 靜態成員函式 ,不屬於類的物件
};

void test01() {
	Person  p;
	/*
	空物件佔用記憶體空間為1
	 C++ 編譯器會給每個空物件也分配一個位元組空間,是為了區分空物件佔記憶體的位置
	每個空物件也應該有一個獨一無二的記憶體地址
	 */
	cout << sizeof(p) << endl;
}

int main() {
	test01();

	system("pause");
	return 0;
}

3.2 this 指標

每個非靜態成員函式只會誕生一份函式例項,也就是說多個同型別的物件會共用一塊程式碼

C++ 通過提供特殊的物件指標,this 指標來判斷這一塊程式碼是否被物件呼叫。

this 指標指向被呼叫的成員函式所屬的物件

this 指標是隱含每一個非靜態成員函式內的一種指標

this 指標不需要定義,直接使用即可

用途

  • 當形參和成員變數同名時,可用 this 指標來區分
  • 在類的非靜態成員函式中返回物件本身,可使用 return *this
#include <iostream>
using namespace std;

class Person {
public:
	Person(int age) {
		this->c_age = age;  // this 指標指向被呼叫的成員函式所屬的物件
	}
	int c_age;

	Person& addAge(Person &p) {  // Person& 返回值的引用,如果返回一個值,會建立新的物件
		this->c_age += p.c_age;
		return *this;  // this 指向呼叫函式的物件的指標,故 *this 指向的就是物件的本體 
	}
};

void test01() {
	Person p1(18);
	cout << "p1的年齡為" << p1.c_age << endl;
}
void test02() {
	Person p1(18);
	Person p2(19);

	// 鏈式程式設計思想
	p2.addAge(p1).addAge(p1);
	cout << "p2的年齡為" << p2.c_age << endl;
}

int main() {
	test01();
	test02();

	system("pause");
	return 0;
}

3.3 空指標訪問成員函式

C++ 中空指標也是可以呼叫成員函式的,但是也要注意有沒有用的 this 指標

如果用到 this 指標,需要加以判斷保證程式碼的健壯性

#include <iostream>
using namespace std;


class Person {
public:
	void showClassName() {
		cout << "this is Person class" << endl;
	}

	void showPersonAge() {
		// 報錯的原因是因為傳入的指標為 NULL
		if (this == NULL) {
			return;
		}
		cout << "age=" << c_Age << endl;
	}
	int c_Age;
};

void test01() {
	Person* p = NULL;

	p->showClassName();
	//p->showPersonAge();
}
int main() {
	test01();
	system("pause");
	return 0;
} 

3.4 const 修飾成員函式

常函式

  • 成員函式後加 const 後,我們稱這個函式為常函式
  • 常函式內不可以修改成員屬性
  • 成員屬性宣告時加關鍵字 mutable 後,在常函式中依然可以修改

常物件

  • 宣告函式前加 const 稱該物件為常物件
  • 常物件只能呼叫常函式
// 常函式
class Person {
public:

	// this 指標的本質是指標常量,指標的指向是不可以修改的
	void showPerson() const {  // -> 相當於 const Person * const this;  原本是 Person * const this; 
		// m_A = 100;  // 會報錯
		this->m_B = 100;
	}

	int m_A;
	mutable int m_B;  // 特殊變數,即使在常函式中,也可以修改這個值,加關鍵字 mutable
};

// 常物件
void test() {
	const Person p;  // 其為常物件
	// p.m_A = 100;  // 不允許修改
	p.m_B = 100;  // m_B 是特殊值,在常物件下也可以修改
	// 常物件只能呼叫常函式,其不能呼叫普通成員函式,因為普通成員函式可以修改屬性
}

4、 友元

在程式裡,有些私有屬性也想讓類外特殊的一些函式或者類訪問,就需要用到友元的技術

友元的目的就是讓一個函式或者類訪問另一個類中私有成員

友元的關鍵字為:friend

友元的三種實現

  1. 全域性函式做友元
  2. 類做友元
  3. 成員函式做友元

4.1 全域性函式

// 在類中,新增物件宣告。同時在開頭新增 friend
// 全域性函式做友元
class Building {
	// goodGay 全域性函式是 Building 的好朋友,可以訪問 Building 中私有成員
	friend void goodGay(Building* building);

public:
	Building() {
		m_Bedroom = "臥室";
		m_Sittingroom = "客廳";
	}

public:
	string m_Sittingroom;  

private:
	string m_Bedroom;
};

// 全域性函式
void goodGay(Building* building) {
	cout << "好盆友訪問" << building->m_Sittingroom << endl;
	cout << "好盆友訪問" << building->m_Bedroom << endl;
}
void test01() {
	Building building;
	goodGay(&building);

4.2 類

// 類做友元
class Building {
	// 友元
	friend class GoodGay;

public:
	Building();

public:
	string m_Sittingroom;  

private:
	string m_Bedroom;
};
// 類外寫成員函式
Building::Building() {
	m_Bedroom = "臥室";
	m_Sittingroom = "客廳";
}
// 類
class GoodGay {
public:
	GoodGay();

	void visitHouse();

	Building* building;
};
// 類外寫成員函式
GoodGay::GoodGay() {
	// 建立建築物物件
	building = new Building;
}
void GoodGay::visitHouse() {
	cout << "好朋友正在訪問" << building->m_Sittingroom << endl;
	cout << "好朋友正在訪問" << building->m_Bedroom << endl;
}

void test() {
	GoodGay gg;
	gg.visitHouse();
}

4.3 成員函式

class Building;  // 類宣告
class GoodGay {
public:
	GoodGay();

	void visit();  // 讓函式可以訪問 Building 中私有成員
	void visit1();  // 讓函式不可以訪問私有成員
	Building* building;
};
class Building {
	friend void GoodGay::visit();  // 使用域操符
public:
	Building();
	string m_Sitttingroom;
private:
	string m_Bedroom;
};
// 類外實現成員函式
GoodGay::GoodGay() {
	building = new Building;
}
void GoodGay::visit() {
	cout << "正在訪問" << building->m_Sitttingroom << endl;
	cout << "正在訪問" << building->m_Bedroom << endl;
}
void GoodGay::visit1() {
	cout << "正在訪問" << building->m_Sitttingroom << endl;
	// cout << "正在訪問" << building->m_Bedroom << endl;  // 報錯
}
Building::Building() {
	m_Sitttingroom = "客廳";
	m_Bedroom = "臥室";
}

void test() {
	GoodGay gg;
	gg.visit();
	cout << "---------"<< endl;
	gg.visit1();
}

5、 運算子過載

運算子過載概念:對已有的運算子重新進行定義,賦予其另一個功能,以適應不同的資料型別

5.1 加號運算子

作用:實現兩個自定義資料型別相加的運算

// 加號運算子過載
class Person {
public:
	// 成員函式過載
	Person operator+(Person& p) {
		Person temp;
		temp.m_A = this->m_A + p.m_A;
		temp.m_B = this->m_B + p.m_B;
		return temp;
	}
	int m_A;
	int m_B;
};
// 全域性函式過載
Person operator+(Person& p1, Person& p2) {
	Person temp;
	temp.m_A = p1.m_A + p2.m_A;
	temp.m_B = p1.m_B + p2.m_B;
	return temp;
}
void test() {
	Person p1;
	p1.m_A = 10;
	p1.m_B = 10;
	Person p2;
	p2.m_A = 10;
	p2.m_B = 10;

    // 成員函式的本質呼叫
    // Person p3 = p1.operator+(p2);
    // 全域性函式的本質呼叫
    // Person p3 = operator+(p1, p2);
	Person p3 = p1 + p2;
	cout << "p3.m_A:" << p3.m_A << "\tp3.m_B:" << p3.m_B << endl;
}

運算子過載也可以發生函式過載

Person operator+(Person& p1, int num) {
	Person temp;
	temp.m_A = p1.m_A + num;
	temp.m_B = p1.m_B + num;
	return temp;
}  // 數字相加

對於內建的資料型別的表示式的運算子是不可以改變的

不要濫用運算子過載

5.2 左移運算子

作用:可以輸出自定義的資料型別

// 左移運算子過載
class Person
{
public:
	int m_A;
	int m_B;
	/* 
	利用成員函式過載:p.operator<<(cout); -> 簡化函式 p << cout;
	不會利用成員函式過載<<運算子,因為無法實現 cout 在左側
	void operator<< (ostream& cout)
	{
		cout << "a:" << m_A << " b:" << m_B << endl;
	} 
	*/
};
void test()
{
	Person p;
	p.m_A = 10;
	p.m_B = 10;
	cout << p << endl;
}
// 只能利用全域性函式過載,ostream其屬於標準輸出流
ostream& operator<< (ostream& cout, Person p)  // 本質:operator<< (cout, p); -> 簡化 cout << p;
{
	cout << "a:" << p.m_A << " b:" << p.m_B;
	return cout;  // 鏈式程式設計,要有返回值
}

總結:過載左移運算子配合友元可以實現輸出自定義資料型別

5.3 遞增運算子

作用:通過過載遞增運算子,實現自己的整型資料

// 過載自增運算子

// 自定義整型
class MyInt
{
	friend ostream& operator<<(ostream& cout, MyInt i);  // 友元訪問私有元素
public:
	MyInt()
	{
		m_num = 0;
	}
	// 過載前置自增運算子
	MyInt& operator++()
	{
		++m_num;  // 自增操作
		return *this;
	}
	// 過載後置自增運算子
	MyInt operator++(int)  // int 代表佔位引數,用於區分前置遞增和後置遞增,注意不要使用引用
	{
		// 先記錄當時結果
		MyInt temp = *this;
		// 後自增
		m_num++;
		// 最後返回結果
		return temp;
	}
private:
	int m_num;
};
// 過載左移運算子
ostream& operator<<(ostream& cout, MyInt i)
{
	cout << i.m_num;
	return cout;
}
void test()
{
	MyInt i;
	cout << ++i << "--" << i << endl;
	cout << "=====" << endl;
	cout << i++ << "--" << i << endl;
}

5.4 賦值運算子

C++ 編譯器至少給一個類新增四個函式

  1. 預設建構函式(無參,函式體為空)
  2. 預設解構函式(無參,函式體為空)
  3. 預設拷貝函式,對屬性進行值拷貝
  4. 賦值運算子 operator=,對屬性進行值拷貝

如果中有屬性指向堆區,做賦值操作時也會出現深淺拷貝問題

class Person
{
public:
	Person(int age)
	{
		m_Age = new int(age);  // 將資料開闢到堆區
	}
	// 過載賦值運算子
	Person& operator=(Person& p)
	{
		// 編譯器提供淺拷貝 this->m_Age = p.m_Age;
		// 應該判斷是否有屬性在堆區,如果有先釋放乾淨,然後再深拷貝
		if (this->m_Age)  // 相當於 if (this->m_Age != NULL)
		{
			delete this->m_Age;  // 刪除資料
			m_Age = NULL;
		}
		m_Age = new int(*p.m_Age);
		// 返回物件本身
		return *this;
	}
	int* m_Age;
	~Person()
	{
		if (m_Age)  // 當m_Age不為空時
		{
			delete m_Age;
			m_Age = NULL;
		}
	}
};
void test()
{
	Person p1(10);
	Person p2(20);
	p2 = p1;  // 賦值操作,預設為淺拷貝
	cout << "p1的年齡為:" << *p1.m_Age << endl;
	cout << "p2的年齡為:" << *p2.m_Age << endl;
}

5.5 關係運算子

作用:過載關係運算子,可以讓兩個自定義物件進行對比操作

// 過載關係運算子
class Person
{
public:
	Person(string name, int age);
	// 過載 == 號,其餘類似
	bool operator==(Person& p); 
	string m_Name;
	int m_Age;
};
Person::Person(string name, int age)
{
	this->m_Name = name;
	this->m_Age = age;
}
bool Person::operator==(Person& p)
{
	if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
	{
		return true;
	}
	else
	{
		return false;
	}
}
void test()
{
	Person p1("Tom", 18);
	Person p2("Tom", 18);
	Person p3("李華", 18);
	if (p1 == p2)
	{
		cout << "p1 == p2" << endl;
	}
	else
	{
		cout << "p1 != p2" << endl;
	}
}

5.6 函式呼叫運算子

  • 函式呼叫運算子()也可過載
  • 由於過載後使用的方式非常像函式的呼叫,因此稱為仿函式
  • 仿函式沒有固定的寫法,非常靈活
class MyPrint
{
public:
	void operator()(string test)
	{
		cout << test << endl;
	}
};
class Add
{
public:
	int operator()(int num1, int num2)
	{
		return num1 + num2;
	}
};
void test()
{
	MyPrint myPrint;
	myPrint("hello world");  // 仿函式

	Add add;
	cout << "和為:" << add(1, 2) << endl;  // 仿函式非常靈活沒有固定的寫法
	cout << "和為:" << Add()(2, 3) << endl;  // 匿名的函式物件 `Add()`
}

6、 繼承

繼承是物件導向三大特性之一

有些類與類之間存在特殊關係

如:動物 -> 貓 -> 加菲貓

我們發現,定義這些類時,下級別的成員除了擁有上一級的共性,還有自己的屬性

這時候我們可以考慮利用繼承的技術,減少重複程式碼

6.1 基礎語法

語法:class 子類 : 繼承方式 父類

例如,很多網站中,都有公共的頭部,公共的底部,甚至公共的左側列表欄

/*
class Java
{
public:
	Java()  // 使用建構函式,輸出內容
	{
		cout << "Java 頁面:" << endl;
		header();
		buttom();
		left();
		content();
		cout << "=====" << endl;
	}
	void header()
	{
		cout << "首頁、公開課、登入、註冊..." << endl;
	}
	void buttom()
	{
		cout << "幫助中心、交流合作、站內地圖..." << endl;
	}
	void left()
	{
		cout << "Java / Python / C++ ..." << endl;
	}
	void content()
	{
		cout << "關於 Java 的課程" << endl;
	}
};
class Python  // 許多內容和 Java 的內容一樣
{
public:
	Python()  
	{
		cout << "Python 頁面:" << endl;
		header();
		buttom();
		left();
		content();
		cout << "=====" << endl;
	}
	void header()
	{
		cout << "首頁、公開課、登入、註冊..." << endl;
	}
	void buttom()
	{
		cout << "幫助中心、交流合作、站內地圖..." << endl;
	}
	void left()
	{
		cout << "Java / Python / C++ ..." << endl;
	}
	void content()
	{
		cout << "關於 Python 的課程" << endl;
	}
};
// 裡面有很多函式重複
*/
// 使用繼承思想
class PublicPage  // 定義公共頁面
{
public:
	void header()
	{
		cout << "首頁、公開課、登入、註冊..." << endl;
	}
	void buttom()
	{
		cout << "幫助中心、交流合作、站內地圖..." << endl;
	}
	void left()
	{
		cout << "Java / Python / C++ ..." << endl;
	}
};
class Java : public PublicPage
{
public:
	Java()
	{
		cout << "Java 頁面:" << endl;
		header();
		buttom();
		left();
		content();
		cout << "=====" << endl;
	}
	void content()
	{
		cout << "關於 Java 的課程" << endl;
	}

};
class Python : public PublicPage
{
public:
	Python()
	{
		cout << "Python 頁面:" << endl;
		header();
		buttom();
		left();
		content();
		cout << "=====" << endl;
	}
	void content()
	{
		cout << "關於 Python 的課程" << endl;
	}

};
void test()
{
	Java java;
	Python python;
}

子類也稱為派生類

父類也稱為基類

6.2 繼承方式

繼承不能訪問私有許可權的內容

繼承方式有三種

  • 公共繼承

    • 基類中所有 public 成員在派生類中為 public 屬性
    • 基類中所有 protected 成員在派生類中為 protected 屬性
    • 基類中所有 private 成員在派生類中不能使用
  • 保護繼承

    • 基類中的所有 public 成員在派生類中為 protected 屬性
    • 基類中的所有 protected 成員在派生類中為 protected 屬性
    • 基類中的所有 private 成員在派生類中不能使用。
  • 私有繼承

    • 基類中的所有 public 成員在派生類中均為 private 屬性;
    • 基類中的所有 protected 成員在派生類中均為 private 屬性
    • 基類中的所有 private 成員在派生類中不能使用
繼承方式/基類成員 public成員 protected成員 private成員
public繼承 public protected 不可見
protected繼承 protected protected 不可見
private繼承 private private 不可見

6.3 物件模型

從父類繼承過來的成員,哪些屬於子類物件中?

// 繼承中的物件模型
class Base
{
public:
	int m_A;
private:
	int m_B;
protected:
	int m_C;
};
class Son : public Base
{
public:
	int m_D;
};
void test()
{
	cout << "size of son:" << sizeof(Son) << endl;
	// 在父類中所有非靜態成員屬性都會被子類繼承下去
	// 父類中私有成員屬性,是被編譯器給隱藏了,因此是訪問不到的,但是繼承下去了
}

利用開發人員命令提示工具檢視物件模型

  1. 跳轉碟符
  2. 跳轉檔案路徑:cd 具體路徑
  3. 檢視命令:cl /d1 reportSingleClassLayout類名 檔名

6.4 構造和析構順序

子類繼承父類後,當建立子類物件,也會呼叫父類的建構函式

class Base
{
public:
	Base()
	{
		cout << "Base的建構函式" << endl;
	}
	~Base()
	{
		cout << "Base的解構函式" << endl;
	}
};
class Son : public Base
{
public:
	Son()
	{
		cout << "Son的建構函式" << endl;
	}
	~Son()
	{
		cout << "Son的解構函式" << endl;
	}

};
void test()
{
	Son son;  // 套娃,白髮人送黑髮人
}

6.5 同名成員處理

當子類與父類出現同名成員,如何通過子類物件,訪問到子類或父類中同名的資料呢?

  • 訪問子類同名成員:直接訪問
  • 訪問父類同名成員:需要加作用域
class Base
{
public:
	Base()
	{
		m_A = 100;
	}
	int m_A;
	void fn() 
	{
		cout << "Base" << endl;
	}
    void fn(int a)
    {
        cout << a << endl;
    }

};
class Son : public Base
{
public:
	Son()
	{
		m_A = 101;
	}
	int m_A;
	void fn()
	{
		cout << "Son" << endl;
	}
};
void test()
{
	Son s;
	cout << "s.m_A=" << s.m_A << endl;  // 直接訪問
	cout << "Base.m_A=" << s.Base::m_A << endl;  // 新增作用域
	s.fn();
	s.Base::fn();
    s.Base::fn(100);
}

同名函式呼叫方式類似:新增作用域

如果子類中出現和父類同名的成員函式,子類的同名成員函式會隱藏掉父類中所有同名成員函式

如果想訪問到父類中被隱藏的同名成員函式,需要加作用域

6.6 同名靜態成員處理

靜態成員和非靜態成員出現同名,處理方式一致

  • 訪問子類同名成員,直接訪問即可
  • 訪問父類同名成員,需要加作用域
class Base
{
public:
	static int m_A;  // 類內宣告
	static void fn()
	{
		cout << "Base" << endl;
	}
};
int Base::m_A = 100;  // 類外要初始化
class Son : public Base
{
public:
	static int m_A;
	static void fn()
	{
		cout << "Son" << endl;
	}
};
int Son::m_A = 101;

void test() 
{
	cout << "通過類名訪問資料" << endl;
	Son s;
	cout << "s.m_A=" << s.m_A << endl;
	cout << "Base.m_A=" << s.Base::m_A << endl;

	cout << "通過類名訪問資料" << endl;
	cout << "Son.m_A=" << Son::m_A << endl;
	cout << "Base.m_A=" << Son::Base::m_A << endl; 
	// 第一對冒號代表通過類名的方式訪問作用域,第二對冒號代表訪問父類作用域下
}

函式呼叫方式類似

6.7 多繼承語法

C++ 允許一個類繼承多個類

語法:class 子類 : 繼承方式 父類1, 繼承方式 父類2···

多繼承可能會引發父類中有同名成員出現,需要加作用域區分

C++ 實際開發不建議使用多繼承

6.8 菱形繼承

C++核心程式設計

問題

  1. 當 B 繼承了 A 的資料,C 同樣繼承了 A 的資料,當 D 使用資料時,就會產生二義性
  2. D 繼承了兩份資料,其實,我們清楚只要繼承一份資料就可以了
class A
{
public:
	int m_Age;
};
/* 
利用虛繼承解決菱形繼承的問題
繼承之前,加上關鍵字 virtual 變為虛繼承
A 類稱為虛基類
*/
class B : virtual public A{};
class C : virtual public A{};
class D : public B, public C{};
void test()
{
	D d;
	d.B::m_Age = 18;
	d.C::m_Age = 28;
	// 當出現菱形繼承時,兩個父類擁有相同的資料,需要加以區分
	cout << "B.m_Age = " << d.B::m_Age << endl;
	cout << "C.m_Age = " << d.C::m_Age << endl;
	// 這份資料只要有一份資料就可以了,菱形繼承導致有兩份資料,資源浪費
}

利用虛繼承解決菱形繼承的問題
繼承之前,加上關鍵字 virtual 變為虛繼承
A 類稱為虛基類

7、 多型

7.1 基本概念

多型是 C++ 物件導向三大特性之一

多型分為兩類

  • 靜態多型:[函式過載](#3、 函式過載) 和 [運算子過載](#5、 運算子過載)屬於靜態多型,複用函式名
  • 動態多型:派生類 和 虛擬函式實現執行時多型

靜態多型 和 動態多型的區別

  • 靜態多型的函式地址早繫結 - 編譯階段函式地址
  • 動態多型的函式地址晚繫結 - 執行階段確定函式地址
class A
{
public:
	virtual void speak()  // 虛擬函式,可以實現地址晚繫結
	{
		cout << "A is speaking" << endl;
	}
	
};
class B : public A 
{
public:
	void speak()
	{
		cout << "B is speaking" << endl;
	}
};
// 執行說話函式
void doSpeak(A& a)  // A& a = b;
{
	a.speak();  // 如果沒加virtual,則地址早繫結,在編譯階段確定函式地址
	// 如果要執行 b.speak(); 需要在執行階段進行繫結,地址晚繫結
}
void test()
{
	B b;
	doSpeak(b);
}

重寫函式:函式返回值型別,函式名,引數列表完全相同

動態多型滿足條件

  • 有繼承關係
  • 子類重寫父類的虛擬函式,子類重寫函式可以不為虛擬函式

動態多型使用

  • 父類指標或者引用,執行子類物件

多型優點

  • 程式碼組織結構清晰
  • 可讀性強
  • 利於前期和後期的擴充套件以及維護

7.2 案例-計算器類

// 如果有擴充套件功能,要實現開閉原則:對擴充套件進行開放,對修改進行關閉
class AbstarctCalc  // 抽象類
{
public:
	virtual int getResult()  // 虛擬函式
	{
		return 0;
	}
	int m_A;
	int m_B;
};
class Add : public AbstarctCalc  // 加法
{
public:
	int getResult()
	{
		return m_A + m_B;
	}
};
class Sub : public AbstarctCalc
{
public:
	int getResult()
	{
		return m_A - m_B;
	}
};
void test()
{
	// 多型使用條件
	// 父類指標或引用指向子類物件
	AbstarctCalc* abc = new Add;
	abc->m_A = 10;
	abc->m_B = 20;
	cout << abc->m_A << " + " << abc->m_B << " = " << abc->getResult() << endl;  // 執行子類物件
	delete abc;  // 銷燬資料
	
	abc = new Sub;  
	abc->m_A = 10;
	abc->m_B = 20;
	cout << abc->m_A << " - " << abc->m_B << " = " << abc->getResult() << endl; 
}

7.3 純虛擬函式和抽象類

在多型中,通常父類中虛擬函式的實現是毫無意義的,主要都是呼叫子類重寫的內容

因此,可以將虛擬函式改為純虛擬函式

純虛擬函式語法:virtual 返回值型別 函式名 (引數列表) = 0

當類中有了純虛擬函式,這個類也稱為抽象類

抽象類的特點

  • 無法實現例項化物件
  • 子類必須重寫抽象類中的純虛擬函式,否則也屬於抽象類
class Base
{
public:
	virtual void func() = 0;
};
class Son : public Base
{
public:
	void func()
	{
		cout << "Son" << endl;
	}
};
void test(Base& b)  // 引用父類指標
{
	b.func();
}
void test()
{
	Son s;
	test(s);
}

7.4 虛析構和純虛析構

多型使用時,如果子類中有屬性開闢到堆區,那麼父類指標在釋放時無法呼叫到子類的析構程式碼

解決方式:將父類中的解構函式改為虛析構或者純虛析構

虛析構和純虛析構的共性

  • 可以解決父類指標釋放子類物件
  • 都需要有具體的函式實現

虛析構和純虛析構的區別

  • 如果是純虛析構,該類屬於抽象類,無法例項化物件

虛析構語法:virtual ~類名(){}

純虛析構語法:類內部:virtual ~類名() = 0; | 類外:類名::~類名() {}

// 虛析構和純虛析構
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	virtual void speak() = 0;
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
	// 純虛析構 virtual ~A() = 0;
};
// A::~A(){}  // 純虛析構需要有函式實現
class C : public A
{
public:
	C(string name)
	{
		cout << "C()" << endl;
		m_name = new string(name);  // 將資料開闢到堆區
	}
	void speak()
	{
		cout << *m_name << " is speaking" << endl;
	}
	string* m_name;  // 沒有加 virtual 時,沒有呼叫此解構函式
	~C()
	{
		if (m_name)
		{
			cout << "~C()" << endl;
			delete m_name;
			m_name = NULL;
		}
	}
};
void test()
{
	A* a = new C("Tom");
	a->speak();
	// 父類指標在析構時候,不會呼叫子類中解構函式,導致子類如果有堆區的屬性,會出現資源浪費
	delete a;
}

虛解構函式或純虛解構函式使用來解決通過父類指標釋放子類物件問題

如果子類中沒有資料開闢到堆區,可以不寫為虛析構或純虛析構

擁有純虛解構函式的類也屬於抽象類

五、 檔案操作

程式執行時產生的資料都屬於臨時資料,程式一旦執行結束都會被釋放

通過檔案可以將資料持久化儲存

C++中對檔案操作需要包括標頭檔案<fstream>

  1. 文字檔案:檔案以文字的 ASCII 碼形式儲存在計算機中
  2. 二進位制檔案:檔案以文字的二進位制形式儲存在計算機中,使用者一般不能直接讀懂

操作檔案分為三大類

  1. ofstream:寫操作
  2. ifstream:讀操作
  3. fstream:讀寫操作

1、 文字檔案

1.1 寫檔案

1.1.1 步驟

  1. 包含標頭檔案

    • #include <fstream>
      
  2. 建立流物件

    • ofstream ofs;
      
  3. 開啟檔案

    • ofs.open("檔案路徑", 開啟方式);
      
  4. 寫資料

    • ofs << "寫入的資料";
      
  5. 關閉檔案

    • ofs.close();
      

1.1.2 檔案開啟方式

檔案開啟模式標記
模式標記 適用物件 作用
ios::in ifstream
fstream
開啟檔案用於讀取資料。如果檔案不存在,則開啟出錯。
ios::out ofstream
fstream
開啟檔案用於寫入資料。如果檔案不存在,則新建該檔案;如果檔案原來就存在,則開啟時清除原來的內容。
ios::app ofstream
fstream
開啟檔案,用於在其尾部新增資料。如果檔案不存在,則新建該檔案。
ios::ate ifstream 開啟一個已有的檔案,並將檔案讀指標指向檔案末尾(讀寫指 的概念後面解釋)。如果檔案不存在,則開啟出錯。
ios:: trunc ofstream 開啟檔案時會清空內部儲存的所有資料,單獨使用時與 ios::out 相同。
ios::binary ifstream
ofstream
fstream
以二進位制方式開啟檔案。若不指定此模式,則以文字模式開啟。
ios::in | ios::out fstream 開啟已存在的檔案,既可讀取其內容,也可向其寫入資料。檔案剛開啟時,原有內容保持不變。如果檔案不存在,則開啟出錯。
ios::in | ios::out ofstream 開啟已存在的檔案,可以向其寫入資料。檔案剛開啟時,原有內容保持不變。如果檔案不存在,則開啟出錯。
ios::in | ios::out | ios::trunc fstream 開啟檔案,既可讀取其內容,也可向其寫入資料。如果檔案本來就存在,則開啟時清除原來的內容;如果檔案不存在,則新建該檔案。

檔案開啟方式可以配合|使用

1.2 讀檔案

讀檔案和寫檔案步驟相似,但是讀取方式相對比較多

  1. 包含標頭檔案

    • #include <fstream>
      
  2. 建立流物件

    • ifstream ifs;
      
  3. 開啟檔案

    • ifs.open("檔案路徑", 開啟方式);
      if (!ifs.is_open())
      {
          cout << 檔案開啟失敗 << endl;
          return;
      }  // ifs.is_open() 判斷檔案是否開啟失敗
      
  4. 讀資料

    • // 四種方式讀取
      // 第一種
      char buf[1024] = { 0 };  // 初始化字元陣列
      while (ifs >> buf)
      {
          cout << buf << endl;
      }
      // 第二種
      char buf[1024] = { 0 };  // 初始化字元陣列
      while (ifs.getline(buf, sizeof(buf)))
      {
          coutt << buf << endl;
      }
      // 第三種
      string buf;
      while (getline(ifs, buf))
      {
          cout << buf << endl;
      }
      // 第四種(不推薦)
      char c;
      while (c = ifs.get() != EOF)  // EOF 檔案結尾
      {
          cout << c;
      }
      
  5. 關閉檔案

    • ifs.close();
      

2、 二進位制檔案

以二進位制的方式對檔案進行讀寫操作

開啟方式為 ios::binary

2.1 寫檔案

二進位制方式寫檔案主要利用流物件呼叫成員函式write

語法:ostream& write(const char * buffer, int len);

引數

  • buffer:字元指標,指向記憶體中一段儲存空間
  • len:讀寫的位元組數
class Person
{
public:
	char m_Name[64];
	int m_Age;
};
// 包含標頭檔案
// #include <fstream>

// 建立流物件
ofstream ofs;

// 開啟檔案
ofs.open("Person.txt", ios::binary | ios::out);
// 也可以:ofstream ofs("Person.txt", ios::binary | ios::out);

// 寫檔案
Person p = { "張三", 18 };
ofs.write((const char*)&p, sizeof(Person));

// 關閉檔案
ofs.close();

2.2 讀檔案

二進位制方式讀檔案主要利用流物件呼叫成員函式read

語法:istream& read(char *buffer, int len)

引數

  • buffer:字元指標,指向記憶體中一段儲存空間
  • len:讀寫的位元組數
class Person
{
    public:
    char m_Name[64];
    int m_Age;
};
// 包含標頭檔案 #include <fstream>

// 建立流物件
ifstream ifs;
// 開啟檔案,並判斷檔案是否開啟成功
ifs.open("Person.txt", ios::in | ios::binary);  
if (!ifs.is_open())
{
    cout << "檔案開啟失敗" << endl;
    return;
}
// 讀檔案
Person p;
ifs.read((char*)&p, sizeof(Person));
cout << "姓名:" << p.m_Name << " 年齡:" << p.m_Age << endl;

// 關閉檔案
ifs.close();

相關文章