C++核心程式設計

离盏發表於2024-08-02

C++核心程式設計

主要針對C++物件導向程式設計技術,探討C++中的核心和精髓

1 記憶體分割槽模型

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

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

記憶體四區的意義:

不同區域存放的資料,賦予不同生命週期,給我們更大的靈活程式設計

1.1 程式執行前

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

程式碼區:

​ 存放CPU執行的機器指令

​ 程式碼是共享的,共享的目的是對於頻繁被執行多的程式,只需要在記憶體中有一份程式碼即可

​ 程式碼是隻讀的,使其只讀的原因是防止程式意外地修改了它的指令

全域性區:

​ 全域性變數和靜態變數存放在此

​ 全域性變數還包含了常量區,字串常量和其他常量也存放在此

​ 該區域的資料在程式結束後由作業系統釋放

示例

#include <iostream>
#include <string>
using namespace std;

int g_a = 10;
int g_b = 10;

const int c_g_a = 10;
const int c_g_b = 10;

int main()
{
	//區域性變數
	int a = 10;
	int b = 10;

	//列印地址
	cout << "區域性變數a地址為:" << (int)&a << endl;
	cout << "區域性變數b地址為:" << (int)&b << endl;

	cout << "區域性變數g_a地址為:" << (int)&g_a << endl;
	cout << "區域性變數g_b地址為:" << (int)&g_b << endl;

	//靜態變數
	static int s_a = 10;
	static int s_b = 10;

	cout << "靜態變數s_a地址為:" << (int)&s_a << endl;
	cout << "靜態變數s_b地址為:" << (int)&s_b << endl;

	cout << "字串常量地址為:" << (int)&"hello world" << endl;
	cout << "字串常量地址為:" << (int)&"hello world1" << endl;

	cout << "全域性常量c_g_a地址為:" << (int)&c_g_a << endl;
	cout << "全域性常量c_g_b地址為:" << (int)&c_g_b << endl;

	const int c_l_a = 10;
	const int c_l_b = 10;

	cout << "區域性常量c_l_a地址為:" << (int)&c_l_a << endl;
	cout << "區域性常量c_l_b地址為:" << (int)&c_l_b << endl;

	system("pause");

	return 0;
}

列印結果

總結

  • C++中在程式執行前分為全域性區和程式碼區
  • 程式碼區特點是共享和只讀
  • 全域性區中存放全域性變數、靜態變數、常量
  • 常量區中存放const修飾的全域性常量字串常量

1.2 程式執行後

棧區

​ 由編譯器自動分配釋放,存放函式的引數值,區域性變數等

​ 注意事項:不要返回區域性變數的地址,棧區開闢的資料由編譯器自動釋放

示例

#include <iostream>
using namespace std;

//棧區資料注意事項 ---- 不要返回區域性變數的地址
//棧區的資料由編譯器管理開闢和釋放

int *func(/*int a 形引數據也會放在棧區*/)
{
	int a = 10;//區域性變數  存放在棧區,棧區的資料在函式執行完後自動釋放
	return &a;//返回區域性變數的地址
}

int main()
{
	//接受func函式的返回值
	int * p = func();

	cout << *p << endl;//第一次可以列印正確的數字,是因為編譯器做了保留
	cout << *p << endl;//第二這個資料就不再保留了

	system("pause");

	return 0;
}

堆區

​ 由程式設計師分配釋放,若程式設計師不釋放,程式結束時由作業系統回收

​ 在C++中主要利用new在堆區中開闢記憶體

1.3 new運算子

​ C++中利用new運算子在堆區開闢資料

​ 堆區開闢的資料,由程式設計師手動開闢,手動釋放,釋放利用運算子delete

​ 語法new資料型別

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

示例

#include <iostream>
using namespace std;

//1、new的基本語法
int * func()
{
	//在堆區建立整型資料
	//new返回是 該資料型別的指標
	int *p = new int(10);
	return p;
}

void test01()
{
	int* p = func();
	cout << *p << endl;
	cout << *p << endl;
	cout << *p << endl;
	//堆區的資料 由程式設計師管理開闢,程式設計師管理釋放
	//如果想釋放堆區中的資料,利用關鍵字delete
	delete p;
	//cout << *p << endl; //記憶體已經被釋放,再次訪問就是非法操作,會報錯

}

//2、在堆區利用new開闢陣列
void test02()
{
	//建立10整形資料的陣列,在堆區
	int *arr = new int[10];	//10代表陣列由10個元素

	for(int i = 0; i < 10; i++)
	{
		arr[i] = i + 100;//給10個元素賦值		100 ~ 109
	}

	for(int i = 0; i < 10; i++)
	{
		cout << arr[i] << endl;
	}

	//釋放堆區陣列
	//釋放陣列的時候 要加一個[]才可以
	delete[] arr;
}

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

	system("pause");

}

2 引用

2.1 引用的基本使用

作用:給變數起別名

語法:資料型別 &別名 原名

示例

#include <iostream>
using namespace std;

int main()
{
	
	//引用基本語法
	//資料型別	&別名 = 原名

	int a = 10;
	//建立引用
	int &b = a;

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	b = 100;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	system("pause");

}

2.2 引用注意事項

  • 引用必須初始化
  • 引用在初始化後,不可改變

示例

#include <iostream>
using namespace std;

int main()
{
	int a = 10;

	//1、引用必須初始化
	//int &b;	//錯誤,必須要初始化
	int &b = a;

	//2、引用在初始化後,不可以改變
	int c = 20;

	b = c;//賦值操作,而不是更改引用

	cout << "a =" << a << endl;
	cout << "b =" << b << endl;
	cout << "c =" << c << endl;

	system("pause");

}

2.3 引用做函式引數

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

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

示例:

#include <iostream>
using namespace std;

//1、值傳遞
void mySwap01(int a,int b)
{
	int temp = a;
	a = b;
	b = temp;

	/*cout << "Swap01 a =" << a << endl;
	cout << "Swap01 b =" << b << endl;*/
}

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

//3、引用傳遞
void mySwap03(int &a,int &b)
{
	int temp = a;
	a = b;
	b = temp;
}


int main()
{
	int a = 10;
	int b = 20;
//	mySwap01(a, b);//值傳遞,形參不會修飾實參
//	mySwap02(&a, &b);//地址傳遞,形參會修飾實參
	mySwap03(a, b);//引用傳遞,形參也會修飾實參

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	system("pause");
  	return 0
}

總結:透過引用產生的效果同按地址傳遞是一樣的,引用的語法更清楚簡單

2.4 引用做函式返回值

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

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

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

示例

#include <iostream>
using namespace std;

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

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


int main()
{
	
	int &ref = test01();

	cout << "ref = " << ref << endl;//第一次結果正確,是因為編譯器做了保留
	cout << "ref = " << ref << endl;//第二次結果錯誤,是因為a的記憶體已經釋放

	int &ref2 = test02();

	cout << "ref2 = " << ref2 << endl;
	cout << "ref2 = " << ref2 << endl;

	test02() = 1000;//如果函式的返回值是引用,這個函式呼叫可以作為左值
	cout << "ref2 = " << ref2 << endl;
	cout << "ref2 = " << ref2 << endl;

	system("pause");
	return 0;
}

2.5 引用的本質

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

​ 引用一旦初始化,就不可以改變

講解示例:

#include <iostream>
using namespace std;

//發現是引用,轉換為 int* const ref = 100
void func(int& ref)
{
	ref = 100;//ref是引用,轉換為*ref = 100
}

int main()
{
	int a;

	//自動轉換為 int* const ref = &a;指標常量是指標指向不改變,也說明為什麼引用不可更改
	int& ref = a;
	ref = 20;//內部發現ref是引用,自動幫我們轉換為: *ref = 20

	cout << "a:" << a << endl;
	cout << "ref:" << ref << endl;

	func(a);

	system("pause");
	return 0;
}

結論:C++推薦使用引用技術,因為語法方便,引用本質是指標常量,但是所有的指標操作編譯器都幫我們做了

2.6 常量引用

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

在函式形參列表中,可以加const修飾形參,防止形參改變實參

示例:

#include <iostream>
using namespace std;

//列印資料函式
void ShowValue(const int & val)
{
	//val = 1000;
	cout << "val = " << val << endl;
}

int main()
{
	//常量引用
	//使用場景:用來修飾形參防止誤操作

	//int a = 10;

	//加上const之後 編譯器將程式碼修改  int temp = 10; const int &ref = temp;
	//const int & ref = 10;//引用必須引一塊合法的記憶體空間
	//ref = 20; //加入const之後變為只讀,不可以修改
	int a = 100;
	ShowValue(a);
	cout << "a = " << a <<endl;

	system("pause");
	return 0;
}

3 函式提高

3.1 函式預設引數

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

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

示例:

#include <iostream>
using namespace std;

//函式預設引數

//如果我們自己傳入資料,就用自己的資料,如果沒有,那麼用預設值
int func(int a, int b = 20, int c = 30)
{
	return a + b + c;
}


//注意事項
//1、如果某個位置已經有了預設引數,那麼從這個位置往後,從左到右都必須有預設值
//int func2(int a = 10, int b, int c, int d)
//{
//	return a + b + c;
//}

//2、如果函式宣告有預設引數,函式實現就不能有預設引數
//宣告和實現只能有一個有預設引數
int func2(int a = 10, int b = 10);

int func2(int a, int b)
{
	return a + b;
}

int main()
{
	cout << func(10) << endl; 
	cout << func2() << endl; 

	system("pause");
	return 0;
}

3.2 函式佔位引數

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

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

