C++的重要性質:虛函數和多態性

1. 封裝、繼承和this指針

1.1 封裝(Encapsulation)

把數據成員聲明爲private,不允許外界隨意存取,只能通過特定的接口來操作,這就是面向對象的封裝特性。

1.2 繼承(Inheritance)

子類“暗自(implicit)”具備了父類的所有成員變量和成員函數,包括private屬性的成員(雖然沒有訪問權限)

1.3 this指針

矩形類CRect如下:
class CRect
{
private:    
    int m_color;
public:
    void setcolor(int color)
    {
        m_color=color;
    }
};
有兩個CRect對象rect1和rect2,各有各自的m_color成員變量。rect1.setcolor和rect2.setcolor調用的是唯一的CRect::setcolor成員函數,卻處理了各自的m_color。
這是因爲成員函數是屬於類的而不是屬於某個對象的,只有一個。成員函數都有一個隱藏參數,名爲this指針,當你調用
rect1.setcolor(2);
rect2.setcolor(3);

時,編譯器實際上爲你做出來的代碼是:
CRect.setcolor(2,(CRect*) &rect1);
CRect.setcolor(3,(CRect*) &rect2);

2. 虛函數與多態

2.1 多態性(Polymorphism)

以相同的指令卻調用了不同的函數,這種性質成爲Polymorphism,意思是“the ability to assume many forms”(多態)。有如下四個類:

#include <string.h>

class CEmployee  //職員
{
    private:
        char m_name[30];

    public:
        CEmployee();
        CEmployee(const char* nm) { strcpy(m_name, nm); }
};
//----------------------------------// 時薪職員是一種職員
class CWage : public CEmployee
{
    private :
        float m_wage;//鐘點費
        float m_hours;//每週工時

    public :
        CWage(const char* nm) : CEmployee(nm) { m_wage = 250.0; m_hours = 40.0; }
        void setWage(float wg) { m_wage = wg; }
        void setHours(float hrs) { m_hours = hrs; }
        float computePay();
};
//----------------------// 銷售員是一種時薪職員
class CSales : public CWage
{
    private :
        float m_comm;//佣金
        float m_sale;//銷售額

    public :
        CSales(const char* nm) : CWage(nm) { m_comm = m_sale = 0.0; }
        void setCommission(float comm)      { m_comm = comm; }
        void setSales(float sale)          { m_sale = sale; }
        float computePay();
};
//------------------------// 經理也是一種職員
class CManager : public CEmployee
{
    private :
        float m_salary;//薪水
    public :
        CManager(const char* nm) : CEmployee(nm) { m_salary = 15000.0; }
        void setSalary(float salary)             { m_salary = salary; }
        float computePay();
};
//---------------------------------------------------------------
void main()
{
    CManager aManager("陳美靜");
    CSales   aSales("侯俊傑");
    CWage    aWager("曾銘源");
}

1)則aManageer,aSale和aWager含有的變量如下圖:

注意:子類確實繼承了父類的private成員,只是沒有訪問的權限。要訪問父類的成員函數,必須使用scope resolution operator(::)明白指出。
    a)計算侯俊傑底薪應該是
a.Sales.CWage::computePay();
    b)計算侯俊傑的全薪應該是
aSales.computePay();
2)  父類與子類的轉換
//銷售員是時薪職員之㆒,因此這樣做是合理的:
aWager = aSales; // 合理,銷售員必定是時薪職員。
//這樣就不合理:
aSales = aWager; // 錯誤,時薪職員未必是銷售員。
//如果你㆒定要轉換,必須使用指標,並且明顯做型別轉換(cast)動作 :
CWage* pWager;
CSales* pSales;
CSales aSales("侯俊傑");
pWager = &aSales; // 把一個基類指針指向子類的對象,合理且自然。
pSales = (CSales *)pWager; // 強迫轉型。語法上可以,但不符合現實生活。
3)到底會調用那個函數?
看下面代碼:
CSales aSales("侯俊傑");
CSales* pSales;
CWage* pWager;
pSales = &aSales;
pWager = &aSales; // 以基類指針指向子類對象
pWager->setSales(800.0); // 錯誤(編譯器會檢測出來),
// 因爲 CWage 並沒有定義 setSales 函數
pSales->setSales(800.0); // 正確,調用 CSales::setSales 函數
雖然pSales 和pWager 指向同一對象,但卻因指針的原始類型不同而使兩者之間有了差異。如果你一個“基類指針”指向派生類的對象,那麼經由該指針你只能夠調用基類所定義的函數。
再看下面的代碼:
pWager->computePay(); // 調用 CWage::computePay()
pSales->computePay(); // 調用 CSales::computePay()
雖然aSales和pWager實際上指向的是同一個對象,但是兩者調用computePay卻不同。到底應該調用哪個函數必須視指針的類型而定,與指針實際指向的對象無關。
4)
總結
  1. 如果你一個“基類指針”指向派生類的對象,那麼經由該指針你只能夠調用基類所定義的函數。
  2. 如果要用一個派生類指針指向一個基類對象,你必須做顯式類型轉換(explict cast),這種做法不推薦。
  3. 如果基類和派生類定義了相同名稱的成員函數,那麼通過指針調用成員函數時,到底會調用哪一個函數,必須視指針的類型而定,與指針實際指向的對象無關。

