目錄
- 1. 多型的概念和意義
- 2. C++多型的實作原理
- 3. 構造析構與虛函式
- 4. C++物件模型分析
- 繼承物件模型
- 多型物件模型
- 5. 用C實作面向物件,展現繼承與多型本質
- 代碼實作
- 總體設計思路
- 封裝設計分析
- 繼承設計分析
- 多型設計分析
1. 多型的概念和意義
回憶一下上一節繼承(二)函式重寫示例代碼中的how_to_print(),當時我們所期望的結果是
- 根據實際的物件型別判斷如何呼叫重寫函式
- 父類指標(參考)指向
- 父類物件則呼叫父類中定義的函式
- 子類物件則呼叫子類中定義的重寫函式
要實作上述期望結果,需要用到多型的知識,
多型是面向物件理論中的概念,其含義為
- 根據實際的物件型別決定函式呼叫的具體目標
- 同樣的呼叫陳述句在實際運行時有多種不同的表現形態

C++語言直接支持多型的概念
- 通過使用virtual關鍵字對多型進行支持
- 被virtual宣告的函式叫做虛函式
- 虛函式被重寫后可表現出多型的特性
靜態聯編 VS 動態聯編
- 靜態聯編:在程式編譯期間就能確定具體的函式呼叫,如函式多載
- 動態聯編:在程式運行期間才能確定具體的函式呼叫,如重寫虛函式
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
virtual void print()
{
cout << "I'm Parent." << endl;
}
};
class Child : public Parent
{
public:
/* 重寫父類虛函式,改變列印陳述句行為 */
void print()
{
cout << "I'm Child." << endl;
}
};
void how_to_print(Parent *p)
{
p->print(); // 動態聯編,展現多型的行為
}
int main()
{
Parent p;
Child c;
p.print(); // 靜態聯編
c.print(); // 靜態聯編
how_to_print(&p); // 動態聯編,Expected: I'm Parent
how_to_print(&c); // 動態聯編,Expected: I'm Child
return 0;
}

多型的意義
- 在程式運行程序中展現出動態的特性
- 函式重寫只可能發生在父類與子類之間,且必須多型實作,否則沒有意義
- 多型是面向物件組件化程式設計的基礎特性
示例代碼:江湖恩怨
#include <iostream>
#include <string>
using namespace std;
class Boss
{
public:
int fight()
{
int ret = 10;
cout << "Boss::fight() : " << ret << endl;
return ret;
}
};
class Master
{
public:
virtual int eightSwordKill()
{
int ret = 8;
cout << "Master::eightSwordKill() : " << ret << endl;
return ret;
}
};
class NewMaster : public Master
{
public:
int eightSwordKill()
{
int ret = Master::eightSwordKill() * 2;
cout << "NewMaster::eightSwordKill() : " << ret << endl;
return ret;
}
};
void field_pk(Master *master, Boss *boss)
{
int k = master->eightSwordKill();
int b = boss->fight();
if( k < b )
{
cout << "Master is killed..." << endl;
}
else
{
cout << "Boss is killed..." << endl;
}
cout << endl;
}
int main()
{
Boss boss;
Master master;
NewMaster newMaster;
cout << "Master vs Boss" << endl;
field_pk(&master, &boss);
cout << "NewMaster vs Boss" << endl;
field_pk(&newMaster, &boss);
return 0;
}

2. C++多型的實作原理
- 當類中宣告虛函式時,編譯器會在類中生成一個虛函式表
- 虛函式表是一個存盤成員函式地址的資料結構,該資料結構由編譯器自動生成與維護
- 虛函式會被編譯器放入虛函式表中
- 存在虛函式時,每個類物件中都有一個指向虛函式表的指標
- 虛函式表指標位于類物件的起始位置,再往后才是成員變數
3. 構造析構與虛函式
- 建構式不可能成為虛函式,建構式中不可能發生多型行為
- 在建構式執行結束后,虛函式表指標才會被正確的初始化
- 解構式可以成為虛函式,解構式中不可能發生多型行為
- 在解構式執行時,虛函式表指標已經被銷毀
- 建議在設計類時將解構式宣告為虛函式
/*
* 1、建構式不可能成為虛函式,解構式可以并且建議設計為虛函式
* 2、建構式和解構式中呼叫虛函式不能發生多型行為,只會呼叫當前類中定義的函式版本
*/
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base()" << endl;
func();
}
virtual void func()
{
cout << "Base::func()" << endl;
}
virtual ~Base()
{
func();
cout << "~Base()" << endl;
}
};
class Derived : public Base
{
public:
Derived()
{
cout << "Derived()" << endl;
func();
}
virtual void func()
{
cout << "Derived::func()" << endl;
}
~Derived()
{
func();
cout << "~Derived()" << endl;
}
};
int main()
{
Base *p = new Derived();
cout << endl;
delete p;
return 0;
}

4. C++物件模型分析
繼承物件模型
在C++編譯器的內部,類可以理解為結構體
- 在記憶體中class依舊可以看作變數的集合
- class與struct遵循相同的記憶體對齊規則
- class中的成員函式與成員變數的分開存放的
程式運行時,類物件退化為結構體的形式
- 所有成員變數在記憶體中依次排布
- 成員變數間可能存在記憶體空隙(結構體位元組對齊)
- 可以通過記憶體地址直接訪問成員變數
- 訪問權限關鍵字在運行時失效
子類是由父類成員(包括父類虛函式表指標)疊加子類新成員得到的

#include <iostream>
#include <string>
using namespace std;
class Demo
{
protected:
int mi;
int mj;
public:
virtual void print()
{
cout << "mi = " << mi << ", "
<< "mj = " << mj << endl;
}
};
class Derived : public Demo
{
private:
int mk;
public:
Derived(int i, int j, int k)
{
mi = i;
mj = j;
mk = k;
}
void print()
{
cout << "mi = " << mi << ", "
<< "mj = " << mj << ", "
<< "mk = " << mk << endl;
}
};
struct Test
{
void *pVTable;
int mi;
int mj;
int mk;
};
int main()
{
cout << "sizeof(Demo) = " << sizeof(Demo) << endl; //sizeof(Demo) = 12,1個虛函式表指標 + int mi + int mj
cout << "sizeof(Derived) = " << sizeof(Derived) << endl; //sizeof(Derived) = 16,1個虛函式表指標 + int mi + int mj + int mk
Derived d(1, 2, 3);
Test *p = reinterpret_cast<Test *>(&d);
cout << endl;
cout << "Before changing ..." << endl;
d.print();
/*
* 通過p指標改變了d物件中成員變數的值,說明Derived的記憶體模型與struct Test相同:
* 1.起始位置為虛函式表指標
* 2.再往后為父類成員疊加子類新成員
*/
p->mi = 4;
p->mj = 5;
p->mk = 6;
cout << endl;
cout << "After changing ..." << endl;
d.print();
return 0;
}

多型物件模型
父類子類虛函式表
class Demo
{
protected:
int mi;
int mj;
public:
virtual int add(int value)
{
return mi + mj + value;
}
};
class Derived : public Demo
{
private:
int mk;
public:
virtual int add(int value)
{
return mk + value;
}
};

程式運行時展現多型行為
void run(Demo *p, int v)
{
p->add(v);
}

- 編譯器確認add()是否為虛函式
- Yes:編譯器在具體物件VPTR所指向的虛函式表中查找add()的地址
- No:編譯器直接可以確定add()成員函式的地址
5. 用C實作面向物件,展現繼承與多型本質
代碼實作
c_oop.h
#ifndef _C_OOP_H_
#define _C_OOP_H_
typedef void Base;
typedef void Derived;
Base *Base_Create(int i, int j);
int Base_GetI(Base *pThis);
int Base_GetJ(Base *pThis);
int Base_Add(Base *pThis, int value);
void Base_Free(Base *pThis);
Derived *Derived_Create(int i, int j, int k);
int Derived_GetK(Derived *pThis);
int Derived_Add(Derived *pThis, int value);
void Derived_Free(Derived *pThis);
#endif
c_oop.c
#include "c_oop.h"
#include <stdlib.h>
struct VTabel
{
int (*pAdd)(void *, int);
};
struct ClassBase
{
struct VTabel *vptr;
int mi;
int mj;
};
struct ClassDerived
{
struct ClassBase base;
int mk;
};
static int Base_Virtual_Add(Base *pThis, int value);
static Derived_Virtual_Add(Derived *pThis, int value);
static struct VTabel Base_vtab =
{
Base_Virtual_Add
};
static struct VTabel Derived_vtab =
{
Derived_Virtual_Add
};
Base *Base_Create(int i, int j)
{
struct ClassBase *ret = (struct ClassBase *)malloc(sizeof(struct ClassBase));
if (ret != NULL)
{
ret->vptr = &Base_vtab;
ret->mi = i;
ret->mj = j;
}
return ret;
}
int Base_GetI(Base *pThis)
{
struct ClassBase *pBase = (struct ClassBase *)pThis;
return pBase->mi;
}
int Base_GetJ(Base *pThis)
{
struct ClassBase *pBase = (struct ClassBase *)pThis;
return pBase->mj;
}
static int Base_Virtual_Add(Base *pThis, int value)
{
struct ClassBase *pBase = (struct ClassBase *)pThis;
return pBase->mi + pBase->mj + value;
}
int Base_Add(Base *pThis, int value)
{
struct ClassBase *pBase = (struct ClassBase *)pThis;
return pBase->vptr->pAdd(pThis, value);
}
void Base_Free(Base *pThis)
{
if (pThis != NULL)
{
free(pThis);
}
}
Derived *Derived_Create(int i, int j, int k)
{
struct ClassDerived *ret = (struct ClassDerived *)malloc(sizeof(struct ClassDerived));
if (ret != NULL)
{
ret->base.vptr = &Derived_vtab; //關聯子類物件和子類虛函式表
ret->base.mi = i;
ret->base.mj = j;
ret->mk = k;
}
return ret;
}
int Derived_GetK(Derived *pThis)
{
struct ClassDerived *pDerived = (struct ClassDerived *)pThis;
return pDerived->mk;
}
static Derived_Virtual_Add(Derived *pThis, int value)
{
struct ClassDerived *pDerived = (struct ClassDerived *)pThis;
return pDerived->mk + value;
}
int Derived_Add(Derived *pThis, int value)
{
struct ClassDerived *pDerived = (struct ClassDerived *)pThis;
return pDerived->base.vptr->pAdd(pThis, value);
}
main.c
#include "c_oop.h"
#include <stdio.h>
void run(Base *p, int v)
{
int r = Base_Add(p, v);
printf("r = %d\n", r);
}
int main()
{
Base *pb = Base_Create(1, 2);
Derived *pd = Derived_Create(3, 4, 5);
printf("pb->Add(3) = %d\n", Base_Add(pb, 3));
printf("pd->Add(3) = %d\n", Derived_Add(pd, 3));
run(pb, 3);
run(pd, 3);
Base_Free(pb);
Base_Free(pd);
return 0;
}

總體設計思路
面向物件程式最關鍵的地方在于必須能夠表現三大特性:封裝,繼承,多型!
- 封裝指的是類中的敏感資料在外界是不能訪問的
- 繼承指的是可以對已經存在的類進行代碼復用,并使得類之間存在父子關系
- 多型指的是相同的呼叫陳述句可以產生不同的呼叫結果
因此,如果希望用C語言完成面向物件的程式,那么必須實作這三個特性;
否則,最多只算得上基于物件的程式(程式中能夠看到物件的影子,但是不完全具備面向物件的三大特性),
封裝設計分析
通過void*指標保證具體的結構體成員不能在外界被訪問,以此模擬C++中private和protected,因此,在頭檔案中定義了如下的陳述句:
typedef void Base;
typedef void Derived;
用Base*指標和Derived*指標指向具體的物件時,無法訪問物件中的成員變數,這樣就達到了“外界無法訪問類中私有成員” 的封裝效果!
繼承設計分析
繼承的本質是父類成員與子類成員的疊加,所以在用C語言寫面向物件程式的時候,可以直接考慮結構體成員的疊加,
代碼中的實作直接將struct ClassBase base作為struct ClassDerived的第一個成員,以此表現兩個自定義資料型別間的繼承關系,所以有了下面的代碼:
struct ClassBase
{
struct VTabel *vptr;
int mi;
int mj;
};
struct ClassDerived
{
struct ClassBase base;
int mk;
};
多型設計分析
多型在C++中是通過虛函式表完成的,而虛函式表是C++編譯器自主產生和維護的資料結構,因此,接下來要解決的問題就是如何在C語言中自定義虛函式表,
通過結構體變數模擬C++中的虛函式表是比較理想的一種選擇,所以有了下面的代碼:
struct VTabel
{
int (*pAdd)(void *, int);
};
有了型別后就可以定義實際的虛函式表了,在C語言中用具有檔案作用域的全域變數表示實際的虛函式表是最合適的,因此有了下面的代碼:
static int Base_Virtual_Add(Base *pThis, int value);
static Derived_Virtual_Add(Derived *pThis, int value);
static struct VTabel Base_vtab =
{
Base_Virtual_Add
};
static struct VTabel Derived_vtab =
{
Derived_Virtual_Add
};
每個物件中都擁有一個指向虛函式表的指標,而所有父類物件都指向Base_vtab,所有子類物件都指向Derived_vtab,
當一切就緒后,實際呼叫虛函式的程序就是通過虛函式表中的對應指標來完成的,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/102072.html
標籤:C++
上一篇:繼承(二)
下一篇:抽象類和介面