在現階段函式的佔位引數存在意義不大,但是後面會擁到該技術

示例:

#include <iostream>
using namespace std;

//佔位引數
//返回值型別	函式名(資料型別)

void func(int a, int)
{
	cout << "this is func " << endl;
}

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

	system("pause");
}

3.3 函式過載

3.3.1 函式過載概述

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

函式重灌載滿足條件:

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

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

示例:

#include <iostream>
using namespace std;

//函式過載
//可以讓函式名相同,提高複用性

//函式過載的滿足條件
//1、同一個作用域下
//2、函式名稱相同
//3、函式引數型別不同,或者個數不同,或者順序不同
void func()
{
	cout << "func 的呼叫" << endl;
}

void func(int a)
{
	cout << "func(int a) 的呼叫" << endl;
}

void func(double a)
{
	cout << "func(double a) 的呼叫" << endl;
}

void func(double a, int b)
{
	cout << "func(double a, int b) 的呼叫" << endl;
}

void func(int a, double b)
{
	cout << "func(int a, double b) 的呼叫" << endl;
}

//注意事項
//函式的返回值不可以作為函式過載的條件
//int func(int a, double b)
//{
//	cout << "func(int a, double b) 的呼叫" << endl;
//}

int main()
{
	//func();
	//func(10);
	//func(3.14);
	//func(10, 3.14);
	func(3.14, 10);

	system("pause");
}

3.3.2 函式過載注意事項

  • 引用作為過載條件
  • 函式過載碰到函式預設引數

示例:

#include <iostream>
using namespace std;

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

void func(const int &a)	//const int &a = 10;	合法
{
	cout << "func(const int &a) 的呼叫" << endl;
}

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

void func2(int a)
{
	cout << "func2(int a) 的呼叫" << endl;
}

int main()
{
	//int a = 10;
	//func(a);

	//func(10);

	//func2(10);//當函式過載碰到預設引數,出現二義性,報錯,儘量避免這種情況
	//func(10, 10);合法

	system("pause");
    return 0;
}

4 類和物件

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

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

例如:

​ 人可以作為物件,屬性有姓名、身高、體重...,行為有走、跑、跳、吃飯、唱歌

​ 車也可以作為物件,屬性有輪胎,方向盤,車燈...,行為有載人,放音樂,放空調

​ 具體相同性質的物件,我們可以抽象為類,人屬於人類,車屬於車類

4.1 封裝

4.1.1 封裝的意義

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

封裝的意義:

  • 將屬性和行為作為一個整體,表現生活中的事務
  • 將屬性和行為加以許可權控制

封裝的意義一:

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

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

示例1:設計一個圓類,求圓的周長

#include <iostream>
using namespace std;

//圓周率
const double PI = 3.14;

//設計一個圓類,求圓的周長
//圓求周長的公式:2 * PI * 半徑

class Circle
{
	//訪問許可權
	//公共許可權
public:

	//屬性
	//半徑
	int m_r;

	//行為
	//獲取圓的周長
	double calculateZC()
	{
		return 2 * PI * m_r;
	}
};

int main()
{
	//透過圓類建立一個具體的圓(物件)
	//例項化 (透過一個類 建立一個物件的過程)
	Circle cl;
	//給圓物件 的屬性進行賦值
	cl.m_r = 10;

	//2 * PI * 10 = 62.8
	cout << "圓的周長為:" << cl.calculateZC() << endl;

	system("pause");
	return 0;
}

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

#include <iostream>
#include <string>
using namespace std;
//設計一個學生類,屬性有姓名和學號
//可以給姓名和學號賦值,可以顯示學生的姓名和學號

//設計學生類
class student 
{
public:		//公共許可權
	//類中的屬性和行為 我們統一稱為 成員
	//屬性	成員屬性	成員變數
	//行為	成員函式	成員方法
	//屬性
	string m_Name;	//姓名
	int m_Id;	//學號

	//行為
	//顯示姓名和學號
	void showStudent()
	{
		cout << "姓名: " << m_Name << "  學號: " << m_Id << endl;
	}

	//給姓名賦值
	void SetName(string name)
	{
		m_Name = name;
	}

	//給學號賦值
	void SetId(int id)
	{
		m_Id = id;
	}
};

int main()
{
	//建立一個具體學生	例項化物件
	student s1;
	//給s1物件	進行屬性賦值操作
	//s1.m_Name = "張三";
	s1.SetName("張三");
	//s1.m_Id = 1;
	s1.SetId(1);
	//顯示學生資訊
	s1.showStudent();

	student s2;
	//給s2物件	進行屬性賦值操作
	s2.m_Name = "李四";
	s2.m_Id = 2;
	//顯示學生資訊
	s2.showStudent();

	system("pause");
	return 0;
}

封裝意義二:

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

訪問許可權有以下三種:

  1. punlic 公共許可權
  2. protected 保護許可權
  3. private 私有許可權

示例:

#include <iostream>
using namespace std;

//訪問許可權
//三種
//公共許可權	public		成員  類內可以訪問 類外可以訪問
//保護許可權	protected	成員  類內可以訪問 類外不可以訪問	兒子可以訪問父親中的保護內容
//私有許可權	private		成員  類內可以訪問 類外不可以訪問	兒子不可以訪問父親的私有內容

class Person 
{
public:
	//公共許可權
	string m_Name;

protected:
	//保護許可權
	string m_Car;

private:
	//私有許可權
	int m_Password;

public:
	void func()
	{
		m_Name = "張三";
		m_Car = "拖拉機";
		m_Password = 123456;
	}
};

int main()
{
	Person p1;
	p1.m_Name = "李四";
	//p1.m_Car = "賓士";	//保護許可權內容,在類外訪問不到
	//p1.m_Password = 123;//私有許可權內容,在類外訪問不到
	p1.func();

	system("pause");
	return 0;
}

4.1.2 struct 和 class 區別

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

區別:

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

class C1
{
	int m_A;	//預設許可權	是私有
};

struct C2
{
	int m_A;	//預設許可權	是公共
};

int main()
{
	//struct 和class區別
	//struct 預設許可權是	公共 public
	//class	 預設許可權是	私有 private

	C1 c1;
	//c1.m_A = 100;	//在class中預設許可權為私有,因此不可以訪問

	C2 c2;
	c2.m_A = 100;	//在struct中預設許可權為公共,因此可以訪問

	system("pause");
	return 0;
}

4.1.3 成員屬性設定為私有

優點1:將所有成員屬性設定為私有,可以自己控制讀寫許可權

優點2:對於寫許可權,我們可以檢測資料的有效性

#include <iostream>
using namespace std;

//成員屬性設定私有
//1、可以自己控制讀寫許可權
//2、對於寫可以檢測資料的有效性

//人類
class Person
{
public:
	//設定姓名
	void setName(string name)
	{
		m_Name = name;
	}
	//獲取姓名
	string getName()
	{
		return m_Name;
	}
	//獲取年齡
	int getAge()
	{
		return m_Age;
	}
	//設定年齡(0~150)
	void setAge(int age)
	{
		if (age < 0 || age > 150)
		{
			cout << "年齡" << age << "輸入有誤,賦值失敗" << endl;
			return;
		}
		m_Age = age;
	}
	//設定偶像
	void setm_Idol(string idol)
	{
		m_Idol = idol;
	}

private:
	string m_Name;	//姓名	可讀可寫

	int m_Age = 18;	//年齡	只讀			也可以寫(年齡必須在0~150之間)

	string m_Idol;	//偶像	只寫
};

int main()
{
	Person p;
	//姓名設定
	p.setName("張三");
	//獲取姓名
	cout << "姓名:" << p.getName() << endl;

	//年齡設定
	p.setAge(160);
	//p.m_Age = 20;
	//獲取年齡
	cout << "年齡:" << p.getAge() << endl;

	//偶像設定
	p.setm_Idol("小明");//只寫
	//cout << "偶像" << p.getIdol() << endl;//只寫狀態,外界讀不到

	system("pause");
	return 0;
}

4.2 物件的初始化和清理

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

4.2.1 建構函式和解構函式

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

​ 一個物件或者變數沒有初始狀態,對其使用後果是未知的

​ 同樣的使用完一個物件或變數,沒有及時清理,也會造成一定的安全問題

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

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

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

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

建構函式:類名(){}

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

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

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

示例:

#include <iostream>
using namespace std;

//物件的初始化和清理
//1、建構函式 進行初始化操作
class Person
{
public:
	//1.1、建構函式
	//沒有返回值	不用寫void
	//函式名 與類名相同
	//建構函式可以有引數,可以發生過載
	//建立物件的時候,建構函式會自動呼叫,而且只呼叫一次
	Person()
	{
		cout << "Person 建構函式的呼叫" << endl;
	}

	//2、解構函式,進行清理操作
	//沒有返回值	不用寫void
	//函式名 與類名相同	在名稱前加~
	//建構函式不可以有引數,因此不可以發生過載
	//物件在銷燬前會自動呼叫解構函式,而且只呼叫一次
	~Person()
	{
		cout << "Person 解構函式的呼叫" << endl;
	}
};

//構造和析構都是必須有的實現,如果我們自己不提供,編譯器會提供一個空實現的構造和析構
void test01()
{
	Person P;//在棧上的資料,test01執行完畢後,釋放這個物件
}

int main()
{
	//test01();
	Person P;
	system("pause");
	return 0;
}

4.2.2 建構函式的分類和呼叫

兩種分類方式:

​ 按引數分為:有參構造和無參構造

​ 按型別分為:普通構造和複製構造

三種呼叫方式:

​ 括號法

​ 顯示法

​ 隱式轉換法

示例:

#include <iostream>
using namespace std;

//1、建構函式的分類及呼叫
//分類
//		按照引數分類	無參構造(預設構造)和 有參構造
//		按照型別分類 普通構造 複製構造
class Person
{
public:
	//建構函式
	Person()
	{
		cout << "Person的無參建構函式呼叫" << endl;
	}
	Person(int a)
	{
		age = a;
		cout << "Person的有參建構函式呼叫" << endl;
	}
	//複製建構函式
	Person(const Person& p)
	{
		//將傳入的人身上的所有屬性,複製到我身上
		age = p.age;
		cout << "Person的複製建構函式呼叫" << endl;
	}
	~Person()
	{
		cout << "Person的解構函式呼叫" << endl;
	}
	int age;
};

//呼叫
void test01()
{
	//1、括號法
	//Person p1;//預設建構函式呼叫
	//Person p2(10);//有參建構函式
	//Person p3(p2);//複製建構函式

	//注意事項1
	//呼叫預設建構函式時候,你要加()
	//因為下面這行程式碼,編譯器會認為是一個函式宣告,不會認為在建立物件
	//Person p1();

	//cout << "p2的年齡為: " << p2.age << endl;
	//cout << "p3的年齡為: " << p3.age << endl;

	//2、顯示法
	//Person p1;
	//Person p2 = Person(10);//有參構造
	//Person p3 = Person(p2);//複製構造

	//Person(10);	//匿名物件	特點:當前執結束後,系統會立即回收掉匿名物件
	//cout << "aaaaa" << endl;

	//注意事項2
	//不要利用複製建構函式	初始化匿名物件	編譯器會認為	Person(p3); 等價於 Person p3;	物件宣告
	//Person(p3);

	//3、隱式轉換法
	Person p4 = 10;//相當於	寫了Person p4 = Person(10);	有參構造
	Person p5 = p4;//複製構造
}


int main()
{
	test01();
	system("pause");
	return 0;
}

4.2.3 複製建構函式呼叫時機

C++中複製建構函式呼叫時機通常有三種情況

  • 使用一個已經建立完畢的物件來初始化一個新物件
  • 值傳遞的方式給函式引數傳值
  • 以值方式返回區域性物件

示例:

#include <iostream>
using namespace std;

//複製建構函式的呼叫時機
class Person 
{
public:
	Person()
	{
		cout << "Person 預設建構函式呼叫" << endl;
	}
	Person(int age)
	{
		cout << "Person 有參建構函式呼叫" << endl;
		m_age = age;
	}
	Person(const Person &p)
	{
		cout << "Person 複製建構函式呼叫" << endl;
		m_age = p.m_age;
	}
	~Person()
	{
		cout << "Person 解構函式呼叫" << endl;
	}
	int m_age;
};

//1、使用一個已經建立完畢的物件來初始化一個新物件
void test01()
{
	Person p1(20);
	Person p2(p1);

	cout << "p2的年齡為:" << p2.m_age << endl;
}

//2、值傳遞的方式給函式引數傳值
void doWork(Person p)
{

}

void test02()
{
	Person p;
	doWork(p);
}

//3、以值方式返回區域性物件
Person doWork3()
{
	Person p1;
	cout << (int*)&p1 << endl;
	return p1;
}

void test03()
{
	Person p = doWork3();
	cout << (int*)&p << endl;
}

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

	system("pause");
	return 0;
}

4.2.4 建構函式呼叫規則

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

  1. 預設建構函式(無參,函式體為空)
  2. 預設解構函式(無參,函式體為空)
  3. 預設複製函式,對屬性進行值複製

建構函式呼叫規則如下:

  • 如果使用者定義有參建構函式,C++不再提供預設無參構造,但是會提供預設複製構造
  • 如果使用者定義複製建構函式,C++不會再提供其他建構函式

示例:

#include <iostream>
using namespace std;

//建構函式呼叫規則
// 1、建立一個類,C++編譯器會給每個類都新增至少3個函式
// 預設構造	(空實現)
// 解構函式	(空實現)
// 複製函式	(值複製)


//2、(依次註釋測試)
// 如果我們寫了有參建構函式,編譯器就不再提供預設構造,依然提供複製構造
// 如果我們寫了複製建構函式,編譯器就不再提供其他建構函式
class Person
{
public:

	//Person()
	//{
	//	cout << "Person的預設建構函式呼叫" << endl;
	//}

	Person(int age)
	{
		m_Age = age;
		cout << "Person的有參建構函式呼叫" << endl;
	}

	//Person(const Person &p)
	//{
	//	m_Age = p.m_Age;
	//	cout << "Person的複製建構函式呼叫" << endl;
	//}

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

	int m_Age;
};
/*
void test01()
{
	Person p;
	p.m_Age = 18;

	Person p2(p);

	cout << "p2的年齡為: " << p2.m_Age << endl;
}*/

void test02()
{
	Person p(28);
	Person p2(p);
}

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

	system("pause");
	return 0;
}

4.4.5 深複製與淺複製

深淺複製是面試經典問題,也是常見的一個坑

淺複製:簡單的賦值複製操作

深複製:在堆區重新申請空間,進行複製操作

示例:

#include <iostream>
using namespace std;

//深複製與淺複製

class Person
{
public:

	Person()
	{
		cout << "Person的預設建構函式呼叫" << endl;
	}

	Person(int age,int height)
	{
		m_Age = age;
		m_Height = new int(height);
		cout << "Person的有參建構函式呼叫" << endl;
	}

	~Person()
	{
		//析構程式碼,將堆區開闢資料做釋放操作
		if (m_Height != NULL)
		{
			delete m_Height;
			m_Height = NULL;
		}
		cout << "Person的解構函式呼叫" << endl;
	}

	//自己實現複製建構函式	解決淺複製帶來的問題
	Person(const Person& p)
	{
		cout << "Person 複製建構函式呼叫" << endl;
		m_Age = p.m_Age;
		//m_Height = p.m_Height;	//編譯器預設實現的就是這行程式碼
		//深複製操作
		m_Height = new int(*p.m_Height);

	}
	int m_Age;//年齡
	int* m_Height;//身高
};

void test01()
{
	Person p1(18,160);
	cout << "p1的年齡為: " << p1.m_Age << "身高為:" << *p1.m_Height << endl;

	Person p2(p1);

	cout << "p2的年齡為: " << p2.m_Age << "身高為:" << *p2.m_Height << endl;
}

int main()
{
	test01();

	system("pause");
	return 0;
}

總結:如果屬性有在堆區開闢的,一定要自己提供複製建構函式,防止淺複製帶來的問題

4.2.6 初始化列表

作用:

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

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

示例:

#include <iostream>
using namespace std;

//初始化列表
class Person
{
public:
	//傳統初始化操作
	//Person(int a, int b, int c)
	//{
	//	m_A = a;
	//	m_B = b;
	//	m_C = c;
	//}

	//初始化列表初始化屬性		注意冒號位置
	Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c)
	{

	}
	int m_A;
	int m_B;
	int m_C;
};

void test01()
{
	//Person p(10, 20, 30);
	Person p(30, 20, 10);
	cout << "m_A = " << p.m_A << endl;
	cout << "m_B = " << p.m_B << endl;
	cout << "m_C = " << p.m_C << endl;
}

int main()
{
	test01();

	system("pause");
	return 0;
}

4.4.7 類物件作為類成員

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

例如:

class A {}
class B
{
	A a;
}

B類中有物件A作為成員,A為物件成員

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

示例:

#include <iostream>
#include<string>
using namespace std;

//類物件作為類成員

//手機類
class Phone
{
public:
	Phone(string PName)
	{
		cout << "Phone 的建構函式呼叫" << endl;
		m_PName = PName;
	}
	~Phone()
	{
		cout << "Phone 的解構函式呼叫" << endl;
	}
	//手機品牌名稱
	string m_PName;
};

//人類
class Person
{
public:
	//Phone m_Phone = pName;//隱式轉換法
	Person(string Name, string pName):m_Name(Name),m_Phone(pName)
	{
		cout << "Person 的建構函式呼叫" << endl;
	}
	~Person()
	{
		cout << "Person 的解構函式呼叫" << endl;
	}
	//姓名
	string m_Name;
	//手機
	Phone m_Phone;
};

//當其他類物件作為本類成員,構造時先構造類物件,再構造自身		析構的順序與構造相反

void test01()
{
	Person p("張三", "蘋果MAX");

	cout << p.m_Name << "拿著" << p.m_Phone.m_PName << "手機" << endl;
}

int main()
{
	test01();

	system("pause");
	return 0;
}

執行結果

Phone 的建構函式呼叫
Person 的建構函式呼叫
張三拿著蘋果MAX手機
Person 的解構函式呼叫
Phone 的解構函式呼叫

先構造類物件,再構造自身 析構的順序與構造相反

4.2.8 靜態成員

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

靜態成員分為:

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

示例1:靜態成員變數

#include <iostream>
#include<string>
using namespace std;

//靜態成員變數
class Person
{
public:
	//1、所有物件都共享同一份資料
	//2、編譯階段就分配了記憶體
	//3、類內宣告,類外初始化操作
	static int m_A;

//靜態成員變數也是有訪問許可權的
private:
	static int m_B;
};

int Person::m_A = 100;
int Person::m_B = 200;

void test01()
{
	Person p;
	//100
	cout << p.m_A << endl;

	Person p2;
	p2.m_A = 200;
	//200
	cout << p.m_A << endl;
}

void test02()
{
	//靜態成員變數	不屬於某個物件上,所有物件都共享同一份資料
	//因此靜態成員變數有兩種訪問方式

	//1、透過物件進行訪問
	//Person p;
	//cout << p.m_A << endl;

	//2、透過類名進行訪問
	cout << Person::m_A << endl;

	cout << Person::m_B << endl;	//類外訪問不到私有靜態成員變數
}


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

	system("pause");
	return 0;
}

示例2:靜態成員函式

#include <iostream>
#include<string>
using namespace std;

//靜態成員函式
//1、所有物件共享同一個函式
//2、靜態成員函式只能訪問靜態成員變數

class Person
{
public:
	//靜態成員函式
	static void func()
	{
		m_A = 100;	//靜態成員函式可以訪問	靜態成員變數
		//m_B = 200;	//靜態成員函式不可以訪問	非靜態成員變數	因為無法區分是哪個物件的m_B
		cout << "static void func 呼叫" << endl;
	}

	static int m_A;	//靜態成員變數
	int m_B;	//非靜態成員變數

	//靜態成員函式也是有訪問許可權的
private:
	static void func2()
	{
		cout << "static void func2 呼叫" << endl;
	}
};

int Person::m_A = 0;

//有兩種訪問方式
void test01()
{
	//1、透過物件訪問
	//Person p;
	//p.func();

	//2、透過類名訪問
	Person::func();

	//Person::func2();	//類外訪問不到私有靜態成員函式
}

int main()
{
	test01();

	system("pause");
	return 0;
}

4.3 C++物件模型和this指標

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

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

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

示例:

#include <iostream>
using namespace std;

//成員變數 和 成員函式 分開儲存的

class Person
{
	int m_A;	//非靜態成員變數	屬於類的物件上

	static int m_B;	//靜態成員變數	不屬於類的物件上

	void func() {}	//非靜態成員函式		不屬於類的物件上

	static void func2() {}	//靜態成員函式	不屬於類的物件上
};

int Person::m_B = 0;

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

void test02()
{
	Person p;
	cout << "sizeof p = " << sizeof(p) << endl;
}


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

	system("pause");
	return 0;
}

4.3.2 this指標概念

透過4.3.1我們還知道在C++中成員變數和成員函式是分開儲存的

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

那麼問題是:這一塊程式碼是如何區分那個物件呼叫的自己呢?

C++透過提供特殊的物件指標,this指標,解決上述問題。this指標指向被呼叫的成員函式所屬的物件

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

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

this指標的用途:

  • 當形參和成員變數同名時,可用this指標來區分
  • 在類的非靜態成員函式返回物件本身,可使用return *this

示例:

#include <iostream>
using namespace std;

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

	Person& PersonAddAge(Person &p)
	{
		this->age += p.age;
		// this指向p2的指標,而*this指向的就是p2這個物件本體
		return *this;
	}
	int age;
};

//1、解決名稱衝突
void test01()
{
	Person p1(18);
	cout << "p1的年齡為:" << p1.age << endl;
}

//2、返回物件本身用 *this
void test02()
{
	Person p1(10);

	Person p2(10);
	//鏈式程式設計思想
	p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);

	cout << "p2的年齡為: " << p2.age << endl;
}

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

	system("pause");
	return 0;
}

4.3.3 空指標訪問成員函式

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

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

示例:

#include <iostream>
using namespace std;

//空指標呼叫成員函式
class Person 
{
public:
	void showClassName()
	{
		cout << "this is Person class" << endl;
	}

	void showName()
	{
		//報錯的原因是因為傳入的指標為NULL
		if (this == NULL)
		{
			return;
		}
		cout << "Age = " << m_Age << endl;
	}

	int m_Age;
};

void test01()
{
	Person * p = NULL;

	p->showClassName();

	//p->showName();
}

int main()
{
	test01();

	system("pause");
	return 0;
}

4.3.4 const修飾成員函式

常函式:

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

常物件:

  • 宣告物件前加const稱該物件為常物件
  • 常物件只能呼叫常函式

示例:

#include <iostream>
using namespace std;

//常函式
class Person
{
public:

	//this指標的本質	是指標常量	指標的指向是不可以修改的
	//const Person * const this;(第一個const與函式最後的const一致)
	//在成員函式後面加const,修飾的是this指向,讓指標指向的值也不可以修改
	void showPerson() const
	{
		this->m_B = 100;

		//this->m_A = 100;
		//this = NULL;//this指標不可以修改指標的指向
	}

	void func()
	{

	}

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

void test01()
{
	Person p;
	p.showPerson();
}

//常物件
void test02()
{
	const Person p;//在物件前加const,變為常物件
	//p.m_A = 100;
	p.m_B = 100;	//m_B是特殊值,在常物件下也可以修改

	//常物件只能呼叫常函式
	//p.func();	//常物件	不可以呼叫普通成員函式,因為普通成員函式可以修改屬性
}

int main()
{
	test01();

	system("pause");
	return 0;
}

4.4 友元

生活中你的家有客廳(public),有你的臥室(private)

客廳所有來的客人都可以進去,但你的臥室是私有的,也就是說只有你能進去

但是呢,你也可以允許你的好閨蜜好基友進去。

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

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

友元的關鍵字為 friend

友元的三種實現

  • 全域性函式做友元
  • 類做友元
  • 成員函式做友元

4.4.1 全域性函式做友元

示例:

#include <iostream>
#include<string>
using namespace std;

class Building
{
	//goodGay全域性函式是 Building好朋友,可以訪問Building中私有成員
	friend void goodGay(Building* building);

public:
	Building()
	{
		m_SittingRoom = "客廳";
		m_BedRoom = "臥室";
	}

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);
}

int main()
{
	test01();

	system("pause");
	return 0;
}

4.4.2 類做友元

示例:

#include <iostream>
#include<string>
using namespace std;

//類做友元
class Building;
class GoodGay
{
public:
	GoodGay();
	void visit();	//參觀函式	訪問Building中的屬性
	Building* building;
};

class Building
{
	//GoodGay是本類的好朋友,可以訪問本類中私有成員
	friend class GoodGay;
public:
	Building();
	string m_SittingRoom;	//客廳
private:
	string m_BedRoom;	//臥室
};

//類外寫成員函式
Building::Building()
{
	this->m_SittingRoom = "客廳";
	this->m_BedRoom = "臥室";
}

GoodGay::GoodGay()
{
	//建立建築物物件
	building = new Building;
}

void GoodGay::visit()
{
	cout << "好基友全域性函式 正在訪問:" << building->m_SittingRoom << endl;

	cout << "好基友全域性函式 正在訪問:" << building->m_BedRoom << endl;
}

void test01()
{
	GoodGay gg;
	gg.visit();
}

int main()
{
	test01();

	system("pause");
	return 0;
}

4.4.3 成員函式做友元

示例:

#include <iostream>
#include<string>
using namespace std;

//成員函式做友元
class Building;
class GoodGay
{
public:
	GoodGay();

	void visit();	//讓visit函式可以訪問Building中私有成員
	void visit2();	//讓visit函式不可以訪問Building中私有成員

	Building* building;
};


class Building
{
	//告訴編譯器 GoodGay類下的visit成員函式作為本類的好朋友,可以訪問私有成員
	friend void GoodGay::visit();
public:
	Building();
	string m_SittingRoom;	//客廳
private:
	string m_BedRoom;	//臥室
};

//類外寫成員函式
Building::Building()
{
	this->m_SittingRoom = "客廳";
	this->m_BedRoom = "臥室";
}

GoodGay::GoodGay()
{
	//建立建築物物件
	building = new Building;
}

void GoodGay::visit()
{
	cout << "visit 函式 正在訪問:" << building->m_SittingRoom << endl;

	cout << "visit 函式 正在訪問:" << building->m_BedRoom << endl;
}
void GoodGay::visit2()
{
	cout << "visit2 函式 正在訪問:" << building->m_SittingRoom << endl;

	//cout << "visit2 函式 正在訪問:" << building->m_BedRoom << endl;
}

void test01()
{
	GoodGay gg;
	gg.visit();
	gg.visit2();
}

int main()
{
	test01();

	system("pause");
	return 0;
}

4.5 運算過載符

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

4.5.1 加號運算子過載

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

示例:

#include <iostream>
#include<string>
using namespace std;

//加號運算子過載
class Person
{
public:
	//1、成員函式過載+號
	//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;
};

//2、全域性函式過載+號
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;
}

//函式過載版本
Person operator+(Person& p1, int num)
{
	Person temp;
	temp.m_A = p1.m_A + num;
	temp.m_B = p1.m_B + num;
	return temp;
}

void test01()
{
	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 + 20;		//Person+int

	//Person p3 = p1 + p2;
	cout << "p3.m_A = " << p3.m_A << " p3.m_B = " << p3.m_B << endl;
}

int main()
{
	test01();

	system("pause");
	return 0;
}

總結1:對於內建的資料型別的表示式的運算是不可能改變的

總結2:不要濫用運算子過載

4.5.2 左移運算過載符

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

示例:

#include <iostream>
#include<string>
using namespace std;

//左移運算子過載
class Person
{
	friend ostream& operator<<(ostream& cout, Person& p);

public:
	Person(int a, int b)
	{
		m_A = a;
		m_B = b;
	}

private:

	//利用成員函式過載 左移運算子	p.operator<<(cout)	簡化版本	p<<cout
	//不會利用成員函式過載<<運算子,因為無法實現cout在左側
	//void operator<<(cout)
	//{
	//
	//}

	int m_A;
	int m_B;
};

