c++ virtual function 虚函数(转)
多态提供了与具体实现相隔离的另一类接口,即把“what”从“how”中分离出来,多态性提高了代码的组织性和可读性。多态性是一种对消息的处理方式可以随接手消息的处理对象而变的一种机制。
1.向上映射
将派生类对象通过引用或者指针变成基类对象,引用或者指针的活动称为向上映射。向上映射总是安全的,因为是从更专门的类型到更一般的类型
eg:
derived d;
base* b1 = &d;//发生向上映射
base& b2 = d;//发生向上映射
void fun(base&){}
fun(d);//发生向上映射
2.函数调用与捆绑
捆绑:把函数体与函数调用相联系
早捆绑:由编译器连接器完成,捆绑在程序运行之前
晚捆绑:捆绑在运行时发生,实现晚捆绑事必须有一种机制在运行时确定对象的类型和合适的调用函数
c++中晚捆绑的实现:
关键字virtual告诉编译器它应该实行晚捆绑
编译器对每个包含虚函数的类创建VTABLE,放置类的虚函数地址
编译器秘密放置vpointer(VPTR)
当多态调用时,使用VPTR在VTABLES表中查找函数地址
3.虚函数
为了引起晚捆绑的特定的函数
虚函数提供了一种分辨对象执行不同功能的能力
但是从效率角度出发时,虚函数不是高效的,它需要VPTR指针的压栈出栈操作等
4.纯虚函数
纯虚函数是一种特殊的虚函数,它的一般格式如下:
class <类名>
{
virtual <类型><函数名>(<参数表>)=0;
…
};
在许多情况下,在基类中不能对虚函数给出有意义有实现,而把它说明为纯虚函数,它的实现留给该基类的派生类去做
纯虚函数和虚函数的区别:
1)虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能被称为抽象类(abstract class)。
2)虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用,因为纯虚函数在基类(base class)只有声明而没有定义。
纯虚函数是非常有用的,因为它们使得类具有明显的抽象性
5.抽象类
含有纯虚函数的类称为抽象类
任何对抽象类的实例化操作都将导致错误的产生,因为抽象类是不能直接被调用的,必须被子类继承重载后,根据要求调用其子类的方法
6.对象切片
C++提供了继承机制和虚拟,并通过(且只能通过)指向同一类族的指针或者引用来实现多态,否则多态不起作用。原因之一著名的对象切片(Object slicing)问题。
1)无虚拟机制继承的对象切片
//:objcut.cpp
#include <iostream.h>
class MyBase
{
public:
void Get(){};
void Set(){};
public:
int b;
};
class DerivedMyBase: public MyBase
{
public:
void Print(){};
void GetD(){};
};
main()
{
DerivedMyBase aDMB;
MyBase aMB = aDMB; //或 MyBase* aMB = &aDMB;
aMB->Print();//错误,因为1.因MyBase aMB = aDMB,发生对象切片; 2.因MyBase* aMB = &aDMB,通过基类指针aMB引用到的只能是基类MyBase中成员函数和成员变量,而不能引用到子类的成员函数和成员变量,解决方法是:((DerivedMyBase*)aMB)->Print();
}
编译将出错,这是因为在将aDMB拷贝给aMB时发生了对象切片,在aMB对象中只有MyBase的信息,所有的关于DerivedMyBase类的信息都被切片了。
注释后面那个语句也将导致同样的错误,但是并没有发生对象切片,发生错误的原因是: 因为DerivedMyBase是一个MyBase,所以“MyBase * pMB = &aDMB;”是合法的。而pMB仅仅是一个指针,通过该指针引用的是aDMB,但编译器对于该指针应用对象的了解仅限于MyBase,对于DerivedMyBase类的信息一无所知——这也就是在实践中通常将基类作为抽象类来实现多态的原因,此时派生类中的所有不属于基类的信息都无法通过基类指针或引用来获取,因为编译器在解析该指针或引用指向的内存区时是按照基类的信息来解释的。
2)对象切片的原理
对象切片产生的原因是bitwise的copy构造函数的调用。在“MyBase aMB = aDMB;”中由编译器生成的拷贝构造函数不需要对虚拟机制进行额外的处理,此时依照bitwise copy,所有属于DerivedMyBase的信息都丢掉了。而在“ MyBase * pMB = &aDMB;”中,根本就不需要调用copy ctor,所以切片不会发生。
3)虚拟机制与拷贝方式
当类中没有虚拟机制、没有其他类对象的成员时(只包含built-in类型、指针或者数组),默认copy ctor进行的是bitwise copy,这会导致对象切片的发生。然而,当类中有虚拟机制,或者有其他类对象成员时,默认copy ctor采用的是memberwise copy,并且会对虚拟机制进行正确的拷贝。
因为包含虚拟机制的类在定义一个对象时,编译器会向ctor中添加初始化vtable和vbaseclasstable(依赖于具体编译器)的代码,这样可以保证vtable中的内容与类型完全匹配。也就是说MyBase和DerivedMyBase有这相似的VTABLE,但不是完全相同——例如DerivedMyBase中还可以定义自己的virtual函数,这样它的VTABLE就会有更多表项。
而多态的实现是通过将函数调用解析为VTABLE中的偏移量来实现。pMB->Get()可能会被编译器解析成:
(*pMB->__vtable[Offset_of_Get])();
而当MyBase作为虚基类时,访问其中的数据成员可能就是:
pMB->__vBaseClassMyBase->b;
那么,当“aMB = aDMB;”,copy ctor会执行memberwise copy,正确的初始化aMB的VTABLE,而不是仅仅将aDMB的VTABLE拷贝过来。如果是bitwise copy,aMB对象中的VTABLE将是aDMB的,aMB.Get()调用的将是DervieMyBase定义的Get(),这显然是不符合语义和逻辑的。
7.虚函数和构造函数
当创建一个包含有虚函数的对象时,必须初始化它的VPTR以及指向相应的VTABLE,这必须有关虚函数的调用之前完成。编译器在构造函数的开头部分秘密的插入了能初始化VPTR的代码,这个相当与一个小小的内联函数调用
8.虚拟析构函数
如果一个指针是指向基类的,编译器只能知道delete期间调用这个析构函数的基类版本,当把虚构函数声明为虚函数时则可以解决这个问题。
eg:
//:pvdest.cpp
#include <iostream.h>
class base
{
public:
~base()
{
cout << "~base()" << endl;
}
};
class derived : public base
{
public:
~derived()
{
cout << "~derived()" << endl;
}
};
main()
{
base* bp = new derived;
delete bp;
system("pause");
}
输出:~base()
如果改为 virtual ~base()
输出:~derived()
~base()
本文转自:http://hi.baidu.com/guoerguoerguoer/blog/item/1a742bdce41e473e5982ddda.html