【複習】C++面向對象基礎2


面向對象基礎2

帶有指針的class。即class成員中有指針數據成員。

1.Big Three

構造函數

由於成員有指針,在構造該類對象的時候,需要自己初始化這個指針。

析構函數

在析構函數中,需要用戶自己維護這個指針成員數據,即確保對象銷燬(析構)時,指針成員指向的空間也被回收。

// 析構函數
inline
String::~String()
{	
    // delete的時候是delete [] ,這個括號不能省略,告訴編譯器,這個m_data是個數組,通常和new []成對出現
    delete[] m_data;
}

inline
String::String(const char* cstr = 0)
{
    // 默認值是0也是空指針,表示空字符串。
    if (cstr) {
        m_data = new char[strlen(cstr)+1];
        strcpy(m_data, cstr);
    }
    else {  // 未指定初值
        m_data = new char[1];
        *m_data = '\0';
    }
}

拷貝構造(copy constructor)

下圖所示的是沒有拷貝構造時候的系統默認的“淺拷貝”模型。首先創建對象a、b,他們內部的指針分別指向了兩個string。當b=a;代碼執行時,c++默認將a中的數據成員拷貝給b,所以他們的指針成員變量的值就是一樣的了,即指向同一個string。這是出現兩個問題

  • 原來b的那個空間(“world”)內存泄漏了。
  • 現在a和b指向了同一塊內存(“hello”),導致a和b糾纏,改一個,另一個也會改,我們需要的是兩個獨立但內容相同的對象。

1561009205053

“深拷貝”:在構造函數中,自己完成值的拷貝過程。


inline
String::String(const String& str)
{
    // new在堆區申請了另一塊空間,然後將str中的字符拷貝到這個空間中
    // 這樣兩個對象就不是在操作同一塊內存了。但值仍然相同。
	m_data = new char[ strlen(str.m_data) + 1 ];
	strcpy(m_data, str.m_data);
}
// 這裏有個問題,就是原來該string對象的m_data的內存區域並沒有釋放。還是會泄漏。

拷貝賦值(copy operator=)

與拷貝構造一樣,在賦值時,構建新對象,進行“深拷貝”。

inline
String& String::operator=(const String& str)
{
    // 自我檢測,應對:a=a的情況
    if (this == &str)
        return *this;
    
    // 刪除原來的空間
    delete[] m_data;
    // 創建新空間
    m_data = new char[ strlen(str.m_data) + 1 ];
    // 拷貝內容
    strcpy(m_data, str.m_data);
    return *this;
}
// 使用
{
    String s1("hello ");
    String s2(s1);
    s2 = s1;
}

這裏的自我檢測部分一定要注意!!!若沒有自我檢測,則當有代碼a=a;執行時,會訪問錯誤的內存空間!!

完整案例

#pragma once
#include<cstring>
#include<iostream>

using namespace std;

namespace MyString {

class String {
public:
	String(const char* cstr);
	String(const String& str);
	String& String::operator=(const String& str);
	~String();

private:
	char* m_data;
};

inline
String::String(const String& str)
{
	cout << "copy constructor..." << endl;
	// new在堆區申請了另一塊空間,然後將str中的字符拷貝到這個空間中
	// 這樣兩個對象就不是在操作同一塊內存了。但值仍然相同。
	m_data = new char[strlen(str.m_data) + 1];
	strcpy_s(m_data, strlen(str.m_data) + 1,str.m_data);
}

inline
String& String::operator=(const String& str)
{
	cout << "copy op=..." << endl;
	// 自我檢測,應對:a=a的情況
	if (this == &str)
		return *this;

	// 刪除原來的空間
	delete[] m_data;
	// 創建新空間
	m_data = new char[strlen(str.m_data) + 1];
	// 拷貝內容
	strcpy_s(m_data, strlen(str.m_data) + 1, str.m_data);
	return *this;
}

inline
String::~String()
{
	// delete的時候是delete [] ,這個括號不能省略,告訴編譯器,這個m_data是個數組,通常和new []成對出現
	delete[] m_data;
}

inline
String::String(const char* cstr = 0)
{
	cout << "normal constructor..." << endl;
	// 默認值是0也是空指針,表示空字符串。
	if (cstr) {
		m_data = new char[strlen(cstr) + 1];
		strcpy_s(m_data, strlen(cstr) + 1, cstr);
	}
	else {  // 未指定初值
		m_data = new char[1];
		*m_data = '\0';
	}
}

void test() {
	String a("hello");	// normal constructor...
	String b(a);		// copy constructor...
	String c = b;		// copy constructor...
	c = a;				// copy op=...
}

}

2.stack and heap

Stack

Stack是存在於某作用域 (scope) 的一塊內存空間 (memory space)。例如當你調用函數,函數本身即會形成一個 stack 用來放置它所接收的參數,以及返回地址。在函數本體 (function body) 內聲明的任何變量,其所使用的內存塊都取自上述 stack。

Heap

Heap,或謂 system heap,是指由操作系統提供的一塊 global 內存空間,程序可動態分配 (dynamic allocated) 從某中獲得若干區塊 (blocks)。

class Complex {}; 
... 
{
    Complex c1(1,2);  // c1 所佔用的空間來自 stack
    Complex* p = new Complex(3);
    // Complex(3) 是個臨時對象,其所佔用的空間乃是以 new 自 heap 動態分配而得,並由 p 指向。
    // 這裏的p指針變量任然是stack中的local變量
}


3.對象的生命週期

stack object

class Complex { ... }; 
... 
{
    Complex c1(1,2);
}

c1 便是所謂 stack object,其生命在作用域 (scope) 結束之際結束。這種作用域內的 object,又稱為 auto object,因為它會被「自動」清理(作用域結束時,自動調用析構函數)。

static local objects

class Complex {}; 
... 
{
    static Complex c2(1,2);
}

c2 便是所謂 static object,其生命在作用域 (scope) 結束之後仍然存在,直到整個程序結束。之前也提過,static的變量是存在全局數據區的,會一致存在在整個代碼執行期間。

global object

class Complex {}; 
... 
    Complex c3(1,2);
int main()
{
    ...
}

c3 便是所謂 global object,其生命在整個程序結束之後才結束。你也可以把它視為一種 static object,其作用域是「整個程序」。與static變量一樣,全局變量global var也是存在於全局數據區的。

C++程序的內存格局通常分爲四個區:全局數據區(data area),代碼區(code area),棧區(stack area),堆區(heap area)(即自由存儲區)

heap objects

class Complex {}; 
... 
{
    Complex* p = new Complex;
    ... 
    delete p;
}

P 所指的便是 heap object,其生命在它被 deleted 之際結束。所以若不手動delete該內存區域,p 所指的 heap object 仍然存在,但指針 p 的生命卻結束了,作用域之外再也看不到 p(也就沒機會 delete p),也就造成了內存泄漏。


4.new and delete

new的內部機制

Complex* pc = new Complex(1,2);

  • 分配內存:operator new函數,內部調用c的malloc函數。
  • 類型轉換:malloc返回的是void類型的指針,需要轉換到目標類型。pc = static_cast<target_type>(p)
  • 調用構造函數:pc->Complex::Complex(1,2);這裏默認傳入了當前對象,即指針pc。

delete的內部機制

String * ps = new String("Hello");

...
    
 delete ps;
  • 先調用析構:String::~String(ps);這一步的析構是要求用戶自定義實現的,銷燬String內的指針開闢的空間,即用戶自己申請的資源要在這一步銷燬。
  • 再調用operator delete(ps)函數:這一步是編譯器自己執行的,用來銷燬當前對象本身,即當前String對象ps。內部調用的是C的free(ps)函數。
  • 這裏需要有個概念,就是帶有指針的對象它實際上是包含兩部分的,其一是對象本身。其二是對象的指針成員指向的用戶自己開闢的額外空間,這部分用戶自己維護,所以要在析構中自己回收。

5.動態內存分配

在這裏插入圖片描述

new的時候申請了多少內存?

  • debug模式和release模式不同,debug模式會額外申請內存。
  • 同時對象只存儲其數據區,函數是共用的,不會每個對象都額外拷貝一份函數代碼。
  • 分配的內存首尾會分配一個標誌,cookie。它用來紀錄整個分配空間的大小,以及分配狀態。
  • 又分配的內存需要時16的倍數,若不夠,需要填充:padding。

動態分配array,即 new []
在這裏插入圖片描述

  • 連續型內存分配。首先數組就是空間線性分配的。(相對的就是鏈式存儲)
  • array new需要搭配array delete。
    在這裏插入圖片描述

若不delete [],而只是delete,那麼它只會銷燬該數組,但是該數組中的每個元素(對象)指向的堆空間不會被回收。


6.static

C中的static var

  • 普通變量存在於棧空間,作用域結束後,自動回收該棧空間,即出入棧操作。
  • 靜態局部變量使用static修飾符定義,靜態局部變量存儲於進程的全局數據區,作用域結束,該內存區域也任然存在。
  • 全局變量定義在函數體外部,在全局數據區分配存儲空間。與static的local 變量類似。

C中的static func

在函數的返回類型前加上static,就是靜態函數。

  • 靜態函數只能在聲明它的文件中可見,其他文件不能引用該函數
  • 不同的文件可以使用相同名字的靜態函數,互不影響
  • 即將函數隱藏在本文件中,不得外部訪問。

c++中的static

類class的static數據成員變量:該數據成員就是類內的靜態數據成員

  • 靜態數據成員存儲在全局數據區,靜態數據成員在定義時分配存儲空間,所以不能在類聲明中定義。
  • 它屬於類,而不單獨屬於類的任何一個object
  • 靜態數據成員的拷貝只有一個,且對該類的所有對象可見。(而對於非靜態數據成員,每個對象都有自己的一份拷貝。)
  • 靜態數據成員的初始化格式:<數據類型><類名>::<靜態數據成員名>=<值>,必須在聲明class的時候爲靜態數據成員初始化。
  • 類的靜態數據成員有兩種訪問方式:<類對象名>.<靜態數據成員名> 或 <類類型名>::<靜態數據成員名>

類class的static成員函數:靜態成員函數屬於整個類,而不是某一個對象

  • 靜態成員函數沒有this指針,它無法訪問屬於類對象的非靜態數據成員,也無法訪問非靜態成員函數,它只能調用其餘的靜態成員函數。
  • 非靜態成員函數可以任意地訪問靜態成員函數和靜態數據成員。(類的公共屬性(靜態數據成員和函數成員)不能訪問具體的某個對象的屬性,但對象個體可以訪問該class的共同的公共屬性)

在這裏插入圖片描述

  • c1.real,c2.real調用的是同一個Complex::real,但是通過傳入的調用者的指針(this)不同,實現了該函數訪問不同的對象的數據。

  • static的data member和static的member function,以及所有的member functions都只有一份,但member function會根據傳入的this指針訪問不同對象的數據,但,static修飾的data member和function沒有this指針它屬於整個class,不能處理到某個確定的object。

  • static成員變量必須在類外部初始化。調用的時候既可以通過object調用,也可以通過class name調用。

    class Account {
      public:
          static double m_rate;
          static void set_rate(const double& x) { m_rate = x; }
      };
      
      double Account::m_rate = 8.0; 
      
      int main() {
          Account::set_rate(5.0);
          Account a;
          a.set_rate(7.0);  
    }
    

通過static實現單例模式

class A {
public:
    static A& getInstance();
    setup() { ... }
private:
    A();
    A(const A& rhs);
    ...
};

A& A::getInstance()
{
    static A a;
    return a;
}


7.template 模板

類模板

template<typename T> class cla_name{}