//只能利用全域性函式過載左移運算子
ostream & operator<<(ostream &cout, Person &p)	//本質	opeator<<(cout, p)		簡化 cout<<p
{
	cout << "m_A = " << p.m_A << " m_B = " << p.m_B;
	return cout;
}


void test01()
{
	Person p(10, 10);
	//p.m_A = 10;
	//p.m_B = 10;

	cout << p << endl;
}

int main()
{
	test01();

	system("pause");
	return 0;
}

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

4.5.3 遞增運算過載符

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

示例:

#include<iostream>
using namespace std;

//過載遞增運算子

//自定義整型
class MyInteger
{
	friend ostream& operator<<(ostream& cout, MyInteger myint);

public:
	MyInteger()
	{
		m_Num = 0;
	}

	//過載前置++運算子	返回引用是為了一直對一個資料進行遞增操作
	MyInteger& operator++()
	{
		//先進行++運算
		m_Num++;
		//再將自身做返回
		return *this;
	}
	//過載後置++運算子
	//void operator++(int)代表佔位引數,可以用於區分前置和後置
	MyInteger operator++(int)
	{
		//先		記錄一下當時結果
		MyInteger temp = *this;
		//後		遞增
		m_Num++;
		//將記錄結果做返回操作
		return temp;
	}

private:
	int m_Num;
};

//過載<<運算子
ostream& operator<<(ostream& cout, MyInteger myint)
{
	cout << myint.m_Num;
	return cout;
}

void test01()
{
	MyInteger myint;

	cout << ++(++myint) << endl;
	cout << myint << endl;
}

void test02()
{
	MyInteger myint;

	cout << myint++ << endl;
	cout << myint << endl;
}

int main()
{
	//test01();

	test02();

	system("pause");

	return 0;
}

總結:前置遞增返回引用,後置遞增返回值

4.5.4 賦值運算子過載

C++至少給一個類新增4個函式

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

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

示例:

#include<iostream>
using namespace std;

//賦值運算子過載

class Person
{
public:
	Person(int age)
	{
		m_Age = new int(age);
	}
	~Person()
	{
		if (m_Age != NULL)
		{
			delete m_Age;
			m_Age = NULL;
		}
	}
	//過載 賦值運算子
	Person & operator=(Person& p)
	{
		//編譯器是提供淺複製
		//m_Age = p.m_Age;

		//應該先判斷是否有屬性在堆區,如果有釋放乾淨,然後再深複製
		if (m_Age != NULL)
		{
			delete m_Age;
			m_Age = NULL;
		}
		//深複製
		m_Age = new int(*p.m_Age);
		//返回物件本身
		return *this;
	}
	int* m_Age;
};

void test01()
{
	Person p1(18);

	Person p2(20);

	Person p3(20);

	p3 = p2 = p1;	//賦值操作

	cout << "p1的年齡為:" << *p1.m_Age << endl;

	cout << "p2的年齡為:" << *p2.m_Age << endl;

	cout << "p3的年齡為:" << *p3.m_Age << endl;
}

int main()
{
	test01();

	//int a = 10;
	//int b = 20;
	//int c = 30;

	//c = b = a;

	//cout << "a = " << a << endl;
	//cout << "b = " << b << endl;
	//cout << "c = " << c << endl;

	system("pause");

	return 0;
}

4.5.5 關係運算子過載

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

示例:

#include<iostream>
using namespace std;

//過載關係運算子
class Person
{
public:

	Person(string name, int age)
	{
		m_Name = name;
		m_Age = age;
	}

	//過載 ==
	bool operator==(Person &p)
	{
		if (this->m_Age == p.m_Age && this->m_Name == p.m_Name)
		{
			return true;
		}
		return false;
	}

	//過載 !=
	bool operator!=(Person& p)
	{
		if (this->m_Age == p.m_Age && this->m_Name == p.m_Name)
		{
			return false;
		}
		return true;
	}

	string m_Name;
	int m_Age;
};

void test01()
{
	Person p1("Tom", 18);

	Person p2("Tom", 18);

	if (p1 == p2)
	{
		cout << "p1 和 p2 是相等的!" << endl;
	}
	else
	{
		cout << "p1 和 p2 是不相等的!" << endl;
	}

	if (p1 != p2)
	{
		cout << "p1 和 p2 是不相等的!" << endl;
	}
	else
	{
		cout << "p1 和 p2 是相等的!" << endl;
	}
}

int main()
{
	test01();

	system("pause");

	return 0;
}

4.5.6 函式呼叫運算子過載

  • 函式呼叫運算子()也可以過載
  • 由於過載後使用的方式非常像函式的呼叫,因此稱為仿函式
  • 仿函式沒有固定寫法,非常靈活

示例:

#include<iostream>
#include<string>
using namespace std;

//函式呼叫運算子過載

//列印輸出類
class MyPrint
{
public:

	//過載函式呼叫運算
	void operator()(string test)
	{
		cout << test << endl;
	}
};

void myPrint02(string test)
{
	cout << test << endl;
}

void test01()
{
	MyPrint myPrint;

	myPrint("hello world");	//由於使用起來非常類似於函式呼叫,因此稱為仿函式

	myPrint02("hello world");//函式呼叫
}

//仿函式非常靈活,沒有固定的寫法
//加法類
class MyAdd
{
public:
	int operator()(int num1, int num2)
	{
		return num1 + num2;
	}
};

void test02()
{
	MyAdd myadd;
	int ret = myadd(100, 100);
	cout << "ret = " << ret << endl;

	//匿名函式物件	(過載了仿函式的沒有名字的物件)
	cout << MyAdd()(100, 100) << endl;
}

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

	system("pause");

	return 0;
}

4.6 繼承

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

有些類與類之間存在特殊關係,例如下圖中:

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

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

4.6.1 繼承的基本語法

例如我們看到很多網站中,都有公共的頭部,公共的底部,甚至公共的左側列表欄,只有中心內容不同

接下來我們分別利用普通寫法和繼承的寫法來實現網頁中的內容,看一下繼承存在的意義以及好處

示例:

#include<iostream>
#include<string>
using namespace std;

//普通實現頁面

//Java頁面
/*
class Java
{
public:
	void header()
	{
		cout << "首頁、公開課、登入、註冊...(公共)" << endl;
	}
	void footer()
	{
		cout << "幫助中心、交流合作、站內地圖...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java、Python、C++、...(公共分類列表)" << endl;
	}
	void content()
	{
		cout << "Java學科影片" << endl;
	}
};

//Python頁面
class Python
{
public:
	void header()
	{
		cout << "首頁、公開課、登入、註冊...(公共)" << endl;
	}
	void footer()
	{
		cout << "幫助中心、交流合作、站內地圖...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java、Python、C++、...(公共分類列表)" << endl;
	}
	void content()
	{
		cout << "Python學科影片" << endl;
	}
};

//C++頁面
//Python頁面
class Cpp
{
public:
	void header()
	{
		cout << "首頁、公開課、登入、註冊...(公共)" << endl;
	}
	void footer()
	{
		cout << "幫助中心、交流合作、站內地圖...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java、Python、C++、...(公共分類列表)" << endl;
	}
	void content()
	{
		cout << "C++學科影片" << endl;
	}
};
*/

//繼承實現頁面
class BasePage
{
public:
	void header()
	{
		cout << "首頁、公開課、登入、註冊...(公共)" << endl;
	}
	void footer()
	{
		cout << "幫助中心、交流合作、站內地圖...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java、Python、C++、...(公共分類列表)" << endl;
	}
};

//繼承的好處:減少重複程式碼
//語法:class 子類 :繼承方式 父類
//子類 也成為 派生類
//父類 也成為 基類

//Java頁面
class Java :public BasePage
{
public:
	void content()
	{
		cout << "Java學科影片" << endl;
	}
};

//Python頁面
class Python :public BasePage
{
public:
	void content()
	{
		cout << "Python學科影片" << endl;
	}
};

//C++頁面
class Cpp :public BasePage
{
public:
	void content()
	{
		cout << "C++學科影片" << endl;
	}
};

void test01()
{
	cout << "Java下載影片頁面如下:" << endl;
	Java ja;
	ja.header();
	ja.footer();
	ja.left();
	ja.content();

	cout << "----------------------------" << endl;

	cout << "Python下載影片頁面如下:" << endl;
	Python py;
	py.header();
	py.footer();
	py.left();
	py.content();

	cout << "----------------------------" << endl;

	cout << "C++下載影片頁面如下:" << endl;
	Cpp cpp;
	cpp.header();
	cpp.footer();
	cpp.left();
	cpp.content();
}

int main()
{
	test01();

	system("pause");

	return 0;
}

總結:

繼承的好處:可以減少程式碼的重複量

class A : public B

A類稱為 子類派生類

B類稱為 父類基類

派生類中成員,包含兩大部分:

一類是從基類繼承過來的,一類是自己增加的成員

從基類繼承過來的表現其共性,而新增的成員體現了其個性

4.6.2 繼承方式

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

繼承方式一共有三種:

  • 共有繼承
  • 保護繼承
  • 私有繼承

示例:

#include<iostream>
#include<string>
using namespace std;

//繼承方式

//公共繼承
class Base1
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

class Son1 :public Base1
{
public:

	void func1()
	{
		m_A = 10;//父類中的公共許可權成員 到子類中依然是公共許可權
		m_B = 10;//父類中的保護許可權成員 到子類中依然是保護許可權
		//m_C = 10;//父類中的私有許可權成員 子類訪問不到
	}
};

void test01()
{
	Son1 s1;
	s1.m_A = 100;
	//s1.m_B = 100;//到Son1中 m_B是保護許可權 類外訪問不到

}