2.2 虛函數

如果將上述4個類中的computePay函數前都加上virtual保留字:
CEmployee* pEmp;
CWage aWager("曾銘源");
CSales aSales("侯俊傑");
CManager aManager("陳美靜");
pEmp = &aWager;
cout << pEmp->computePay(); // 調用的是 CWage::computePay
pEmp = &aSales;
cout << pEmp->computePay(); // 調用的是 CSales::computePay
pEmp = &aManager;
cout << pEmp->computePay(); // 調用的是 CManager::computePay
我們通過相同的指令“pmp->computePay()”卻調用了不同的函數,這就是虛函數的作用:實現多態性,通過指向派生類的基類指針,訪問派生類中同名覆蓋成員函數。

2.3 類與對象大解剖

爲了達到動態綁定的目的,C++編譯器通過某個表格,在執行時"間接"調用實際上欲綁定的函數。這樣的表格成爲虛函數表(常被稱爲vtable)。每一個內含虛函數的類,編譯器都會爲它做出一個虛函數表,表中的每一個元素都指向一個虛函數的地址。此外,編譯器當然也會類加上一項成員變量,是一個指向該虛函數表的指針(常被成爲vptr)。
#include <iostream.h>
#include <stdio.h>

class ClassA
{
	public:
		int m_data1;
		int m_data2;
		void func1() { }
		void func2() { }
		virtual void vfunc1() { }
		virtual void vfunc2() { }
};

class ClassB : public ClassA
{
	public:
		int m_data3;
		void func2() { }
		virtual void vfunc1() { }
};

class ClassC : public ClassB
{
	public:
		int m_data1;
		int m_data4;
		void func2() { }
		virtual void vfunc1() { }
};

void main()
{
	cout << sizeof(ClassA) << endl;
	cout << sizeof(ClassB) << endl;
	cout << sizeof(ClassC) << endl;

	ClassA a;
	ClassB b;
	ClassC c;

	b.m_data1 = 1;
	b.m_data2 = 2;
	b.m_data3 = 3;
	c.m_data1 = 11;
	c.m_data2 = 22;
	c.m_data3 = 33;
	c.m_data4 = 44;
	c.ClassA::m_data1 = 111;

	cout << b.m_data1 << endl;
	cout << b.m_data2 << endl;
	cout << b.m_data3 << endl;
	cout << c.m_data1 << endl;
	cout << c.m_data2 << endl;
	cout << c.m_data3 << endl;
	cout << c.m_data4 << endl;
	cout << c.ClassA::m_data1 << endl;

	cout << &b << endl;
	cout << &(b.m_data1) << endl;
	cout << &(b.m_data2) << endl;
	cout << &(b.m_data3) << endl;
	cout << &c << endl;
	cout << &(c.m_data1) << endl;
	cout << &(c.m_data2) << endl;
	cout << &(c.m_data3) << endl;
	cout << &(c.m_data4) << endl;
	cout << &(c.ClassA::m_data1) << endl;
}
執行結果及說明:

對象a.b.c中的內容如下圖所示:

  1. C++類的成員函數可以想象爲C語言中的函數。它時被編譯器改過名稱(加入了類名::,如上圖中灰色框內),並加了一個this指針的參數。所以成員函數並不在對象的內存區塊種,成員函數爲該類所有的對象共享。
  2. 如果基類中含有虛函數,那麼每一個由此類派生出來的類的對象都一個這麼一個vptr。當我們通過這個對象調用虛函數時,事實上是通過vptr找到虛函數表,再找出虛函數的真正地址。
  3. 派生類會繼承基類的虛函數表,當我們再派生類中改寫虛函數時,虛函數表就受了影響:表中元素所指的函數地址將不再是基類的函數地址,而是派生類的函數地址。
上文說到“如果基類和派生類定義了相同名稱的成員函數,那麼通過指針調用成員函數時,到底會調用哪一個函數,必須視指針的類型而定,與指針實際指向的對象無關。”而虛函數實現了,要調用哪一個函數不是視指針的類型而定,而是跟具體指向的對象有關。這是因爲,虛函數在基類和派生類都增加了一個虛函數指針vptr,當派生類改寫虛函數時,改變了虛函數中實際指向的函數。一言以蔽之,虛函數的巧妙之處在於通過虛函數指針間接的改變了要調用函數。

2.4 虛析構函數

基類的析構函數一般寫成虛函數,這樣做是爲了當用一個基類的指針刪除一個派生類的對象時,派生類的析構函數會被調用。否則會造成內存泄露。
class ClxBase
{
public:
    ClxBase() {};
    virtual ~ClxBase() {};

    virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};

class ClxDerived : public ClxBase
{
public:
    ClxDerived() {};
    ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; }; 

    void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
};
ClxBase *pTest = new ClxDerived;
pTest->DoSomething();
delete pTest;
輸出:
Do something in class ClxDerived!
Output from the destructor of class ClxDerived!

參考資料:

主要參考 侯俊傑,《深入淺出MFC》第二章 C++的重要性質
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章