C++初階(封裝+多型--整理的自認為很詳細)

2022-11-19 21:01:15

繼承

概念:繼承機制是物件導向程式設計使程式碼可以複用的最重要的手段,它允許程式設計師在保持原有類特性的基礎上進行擴充套件,增加功能,這樣產生新的類,稱派生類。繼承呈現了物件導向程式設計的層次結構,體現了由簡單到複雜的認知過程。以前我們接觸的複用都是函數複用,繼承是類設計層次的複用

語法:

//基礎類別(父類別)
class Base
{
private:
	int m1;
	int m2;
}
//派生類
class Son:public Base
{
private:
	int v3;
	int v4;
}

繼承方式

存取限定符:

  • public存取
  • protected存取
  • private存取

1.公有繼承

  • 父類別的公有屬性和成員,到子類還是公有
  • 父類別的私有屬性和成員,到子類還是私有,但是子類成員不可以存取這些屬性和成員
  • 父類別的保護屬性和成員,到子類還是保護

2.保護繼承

  • 父類別的公有屬性和成員,到子類是保護
  • 父類別的私有屬性和成員,到子類還是私有,但是子類成員不能存取這些屬性和成員
  • 父類別的保護屬性和成員,到子類還是保護

3.私有繼承

  • 父類別的公有屬性和成員,到子類是私有

  • 父類別的私有屬性和成員,到子類還是私有,但是子類成員不能存取這些屬性和成員

  • 父類別的保護屬性和成員,到子類是私有

類成員/繼承方式 public繼承 protected繼承 private繼承
基礎類別的public成員 派生類的public成員 派生類的protected成員 派生類的private成員
基礎類別的protected成員 派生類的protected成員 派生類的protected成員 派生類的private成員
基礎類別的private成員 派生類中不可見 派生類中不可見 派生類中不可見

總結:

  • 基礎類別的private成員在派生類中都是不可見的,這裡的不可見是指基礎類別的私有成員還是被繼承到了派生類物件中,但是語法上限制派生類物件不管在類裡面還是類外面都不能去存取它。
  • 基礎類別成員在父類別中的存取方式=min(成員在基礎類別的存取限定符,繼承方式),public>protected>private。
  • 一般會把基礎類別中不想讓類外存取的成員設定為protecd成員,不讓類外存取,但是讓派生類可以存取。

基礎類別和派生類物件之間的賦值轉換

派生類物件會通過 「切片」「切割」 的方式賦值給基礎類別的物件、指標或參照。但是基礎類別物件不能賦值給派生類物件。

注意:

  • 從父類別繼承過來的成員變數,本質還是原來父類別的成員變數,兩個變數是一個地址
  • 如果子類和父類別出現兩份一模一樣的成員變數,要存取父類別中的變數,必須使用作用域分辨符,如果不想使用作用域分辨符,對這個名字修改預設修改的就是子類的成員變數,不想使用作用域分辨符,那就在設計類的時候讓兩個名字不要衝突
  • 記住,成員變數只要父子不同名,那麼用的都是同一塊地址,子類中的那個成員變數就是父類別的,但是如果子類和父類別成員變數重名,那麼就會隱藏掉父類別的成員變數,除非用::存取
class Person
{
public:
	Person(const char* name = "")
		:_name(name)
	{}
	void Print()
	{
		cout << "name:" << _name << " age:" << _age << endl;
	}
protected:
	string _name = "";
	int _age = 1;
};
class Student : public Person
{
public:
	Student()
		:Person("xiaoming")
	{}
	void Print()
	{
		cout << "name:" << _name << " age:" << _age << " _stuid:" << _stuid << " _major:" << _major << endl;
	}
private:
	int _stuid = 0;// 學號
	int _major = 0;// 專業
};
int main()
{
	Student s;
	// 子類物件可以賦值給父類別的物件、指標和參照,反過來不行
	// Student物件通過 「切片」 或 「切割」 的方式進行賦值
	Person p1 = s;
	Person* p2 = &s;
	Person& p3 = s;

	p1.Print();
	p2->Print();
	p3.Print();

	// 基礎類別的指標可以通過強制型別轉換賦值給派生類的指標
	Student* ps = (Student*)p2;

	ps->Print();

	return 0;
}