//保護繼承
class Base2
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

class Son2 :protected Base2
{
public:
	void func()
	{
		m_A = 100;		//父類中公共成員,到子類中變為保護許可權
		m_B = 100;		//父類中保護成員,到子類中變為保護許可權
		//m_C = 100;	//父類中私有成員	子類訪問不到
	}
};

void test02()
{
	Son2 s1;
	//s1.m_A = 100;	//在Son2中 m_A變為保護許可權,因此類外訪問不到
	//s1.m_B = 100;	//在Son2中 m_B保護許可權	不可以訪問
}

//私有繼承
class Base3
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

class Son3 :private Base3
{
	void func()
	{
		m_A = 100;		//父類中公共成員,到子類中變為私有許可權
		m_B = 100;		//父類中保護成員,到子類中變為私有許可權
		//m_C = 100;	//父類中私有成員	子類訪問不到
	}
};

void test03()
{
	Son3 s1;
	//s1.m_A = 1000;		//到Son3中變為了私有成員,類外訪問不到
	//s1.m_B = 1000;		//到Son3中變為了私有成員,類外訪問不到
}

class GrandSon3 :public Son3
{
public:
	void func()
	{
		//m_A = 1000;		//到了Son3 m_A變為了私有,即使是兒子,也是訪問不到
		//m_B = 1000;		//到了Son3 m_B變為了私有,即使是兒子,也是訪問不到
	}
};

int main()
{
	system("pause");

	return 0;
}

4.6.3 繼承中的物件模型

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

示例:

#include<iostream>
#include<string>
using namespace std;

//繼承中的物件模型

class Base
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

class Son :public Base
{
public:
	int m_D;
};

//利用開發人員命令提示工具檢視物件模型
//跳轉磁碟機代號	F:
//跳轉檔案路徑 cd 具體路徑如下 E:\projects\C++Code\helloworld
//檢視命令
//cl /dl reportsingleClassLayout 類名 檔名

void test01()
{
	//16
	//父類中所有非靜態成員屬性都會被子類繼承下去
	//父類中的私有成員屬性 是被編譯器給隱藏了,因此訪問不到,但是確實被繼承了
	cout << "size of Son = " << sizeof(Son) << endl;
}

int main()
{
	test01();

	system("pause");

	return 0;
}

命令操作:

4.6.4 繼承中構造和析構順序

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

問題:父類和子類的構造和析構的順序是誰先誰後?

示例:

#include<iostream>
#include<string>
using namespace std;

//繼承中的物件模型
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 test01()
{
	//Base b;
	
	//繼承中的構造和析構順序如下:
	//先構造父類,在構造子類,析構的順序和構造的順序相反
	Son s;
}

int main()
{
	test01();

	system("pause");

	return 0;
}

總結:繼承中先呼叫父類建構函式,再呼叫子類建構函式,析構順序與構造順序相反

4.6.5 繼承同名成員處理方式

問題:當子類和父類出現同名的成員,如何透過子類物件,訪問到子類或者父類中的同名的資料呢?

  • 訪問子類同名成員,直接訪問即可
  • 訪問父類同名成員,需要加作用域

示例:

#include<iostream>
#include<string>
using namespace std;

//繼承中的同名成員處理方式
class Base
{
public:
	Base()
	{
		m_A = 100;
	}

	void func()
	{
		cout << "Base - func()呼叫" << endl;
	}

	void func(int a)
	{
		cout << "Base - func(int)呼叫" << endl;
	}
	int m_A;
};

class Son :public Base
{
public:
	Son()
	{
		m_A = 200;
	}

	void func()
	{
		cout << "Son - func()呼叫" << endl;
	}
	int m_A;
};

//同名成員屬性處理方式
void test01()
{
	Son s;
	cout << "Son 下 m_A = " << s.m_A << endl;
	//如果透過子類物件 訪問父類中同名成員,需要加作用域
	cout << "Base 下 m_A = " << s.Base::m_A << endl;
}

//同名成員函式處理方式
void test02()
{
	Son s;
	s.func();	//直接呼叫 呼叫的是子類中的同名成員

	//如何呼叫到父類中的同名成員
	s.Base::func();

	//如果子類中出現和父類同名的成員函式,子類的同名成員會隱藏掉父類中所有同名成員函式(包括過載的函式)
	//如果想訪問到父類中被隱藏的同名函式,需要加作用域
	s.Base::func(100);	
}

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

	system("pause");

	return 0;
}

總結:

  1. 子類物件可以直接訪問到子類中同名成員
  2. 子類物件加作用域可以訪問到父類同名成員
  3. 當子類與父類擁有同名的成員函式,子類會隱藏父類中的同名成員函式,加作用域可以訪問到父類中的同名函式

4.6.6 繼承同名靜態成員處理方式

問題:繼承中同名的靜態成員在子類物件上如何進行訪問?

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

  • 訪問子類同名成員,直接訪問即可
  • 訪問父類同名成員,需要加作用域

示例:

#include<iostream>
#include<string>
using namespace std;

//繼承中的同名靜態成員處理方式
class Base
{
public:
	static int m_A;

	static void func()
	{
		cout << "Base - static void func()" << endl;
	}

	static void func(int a)
	{
		cout << "Base - static void func(int)" << endl;
	}
};
int Base::m_A = 100;

class Son :public Base
{
public:
	static int m_A;

	static void func()
	{
		cout << "Son - static void func()" << endl;
	}
};
int Son::m_A = 200;

//同名靜態成員屬性
void test01()
{
	//1、透過物件訪問
	cout << "透過物件訪問:" << endl;
	Son s;
	cout << "Son 下 m_A = " << s.m_A << endl;
	cout << "Base 下 m_A = " << s.Base::m_A << endl;

	//2、透過類名訪問
	cout << "透過類名訪問:" << endl;
	cout << "Son 下 m_A = " << Son::m_A << endl;
	//第一個::代表透過類名方式訪問		第二個::代表訪問父類作用域下
	cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}

//同名靜態成員函式
void test02()
{
	//1、透過物件訪問
	cout << "透過物件訪問:" << endl;
	Son s;
	s.func();
	s.Base::func();

	//2、透過類名訪問
	cout << "透過類名訪問:" << endl;
	Son::func();
	Son::Base::func();

	//子類出現和父類同名靜態成員函式,也會隱藏父類中所有同名成員函式
	//如果想訪問父類中被隱藏同名成員,需要加作用域
	Son::Base::func(10);
}

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

	system("pause");

	return 0;
}

總結:同名靜態成員處理方式和非靜態處理方式一樣,只不過有兩種訪問的方式(透過物件 和 透過類名)

4.6.7 多繼承語法

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

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

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

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

示例:

#include<iostream>
#include<string>
using namespace std;

//多繼承語法
class Base1
{
public:

	Base1()
	{
		m_A = 100;
	}
	int m_A;
};

class Base2
{
public:

	Base2()
	{
		m_A = 200;
	}
	int m_A;
};

//子類需要繼承 Base1 和 Base2
//語法:class 子類 : 繼承方式 父類1 , 繼承方式 父類2...
class Son :public Base1, public Base2
{
public:
	Son()
	{
		m_C = 300;
		m_D = 400;
	}
	int m_C;
	int m_D;
};

void test01()
{
	Son s;

	cout << "sizeof Son = " << sizeof(s) << endl;
	//當父類中出現同名成員,需要加作用域區分
	cout << "Base1::m_A = " << s.Base1::m_A << endl;
	cout << "Base2::m_A = " << s.Base2::m_A << endl;
}

int main()
{
	test01();

	system("pause");

	return 0;
}

總結:多繼承中如果父類中出現了同名情況,子類使用的時候要加作用域

4.7.8 菱形繼承

菱形繼承概念:

​ 兩個派生類繼承同一個基類

​ 又有某個類同時繼承兩個派生類

​ 這種繼承被稱為菱形繼承,或者鑽石繼承

典型的菱形繼承案例:

菱形繼承問題:

  1. 羊繼承了動物的資料,駝同樣繼承了動物的資料,當草泥馬使用資料時,就會產生二義性
  2. 草泥馬繼承自動物的資料繼承了兩份,其實我們應該清楚,這份資料我們只需要一份即可

示例:

#include<iostream>
#include<string>
using namespace std;

//動物類
class Animal
{
public:
	int m_Age;
};

//利用虛繼承 解決菱形繼承的問題
//在繼承之前 加上關鍵字 virtual 變為虛繼承
// Animal 類稱為虛基類
//羊類
class Sheep :virtual public Animal {};	//vfptr		v-virtual	f-function	ptr-pointer	(虛擬函式指標)

//駝類
class Tuo :virtual public Animal {};

//羊駝類
class SheepTuo :public Sheep, public Tuo {};

void test01()
{
	SheepTuo st;

	st.Sheep::m_Age = 18;
	st.Tuo::m_Age = 28;
	//當菱形繼承,兩個父類擁有相同資料,需要加以作用域區分
	cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
	cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
	cout << "st.m_Age = " << st.m_Age << endl;

	//這份資料我們知道 只要有一份就可以,菱形繼承導致資料有兩份,資源浪費
}

int main()
{
	test01();

	system("pause");

	return 0;
}

總結:

  • 菱形繼承帶來的主要問題是子類繼承兩份相同的資料,導致資源浪費以及毫無意義
  • 利用虛繼承可以解決菱形繼承問題 關鍵字virtual

4.7 多型

4.7.1 多型基本概念

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

