又一次吃了浮点数的亏 = =
C++ 的 ScopeExitGuard

指针和虚函数表

輝夜(tadvent) posted @ 2014年12月10日 21:45 in C/C++ with tags c++ , 1326 阅读

前两天在水木 C++ 版看到两个题目,分别是关于指针和 class 的虚函数表的。关于指针和数组,之前已经打过很久的交道了,比较简单。不过虚函数表以及 class 成员的内存布局一直是模模糊糊的概念,趁此机会把《C++对象模型》拿出来研究了一把。

题目一

#include<stdio.h>  
int main()  
{  
    int a[5] = {1, 2, 3, 4, 5};  
    int *ptr = (int*)(&a + 1);    
    printf("%d,%d\n", *(a+1), *(ptr-1));  
} 

输出什么?

数组和指针类型的互相转换,只要弄清楚指针所指地址以及所指的类型就容易分析了:

  1. a 的类型是 int[5],表示一个长度为 5 的 int 型数组
  2. 由于 a 的类型是数组,因此对 a 进行算数运算时,其所指地址是以 sizeof(A[0]) 为单位变化的,这里即是 sizeof(int),因此 *(a+1)a[1]
  3. a 取地址,得到的类型是 int (*)[5],这是一个指针,其指向一个含有 5 个 int 的数组。因此 &a+1 会使地址向后移动 5 个 int 的空间,得到 a[5] 的地址。
  4. ptrint* 类型,因此它的算数运算的单位是 sizeof(int)ptr-1 即可得到 a[4] 的地址。
  5. 综上,程序输出 2,5

题目二

#include <iostream>
using namespace std;

class A {
    virtual void g() {
        cout << "A::g" << endl;
    }
private:
    virtual void f() {
        cout << "A::f" << endl;
    }
};

class B : public A {
    void g() {
        cout << "B::g" << endl;
    }
    virtual void h() {
        cout << "B::h" << endl;
    }
};

typedef void(*Fun)(void);

int main() {
    B b;
    Fun pFun;
    for (int i = 0; i < 3; i++) {
        pFun = (Fun)*((int*)* (int*)(&b) + i);
        pFun();
    }
}

输出什么?

看那一串丧心病狂的指针操作!!

class 层次很简单,A 里有两个虚函数 g() 和 f(),B 里重写了 g(),又添了一个 h()。A 里那个 private 是多余,所有函数都是 private 的。 main() 里主要就是对那个函数指针 pFun 赋值,然后调用。看来所有的内容都在那个指针操作里。

  1. (&b) 得到 b 的地址,类型是 B*
  2. *(int*)(&b)b 的地址按 int 型指针来解释,并取出这个 int 值。相当于将 b 的前 4 个字节 (32位环境下) 当成一个 int 型读出。假设读出的值是 vptr (好吧……其实就是 vptr)
  3. *((int*)vptr + i) 将上一个操作取出的值再当成一个地址,并添加 i 个单位偏移。由于是 int* 类型,因此每次偏移一个 sizeof(int) 的空间,即 4 字节。粗略来说就相当于读出 vptr[i] 的值。
  4. 最后再转换为 Fun 的函数指针。

用一个图来看一下:

class A 里有两个虚函数,因此它的虚函数表只有两项,分别是 A::g() 和 A::f()。class B 继承自 A,因此它必须首先照搬 A 的虚函数表(前 2 项),然后再添加自己新加的 B::h(),同时在 B 中又重写了 g(),因此 B 的虚函数表里把 A::g() 的位置替换为 B::g()。

根据前面指针的分析可以看出,三次循环分别调用了 B::g(), A::f() 和 B::h()。不过这个调用比较粗暴,完全无视了函数参数。本身类成员函数是带有一个 this 指针做参数的,不过既然这里也没有使用成员变量,因此也就没什么影响了。

以上程序能够成功运行的条件是 class 的虚函数指针位于对象的头部。我试着在 class A 和 class B 中分别添加一个 int 成员,输出结果依然不变(VS2013 和 gcc4.9.2 下),说明这两个编译器都将虚函数指针放在对象头部,没有再试验其他环境,不知道会不会有不同结果。另外,如果是 64位 环境,应将指针变换那一行的所有 int 换成 int64_t 或 long long,不然指针和 int 所占空间不同,地址运算会出错。

 


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter