(1)要有继承
(2)要有虚函数重写(即动态绑定)
(3)至少有一个基类类型的指针或基类类型的引用。这个指针和引用可以对virtual成员函数进行调用
C++的多态性分为两种:编译时的多态、运行时的多态; 使用重载来实现编译时的多态,使用虚函数来实现运行时的多态。
(1)在main函数中,如果有基类或者派生类的实例对象,就需要有基类的虚函数的实现。
(2)在main函数中,如果没有基类或者派生类的实例对象,可以不实现基类的虚函数。
(3)如果把虚函数写成纯虚函数,也就不需要实现了。
先从反面来讲:如果含有纯虚函数的类可以定义对象,那么该对象就应该可以调用类中的纯虚函数,但是纯虚函数是没有实现的,这就是个矛盾的。
正面来讲:普通类具有成员函数,构造类的对象时,会对成员变量和成员函数分配内存。含有纯虚函数的类,定义了成员函数的地址是空,无法分配内存,该成员函数对类是没有意义的,失去了普通类的数据和方法绑定于同一对象中的意义,因此无法构造对象,只能通过派生类继承这些成员函数并实现,才能构造派生类对象。此时抽象类就起到了定义接口的作用。
抽象类将事物的描述和实现区分开来,选择纯虚函数的概念,是想将一个类声明为抽象类的思想明确化,选择性的定义函数是一种灵活的多的方式,是实现多态的基础。
动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。程序运行过程中,把函数(或过程)调用与响应调用所需要的代码相结合的过程称为动态绑定。
含有虚函数的基类对象和派生类对象都有vptr指针,每个类的vptr指针指向相应的虚函数表。基类指针(或引用)指向派生类对象(或引用)时调用虚函数时,派生类vptr指针找到虚函数表,根据虚函数表找到相应虚函数的入口地址,然后进行调用。这是动态联编。
动态联编是指在程序运行的时候,编译器才去判断调用哪个函数或如何执行程序。如if-else结构、switch结构等。
静态联编是指程序在编译阶段就确定了如何执行。
为什么虚函数在编译期间无法绑定?
因为不确定运行期的状态,假设 func() 为虚函数,指针p 的类型为基类A,那么 p->func() 可能调用 A 类的函数,也可能调用派生类 B、C 的函数,不能根据指针 p 的类型对函数重命名。也就是说,虚函数在编译期间无法绑定。
(1)指向基类的指针可以指向派生类对象,当基类指针指向派生类对象时,这种指针只能访问派生对象从基类继承而来的那些成员,不能访问子类特有的元素,除非应用强类型转换,例如有基类B和从B派生的子类D,则B *p;D dd; p=&dd是可以的,指针p只能访问从基类派生而来的成员,不能访问派生类D特有的成员.因为基类不知道派生类中的这些成员。
例如:
class A
{
public:
virtual void func()
{
cout << "father" << endl;
}
private:
};
class B :public A
{
public:
virtual void func1()
{
cout << "son" << endl;
}
private:
};
int main()
{
A *pa = NULL;
B* pb = NULL;
pb = (B*)pa; //父类指针强制转换为子类指针
system("pause");
return 0;
}
(2)不能使用派生类指针指向基类对象,派生类对象可以直接给父类对象赋值。
当基类的指针(P)指向派生类的时候,只能操作派生类中从基类中继承过来的数据。
派生类可能不仅仅从基类处继承了基类成员,还可能自己拥有特有的成员,所以派生类的指针,内存空间可能会比基类长,派生类指针可能会调用基类对象所不具有的数据,可能会造成错误,因此不能用派生类指针指向基类对象。
因为子类对象会继承父类的数据,因此子类对象可以对父类对象赋值;而父类可能会不具有子类特有的数据,因此父类对象不能对子类对象进行赋值。
类型兼容性原则:
子类对象可以当做父类对象使用;
子类对象可以直接给父类对象赋值;
子类对象可以直接初始化父类对象;
父类指针可以指向子类对象;
父类引用可以直接引用子类对象;
(3)如果派生类中覆盖了基类中的成员变量或函数(无虚函数重写),则当声明一个基类指针指向派生类对象时,这个基类指针只能访问基类中的成员变量或函数。
(4)如果基类指针指向派生类对象,则当对其进行增减运算时,它将指向它所认为的基类的下一个对象,而不会指向派生类的下一个对象,因此,应该认为对这种指针进行的增减操作是无效的。
(1)非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。实际上,优秀的程序员常常把基类的析构函数定义为虚函数。
因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。
(2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。
(3)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、参数类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种非虚的同名、同返回值、同参数个数、同参数类型函数。
(1) 静态函数的目的是通过类名+函数名访问类的static变量,或者通过对象调用staic函数实现对static成员变量的读写,要求内存中只有一份数据。而虚函数在子类中重写,并且通过多态机制实现动态调用,在内存中需要保存不同的重写版本。
(2) 构造函数的作用是构造对象,而虚函数的调用是在对象已经构造完成,并且通过调用时动态绑定。动态绑定是因为每个类对象内部都有一个指针,指向虚函数表的首地址。而且虚函数,类的成员函数,static成员函数都不是存储在类对象中,而是在内存中只保留一份。
当我们定义一个类的对象为空时,这时我们调用该对象中的函数,我们会发现当调用非虚函数时仍可以正常调用,而如果要调用虚函数则会报错。如下示例:
class Fa
{
public:
virtual void func1()
{
cout << "father func1" << endl;
}
void func2()
{
cout << "father func2" << endl;
}
};
int main()
{
Fa *fa = NULL;
fa->func2();
system("pause");
return 0;
}
结果为:
当我们调用虚函数func1()时,会报一个内存错误:
当我们把申请的内存delete后,再测一下,看情况如何:
int main()
{
Fa *fa = new Fa;;
delete fa;
fa->func2();
system("pause");
return 0;
}
结果如下,可以看出我们仍然可以调用该成员函数
但当我们,调用虚函数func1()时,仍然会报错。
由以上几种情况我们发现当类的对象指针为空时,并不是指该对象就不存在,当我们的指针没有用来指向其他地方时,此时该指针都可以调用类中成员函数,但无法调用其虚函数。同样的道理,当我们用 delete 释放一个类的对象内存空间时,并不是将该对象内存空间进行清除,而是将这块内存的使用权释放了,但里面的东西可能还依然存在,所以在这块内存被其他对象占用之前里面的东西依然会存在,所以即使我们将对象指针指向空后,依然可以调用类中的函数。
这里要强调的是,delete只是把指针所指的内存释放掉,并没有把指针本身给干掉。我们平常将指针所指内存释放掉之后会让内存指向空,这是为了防止产生野指针,为了下次调用指针的时候方便判断使用,并不是将该指针指向NULL后指针什么都没有了。
C++只关心你的指针类型,不关心指针指向的对象是否有效,C++要求程序员自己保证指针的有效性。
编译期绑定(静态绑定)的函数,会给出一个入口地址,因此可以通过这个地址调用函数。
而在运行期绑定(动态绑定)的函数(如func1),不会提前确定该函数的入口地址,指针为NULL,无法找到虚函数表,所以也就无法调用func1()函数。