多型分為兩類:

  • 靜態多型:函式過載 和 運算子過載屬於靜態多型,複用函式名
  • 動態多型:派生類和虛擬函式實現執行時多型

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

  • 靜態多型的函式地址早繫結 - 編譯階段確定函式地址
  • 動態多型的函式地址晚繫結 - 執行階段確定函式地址

示例:

#include<iostream>
#include<string>
using namespace std;

//多型

//動物類
class Animal
{
public:
	//虛擬函式
	virtual void speak()
	{
		cout << "動物在說話" << endl;
	}
};

//貓類
class Cat :public Animal
{
public:
	void speak()
	{
		cout << "小貓在說話" << endl;
	}
};

//執行說話的 函式
//地址早繫結 在編譯階段確定函式地址
//如果想執行讓貓說話,那麼這個函式地址就不能提前繫結,需要在執行階段進行繫結,地址晚繫結(加virtual)

//動態多型滿足條件
//1、有繼承關係
//2、子類要重寫父類的虛擬函式

//動態多型使用
//父類的指標或引用 指向子類物件

void doSpeak(Animal& animal)	//Animal & animal = cat;
{
	animal.speak();
}

//狗類
class Dog :public Animal
{
public:
	void speak()
	{
		cout << "小狗在說話" << endl;
	}
};
void test01()
{
	Cat cat;
	doSpeak(cat);

	Dog dog;
	doSpeak(dog);
}

int main()
{
	test01();

	system("pause");

	return 0;
}

總結:

多型滿足條件

  • 有繼承關係
  • 子類重寫父類中的虛擬函式

多型使用條件

  • 父類指標或引用指向子類物件

重寫:函式返回值型別 函式名 引數列表 完全一致稱為重寫

4.7.2 多型案例一:計算器類

案例描述:

分別利用普通寫法和多型技術,設計實現兩個運算元進行運算的計算器類

多型的優點:

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

示例:

#include<iostream>
#include<string>
using namespace std;

//分別利用普通寫法和多型技術實現計算器

//普通寫法
class calculator
{
public:
	int getResult(string oper)
	{
		if (oper == "+")
		{
			return m_Num1 + m_Num2;
		}
		else if (oper == "-")
		{
			return m_Num1 - m_Num2;
		}
		else if (oper == "*")
		{
			return m_Num1 * m_Num2;
		}
		//如果想擴充套件新的功能,需要修改原始碼
		//在真實的開發中 提倡 開閉原則
		//開閉原則:對擴充套件進行開放,對修改進行關閉
	}
	int m_Num1;	//運算元1
	int m_Num2;	//運算元2
};

void test01()
{
	//建立計數器物件;
	calculator c;
	c.m_Num1 = 10;
	c.m_Num2 = 10;

	cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;
	cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl;
	cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl;
}

//利用多型實現計算器
//多型好處:
//1、組織結構清晰
//2、可讀性強
//3、對於前期和後期的維護性高

//實現計算器抽象類
class AbstractCalculator
{
public:
	virtual int getResult()
	{
		return 0;
	}

	int m_Num1;
	int m_Num2;
};

//加法計算器類
class AddCalculator :public AbstractCalculator
{
public:
	int getResult()
	{
		return m_Num1 + m_Num2;
	}
};

//減法計算器類
class SubCalculator :public AbstractCalculator
{
public:
	int getResult()
	{
		return m_Num1 - m_Num2;
	}
};

//乘法計算器類
class MulCalculator :public AbstractCalculator
{
public:
	int getResult()
	{
		return m_Num1 * m_Num2;
	}
};

void test02()
{
	//多型使用條件
	//父類指標或者引用指向子類物件

	//加法運算
	AbstractCalculator* abc = new AddCalculator;
	abc->m_Num1 = 100;
	abc->m_Num2 = 100;
	cout << abc->m_Num1 << " + " << abc->m_Num2 << "=" << abc->getResult() << endl;
	//用完後記得銷燬
	delete abc;

	//減法運算
	abc = new SubCalculator;
	abc->m_Num1 = 100;
	abc->m_Num2 = 100;
	cout << abc->m_Num1 << " - " << abc->m_Num2 << "=" << abc->getResult() << endl;
	//用完後記得銷燬
	delete abc;

	//乘法運算
	abc = new MulCalculator;
	abc->m_Num1 = 100;
	abc->m_Num2 = 100;
	cout << abc->m_Num1 << " * " << abc->m_Num2 << "=" << abc->getResult() << endl;
	//用完後記得銷燬
	delete abc;
}

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

	system("pause");

	return 0;
}

總結:C++開發中提倡利用多型設計程式架構,因為多型優點很多

4.7.3 純虛擬函式和抽象類

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

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

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

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

抽象類特點:

  • 無法例項化物件
  • 子類必須重寫抽象類中的純虛擬函式,否則也屬於抽象類

示例:

#include<iostream>
#include<string>
using namespace std;

//純虛擬函式和抽象類
class Base
{
public:
	//純虛擬函式
	//只要有一個純虛擬函式,這個類稱為抽象類
	//抽象類特點:
	//1、無法例項化物件
	//2、抽象類的子類 必須要重寫父類中的純虛擬函式,否則也屬於抽象類
	virtual void func() = 0;
};

class Son :public Base
{
public:
	virtual void func()
	{
		cout << "func函式呼叫" << endl;
	}
};

void test01()
{
	//Base b;	//抽象類無法例項化物件
	//new Base;	//抽象類無法例項化物件

	//Son s;	//子類必須重寫父類中的純虛擬函式,否則無法例項化物件

	Base* base = new Son;
	base->func();
}

int main()
{
	test01();

	system("pause");

	return 0;
}

4.7.4 多型案例二:製作飲品

案例描述:

製做飲品的大致流程為:煮水 - 沖泡 - 倒入杯中 - 加入輔料

利用多型技術實現本案例,提供抽象製作飲品基類,提供子類製作咖啡和茶葉

示例:

#include<iostream>
#include<string>
using namespace std;

//多型案例2: 製作飲品
class AbstractDrinking
{
public:

	//煮水
	virtual void Boil() = 0;

	//沖泡
	virtual void Brew() = 0;

	//倒入杯中
	virtual void PourInCup() = 0;

	//加入輔料
	virtual void PutSomething() = 0;

	//製作飲品
	void makeDrink()
	{
		Boil();
		Brew();
		PourInCup();
		PutSomething();
	}
};

//製作咖啡
class Coffee :public AbstractDrinking
{
public:
	//煮水
	virtual void Boil()
	{
		cout << "煮農夫山泉水" << endl;
	}

	//沖泡
	virtual void Brew()
	{
		cout << "沖泡咖啡" << endl;
	}

	//倒入杯中
	virtual void PourInCup()
	{
		cout << "倒入杯中" << endl;
	}

	//加入輔料
	virtual void PutSomething()
	{
		cout << "加入糖和牛奶" << endl;
	}
};

//製作茶水
class Tea :public AbstractDrinking
{
public:
	//煮水
	virtual void Boil()
	{
		cout << "煮礦泉水" << endl;
	}

	//沖泡
	virtual void Brew()
	{
		cout << "沖泡茶葉" << endl;
	}

	//倒入杯中
	virtual void PourInCup()
	{
		cout << "倒入杯中" << endl;
	}

	//加入輔料
	virtual void PutSomething()
	{
		cout << "加入檸檬" << endl;
	}
};

//製作函式
void doWork(AbstractDrinking* abs)	//AbstractDrinking * abs = new Coffee
{
	abs->makeDrink();
	delete abs;	//釋放
}

void test01()
{
	//製作咖啡
	doWork(new Coffee);

	cout << "-------------------" << endl;

	//製作茶水
	doWork(new Tea);
}

int main()
{
	test01();

	system("pause");

	return 0;
}

4.7.5 虛析構和純虛析構

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

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

虛析構和純虛析構共性:

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

虛析構和純虛析構的區別:

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

虛析構語法:

virtual ~類名(){}

純虛析構語法:

virtual ~類名(){} = 0;

類名::~類名(){}

示例:

#include<iostream>
#include<string>
using namespace std;

//虛析構和純虛析構
class Animal
{
public:
	Animal()
	{
		cout << "Animal 建構函式呼叫" << endl;
	}
	//利用虛析構可以解決 父類指標釋放子類物件時不乾淨的問題
	//virtual ~Animal()
	//{
	//	cout << "Animal 解構函式呼叫" << endl;
	//}

	//純虛析構	需要宣告也需要實現
	//有了出虛析構之後,這個類也屬於抽象類,無法例項化
	virtual ~Animal() = 0;

	//純虛擬函式
	virtual void speak() = 0;
};

Animal:: ~Animal()
{
	cout << "Animal 純虛解構函式呼叫" << endl;
}

class Cat :public Animal
{
public:
	Cat(string name)
	{
		cout << "Cat建構函式呼叫" << endl;
		m_Name = new string(name);
	}

	virtual void speak()
	{
		cout << *m_Name << "小貓在說話" << endl;
	}

	~Cat()
	{
		if (m_Name != NULL)
		{
			cout << "Cat的解構函式呼叫" << endl;
			delete m_Name;
			m_Name = NULL;
		}
	}

	string* m_Name;
};

void test01()
{
	Animal* animal = new Cat("Tom");
	animal->speak();
	//父類指標在析構的時候 不會呼叫子類中的解構函式,導致子類如果有堆區屬性,會出現記憶體洩漏情況
	delete animal;
}

int main()
{
	test01();

	system("pause");

	return 0;
}

總結:

  1. 虛析構或純虛析構就是用來解決透過父類指標釋放子類物件
  2. 如果子類沒有堆區資料,可以不寫為虛析構或純虛析構
  3. 擁有純虛析構的類也屬於抽象類

4.7.6 多型案例三:電腦組裝

案例描述:

電腦主要組成部件為CPU(用於計算),顯示卡(用於顯示),記憶體條(用於儲存)

將每個零件封裝出抽象基類,並且提供不同的廠商生產不同的零件,例如 intel 廠商和 lenovo 廠商

建立電腦類提供讓電腦工作的函式,並且呼叫每個零件工作的介面

測試時組裝三臺不同的電腦進行工作

示例:

#include<iostream>
#include<string>
using namespace std;

//抽象不同零件類
//抽象CPU類
class CPU
{
public:
	//抽象計算函式
	virtual void calculate() = 0;
};

//抽象顯示卡類
class VideoCard
{
public:
	//抽象計算函式
	virtual void display() = 0;
};

//抽象記憶體條類
class Memory
{
public:
	//抽象儲存函式
	virtual void storage() = 0;
};

//電腦類
class Computer
{
public:
	Computer(CPU* cpu, VideoCard* vc, Memory* mem)
	{
		m_cpu = cpu;
		m_vc = vc;
		m_mem = mem;
	}

	//提供工作函式
	void work()
	{
		//讓零件工作起來,呼叫介面
		m_cpu->calculate();
		m_vc->display();
		m_mem->storage();
	}

	//提供虛構函式 釋放3個電腦零件
	~Computer()
	{
		//釋放CPU零件
		if (m_cpu != NULL)
		{
			delete m_cpu;
			m_cpu = NULL;
		}
		//釋放顯示卡零件
		if (m_vc != NULL)
		{
			delete m_vc;
			m_vc = NULL;
		}
		//釋放記憶體條零件
		if (m_mem != NULL)
		{
			delete m_mem;
			m_mem = NULL;
		}
	}

private:

	CPU* m_cpu;	//CPU零件指標
	VideoCard* m_vc;//顯示卡零件指標
	Memory* m_mem;//記憶體條零件指標
};

//具體廠商
//intel廠商
class IntelCPU :public CPU
{
public:
	virtual void calculate()
	{
		cout << "Intel的CPU開始計算了!" << endl;
	}
};

class IntelVideoCard :public VideoCard
{
public:
	virtual void display()
	{
		cout << "Intel的顯示卡開始顯示了!" << endl;
	}
};

class IntelMemory :public Memory
{
public:
	virtual void storage()
	{
		cout << "Intel的記憶體條開始儲存了!" << endl;
	}
};

//lenovo廠商
class lenovoCPU :public CPU
{
public:
	virtual void calculate()
	{
		cout << "lenovo的CPU開始計算了!" << endl;
	}
};

class lenovoVideoCard :public VideoCard
{
public:
	virtual void display()
	{
		cout << "lenovo的顯示卡開始顯示了!" << endl;
	}
};

class lenovoMemory :public Memory
{
public:
	virtual void storage()
	{
		cout << "lenovo的記憶體條開始儲存了!" << endl;
	}
};

void test01()
{
	//第一臺電腦零件
	CPU* intelCpu = new IntelCPU;
	VideoCard* intelCard = new IntelVideoCard;
	Memory* intelMem = new IntelMemory;

	cout << "第一臺電腦開始工作:" << endl;
	//建立第一臺電腦
	Computer* computer1 = new Computer(intelCpu, intelCard, intelMem);
	computer1->work();
	delete computer1;

	cout << "--------------------------" << endl;
	cout << "第二臺電腦開始工作:" << endl;
	//第二臺電腦組裝
	Computer* computer2 = new Computer(new lenovoCPU, new lenovoVideoCard, new lenovoMemory);
	computer2->work();
	delete computer2;

	cout << "--------------------------" << endl;
	cout << "第三臺電腦開始工作:" << endl;
	//第二臺電腦組裝
	Computer* computer3 = new Computer(new IntelCPU, new lenovoVideoCard, new IntelMemory);
	computer3->work();
	delete computer3;
}

int main()
{
	test01();

	system("pause");

	return 0;
}

5 檔案操作

程式執行時產生的資料都屬於臨時資料,程式一旦執行結束都會被釋放
透過檔案可以將資料持久化
C++中對檔案操作需要包含標頭檔案<fstream>

檔案型別分為兩種:

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

操作檔案三大類:

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

5.1 文字檔案

5.1.1 寫檔案

寫檔案步驟如下:

  1. 包含標頭檔案

    #include <fstream>

  2. 建立流資料

    ofstream ofs;

  3. 開啟檔案

    ofs.open("檔案路徑", 開啟方式)

  4. 寫資料

    ofs << "寫入資料";

  5. 關閉檔案

    ofs.close();

檔案開啟方式:

開啟方式 解釋
ios::in 為讀檔案而開啟資料夾
ios::out 為寫檔案而開啟資料夾
ios::ate 初始位置:檔案尾
ios::app 追加方式寫檔案
ios::trunc 如果檔案存在先刪除,再建立
ios::binary 二進位制方式

注意:檔案開啟方式可以配合使用|運算子
例如:用二進位制方式寫檔案ios::binary | ios:out

示例:

#include<iostream>
#include<fstream>
using namespace std;

//文字檔案 寫檔案

void test01()
{
	//1、包含標頭檔案	fstream

	//2、建立流物件
	ofstream ofs;

	//3、指定開啟方式
	ofs.open("test.txt", ios::out);

	//4、寫內容
	ofs << "姓名:張三" << endl;
	ofs << "性別:男" << endl;
	ofs << "年齡:18" << endl;

	//5、關閉檔案
	ofs.close();
}

int main()
{
	test01();

	system("pause");

	return 0;
}

總結:

  • 檔案操作必須包含標頭檔案 fstream
  • 讀檔案可以利用ofstream,或者fstream類
  • 開啟檔案時候需要指定操作檔案的路徑,以及開啟方式
  • 利用 << 可以向檔案中寫入資料
  • 操作完畢需要關閉檔案

5.1.2 讀檔案

讀檔案與寫檔案類似,但是讀取方式相對於比較多

讀檔案包含步驟如下:

  1. 包含標頭檔案

    #include <fstream>

  2. 建立流物件

    ifstream ifs;

  3. 開啟檔案並判斷檔案是否開啟成功

    ifs.open("檔案路徑", 開啟方式)

  4. 讀資料

    四種方式讀取

  5. 關閉檔案

    ifs.close();

示例:

#include<iostream>
#include<fstream>
#include<string>
using namespace std;

//文字檔案 讀檔案

void test01()
{
	//1、包含標頭檔案	fstream

	//2、建立流物件
	ifstream ifs;

	//3、開啟檔案 並且判斷是否開啟成功
	ifs.open("test.txt", ios::in);
	if (!ifs.is_open())
	{
		cout << "檔案開啟失敗" << endl;
		return;
	}

	//4、讀資料

	//第一種
	//char Buffer[1024] = { 0 };
	//while (ifs >> Buffer)
	//{
	//	cout << Buffer << endl;
	//}

	//第二種
	//char Buffer[1024] = { 0 };
	//while (ifs.getline(Buffer, sizeof(Buffer)))
	//{
	//	cout << Buffer << endl;
	//}

	//第三種
	//string Buffer;
	//while (getline(ifs, Buffer))
	//{
	//	cout << Buffer << endl;
	//}

	//第四種
	char c;
	while ((c = ifs.get()) != EOF)	//EOF	end of file
	{
		cout << c;
	}

	//5、關閉檔案
	ifs.close();
}

int main()
{
	test01();

	system("pause");

	return 0;
}

總結:

  • 讀檔案可以利用 ifstream,或者fstream類
  • 利用is_open函式可以判斷檔案是否開啟成功
  • close 關閉檔案

5.2 二進位制檔案

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

開啟方式要指定為ios::binary

5.2.1 寫檔案

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

函式原型:ostream& write(const char * buffer, int len)

引數解釋:字元指標 buffer 指向記憶體中一段儲存空間。len 是讀寫的位元組數

示例:

#include<iostream>
#include<fstream>
#include<string>
using namespace std;

//二進位制檔案	寫檔案
class Person
{
public:

	char m_Name[64];	//姓名
	int m_Age;		//年齡
};

void test01()
{
	//1、包含標頭檔案
	// 
	//2、建立流物件
	ofstream ofs("person.txt", ios::out | ios::binary);

	//3、開啟檔案
	//ofs.open("person.txt", ios::out | ios::binary);
	
	//4、寫檔案
	Person p = { "張三",18 };	//初始化物件
	ofs.write((const char*)&p, sizeof(Person));

	//5、關閉檔案
	ofs.close();
}

int main()
{
	test01();

	system("pause");
	return 0;
}

總結:

  • 檔案輸出流物件可以透過write函式,以二進位制方式寫資料

5.3.3 讀檔案

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

函式原型:istream& read(char *buffer, int len);

引數解釋:字元指標 buffer 指向記憶體中一段儲存空間,len 是讀寫的位元組數

#include<iostream>
#include<fstream>
#include<string>
using namespace std;

//二進位制檔案	讀檔案
class Person
{
public:

	char m_Name[64];	//姓名
	int m_Age;		//年齡
};

void test01()
{
	//1、包含標頭檔案
	// 
	//2、建立流物件
	ifstream ifs;

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

	//5、關閉檔案
	ifs.close();
}

int main()
{
	test01();

	system("pause");
	return 0;
}

總結:

  • 檔案輸入流物件 可以透過 read 函式,以二進位制方式讀資料

相關文章