template<typename T>
class complex
{
public:
    complex (T r = 0, T i = 0)
    : re (r), im (i) 
    { }
    complex& operator += (const complex&);
    T real () const { return re; }
    T imag () const { return im; }
private:
    T re, im;
    friend complex& __doapl (complex*, const complex&); 
};

// 使用
{
    complex<double> c1(2.5,1.5);
    complex<int> c2(2,6);
    ...
}

模板類會根據實際類型,爲每個類型都編譯出一份代碼,這就是所謂“代碼膨脹”。所以上例中,如果有static數據的話,complex<double>和complex<int>將會有兩份static,而不是1份。若是Java,靜態數據就只會有1份。

函數模板function template

與類模板最大的區別是,函數模板不用顯式規定模板類型,它會根據傳入參數的類型自動推斷。


class stone
{
public:
    stone(int w, int h, int we) : _w(w), _h(h), _weight(we){ }
    bool operator< (const stone& rhs) const { 
        // 這裏需要T的實參類設計者實現<的操作符重載操作
        return _weight < rhs._weight; 
    }
    private:
    int _w, _h, _weight;
};


// 定義
template <class T>
inline const T& min(const T& a, const T& b) 
{
	return b < a ? b : a;
}

// 使用
{
    stone r1(2,3), r2(3,3), r3;
	r3 = min(r1, r2);
	stone r1(2,3), r2(3,3), r3;
	r3 = min(r1, r2);
}


8.explicit

防止隱式轉換類型而構造對象

explicit關鍵字只需用於類內的單參數構造函數前面。由於無參數的構造函數和多參數的構造函數總是顯示調用,這種情況在構造函數前加explicit無意義。

google的c++規範中提到explicit的優點是可以避免不合時宜的類型變換,缺點無。所以google約定所有單參數的構造函數都必須是顯示的,只有極少數情況下拷貝構造函數可以不聲明稱explicit。例如作爲其他類的透明包裝器的類。

ref:https://www.cnblogs.com/rednodel/p/9299251.html

class CxString  // 沒有使用explicit關鍵字的類聲明, 即默認爲隱式聲明  
{  
public:  
    char *_pstr;  
    int _size;  
    CxString(int size)  
    {  
        _size = size;                // string的預設大小  
        _pstr = malloc(size + 1);    // 分配string的內存  
        memset(_pstr, 0, size + 1);  
    }  
    CxString(const char *p)  
    {  
        int size = strlen(p);  
        _pstr = malloc(size + 1);    // 分配string的內存  
        strcpy(_pstr, p);            // 複製字符串  
        _size = strlen(_pstr);  
    }  
    // 析構函數這裏不討論, 省略...  
};  
  
    // 下面是調用:  
  
    CxString string1(24);     // 這樣是OK的, 爲CxString預分配24字節的大小的內存  
    CxString string2 = 10;    // 這樣是OK的, 爲CxString預分配10字節的大小的內存  
    CxString string3;         // 這樣是不行的, 因爲沒有默認構造函數, 錯誤爲: “CxString”: 沒有合適的默認構造函數可用  
    CxString string4("aaaa"); // 這樣是OK的  
    CxString string5 = "bbb"; // 這樣也是OK的, 調用的是CxString(const char *p)  
    // 這裏注意!!!並不是創建c字符串!
	CxString string6 = 'c';   // 這樣也是OK的, 其實調用的是CxString(int size), 且size等於'c'的ascii碼  
    string1 = 2;              // 這樣也是OK的, 爲CxString預分配2字節的大小的內存  
    string2 = 3;              // 這樣也是OK的, 爲CxString預分配3字節的大小的內存  
    string3 = string1;        // 這樣也是OK的, 至少編譯是沒問題的, 但是如果析構函數裏用free釋放_pstr內存指針的時候可能會報錯, 完整的代碼必須重載運算符"=", 並在其中處理內存釋放

// 這裏的 CxString string2 = 10; 就是一個隱式的類型轉換構造,等同於
CxString temp(10);  
CxString string2 = temp;

// 若將構造函數聲明成:explicit CxString(int size)  
// 則CxString string2 = 10;會調用失敗

9.對象關係

C++中的面向對象,往往被描述爲三大特性:封裝、繼承、多態。但其實對象之間的關係也是面向對象編程設計的一個重要概念。它被分爲3類:複合(composition)、委託(delegation)、繼承(inheritance)

複合(composition)

  • class A中的成員爲class B對象。即A中需要用到B提供的內容(數據、函數)。這種情況下,表示A中有B(has-a),需要注意兩點:內存模型和生命週期。

  • 內存模型:如標準庫中的queue類。queue複合了deque,deque複合了兩個Itr對象。
    在這裏插入圖片描述在這裏插入圖片描述
    內存中,是包含關係。queue是40個字節,其數據成員只有一個deque對象,deque正好是40字節,而deque又是兩個16字節的Itr(這個Itr從命名以及struct結構看,應該是一個迭代器)。

  • 生命週期:構造函數、析構函數。

    container的構造函數先調用component默認的構造函數(這是編譯器默認的,若compoment沒有默認構造怎麼辦?——被用戶override了),然後才執行自己的構造函數。

如果一個類,你沒有定義構造函數,那麼系統默認會有一個無參的構造函數。但如果你定義了一個有參的構造函數,爲了保證正確性,系統不會創建無參構造函數,這時候,如果你還想允許無參構造,就必須顯式的聲明一個

  #include<iostream>
using namespace std;
  
class componet_a {
  public:
  	// 不override默認的無參構造器
  	// 不寫任何構造函數
      // 情況三:什麼都不做,編譯器自動給出一個默認的無參數的構造器
  private:
  	// 隨便給個數據
  	int a;
  };
  
  class componet_b {
  public:
  	// 給定一個構造器
  	componet_b(int i=0) :a(i) {
  		cout << "componet_b, constructor with parameter.." << endl;
  	}
      /*
      情況一:int i,不給默認值0,此時相當於component給了一個帶參數的構造器,此時編譯器不再爲該class提供默認的無參構造器,main中container ct;這句會編譯不通過。
      情況二:int i=0,此時等價於提供了一個無參構造器,因爲有默認值i,所以可以無參數,這樣container知道如何初始化自己的成員componet_b comb。
      */
  private:
  	int a;
  };
  
  class container {
  private:
  	componet_a coma;
  	componet_b comb;
  
  public:
  	container() {
  		cout << "container constructor.." << endl;
  	}
  };
  
  int main() {
  	container ct;
  	return 0;
  }
  

container的析構函數首先執行自己,然後函數體內部默認調用各個componet的析構函數(可以看一下析構的順序,就是說默認的析構component的代碼是在container的析構代碼的開頭還是結尾部分)。

#include<iostream>
using namespace std;

class componet_b {
public:
	// override析構函數
	~componet_b() {
		cout << "componet_b, destructor .." << endl;
	}
private:
	int a;
};

class container {
private:
	componet_b comb;

public:
	~container() {
		cout << "container destructor.." << endl;
        // component的析構等同於在這個位置默認調用!
	}
};

int main() {
	container ct;
	return 0;
}

// output:
// container destructor..
// componet_b, destructor ..

委託(delegation)

所謂委託其實和複合差不多,唯一區別就是委託並不直接在container中包含component,而是包含component的指針。由於是指針,所以內存模型和生命週期相對複合發生了變化。

  • 內存模型:container並不直接包含componet,即元素大小不再如複合那樣了。因爲指針是4個byte。component的實際存儲空間不是包含在container空間中,而是另一塊內存區域。他們之間通過指針聯繫。

  • 生命週期:container析構時只會默認銷燬這個指針變量,但指針所指的那個對象需要手動調用析構,所以帶有指針的class,需要自己維護這個指針(new和delete需要自己處理,通常在析構函數中delete掉這個指針指向的對象內存)。

  • 由於是指針,需要注意多個指針指向同內存對象的問題。而標準庫利用了這點,進行了字符串的緩存(多個相同的字符串實際在內存中只有一份,若需要修改某一個時,單獨將它拷貝出一份)。

    在這裏插入圖片描述
    如圖標準庫中的String類,實際是通過StringRep指針實現字符串的。而StringRep中有一個reference counting(int count,這個計數應該是對象的,而不是class的——不是static的,因爲這裏是計數相同字符串的“引用”的個數,而不是內存中有多少個StringRep對象的計數,後者通常用static,在Java中jvm的垃圾回收GC好像是類似的做法)。這裏三個String對象a、b、c內容都是“Hello”,所以其實內存中只有一份“hello”,通過指針鏈接到這三個String對象,若其中一個(例如a)需要修改成“Hi”,那麼爲它單獨重新創建一個string“Hi”,然後指針指到“Hi”。