執行結果如下:

總結:

  • 派生類物件可以「切片」或「切割」的方式賦值給基礎類別的物件,基礎類別的指標或基礎類別的參照,就是把基礎類別的那部分切割下來。
  • 基礎類別物件不能給派生類物件賦值。
  • 基礎類別的指標可以通過強制型別轉換賦值給派生類的指標。但必須是基礎類別的指標指向派生類的物件才是安全的,因為如果基礎類別是多型型別,可以使用RTTI來進行識別後進行安全轉換。
  • 子類物件可以賦值給父類別的物件、指標和參照,會將子類物件多出的部分進行分割或切片處理。

繼承中的作用域

在繼承體系中,基礎類別和派生類物件都有獨立的作用域,子類中的成員(成員變數和成員函數)會對父類別的同名成員進行隱藏,也叫重定義。

class Father
{
public:
	Father()
	{
		a = 10;
	}
	void func()
	{
		cout << "Father func" << endl;
	}
	void func(int a)
	{
		cout << "Father func (int a)" << endl;
	}void func(int a, int b)
	{
		cout << "Father func (int a)(int b)" << endl;
	}
public:
	int a;
};
class Son :public Father {
public:
	int a;
public:
	Son()
	{
		a = 20;
	}
	void func()
	{
		cout << "son func" << endl;
	}
};
//當子類和父類別有同名成員的時候,子類的同名成員會隱藏父類別的同名成員
void test()
{

	Son s;
	cout << s.a << endl;
	//通過父類別名+作用域來存取
	cout << s.Father::a << endl;

}
//當子類有和父類別同名函數的時候,父類別的所有函數過載都會被隱藏
//如果真的要存取需要用到作用域
void test02()
{
	Son s;
	s.func();
	//s.func(10);err
	//s.func(10,20);err
	s.Father::func(10);
	s.Father::func(10, 20);
}
int main()
{
	test();
	test02();
	system("pause");
	return EXIT_SUCCESS;
}

執行結果如下:

得出結論: 子類中的成員(成員變數和成員函數)會對父類別的同名成員進行隱藏,如果相要存取父類別的同名成員,必須指定類域存取。

子類的記憶體佈局

例子:

#include <iostream>
 
class Father
{
public:
    Father()
    {
        std::cout << "I am father,this is " << this << std::endl;
    }
 
public:
    void func_father()
    {
        std::cout << "傳入 Fahter::func_father() 的 this 指標是 " << this << std::endl;
    }
private:
    int father;
};
 
class Mother
{
public:
    Mother()
    {
        std::cout << "I am Mother,this is " << this << std::endl;
    }
 
public:
    void func_mother()
    {
        std::cout << "傳入 Mother::func_mother() 的 this 指標是 " << this << std::endl;
    }
private:
    int mother;
};
 
class Son : public Father,
            public Mother
{
public:
    Son()
    {
        std::cout << "I am Son,this is " << this << std::endl;
    }
 
public:
    void func_Son()
    {
        std::cout << "傳入 Son::func_Son() 的 this 指標是 " << this << std::endl;
    }
private:
    int son;
};
 
int main()
{
    Son s;
 
    std::cout << std::endl;
    
    s.func_father();
    s.func_mother();
    s.func_Son();
 
    return 0;
}

結果:

I am father,this is 0xffffcc14
I am Mother,this is 0xffffcc18
I am Son,this is 0xffffcc14
 
傳入 Fahter::func_father() 的 this 指標是 0xffffcc14
傳入 Mother::func_mother() 的 this 指標是 0xffffcc18
傳入 Son::func_Son() 的 this 指標是 0xffffcc14

解釋:

子類的記憶體佈局如下圖所示

由於「Son」繼承順序是「Father」、「Mother」,所以記憶體佈局中 Father 類排布在起始位置,之後是 Mother 類,最後才是 Son 類自身的變數(當然,初始化順序也是 Father 、Mother,最後才是 Son )。

最後還有一個問題,為什麼 Son 的物件呼叫可以呼叫 Father 和 Mother 類的函數呢?

因為編譯器在呼叫 Father 和 Mother 的函數時,調整了傳入到 Func_Father 和 Func_Mother 函數的 this 指標,使 this 指標是各個函數所在類的物件的 this 指標,從而達到了呼叫各個類的函數的目的。
換句話說,每個子類物件都有自己的this指標,在自己的記憶體空間中先初始化父類別的成員變數,最後初始化自己的成員變數,如果有重名的成員變數依舊會往下排,但是呼叫的時候預設呼叫子類的成員變數,並且在子類的記憶體空間中會為每個基礎類別物件分配一個指標,這樣保證了我們在呼叫基礎類別的成員函數的時候,傳入的this指標不同。

繼承中的構造和解構

  • 先呼叫父類別的構造,然後呼叫成員物件的構造,最後呼叫本身的構造
  • 先呼叫本身的解構,再呼叫成員函數的解構,最後呼叫父類別的解構
  • 本質就是個入棧的問題,先入棧的後解構

派生類的預設成員函數

C++中的每個物件中會有6個預設成員函數。預設的意思就是我們不寫,編譯器會生成一個。那麼在繼承中,子類的預設成員函數是怎麼生成的呢?

class Person
{
public:
	Person(const char* name = "", int age = 1) :_name(name), _age(age)
	{
		cout << "Person的建構函式" << endl;
	}
	Person(const Person& p):_name(p._name),_age(p._age)
	{
		cout << "Person的拷貝構造" << endl;
	}
	Person operator=(const Person& p)
	{
		this->_name = p._name;
		this->_age = p._age;
		return *this;
	}
	void Print()
	{
		cout << "name:" << this->_name << "age:" << this->_age << endl;
	}
	~Person()
	{
		cout << "Person的解構函式" << endl;
	}
protected:
	string _name;
	int _age;
};
class Student:public Person
{
public:
	// 此處呼叫父類別的建構函式堆繼承下來的成員進行初始化,不寫的話,編譯器呼叫父類別的預設建構函式
	Student(const char* name, int age, int stuid = 0) :Person(name, age), _stuid(stuid)
	{
		cout << "Student的建構函式" << endl;
	}
	//子類物件可以傳給父類別的物件、指標或者參照
	Student(const Student& s):Person(s),_stuid(s._stuid)
	{
		cout << "Student的拷貝建構函式" << endl;
	}
	//操作過載符也會被派生類繼承
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator=(s);// 先完成基礎類別的複製
			_stuid = s._stuid;
		}

		return *this;
	}
	void Print()
	{
		cout << "name:" << _name << " age:" << _age << " _stuid:" << _stuid << endl;
	}
	~Student()
	{
		// 基礎類別和派生類的解構函式的函數名都被編譯器處理成了destruction,構成隱藏,是一樣指定域存取
		//Person::~Person();// 不需要顯示呼叫 編譯器會自動先呼叫派生類的解構函式,然後呼叫基礎類別的解構函式
		cout << "Student()的解構函式" << endl;
	}
private:
	int _stuid;//學號
};

測試1:建構函式和解構函式

void test1()
{
	Student s("小明", 18, 10);
	s.Print();
}

執行結果如下:

總結1: 子類別建構函式必須呼叫基礎類別的建構函式初始化基礎類別的那一部分成員,如果基礎類別沒有預設建構函式,則必須在派生類建構函式的初始化列表階段顯示呼叫。子類的解構函式會在被呼叫完成後自動呼叫基礎類別的解構函式清理基礎類別的成員。不需要顯示呼叫。這裡子類和父類別的解構函式的函數名會被編譯器處理成destructor,這樣兩個函數構成隱藏。
測試2:拷貝建構函式

void test2()
{
	Student s1("小明", 18, 10);
	Student s2(s1);
}

執行結果如下:

總結2: 子類的拷貝構造必須代用父類別的拷貝構造完成父類別成員的拷貝(自己手動呼叫)

測試3:operator=

void test3()
{
	Student s1("小明", 18, 10);
	Student s2("小花",19,20);
	s1 = s2;
}

執行結果如下:

結論3: 子類的operator=必須呼叫基礎類別的operator完成基礎類別的賦值。

思考:如何設計一個不能被繼承的類

把該類別建構函式設為私有。如果基礎類別的建構函式是私有,那麼派生類不能呼叫基礎類別的建構函式完成基礎類別成員的初始化,則無法進行構造。所以這樣設計的類不可以被繼承。(後面還會將加上final關鍵字的類也不可以被繼承)

總結:

  • 子類別建構函式必須呼叫基礎類別的建構函式初始化基礎類別的那一部分成員,如果基礎類別沒有預設建構函式,則必須在派生類建構函式的初始化列表階段顯示呼叫。
  • 子類的拷貝構造必須代用父類別的拷貝構造完成父類別成員的拷貝。
  • 子類的operator=必須呼叫基礎類別的operator完成基礎類別的賦值。
  • 子類的解構函式會在被呼叫完成後自動呼叫基礎類別的解構函式清理基礎類別的成員。不需要顯示呼叫。
  • 子類物件會先呼叫父類別的構造再呼叫子類的構造。
  • 子類物件會先解構子類的解構再呼叫父類別的解構。

繼承和友元

友元關係不能被繼承。也就是說基礎類別的友元不能夠存取子類的私有和保護成員。

繼承和靜態成員

基礎類別定義的static靜態成員,存在於整個類中,不屬於某個類,無論右多少個派生類,都這有一個static成員

class Person
{
public:
	Person()
	{
		++_count;
	}
	// static成員存在於整個類  無論範例化出多少物件,都只有一個static成員範例
	static int _count;
};

int Person::_count = 0;

class Student :public Person
{
public:
	int _stuid;
};

int main()
{
	Student s1;
	Student s2;
	Student s3;

	// Student()._count = 10;
	cout << "人數:" << Student()._count - 1 << endl;

	return 0;
}
  • 繼承中的靜態成員變數一樣會被同名的子類成員變數隱藏
  • 繼承中的靜態成員函數中,當子類有和父類別同名靜態函數的時候,父類別的所有同名過載靜態函數都會被隱藏
  • 改變從基礎類別繼承過來的靜態函數的某個特徵值、返回值或者引數個數,將會隱藏基礎類別過載的函數
  • static成員存在於整個類 無論範例化出多少物件,都只有一個static成員範例

單繼承和多繼承

單繼承:一個子類只有一個直接父類別的時候稱這個繼承關係為單繼承

多繼承:一個子類有兩個或以上的直接父類別的時候稱這個繼承關係為多繼承

  • 多繼承的問題是,當父類別有同名成員的時候,子類會產生二義性,不建議使用多繼承

菱形繼承:多繼承的一種特殊情況

虛擬繼承

概念:為了解決菱形繼承帶來的資料冗餘和二義性的問題,C++提出來虛擬繼承這個概念。虛擬繼承可以解決前面的問題,在繼承方式前加一個virtual的關鍵字即可。

簡單原理理解

class Person
{
public:
	string _name;
};
// 不要在其他地方去使用。
class Student : virtual public Person
{
public:
	int _num; //學號
};
class Teacher : virtual public Person
{
public:
	int _id; // 職工編號
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修課程
};

虛擬繼承的原理

class A
{
public:
	int _a;
};

class B :virtual public A
{
public:
	int _b;
};

class C :virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 4;
	d._c = 5;
	d._d = 6;

	return 0;
}

我們通過記憶體視窗檢視它的物件模型:

原理: 從上圖可以看出,A物件同時屬於B和C,B和C中分別存放了一個指標,這個指標叫虛基表指標,分別指向的兩張表,叫虛基表,虛基表中存的是偏移量,B和C通過偏移量就可以找到公共空間(存放A物件的位置)。

複雜原理理解

檢視普通多繼承的子類記憶體分佈

class A
{
public:
	int _a;
};

class B : public A
{
public:
	int _b;
};

class C :public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

子類D的記憶體佈局如下:

從子類D的記憶體佈局可以看到,會先初始化父類別的成員變數,最後初始化自己的成員變數,B和C中都包含A的成員_a,此時D包含了B和C的成員,這樣D中總共出現了兩個A的成員,這樣就會造成二義性,子類D在呼叫成員變數A的時候,不知道呼叫哪一個,除非加上::,於是引入虛擬繼承,解決了這個問題,接下來我們看看虛擬繼承中D記憶體的分佈情況。

class A
{
public:
	int _a;
};

class B :virtual public A
{
public:
	int _b;
};

class C :virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

子類D的記憶體佈局如下:

從程式碼中我們可以知道B和C在繼承A的時候使用了virtual關鍵字,也就是虛擬繼承。

可以看到,虛擬繼承的子類在記憶體縫補上和普通的菱形繼承的子類有很大的區別。類B和C中多了一個vbptr指標,並且A類的_a也不再存在兩份,而是隻存在一份。

那麼類D物件的記憶體佈局就變成如下的樣子:

vbptr:繼承自父類別B中的指標
int _b:繼承自父類別B的成員變數
vbptr:繼承自父類別C的指標
int _c:繼承自父類別C的成員變數
int _d:D自己的成員變數
int _a:繼承父類別A的成員變數

顯然,虛繼承之所以能夠實現在多重派生子類中只儲存一份共有基礎類別的拷貝,關鍵在於vbptr指標。那vbptr到底指的是什麼?又是如何實現虛繼承的呢?其實上面的類D記憶體佈局圖中已經給出答案:

實際上,vbptr指的是虛基礎類別表指標,該指標指向了一個虛表,虛表中記錄了vbptr與本類的偏移地址;第二項是vbptr到共有基礎類別元素之間的偏移量。在這個例子中,類B中的vbptr指向了虛表D::$vbtable@B@,虛表表明公共基礎類別A的成員變數距離類B開始處的位移為20,這樣就找到了成員變數_a,而虛繼承也不用像普通多繼承那樣維持著公共基礎類別的兩份同樣的拷貝,節省了儲存空間。

多型

概念: 從字面意思來看,就是事物的多種形態。用C++的語言說就是不同的物件去完成同一個行為會產生不同的效果

多型發生的三個條件:

多型是在不同繼承關係的類物件,去呼叫同一個函數,產生了不同的行為

  • 有繼承
  • 被呼叫的函數必須是虛擬函式,其派生類必須重寫基礎類別的虛擬函式
  • 必須有基礎類別的指標或者參照呼叫

虛擬函式

virtual關鍵字修飾的類成員函數叫做虛擬函式。

class Person
{
public:
	// 虛擬函式
	virtual void BuyTicket()
	{
		cout << "買票全價" << endl;
	}
};

虛擬函式重寫是什麼?

虛擬函式的重寫(覆蓋): 派生類中有一個跟基礎類別完全相同的虛擬函式(即派生類虛擬函式與基礎類別虛擬函式的返回值型別、函數名字、參數列完全相同),稱子類的虛擬函式重寫了基礎類別的虛擬函式。(重寫是對函數體進行重寫)

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "買票全價" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket() // 這裡也可以不寫virtual,因為基礎類別的虛擬函式屬性已經被保留下來了,這裡只是完成虛擬函式的重寫
	{
		cout << "買票半價" << endl;
	}
};

虛擬函式重寫的兩個例外:

