一、多态
多态性是面向对象程序设计的重要特征之一。它是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。比如我们定义一个带有Run的虚函数动物类,让鱼,鸟,和狗继承自这个类,当我们用虚基类指针Animal* p;指向不同派生类,调用函数时会调用派生类的函数,比如鱼是水里游,鸟在天上飞,狗在地上跑,实现同一个指针根据不同指向具有不同的行为,这就是多态。
多态可以通过以下几种方式实现:
- 函数重载
- 运算符重载
- 模板
- 虚函数
示例:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void Fun1()
{
cout<<"Base::Fun1 ..."<<endl;
}
virtual void Fun2()
{
cout<<"Base::Fun2 ..."<<endl;
}
void Fun3()
{
cout<<"Base::Fun3 ..."<<endl;
}
};
class Derived : public Base
{
public:
/*virtual */void Fun1()
{
cout<<"Derived::Fun1 ..."<<endl;
}
/*virtual */void Fun2()
{
cout<<"Derived::Fun2 ..."<<endl;
}
void Fun3()
{
cout<<"Derived::Fun3 ..."<<endl;
}
};
int main(void)
{
Base* p;
Derived d;
p = &d;
p->Fun1(); // Fun1是虚函数,基类之指针指向派生类对象,调用的是派生类对象的虚函数
p->Fun2();
p->Fun3(); // Fun3非虚函数,根据p指针实际类型来调用相应类的成员函数
return 0;
}
构造函数和析构函数可以作为虚函数呢?首先构造函数是不行的,因为虚函数的调用是在运行期才确定了,而构造函数必须在编译期就确定调用哪个函数。但是析构函数是可以作为虚函数的。
示例:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void Fun1()
{
cout<<"Base::Fun1 ..."<<endl;
}
virtual void Fun2()
{
cout<<"Base::Fun2 ..."<<endl;
}
void Fun3()
{
cout<<"Base::Fun3 ..."<<endl;
}
Base()
{
cout<<"Base ..."<<endl;
}
// 如果一个类要做为多态基类,要将析构函数定义成虚函数
virtual ~Base()
{
cout<<"~Base ..."<<endl;
}
};
class Derived : public Base
{
public:
/*virtual */void Fun1()
{
cout<<"Derived::Fun1 ..."<<endl;
}
/*virtual */void Fun2()
{
cout<<"Derived::Fun2 ..."<<endl;
}
void Fun3()
{
cout<<"Derived::Fun3 ..."<<endl;
}
Derived()
{
cout<<"Derived ..."<<endl;
}
~Derived()
{
cout<<"~Derived ..."<<endl;
}
};
int main(void)
{
Base* p;
p = new Derived;
p->Fun1();
delete p;
return 0;
}
虚析构函数的作用就是根据指针指向类型调用派生类的析构函数,保证对象能被正确的释放。如果这里不用虚析构函数的话,会根据指针类型调用基类的析构函数,如果派生类有动态内存调用的话,会导致派生类的内存不能释放造成内存泄露。如果一个类不会被其他派生类继承的话,就没有必要将析构函数定义成虚析构函数。就像Effective C++指出的那样:
二、静态绑定与动态绑定
静态绑定:绑定过程出现在编译阶段,在编译期就已确定要调用的函数。(如普通函数和静态函数)
动态绑定:绑定过程工作在程序运行时执行,在程序运行时才确定将要调用的函数。(虚函数)
三、虚函数
虚函数的概念:在基类中冠以关键字 virtual的成员函数
虚函数的定义:
virtual
函数类型 函数名称
(
参数列表
);
注意:如果一个函数在基类中被声明为虚函数,则他在所有派生类中都是虚函数,且只有通过基类指针或引用调用虚函数才能引发动态绑定
注意:虚函数不能声明为静态(静态函数是类共享的,无法通过对象地址偏移取出实际函数,请看下面虚表指针)
上面已经有虚函数的使用示例,这里不再写示例了。
四、虚表指针
虚函数的动态绑定是编译器在运行期通过虚表来实现的,包含虚函数的类头4个字节存放指向虚表的指针。下面我们通过一个例子来验证一下。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void Fun1()
{
cout<<"Base::Fun1 ..."<<endl;
}
virtual void Fun2()
{
cout<<"Base::Fun2 ..."<<endl;
}
int data1_;
};
class Derived : public Base
{
public:
void Fun2()
{
cout<<"Derived::Fun2 ..."<<endl;
}
virtual void Fun3()
{
cout<<"Derived::Fun3 ..."<<endl;
}
int data2_;
};
typedef void (*FUNC)();
int main(void)
{
cout<<sizeof(Base)<<endl;
cout<<sizeof(Derived)<<endl;
Base b;
long** p = (long**)&b;
FUNC fun = (FUNC)p[0][0];
fun();
fun = (FUNC)p[0][1];
fun();
cout<<endl;
Derived d;
p = (long**)&d;
fun = (FUNC)p[0][0];
fun();
fun = (FUNC)p[0][1];
fun();
fun = (FUNC)p[0][2];
fun();
Base* pp = &d;
pp->Fun2();
return 0;
}
打印结果:
Base对象的内存模型如下(Derived对象模型原理类似,不再画出):
五、object slicing与虚函数
之前我们讲过,在将派生类对象转换为基类对象时,会发生object slicing,派生类特有的数据会被去除。这和虚函数有什么联系呢?如果不是通过指针访问虚函数,那么对象的行为就是和对象的实际类型有关。也就是说,派生类如果被转换成基类对象,那么调用时就是调用基类的成员。
示例:
#include <iostream>
using namespace std;
class CObject
{
public:
virtual void Serialize()
{
cout<<"CObject::Serialize ..."<<endl;
}
};
class CDocument : public CObject
{
public:
int data1_;
void func()
{
cout<<"CDocument::func ..."<<endl;
//调用的是哪个Seriallize()???????
Serialize();
}
virtual void Serialize()
{
cout<<"CDocument::Serialize ..."<<endl;
}
CDocument()
{
cout<<"CDocument()"<<endl;
}
CDocument(const CDocument& other)
{
cout<<"CDocument(const CDocument& other)"<<endl;
}
};
class CMyDoc : public CDocument
{
public:
int data2_;
virtual void Serialize()
{
cout<<"CMyDoc::Serialize ..."<<endl;
}
};
int main(void)
{
CMyDoc mydoc;
CMyDoc* pmydoc = new CMyDoc;
cout<<"#1 testing"<<endl;
mydoc.func();
cout<<"#2 testing"<<endl;
((CDocument*)(&mydoc))->func();//派生类指针指向基类对象
cout<<"#3 testing"<<endl;
pmydoc->func();
cout<<"#4 testing"<<endl;
((CDocument)mydoc).func(); //mydoc对象强制转换为CDocument对象,向上转型
//完完全全将派生类对象转化为了基类对象,所以调用的是CDocument成员
return 0;
}
运行结果:
六、
overload、override 、overwrite
成员函数被重载(overload)的特征:
(1)相同的范围(在同一个类中)
(2)函数名字相同
(3)参数不同
(4)virtual关键字可有可无。
覆盖(override)是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类)
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual关键字。
重定义(overwrite)(派生类与基类):
(1)不同的范围(分别位于派生类与基类)
(2)函数名与参数都相同,无virtual关键字
(3)函数名相同,参数不同,virtual可有可无