繼承(inheritance)

繼承表示的是“is-a”的關係。子類(derived)擁有父類(base)的部分成分(根據繼承的類別)。如圖,_List_node包含父類**_List_node_base**的數據成員的。(通常說的時候是pair-wise:派生類-基類,子類-父類)

在這裏插入圖片描述

繼承:1)繼承了數據。2)繼承了函數的調用權!

子類需要重新定義函數嗎?(override)

  • non-virtual,不希望子類覆寫的函數。(普通函數)
  • virtual,希望子類覆寫,但是在父類中給了默認的實現,子類不覆寫也不會編譯錯誤。(函數用virtual修飾,並且給出函數定義,或者說實現)
  • pure virtual,純虛函數,父類不實現該函數(類似於接口),子類必須實現該函數。(virtual修飾,且無函數定義)
    在這裏插入圖片描述
class A {
public:
    A();
    virtual ~A();
    void f1();
    virtual void f2();
    virtual void f3()=0;
};

class B:public A{
public:
    B();
    virtual ~B();
    void f1();
    virtual void f2();
    virtual void f3();
};

int main(int argc,char * argv[]) {
    A *m_j = new B();
    m_j -> f1();
    m_j -> f2();
    m_j -> f3();
    delete m_j;
    return 0;
}

// f1()是一個隱藏,關於函數的隱藏,可以參考其它詞條.
// 	調用m_j->f1();會去調用A類中的f1(),它是在我們寫好代碼的時候就會定好的.也就是根據它是由A類定義的,這樣就調用這個類的函數.
// f2()是重寫(覆蓋).
// 調用m_j->f2();會調用m_j中到底保存的對象中,對應的這個函數.這是由於new的B對象.(調用派生類的f2())
// f3()與f2()一樣,只是在基類中不需要寫函數實現.
// f2與f2這裏是多態現象,多態的實現基於虛函數表。而f1本身不是虛函數,所以這裏f1調用的還是A類的方法。

繼承的構造與虛構

  • derived子類的構造函數先調用base父類的的默認構造函數,再執行自己的構造函數。
  • derived的析構函數首先執行自己,然後才調用base的析構函數。
#include<iostream>
using namespace std;

class base {
public:
	// 這裏若不給無參構造,則由於給了有參構造,編譯器默認的無參構造將無效,此時derived的構造函數編譯錯誤。
	base() : a(100){
		cout << "base, constructor with non-parameter.." << endl;
	}
	// 給定一個構造器
	base(int i) :a(i) {
		cout << "base, constructor with parameter.." << endl;
	}
	~base(){
		cout << "base, destructor .." << endl;
	}
protected:
	// 隨便給個數據
	int a;
};


class derived : public base {
private:
	float b;

public:
    // 這裏會默認調用父類base的無參構造函數
    // 可以可以改爲:derived(double i):base(i) ,這樣可以指定調用父類的有參構造,且這裏發生double自動向上轉型爲int
	derived() {
		cout << "derived constructor.." << endl;
	}
	~derived(){
		cout << "derived, destructor .." << endl;
        // 父類的析構函數在這裏被自動調用
	}
	int get_a() {
		return this->a;
	}
};

int main() {
	derived dr;
	cout << dr.get_a() << endl;
	return 0;
}

