程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++學習 - 虛表,虛函數,虛函數表指針學習筆記

C++學習 - 虛表,虛函數,虛函數表指針學習筆記

編輯:C++入門知識

C++學習 - 虛表,虛函數,虛函數表指針學習筆記


虛函數

虛函數就是用virtual來修飾的函數。虛函數是實現C++多態的基礎。

虛表

每個類都會為自己類的虛函數創建一個表,來存放類內部的虛函數成員。

虛函數表指針

每個類在構造函數裡面進行虛表和虛表指針的初始化。

下面看一段代碼:

//
//  main.cpp
//  VirtualTable
//
//  Created by Alps on 15/4/14.
//  Copyright (c) 2015年 chen. All rights reserved.
//

#include 
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 
#include 

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 
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的關系。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved