C++解析(23):多型與C++物件模型
0.目錄
1.多型
2.C++物件模型
3.繼承物件模型
4.多型物件模型
5.小結
1.多型
面向物件中期望的行為:
- 根據實際的物件型別判斷如何呼叫重寫函式
- 父類指標(引用)指向
- 父類物件則呼叫父類中定義的函式
- 子類物件則呼叫子類中定義的重寫函式
面向物件中的多型的概念:
- 根據實際的物件型別決定函式呼叫的具體目標
- 同樣的呼叫語句在實際執行時有多種不同的表現形態
C++語言直接支援多型的概念:
- 通過使用virtual關鍵字對多型進行支援
- 被virtual宣告的函式被重寫後具有多型特性
- 被virtual
示例——使用virtual實現多型:
#include <iostream> 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; how_to_print(&p); // Expected to print: I'm Parent. how_to_print(&c); // Expected to print: I'm Child. return 0; }
執行結果為:
[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out
I'm Parent.
I'm Child.
多型的意義:
- 在程式執行過程中展現出動態的特性
- 函式重寫必須實現多型,否則沒有意義
- 多型是面向物件元件化程式設計的基礎特性
理論中的概念:
- 靜態聯編——在程式的編譯期間就能確定具體的函式呼叫(如:函式過載)
- 動態聯編——在程式實際執行後才能確定具體的函式呼叫(如:函式重寫)
示例——靜態聯編與動態聯編:
#include <iostream> using namespace std; class Parent { public: virtual void func() { cout << "void func()" << endl; } virtual void func(int i) { cout << "void func(int i) : " << i << endl; } virtual void func(int i, int j) { cout << "void func(int i, int j) : " << "(" << i << ", " << j << ")" << endl; } }; class Child : public Parent { public: void func(int i, int j) { cout << "void func(int i, int j) : " << i + j << endl; } void func(int i, int j, int k) { cout << "void func(int i, int j, int k) : " << i + j + k << endl; } }; void run(Parent* p) { p->func(1, 2); // 展現多型的特性(動態聯編) } int main() { Parent p; p.func(); // 靜態聯編 p.func(1); // 靜態聯編 p.func(1, 2); // 靜態聯編 cout << endl; Child c; c.func(1, 2); // 靜態聯編 cout << endl; run(&p); run(&c); return 0; }
執行結果為:
[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out
void func()
void func(int i) : 1
void func(int i, int j) : (1, 2)
void func(int i, int j) : 3
void func(int i, int j) : (1, 2)
void func(int i, int j) : 3
2.C++物件模型
回顧本質——class是一種特殊的struct:
- 在記憶體中class依舊可以看作變數的集合
- class與struct遵循相同的記憶體對齊規則
- class中的成員函式與成員變數是分開存放的
- 每個物件有獨立的成員變數
- 所有物件共享類中的成員函式
值得思考的問題:
示例——物件的記憶體排布與執行時訪問許可權:
#include <iostream>
using namespace std;
class A
{
int i;
int j;
char c;
double d;
public:
void print()
{
cout << "i = " << i << ", "
<< "j = " << j << ", "
<< "c = " << c << ", "
<< "d = " << d << endl;
}
};
struct B
{
int i;
int j;
char c;
double d;
};
int main()
{
A a;
cout << "sizeof(A) = " << sizeof(A) << endl;
cout << "sizeof(a) = " << sizeof(a) << endl;
cout << "sizeof(B) = " << sizeof(B) << endl;
a.print();
B* p = reinterpret_cast<B*>(&a);
p->i = 1;
p->j = 2;
p->c = 'c';
p->d = 3;
a.print();
p->i = 100;
p->j = 200;
p->c = 'b';
p->d = 3.14;
a.print();
return 0;
}
執行結果為:
[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out
sizeof(A) = 24
sizeof(a) = 24
sizeof(B) = 24
i = 0, j = 0, c = �, d = 6.95303e-310
i = 1, j = 2, c = c, d = 3
i = 100, j = 200, c = b, d = 3.14
執行時的物件退化為結構體的形式:
- 所有成員變數在記憶體中依次排布
- 成員變數間可能存在記憶體空隙
- 可以通過記憶體地址直接訪問成員變數
- 訪問許可權關鍵字在執行時失效
C++物件模型分析:
- 類中的成員函式位於程式碼段中
- 呼叫成員函式時物件地址作為引數隱式傳遞
- 成員函式通過物件地址訪問成員變數
- C++語法規則隱藏了物件地址的傳遞過程
示例——物件呼叫成員函式:
#include <iostream>
using namespace std;
class Demo
{
int mi;
int mj;
public:
Demo(int i, int j)
{
mi = i;
mj = j;
}
int getI() { return mi; }
int getJ() { return mj; }
int add(int value)
{
return mi + mj + value;
}
};
int main()
{
Demo d(1, 2);
cout << "sizeof(d) = " << sizeof(d) << endl;
cout << "d.getI() = " << d.getI() << endl;
cout << "d.getJ() = " << d.getJ() << endl;
cout << "d.add(3) = " << d.add(3) << endl;
return 0;
}
執行結果為:
[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out
sizeof(d) = 8
d.getI() = 1
d.getJ() = 2
d.add(3) = 6
使用C語言實現物件呼叫成員函式的過程:
// test.h
#ifndef _TEST_H_
#define _TEST_H_
typedef void Demo;
Demo* Demo_Create(int i, int j);
int Demo_GetI(Demo* pThis);
int Demo_GetJ(Demo* pThis);
int Demo_Add(Demo* pThis, int value);
void Demo_Free(Demo* pThis);
#endif
// test.c
#include "test.h"
#include "malloc.h"
struct ClassDemo
{
int mi;
int mj;
};
Demo* Demo_Create(int i, int j)
{
struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo));
if( ret != NULL )
{
ret->mi = i;
ret->mj = j;
}
return ret;
}
int Demo_GetI(Demo* pThis)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mi;
}
int Demo_GetJ(Demo* pThis)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mj;
}
int Demo_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mi + obj->mj + value;
}
void Demo_Free(Demo* pThis)
{
free(pThis);
}
// main.c
#include <stdio.h>
#include "test.h"
int main()
{
Demo* d = Demo_Create(1, 2); // Demo* d = new Demo(1, 2);
printf("d.mi = %d\n", Demo_GetI(d)); // d->getI();
printf("d.mj = %d\n", Demo_GetJ(d)); // d->getJ();
printf("Add(3) = %d\n", Demo_Add(d, 3)); // d->add(3);
// d->mi = 100; // d->mi is private
Demo_Free(d);
return 0;
}
執行結果為:
[[email protected] Desktop]# gcc main.c test.c
[[email protected] Desktop]# ./a.out
d.mi = 1
d.mj = 2
Add(3) = 6
面向物件不是C++的專屬,依然可以用C語言寫面向物件。
3.繼承物件模型
- 在C++編譯器的內部類可以理解為結構體
- 子類是由父類成員疊加子類新成員得到的
示例——證明class Derived與struct Test的記憶體排布一樣:
#include <iostream>
using namespace std;
class Demo
{
protected:
int mi;
int mj;
};
class Derived : public Demo
{
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
{
int mi;
int mj;
int mk;
};
int main()
{
cout << "sizeof(Demo) = " << sizeof(Demo) << endl;
cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
Derived d(1, 2, 3);
Test* p = reinterpret_cast<Test*>(&d);
cout << "Before changing ..." << endl;
d.print();
p->mi = 10;
p->mj = 20;
p->mk = 30;
cout << "After changing ..." << endl;
d.print();
return 0;
}
執行結果為:
[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out
sizeof(Demo) = 8
sizeof(Derived) = 12
Before changing ...
mi = 1, mj = 2, mk = 3
After changing ...
mi = 10, mj = 20, mk = 30
4.多型物件模型
C++多型的實現原理:
- 當類中宣告虛擬函式時,編譯器會在類中生成一個虛擬函式表
- 虛擬函式表是一個儲存成員函式地址的資料結構
- 虛擬函式表是由編譯器自動生成與維護的
- virtual成員函式會被編譯器放入虛擬函式表中
- 存在虛擬函式時,每個物件中都有一個指向虛擬函式表的指標
示例——證明存在指向虛擬函式表的指標:
#include <iostream>
using namespace std;
class Demo
{
protected:
int mi;
int mj;
public:
virtual void print()
{
cout << "mi = " << mi << ", "
<< "mj = " << mj << endl;
}
};
class Derived : public Demo
{
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* p;
int mi;
int mj;
int mk;
};
int main()
{
cout << "sizeof(Demo) = " << sizeof(Demo) << endl;
cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
Derived d(1, 2, 3);
Test* p = reinterpret_cast<Test*>(&d);
cout << "Before changing ..." << endl;
d.print();
p->mi = 10;
p->mj = 20;
p->mk = 30;
cout << "After changing ..." << endl;
d.print();
return 0;
}
執行結果為:
[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out
sizeof(Demo) = 16
sizeof(Derived) = 24
Before changing ...
mi = 1, mj = 2, mk = 3
After changing ...
mi = 10, mj = 20, mk = 30
使用C語言實現多型:
// test.h
#ifndef _TEST_H_
#define _TEST_H_
typedef void Demo;
typedef void Derived;
Demo* Demo_Create(int i, int j);
int Demo_GetI(Demo* pThis);
int Demo_GetJ(Demo* pThis);
int Demo_Add(Demo* pThis, int value);
void Demo_Free(Demo* pThis);
Derived* Derived_Create(int i, int j, int k);
int Derived_GetK(Derived* pThis);
int Derived_Add(Derived* pThis, int value);
#endif
// test.c
#include "test.h"
#include "malloc.h"
static int Demo_Virtual_Add(Demo* pThis, int value);
static int Derived_Virtual_Add(Demo* pThis, int value);
struct VTable // 2. 定義虛擬函式表資料結構
{
int (*pAdd)(void*, int); // 3. 虛擬函式表裡面儲存什麼???
};
struct ClassDemo
{
struct VTable* vptr; // 1. 定義虛擬函式表指標 ==》 虛擬函式表指標型別???
int mi;
int mj;
};
struct ClassDerived
{
struct ClassDemo d;
int mk;
};
static struct VTable g_Demo_vtbl =
{
Demo_Virtual_Add
};
static struct VTable g_Derived_vtbl =
{
Derived_Virtual_Add
};
Demo* Demo_Create(int i, int j)
{
struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo));
if( ret != NULL )
{
ret->vptr = &g_Demo_vtbl; // 4. 關聯物件和虛擬函式表
ret->mi = i;
ret->mj = j;
}
return ret;
}
int Demo_GetI(Demo* pThis)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mi;
}
int Demo_GetJ(Demo* pThis)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mj;
}
// 6. 定義虛擬函式表中指標所指向的具體函式
static int Demo_Virtual_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->mi + obj->mj + value;
}
// 5. 分析具體的虛擬函式!!!!
int Demo_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis;
return obj->vptr->pAdd(pThis, value);
}
void Demo_Free(Demo* pThis)
{
free(pThis);
}
Derived* Derived_Create(int i, int j, int k)
{
struct ClassDerived* ret = (struct ClassDerived*)malloc(sizeof(struct ClassDerived));
if( ret != NULL )
{
ret->d.vptr = &g_Derived_vtbl;
ret->d.mi = i;
ret->d.mj = j;
ret->mk = k;
}
return ret;
}
int Derived_GetK(Derived* pThis)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis;
return obj->mk;
}
static int Derived_Virtual_Add(Demo* pThis, int value)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis;
return obj->mk + value;
}
int Derived_Add(Derived* pThis, int value)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis;
return obj->d.vptr->pAdd(pThis, value);
}
// main.c
#include "stdio.h"
#include "test.h"
void run(Demo* p, int v)
{
int r = Demo_Add(p, v);
printf("r = %d\n", r);
}
int main()
{
Demo* pb = Demo_Create(1, 2);
Derived* pd = Derived_Create(1, 22, 333);
printf("pb->add(3) = %d\n", Demo_Add(pb, 3));
printf("pd->add(3) = %d\n", Derived_Add(pd, 3));
run(pb, 3);
run(pd, 3);
Demo_Free(pb);
Demo_Free(pd);
return 0;
}
執行結果為:
[[email protected] Desktop]# gcc main.c test.c
[[email protected] Desktop]# ./a.out
pb->add(3) = 6
pd->add(3) = 336
r = 6
r = 336
補充說明:
面向物件程式最關鍵的地方在於必須能夠表現三大特性:封裝,繼承,多型!
封裝指的是類中的敏感資料在外界是不能訪問的;繼承指的是可以對已經存在的類進行程式碼複用,並使得類之間存在父子關係;多型指的是相同的呼叫語句可以產生不同的呼叫結果。
因此,如果希望用 C 語言完成面向物件的程式,那麼肯定的,必須實現這三個特性;否則,最多隻算得上基於物件的程式( 程式中能夠看到物件的影子,但是不完全具備面向物件的 3 大特性)。
程式碼中通過 void* 指標保證具體的結構體成員是不能在外界被訪問的,以此模擬 C++ 中 private 和 protected。因此,在標頭檔案中定義瞭如下的語句:
typedef void Demo;
typedef void Derived;
Demo 和 Derived 的本質依舊是 void,所以,用 Demo* 指標和 Derived* 指標指向具體的物件時,無法訪問物件中的成員變數,這樣就達到了“外界無法訪問類中私有成員” 的封裝效果!
繼承的本質是父類成員與子類成員的疊加,所以在用 C 語言寫面向物件程式的時候,可以直接考慮結構體成員的疊加即可。程式碼中的實現直接將 struct ClassDemo d 作為 struct ClassDerived 的第一個成員,以此表現兩個自定義資料型別間的繼承關係。因為 struct ClassDerived 變數的實際記憶體分佈就是由 struct ClassDemo 的成員以及 struct ClassDerived 中新定義的成員組成的,這樣就直接實現了繼承的本質,所以說 struct ClassDerived 繼承自 struct ClassDemo。
下一步要實現的就是多型了!多型在 C++ 中的實現本質是通過虛擬函式表完成的,而虛擬函式表是編譯器自主產生和維護的資料結構。因此,接下來要解決的問題就是如何在 C 語言中自定義虛擬函式表?上述程式碼中認為通過結構體變數模擬 C++ 中的虛擬函式表是比較理想的一種選擇,所以有了下面的程式碼:
struct VTable
{
int (*pAdd)(void*, int);
};
有了型別後就可以定義實際的虛擬函式表了,在 C 語言中用具有檔案作用域的全域性變量表示實際的虛擬函式表是最合適的,因此有了下面的程式碼:
// 父類物件使用的虛擬函式表
static struct VTable g_Demo_vtbl =
{
Demo_Virtual_Add
};
// 子類物件使用的虛擬函式表
static struct VTable g_Derived_vtbl =
{
Derived_Virtual_Add
};
每個物件中都擁有一個指向虛擬函式表的指標,而所有父類物件都指向 g_Demo_vtbl,所以所有子類物件都指向 g_Derived_vtbl。當一切就緒後,實際呼叫虛擬函式的過程就是通過虛擬函式表中的對應指標來完成的。
5.小結
- 函式重寫只可能發生在父類與子類之間
- 根據實際物件的型別確定呼叫的具體函式
- virtual關鍵字是C++中支援多型的唯一方式
- 被重寫的虛擬函式可表現出多型的特性
- C++中的類物件在記憶體佈局上與結構體相同
- 成員變數和成員函式在記憶體中分開存放
- 訪問許可權關鍵字在執行時失效
- 呼叫成員函式時物件地址作為引數隱式傳遞
- 繼承的本質就是父子間成員變數的疊加
- C++中的多型是通過虛擬函式表實現的
- 虛擬函式表是由編譯器自動生成與維護的
- 虛擬函式的呼叫效率低於普通成員函式