第一题
class Animal
{
public:
Animal(string name) :_name(name) {}
//纯虚函数
virtual void bark() = 0;
protected:
string _name;
};
//以下是动物实体类
class Cat : public Animal
{
public:
Cat(string name) :Animal(name) {}
void bark() { cout << _name << " bark: miao miao!" << endl; }
};
class Dog : public Animal
{
public:
Dog(string name) :Animal(name) {}
void bark() { cout << _name << " bark: wang wang!" << endl; }
};
int main()
{
Animal *p1 = new Cat("加菲猫");//vfptr -> Dog vftable
Animal *p2 = new Dog("二哈");//vfptr -> Cat vftable
int *p11 = (int*)p1;
int *p22 = (int*)p2;
//交换 int类型 4字节
int tmp = p11[0];//p11[0]访问的就是Cat的前4个字节
p11[0] = p22[0];//p22[0]访问的就是Dog的前4个字节
p22[0] = tmp;
//把Cat和Dog的前4字节交换了,也就是两个对象的vfptr交换了
p1->bark();//p1 -> Cat vfptr -> Dog vftable Dog的bark
p2->bark();//p2 -> Dog vfptr -> Cat vftable Cat的bark
delete p1;
delete p2;
return 0;
}
第二题
class Base
{
public:
virtual void show(int i = 10)
{
cout << "call Base::show i:" << i << endl;
}
};
class Derive : public Base
{
public:
void show(int i = 20)//覆盖和形参的默认值是多少没有关系的
{
cout << "call Derive::show i:" << i << endl;
}
};
int main()
{
Base *p = new Derive();//要把基类的析构函数设置成虚析构函数
/*
push 0Ah => 函数调用,参数压栈是在编译时期就确定好的
mov eax, dword ptr[p]
mov ecx, dword ptr[eax]
call ecx
*/
p->show();
//看Base作用域的show是虚函数,发生动态绑定,p指向的是派生类对象, p-> Derive vfptr -> Derive vftable
delete p;
return 0;
}
动态绑定,调用的是派生类的show方法,但是为什么打印出来的形参默认值i值是基类show的默认值?
p->show();
我们转去反汇编看看
调用一个函数的时候,先压参数,对于参数带默认值的函数,用户传什么实参,
就压传入的实参,如果用户没有传实参,就压入形参的默认值到栈上。
动态绑定最终调用派生类的show方法是在运行时发生的,在编译阶段
p仅仅是Base类型,编译器只能看到基类Base的show方法,编译的时候
编译器看到这个show方法,调用这个show的指针p是Base类型的,
就去Base类型里面找到这个show,把这个show的默认值10push入栈
push 0Ah => 函数调用,参数压栈是在编译时期就确定好的
mov eax, dword ptr[p]//这就是动态绑定的操作了
mov ecx, dword ptr[eax]//放的是虚函数表的地址
call ecx
真真正正调用的时候,不管最终是动态绑定哪个函数,只要是show方法,它的形参的压栈的值始终是固定的10,和派生类的show的参数默认值没有关系了
函数调用,参数压栈是在编译时期就确定好的,因为生成的指令中已经确定好了
在继承结构中,给基类,派生类的同名覆盖方法添加默认值,而且默认值还不同,就没有意义了。基类指针或者引用指向派生类对象进行调用的时候,派生类的虚函数的形参默认值是永远都用不到。
第三题
把派生类的show方法改成私有的。请问能不能调用成功?
是可以正常调用的
为什么?
指针p最终能调用到Derive的show方法是在运行时期才确定的。(指令发生动态绑定)(运行时,成员变量成员方法,内存都是可以访问的)
成员方法能不能调用,就是说方法的访问权限是不是public,这个是在编译阶段就要确定好的。(访问限定符就是控制在编译阶段,看用户写的代码符不符合语法的规范)
所以,编译器在编译时只能看见p指针(基类指针)调用的show方法,它在编译阶段只能看见Base里的show是什么样的,Base里的show是public,所以编译器放行了,可以调用。但至于最终调用的是基类的方法还是派生类的方法,得看汇编形成的是静态绑定还是动态绑定,静态绑定肯定调用的是基类方法,动态绑定靠的是寄存器ecx,只有在运行的时候才知道,放一个虚函数地址在寄存器,然后再从寄存器取出虚函数的地址进行调用。
如果是把基类的show方法设置成private,就不能调用成功了。
编译器在编译的时候看到p是Base类型的,看到的是Base::show(),看到是show是私有的,不能调用!!!编译都通不过,哪来的运行呢???
换成派生类的指针就可以调用了,编译器编译的时候看到的就是Derive::show了,是公有的,可以调用,编译通过,然后看到show是虚函数,就动态绑定,最终调用的也是Derive::show()
第四题
这两段代码,运行起来,是正确的还是错误的?
代码运行起来有问题。
代码运行OK
我们先来分析第一个代码
基类指针指向基类对象,基类对象的内存是这样的:
在构造的时候调用了clear函数,把内存清0了,vfptr变成0地址了。
然后pb1->show,进行动态绑定的时候,从虚函数指针取虚函数表,就访问不到了,而且0地址也是不能读不能写的,调用的时候出错了。
我们来分析第二个代码
基类指针指向派生类对象。派生类对象的内存结构如下:
首先是调用基类的构造,基类的内存空间清0了。然后调用派生类的构造。
也就是说,在指令上,一定有一个地方类型的vftable虚函数表的地址要写入vfptr虚函数指针里面。这个指令是在什么时候去写的?
每一个函数从左括号到第一行代码之间,有指令生成,
Base()
{
/*
push ebp//把调用方函数的栈地址压进来
mov ebp, esp//ebp指向当前函数的栈底
sub esp, 4Ch//给当前函数开辟栈帧
rep stos esp<->ebp//当前函数栈帧初始化为0xCCCCCCCC(windows VS GCC/G++)//初始化工作做完之后,就执行当前函数的第一行指令
//但是注意:如果这个类里面有虚函数,生成的对象前4个字节有vfptr
//就是现在,函数栈帧开辟完,把虚函数表的地址给虚函数指针vfptr
//然后才是C++代码执行
vfptr <- &Base::vftable
*/
cout << "call Base()" << endl;
clear();
}
当我们new一个Derive对象的时候,首先调用基类的构造函数,基类构造的时候把基类的虚函数表写入到基类的虚函数指针vfptr,然后调用clear,把vfptr指针的值变成0x00000,相当于这个虚函数指针没有指向什么东西了。
基类的构造函数调用完了,该调用派生类的构造函数了。
派生类的构造函数压完栈,做完栈的初始化,继续把派生类的虚函数表的地址写入到虚函数指针里面。这样,指针调用show的时候,因为指针指向的是派生类对象,vfptr就是有效的,指向的是派生类的虚函数表,能够从派生类的虚函数表中取出派生类的show方法。
clear只是在基类的构造函数中去调用而已。
class Base
{
public:
Base()
{
/*
push ebp
mov ebp, esp
sub esp, 4Ch
rep stos esp<->ebp 0xCCCCCCCC(windows VS GCC/G++)
vfptr <- &Base::vftable
*/
cout << "call Base()" << endl;
clear();
}
void clear() { memset(this, 0, sizeof(*this)); }
virtual void show()
{
cout << "call Base::show()" << endl;
}
};
class Derive : public Base
{
public:
Derive()
{
/*
push ebp
mov ebp, esp
sub esp, 4Ch
rep stos esp<->ebp 0xCCCCCCCC(windows VS GCC/G++)
vfptr <- &Derive::vftable
*/
cout << "call Derive()" << endl;
}
void show()
{
cout << "call Derive::show()" << endl;
}
};
int main()
{
//Base *pb1 = new Base();
/*
mov eax, dword ptr[pb1]
mov ecx, dword ptr[eax] eax:0x00000000 不是Base::vftable
call ecx
*/
//pb1->show();//动态绑定
//delete pb1;
Base *pb2 = new Derive();
//动态绑定 call Derive::show()
/*
vfptr里面存储的是vftable的地址
vfptr <- vftable
*/
pb2->show();
delete pb2;
return 0;
}