1.協變:基礎類別和派生類的虛擬函式的返回型別不同。
派生類重寫基礎類別虛擬函式時,與基礎類別虛擬函式返回值型別不同。即基礎類別虛擬函式返回基礎類別物件的指標或者參照,派生類虛擬函式返回派生類物件的指標或者參照時,稱為協變。(也就是基礎類別虛擬函式的返回型別和派生類的虛擬函式的返回型別是父子型別的指標或參照)

// 協變  返回值型別不同,但它們之間是父子或父父關係  返回型別是指標或者參照
// 基礎類別虛擬函式   返回型別  是  基礎類別的指標或者參照  
// 派生類虛擬函式 返回型別  是  基礎類別或派生類的返回型別是基礎類別的指標或參照

class A {};
class B : public A {};
class Person {
public:
	virtual A* f() { return new A; }
};
class Student : public Person {
public:
	virtual A* f() { return new B; }
};

2.解構函式的重寫 :基礎類別與派生類的解構函式的函數名不同
基礎類別和派生類的解構函式的函數名會被編譯器統一處理成destructor,所以只要基礎類別的解構函式加了關鍵字virtual,就會和派生類的解構函式構成重寫。

範例演示:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "買票全價" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket() // 這裡也可以不寫virtual,因為基礎類別的虛擬函式屬性已經被保留下來了,這裡只是完成虛擬函式的重寫
	{
		cout << "買票半價" << endl;
	}
};

void Func1(Person& p) { p.BuyTicket(); }
void Func2(Person* p) { p->BuyTicket(); }
void Func3(Person p) { p.BuyTicket(); }

int main()
{
	Person p;
	Student s;

	// 滿足多型的條件:與型別無關,父類別指標指向的是誰就呼叫誰的成員函數
	// 不滿足多型的條件:與型別有關,型別是誰就呼叫誰的成員函數
	cout << "基礎類別的參照呼叫:" << endl;
	Func1(p);
	Func1(s);

	cout << "基礎類別的指標呼叫:" << endl;
	Func2(&p);
	Func2(&s);

	cout << "基礎類別的物件呼叫:" << endl;
	Func3(p);
	Func3(s);

	return 0;
}

執行結果如圖:

思考:解構函式是否需要加上virtual?答案是需要的。

例子:

class Person
{
public:
	/*virtual*/ ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student: public Person
{
public:
	~Student()
	{
		cout << "~Student()" << endl;
	}
};
int main()
{
	Person* p = new Person;
	Person* ps = new Student;// 不加virtual,不構成多型,父類別指標只會根據型別去呼叫對於的解構函式
	// 加了virtual,構成多型,父類別指標會根據指向的物件去呼叫他的解構函式

	delete p;
	delete ps;

	return 0;
}

解構函式不加上virtual的執行結果:

解構函式加上virtual的執行結果:

可以看出,不加virtual關鍵字時,第二個物件delete時沒有呼叫子類的解構函式清理釋放空間。為什麼呢?因為不加virtual關鍵字時,兩個解構函式不構成多型,所以呼叫解構函式時是與型別有關的,因為都是都是父類別型別,所以只會呼叫父類別的解構函式。加了virtual關鍵字時,因為兩個解構函式被編譯器處理成同名函數了,所以完成了虛擬函式的重寫,且是父類別指標呼叫,所以此時兩個解構函式構成多型,所以呼叫解構函式時是與型別無關的,因為父類別指標指向的是子類物件,所以會呼叫子類的解構函式,子類呼叫完自己的解構函式又會自動呼叫父類別的解構函式來完成對父類別資源的清理。
所以總的來看,基礎類別的解構函式是要加virtual的。

區分一下下面幾個概念:

名稱 作用域 函數名 其他
過載 兩個函數在同一作用域 相同 引數型別不同
重寫 兩個函數分別再基礎類別和派生類的作用域 相同 函數返回型別和引數型別一樣
重定義(隱藏) 兩個函數分別再基礎類別和派生類的作用域 相同 兩個基礎類別和派生類的同名函數不是構成重寫就是重定義

override和final

final: 修飾虛擬函式,表示該虛擬函式不可以被重寫(還可以修飾類,表示該類不可以被繼承)

overide:如果派生類在虛擬函式宣告時使用了override描述符,那麼該函數必須過載其基礎類別中的同名函數,否則程式碼將無法通過編譯

抽象類

概念: 在虛擬函式的後面寫上 =0 ,則這個函數為純虛擬函式。包含純虛擬函式的類叫做抽象類(也叫介面類),抽象類不能範例化出物件。派生類繼承後也不能範例化出物件,只有重寫純虛擬函式,派生類才能範例化象純虛擬函式規範了派生類必須重寫,另外純虛擬函式更體現出了介面繼承

總結出幾個特點:

  1. 虛擬函式後面加上=0
  2. 不能範例化出物件
  3. 派生類如果不重寫基礎類別的純虛擬函式那麼它也是抽象類,不能範例化出物件
  4. 抽象類嚴格限制派生類必須重寫基礎類別的純虛擬函式
  5. 體現了介面繼承
class Car
{
public:
	virtual void Drive() = 0;
};
class Benz : public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz" << endl;
	}
};


class BMW : public Car
{
public:
	virtual void Drive () override
	{
		cout << "BMW" << endl;
	}
};

int main()
{
	Car* pBenZ = new Benz;
	pBenZ->Drive();

	Car* pBMW = new BMW;
	pBMW->Drive();

	delete pBenZ;
	delete pBMW;
	return 0;
}

執行結果:

Benz

BMW

抽象類的意義?

  1. 強制子類完成父類別虛擬函式的重寫
  2. 表示該類是抽象類,沒有實體(例如:花、車和人等)

普通函數的繼承是一種實現繼承,派生類繼承了基礎類別函數,可以使用函數,繼承的是函數的實現。虛擬函式的繼承是一種介面繼承,派生類繼承的是基礎類別虛擬函式的介面,目的是為了重寫,達成多型,繼承的是介面。所以如果不實現多型,不要把函數定義成虛擬函式。

多型的原理

概念: 一個含有虛擬函式的類中至少有一個虛擬函式指標,這個指標指向了一張表——虛擬函式表(簡稱虛表),這張表中存放了這個類中所有的虛擬函式的地址。

class Animal
{
public:
	virtual void speak()
	{
		cout << "Animal speak" << endl;
	};
};
class Dog : public Animal
{
public:
	void speak()
	{
		cout << "Dog speak" << endl;
	}
};

當編譯器中發現有虛擬函式的時候,會建立一張虛擬函式的表,裡面儲存了所有的虛擬函式,但是這個虛擬函式表不屬於類,虛擬函式指標才是屬於類的,這個虛擬函式指標指向了虛擬函式表的入口地址,而在類中會存在這個虛擬函式指標,這個虛擬函式表中包含了所有的虛擬函式,Animal::$vftable@就是Animal類的虛擬函式表,裡面放了Animal::speak函數的首地址。

當子類繼承父類別的時候,父類別的虛擬函式表會拷貝一份,這個拷貝的虛擬函式表是子類獨有的虛擬函式表,而不是父類別的虛擬函式表,但是該表中的虛擬函式地址還是父類別的,因為是從父類別那裡拷貝過來的,子類的虛擬函式指標會指向自己的虛擬函式表

當子類重寫了父類別的虛擬函式,那麼子類的重寫的虛擬函式的地址就會把從父類別那裡拷貝過來的地址覆蓋掉,換成自己的地址。

class Animal
{
public:
	virtual void speak()
	{

	};
};
class Dog : public Animal
{

};

如果子類不重寫父類別的虛擬函式,可以再看看記憶體的儲存情況,子類的虛擬函式表(Dog::$vftable@)中還是儲存的是父類別的虛擬函式地址