// base, constructor with non-parameter..
// derived constructor..
// 100
// derived, destructor ..
// base, destructor ..

繼承+複合

在這裏插入圖片描述

上圖第一種情況:

  • Derived 的構造函數首先調用 Base 的 default 構造函數,然後調用 Component 的 default 構造函數,然後才執行自己。

    Derived::Derived(…): Base(),Component() { … };

  • Derived 的析構函數首先執行自己,然後調用 Component 的 析構函數,然後調用 Base 的析構函數。

    Derived::~Derived(…){ … ~Component(), ~Base() };

第二種情況是簡單的嵌套,調用順序簡單不再多說。

委託+繼承(常用!尤其見於各種設計模式)
在這裏插入圖片描述

應用:觀察者模式

在這裏插入圖片描述

(觀察者模式和監聽模式應該是常用的兩種模式了,在許多GUI框架裏,比如Java的swing裏,有大量的listener,就是採用的監聽模式。在Android開發中,也常用觀察者模式來觸發UI刷新)


10.虛函數與多態

在這裏插入圖片描述

多態:

在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數。如果對象類型是派生類,就調用派生類的函數;如果對象類型是基類,就調用基類的函數。注意new的類型,與申明的類型。new的類型纔是真正的類型,是內存中對象類型。

Son son;Father *pFather=&son;或者 Father *pFather = new Son;

  • 用virtual關鍵字申明的函數叫做虛函數,虛函數肯定是類的成員函數。
  • 存在虛函數的類都有一個一維的虛函數表叫做虛表,類的對象有一個指向虛表開始的虛指針。虛表是和類對應的,虛表指針是和對象對應的。
  • 純虛函數是虛函數再加上 = 0;
  • 抽象類是指包括至少一個純虛函數的類。(即子類必須覆寫方法)

在這裏插入圖片描述

這裏的圖比較清晰:https://www.cnblogs.com/dormant/p/5223215.html

基類:

class Shape {
 public:
     Shape();
     virtual  ~Shape();
     virtual void render();
     void move(const pos&);
     virtual void resize();
protected:
     pos center;
};

內存模型:
在這裏插入圖片描述

派生類:

class Ellipse : public Shape{
public:
    Ellipse (float majr, float minr);
    virtual void render();

protected:
    float major_axis;
    float minor_axis;

};

內存分佈:
在這裏插入圖片描述
這裏的vtable不是對象的,而是屬於類的。

附另一個參考:https://blog.csdn.net/u012630961/article/details/81226351(代碼、圖示、debug非常清楚,贊一個!)

應用:利用多態實現 模板設計模式

#include<iostream>
using namespace std;

// 模板類:泡一杯飲料
// 反正方法都一樣:1,燒開水放到水杯中; 2,倒入飲料; 3, 加配料。
// 但具體泡啥(茶、咖啡都可以),加啥(糖、冰、奶都可以),由用戶決定
class make_drink {
public:
	void make() {
		water(); // 燒水並倒入杯中
		add(); // 添加飲料
		flavouring(); // 添加配料
	}

	void water() {
		cout << "燒水並倒入杯中 .." << endl;
	}
	virtual void add() = 0;

	virtual void flavouring() {
		// 用戶不說口味,那就默認加糖
		cout << "加糖 .." << endl;
	}
};

class make_coffee : public make_drink {
public:
	virtual void add() {
		cout << "倒入coffee .." << endl;
	}
};

class make_juice : public make_drink {
public:
	virtual void add() {
		cout << "倒入juice .." << endl;
	}
	virtual void flavouring() {
		cout << "加冰,來點檸檬 .." << endl;
	}
};

int main(){
	make_drink* mc = new make_coffee;
	mc->make();

	make_drink* mc2 = new make_juice;
	mc2->make();

	return 0;
}

/*
燒水並倒入杯中 ..
倒入coffee ..
加糖 ..

燒水並倒入杯中 ..
倒入juice ..
加冰,來點檸檬 ..
*/


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章