學過C++的應該都對虛表有所耳聞,在此就不過多介紹概念了,通過實
例來演示一下如何獲取虛表地址和虛函數地址。
簡單說一下虛表的概念:在一個類中如果有虛函數,那麼此類的實例中就有
一個虛表指針指向虛表,這個虛表是一塊兒專門存放類的虛函數地址的內存。
圖示說明本文的主題(先看圖更容易後面代碼中的指針操作):
代碼如下(要講解的都在代碼的註釋中說明了):
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
void h() { cout << "Base::h" << endl; }
};
typedef void(*Fun)(void); //函數指針
int main()
{
Base b;
// 這裏指針操作比較混亂,在此稍微解析下:
// *****printf("虛表地址:%p\n", *(int *)&b); 解析*****:
// 1.&b代表對象b的起始地址
// 2.(int *)&b 強轉成int *類型,爲了後面取b對象的前四個字節,前四個字節是虛表指針
// 3.*(int *)&b 取前四個字節,即vptr虛表地址
//
// *****printf("第一個虛函數地址:%p\n", *(int *)*(int *)&b);*****:
// 根據上面的解析我們知道*(int *)&b是vptr,即虛表指針.並且虛表是存放虛函數指針的
// 所以虛表中每個元素(虛函數指針)在32位編譯器下是4個字節,因此(int *)*(int *)&b
// 這樣強轉後爲了後面的取四個字節.所以*(int *)*(int *)&b就是虛表的第一個元素.
// 即f()的地址.
// 那麼接下來的取第二個虛函數地址也就依次類推. 始終記着vptr指向的是一塊內存,
// 這塊內存存放着虛函數地址,這塊內存就是我們所說的虛表.
//
printf("虛表地址:%p\n", *(int *)&b);
printf("第一個虛函數地址:%p\n", *(int *)*(int *)&b);
printf("第二個虛函數地址:%p\n", *((int *)*(int *)(&b) + 1));
Fun pfun = (Fun)*((int *)*(int *)(&b)); //vitural f();
printf("f():%p\n", pfun);
pfun();
pfun = (Fun)(*((int *)*(int *)(&b) + 1)); //vitural g();
printf("g():%p\n", pfun);
pfun();
}
轉載:http://blog.csdn.net/qianghaohao/article/details/51356718
--------------------------------------------------------------------------------------------------------------------------------
轉載:http://blog.csdn.net/alps1992/article/details/45052403
虛函數
虛函數就是用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
的關係。