總結幾點:

  • 子類物件由兩部分構成,一部分是父類別繼承下來的成員,虛表指標指向的虛表有父類別的虛擬函式,也有子類新增的虛擬函式
  • 子類完成父類別虛擬函式的重寫其實是對繼承下來的虛表的中重寫了的虛擬函式進行覆蓋,把地址更換了,語法層是稱為覆蓋
  • 虛擬函式表本質是一個存虛擬函式指標的指標陣列,一般情況這個陣列最後面放了一個nullptr
  • 虛表生成的過程:先將基礎類別中的虛表內容拷貝一份到派生類虛表中,如果派生類重寫了基礎類別中某個虛擬函式,用派生類自己的虛擬函式覆蓋虛表中基礎類別的虛擬函式,派生類自己新增加的虛擬函式按其在派生類中的宣告次序增加到派生類虛表的最後

下面我們來討論一下虛表存放的位置和虛表指標存放的位置

虛表指標肯定是存在類中的,從上面的類物件模型中可以看出。其次虛表存放的是虛擬函式的地址,這些虛擬函式和普通函數一樣,都會被編譯器編譯成指令,然後放程序式碼段。虛表也是存在程式碼段的,因為同型別的物件共用一張虛表

原理

多型是在執行時到指向的物件中的虛表中查詢要呼叫的虛擬函式的地址,然後進行呼叫

為什麼要實現多型必須是父類別的指標或參照,不可以是父類別物件?

子類物件給父類別物件賦值時,會呼叫父類別的拷貝構造對父類別的成員變數進行拷貝構造,但是虛表指標不會參與切片,這樣父類別物件無法找到子類的虛表,所以父類別物件不能夠呼叫子類的虛擬函式。但是子類物件給父類別的指標或參照賦值時,是讓父類別的指標指向父類別的那一部分或參照父類別的那一部分,這樣父類別還是可以拿到子類的虛表指標,通過虛表指標找到子類的虛表,從而可以呼叫虛表中的虛擬函式。

總結:

  1. 多型滿足的兩個條件:一個是虛擬函式的覆蓋,一個是物件的指標和參照呼叫
  2. 滿足多型後,函數的呼叫不是編譯時確認的,而是在執行時確認的。

單繼承的虛表

class Base
{
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	virtual void func3() { cout << "Base::func3" << endl; }
	void func() {}

	int b = 0;
};

class Derive :public Base
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func2() { cout << "Derive::func2" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
	virtual void func5() { cout << "Derive::func5" << endl; }

	int d = 0;
};

觀察它的物件模型:

多繼承的虛表

class Base1 
{
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 
{
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2 = 1;
};
class Derive : public Base1 , public Base2 
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1 = 1;
};

觀察它的物件模型:

可以看到子類會繼承父類別的兩個指標和兩個虛擬函式的表,而且虛表Base2中的func1函數很奇怪,有一個goto指令,這是因為func1函數的地址和虛表2中func1函數其實不是func1函數的真實地址。這兩個地址都分別指向一個jump指令,兩個地址中的jump指令最終都會跳轉到同一個func1中。

幾個值得思考的問題

1.行內函式可以是虛擬函式嗎?
答:可以,但是編譯器會忽略inline屬性(inline只是一種建議),因 為內聯(inline)函數沒有地址,且虛擬函式要把地址放到虛表中去。
2.建構函式可以是虛擬函式嗎?
答:不可以,因為物件中虛擬函式指標是在建構函式初始化列表階段才初始化的。
3.解構函式可以是虛擬函式嗎?
答:可以,且建議設計成虛擬函式,具體原因前面說了。
4.物件存取普通函數快還是虛擬函式更快?
答:首先如果是普通物件,是一樣快的。如果是指標物件或者是參照物件,則呼叫的普通函數快,因為構成多型,執行時呼叫虛擬函式需要到虛擬函式表中去查詢。
5.虛擬函式表是在什麼階段生成的?
答:在編譯階段生成的,存在於程式碼段。
6.靜態成員可以是虛擬函式嗎?
答:不可以。因為靜態成員沒有this指標,使用類域(::)存取成員函數的呼叫方式無法存取到虛表,所以靜態成員函數無法放進虛表。