问题描述
#include <iostream>
using namespace std;
class animal
{
protected:
int age;
public:
virtual void print_age(void) = 0;
};
class dog : public animal
{
public:
dog() {this -> age = 2;}
~dog() { }
virtual void print_age(void) {cout<<"Wang, my age = "<<this -> age<<endl;}
};
class cat: public animal
{
public:
cat() {this -> age = 1;}
~cat() { }
virtual void print_age(void) {cout<<"Miao, my age = "<<this -> age<<endl;}
};
int main(void)
{
cat kitty;
dog jd;
animal * pa;
int * p = (int *)(&kitty);
int * q = (int *)(&jd);
p[0] = q[0];
pa = &kitty;
pa -> print_age();
return 0;
}
Wang, my age = 1今天线上笔试遇到的一道题,很好奇,这几句:
int * p = (int *)(&kitty);
int * q = (int *)(&jd);
p[0] = q[0];
这是为什么呢?
--------------------------------------------------------------------------------
确实是:
p[0] = q[0];
当时读题时,看到 基类,派生类,虚函数,,我就猜到肯定是要考 多态,虚函数表这些知识,
当时就是对这句挺疑惑:
p[0] = q[0];
所以笔试完了,就挺困惑,宿舍熄灯后,怎么也睡不着,就拿到知乎上,想问问各位。
说实话,真的很感谢 @蓝色 大大,这么晚还回答了
这个是昨天笔试完,晚上11点左右提的,不能算我笔试违规吧? T_T
好吧,我也不匿名了,感觉匿名不好,不懂就问,为啥要匿名。
我不是 陈浩大大,只是个普通院校 大三 计算机科班生。
首先
@蓝色大大的答案已经把思路说得很清楚了。这是个hack,不是C++语言规范所保证的行为,而是某些C++编译器采用的C++ ABI的行为。
放个传送门:
为什么bs虚函数表的地址(int*)(&bs)与虚函数地址(int*)*(int*)(&bs) 不是同一个? - RednaxelaFX 的回答其次,这代码不但依赖某些C++编译器的行为,还依赖平台的指针宽度是32位。
int * p = (int *)(&kitty);
int * q = (int *)(&jd);
p[0] = q[0];
这几句不应该用int*,而应该用intptr_t*才对。这样才能保证拷贝的是一个指针宽度的数据,而不是一个int宽度的数据。
- 在32位平台上,int通常是32位,而指针是32位,所以正好匹配了,程序能正常运行;
- 在64位平台上,如果是流行的LP64模型,int是32位而指针是64位,这里实际上只拷贝了指针的一半,程序能否正常运行就看运气了。
如果是在一个64位且小端(little endian)的平台上,那这代码拷贝的是指针的低32位。很可能会运气好能正常运行,因为dog类与cat类的vtable可能正好在内存里处于很近的位置,它们的地址的高32位可能正好相同,地址不同的地方都在低32位,这样这个程序就运气好能正常运行。
如果是在一个64位且大端(big endian)的平台上,那这段代码拷贝的是指针的高32位,那就完全达不到效果了。
不知道谁出的这种题⋯
或者题主把题目的细节记错了。
后面有回答说原本的笔试题不是p[0] = q[0],而是p[1] = q[1]。如果是这样的话那仍然只能在32位平台上能行,在64位平台上就纱布了。
再次,这种题还有很多玩法。例如说一种简单的玩法是像这样:
#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;
}
直接整个vtable伪造出来然后想往里面填啥就填啥。
至于有没有实际应用使用了题主原本写的那种代码,还真有。(但这么用的都该拖出去打pp⋯
例如说Oracle/Sun JDK / OpenJDK里的HotSpot VM,在PermGen Removal之前,有一类叫klassOopDesc的对象是由GC管理的,但里面还嵌套包含一个Klass的子类对象,而Klass类有vptr。为了能正确管理klassOopDesc里嵌套的Klass的vptr,就有了这么个奇葩的东西:
class Klass_vtbl好同学们请不要学这种例子⋯这个奇葩的结构在PermGen Removal后就移除了嗯。