標籤:c++ 虛函數 虛表 虛指標
虛函數
虛函數就是用virtual來修飾的函數。虛函數是實現C++多態的基礎。
虛表
每個類都會為自己類的虛函數建立一個表,來存放類內部的虛函數成員。
虛函數表指標
每個類在建構函式裡面進行虛表和虛表指標的初始化。
下面看一段代碼:
//// main.cpp// VirtualTable//// Created by Alps on 15/4/14.// Copyright (c) 2015年 chen. All rights reserved.//#include <iostream>using namespace std;class Base{public: virtual void func(){ printf("Base\n"); } virtual void hunc(){ printf("HBase\n"); }private: virtual void gunc(){ printf("Base Private\n"); }};class Derive: public Base{public: virtual void func(){ printf("Derive\n"); }};class DeriveSecond: public Base{public: void func(){ printf("Second!\n"); }};class DeriveThird: public Base{};class DeriveForth: public Base{public: void gunc(){ printf("Derive Forth\n"); }};int main(int argc, const char * argv[]) { Derive d; Base *pb = &d; pb->func(); // 1 輸出:Derive DeriveSecond sec; pb = &sec; pb->func(); // 2 輸出:Derive Second DeriveThird thi; pb = &thi; pb->func(); //3 輸出:Base DeriveForth forth; pb = &forth;// pb->gunc(); // 4 報錯 return 0;}
在這個裡面我建立了一個基類Base還有其他衍生類別。
首先// 1部分,表示了雖然我們聲明的是一個Base類的指標,但是指向的是衍生類別的執行個體,所以調用的就是衍生類別的函數。
其次// 2部分,表示的和1差不多,只不過在// 2裡不是虛函數了,覆蓋了父類的虛函數。但還是存放在衍生類別的虛表裡。
在// 3的代碼裡可以看到,衍生類別沒有覆蓋父類的虛函數的時候,雖然指向的是衍生類別的執行個體,但是調用的是父類的方法,是因為在繼承時候,子類也有一個虛表,裡面存放了父類的虛函數表。
在// 4裡是私人的虛函數是不能直接被外部調用的。
虛表詳解
先看如下代碼:代碼來源:RednaxelaFX,程式設計語言廚此人我覺得很厲害,這裡借用一下他的代碼,無任何商用,如果有問題,請聯絡我刪除。
#include <string>#include <iostream>class Object { int identity_hash_;public: Object(): identity_hash_(std::rand()) { } int IdentityHashCode() const { return identity_hash_; } virtual int HashCode() { return IdentityHashCode(); } virtual bool Equals(Object* rhs) { return this == rhs; } virtual std::string ToString() { return "Object"; }};class MyObject : public Object { int dummy_;public: int HashCode() override { return 0; } std::string ToString() override { return "MyObject"; }};int main() { Object o1; MyObject o2; std::cout << o2.ToString() << std::endl << o2.IdentityHashCode() << std::endl << o2.HashCode() << std::endl;}/* Object vtable -16 [ offset to top ] __si_class_type_info -8 [ typeinfo Object ] --> +0 [ ... ]--> +0 [ vptr ] --> +0 [ &Object::HashCode ] +8 [ identity_hash_ ] +8 [ &Object::Equals ] +12 [ (padding) ] +16 [ &Object::ToString ] MyObject vtable -16 [ offset to top ] __si_class_type_info -8 [ typeinfo MyObject ] --> +0 [ ... ]--> +0 [ vptr ] --> +0 [ &MyObject::HashCode ] +8 [ identity_hash_ ] +8 [ &Object::Equals ] +12 [ dummy_ ] +16 [ &MyObject::ToString ]*/
這裡最主要的是我認為R大的這個虛表畫的實在是好看。所以直接借用了,一看就比我上面自己寫的代碼好看多了(T T)。
首先我們學習的時候,可以暫時先無視小於0的虛表內容。從+0開始存放了vptr這個虛表指標指向了類的虛表。可以很清楚的看到在MyObject的虛表裡其中HashCode 和 ToString函數已經是衍生類別的虛函數了,把父類的函數重寫了。
所以這兩個R大畫的類已經很清楚的說明了類的虛表虛函數的操作。
那麼有沒有比較暴力的辦法強行自己來控制虛表呢。其實這個來源於當時我做的一個阿里筆試題,做完當天我就看到知乎的R大已經做了詳細的解釋,這裡還是引用他的代碼好了。
虛表和虛函數地址
以下代碼同出自R大之手:RednaxelaFX,程式設計語言廚
#include <iostream>using namespace std;class animal{protected: int age_; animal(int age): age_(age) { }public: virtual void print_age(void) = 0; virtual void print_kind() = 0; virtual void print_status() = 0;};class dog : public animal{public: dog(): animal(2) { } ~dog() { } virtual void print_age(void) { cout << "Woof, my age = " << age_ << endl; } virtual void print_kind() { cout << "I‘m a dog" << endl; } virtual void print_status() { cout << "I‘m barking" << endl; }};class cat : public animal{public: cat(): animal(1) { } ~cat() { } virtual void print_age(void) { cout << "Meow, my age = " << age_ << endl; } virtual void print_kind() { cout << "I‘m a cat" << endl; } virtual void print_status() { cout << "I‘m sleeping" << endl; }};void print_random_message(void* something) { cout << "I‘m crazy" << endl;}int main(void){ cat kitty; dog puppy; animal* pa = &kitty; intptr_t* cat_vptr = *((intptr_t**)(&kitty)); intptr_t* dog_vptr = *((intptr_t**)(&puppy)); intptr_t fake_vtable[] = { dog_vptr[0], // for dog::print_age cat_vptr[1], // for cat::print_kind (intptr_t) print_random_message }; *((intptr_t**) pa) = fake_vtable; pa->print_age(); // Woof, my age = 1 pa->print_kind(); // I‘m a cat pa->print_status(); // I‘m crazy return 0;}
我們可以看到R大幹了什麼!!喪心病狂的把vtable自己偽造了一個,然後放到虛表指標後面!簡直佩服。看到這個代碼我也是才明白,虛表可以這麼操作。
虛表地址和虛函數地址
虛函數表的地址(int*)&classname)與虛函數的地址(int*)*(int*)(&classname)實際按照R大的說法,這裡的int應該改成intptr_t才更好,這樣能夠防止在LP64模型下,函數指標是8個位元組。而地址擷取不全。
虛函數表的地址和虛函數地址的關係類似於: x 和 *x的關係。
C++學習 - 虛表,虛函數,虛函數表指標學習筆記