c++中多态分为静态多态和动态多态,静态多态是函数重载,在编译阶段就能确定调用哪个函数。动态多态是由继承产生的,指不同的对象根据所接收的消息(成员函数)做出不同的反应。例如,动物都能发出叫声,但不同的动物能发出不同的叫声,这就是多态。
虚函数声明格式:
class A{
权限控制符:
virtual 函数返回值类型 函数名 (参数列表);
};
c++多态满足条件
(1)基类声明虚函数
class Animal {
public:
virtual void Sound();
};
(2)派生类重写基类的虚函数
class Dog:public Animal {
public:
virtual void Sound();
//void Sound();等价于virtual void Sound();
};
注意:派生类中重写的虚函数前是否添加virtual,均被视为虚函数
(3)将派生类对象赋值给基类指针或引用,通过基类指针或引用访问虚函数
Dog dog;
Animal* animal = &dog;//通过指针
animal->Sound();
Animal& animal_1 = dog;//通过引用
dog.Sound();
声明虚函数注意以下几点:
(1)构造函数不能声明为虚函数,因为构造函数执行时,对象还没有创建,但析构函数可以声明为虚函数
(2)虚函数不能是静态成员函数。因为静态成员函数是对象共享的
(3)友元函数不能声明为虚函数,但虚函数可以作为另一个类的友元函数
c++11 final关键字
(1)修饰类,表示该类不可以被继承
class A final{};
(2)修饰虚函数,表示该虚函数不能在派生类中重写
virtual void f() final;
c++虚函数实现多态的原理
虚函数是通过动态绑定实现多态的,当编译器在编译过程中遇到virtual关键字时,他不会对函数进行绑定,而是为包含虚函数的类建立一张虚函数表Vftable.编译器按照虚函数的声明依次保存虚函数地址。同时,编译器会在类中添加一个隐藏的虚函数指针Vfptr,指向虚函数表。在创建对象时,将虚函数指针Vfptr放置在对象的起始位置,为其分配内存空间,而虚函数表不占用对象内存空间。
派生类继承基类时,也继承了基类的虚函数指针。当创建了派生类对象时,派生类对象的虚函数指向自己的虚函数表。如果,派生类重写了基类的虚函数,则派生类虚函数会覆盖基类的同名函数。当通过基类指针操作派生类对象时,已派生类对象内存为准,从对象中获取Vfptr,通过Vfptr找到Vftable,从而调用相应的虚函数,实现了动态绑定。
示例介绍:
class Cattle {
public:
virtual void walk();
virtual void sound1();
virtual void eat1();
};
class Horse {
public:
virtual void walk();
virtual void sound2();
virtual void eat2();
};
class CattleHorse :public Cattle,public Horse {
public:
virtual void walk();
virtual void sound1();
virtual void eat2();
};
其中声明了Cattle类,Horse类,CattleHorse类。CattleHorse重写了walk()方法,Cattle的sound1()方法,Horse的eat2()方法
我们来看一下vftable和vfptr
当创建派生类对象ch赋值给Cattle类时
CattleHorse ch;
Cattle* cattle = &ch;
基类指针cattle从对象ch中获得虚函数指针Vfptr从而获得虚函数表
调用虚函数时
cattle->walk();
cattle->sound1();
cattle->eat1();
我们要理解虚函数被重写之后,派生类虚函数会覆盖基类的同名虚函数的原理
c++纯虚函数和抽象类
有时基类并不需要实现函数,只需声明即可,实现交由派生类即可,这样的函数成为纯虚函数
声明格式:
virtual 返回值类型 函数名(参数列表) = 0;
注意:纯虚函数后面"=0",并不是函数的返回值为0,它只是告诉编译器这是一个纯虚函数
纯虚函数的几个说明
(1)如果一个类中包含了纯虚函数,这样的类称为抽象类。抽象类特点是:不能实例化对象,但可以定义抽象类的指针或引用。
如声明了抽象类Animal
class Animal {
public:
virtual void walk() = 0;
virtual void eat();
};
//Animal animal;不允许
//Animal* animal = new Animal;不允许
Animal* animal;
其中不能写Animal animal,也即不能实例化对象,但可以定义抽象类的指针Animal *animal;这时如果不用派生类对象为其赋值,尽管实现了eat()方法,也是不可以调用eat()方法的
如
animal->eat();//错误,未初始化animal局部变量
(2)派生类都应该实现基类的纯虚函数,如果不实现,则该函数在派生类中仍然是纯虚函数,该派生类也是抽象类,也不能实例化对象。
c++虚析构函数与纯虚析构函数
若派生类中有开辟到堆区的数据,而基类没有声明虚析构函数,在析构派生类对象时,编译器只会调用基类的析构函数,不会调用派生类析构函数,导致派生类对象申请的资源不能正确的释放。所以需要声明虚析构函数
虚析构函数声明格式:
virtual ~析构函数();
纯虚析构函数声明格式:
virtual ~析构函数() = 0;
简单示例:
class Animal {
public:
Animal();
virtual ~Animal();
virtual void sound()=0;
};
class Dog :public Animal {
public:
string *name;
public:
Dog(string name);
virtual void sound();
virtual ~Dog();
};
Dog::Dog(string name) {
cout << "调用Dog子类的构造函数:" << endl;
this->name=new string(name);
}
Dog::~Dog() {
if (this->name != NULL) {
cout << "调用派生类Dog析构函数" << endl;
delete name;
this->name = NULL;
}
}
int main() {
Animal* animal = new Dog("小黄");
animal->sound();
delete animal;
return 0;
}
上图部分代码声明了基类Animal以及构造函数和析构函数,派生类Dog以及构造函数与析构函数,其中派生类为小狗起名声明了name堆区数据,如果不声明基类析构函数为虚析构函数,则派生类堆区数据name就无法释放而造成内存泄漏。
关于虚析构函数的注意事项
(1)在基类声明虚析构函数之后,基类的所有派生类析构函数都自动成为虚析构函数
(2)在析构派生类对象时,先调用派生类析构函数,在调用基类析构函数(栈)
(3)虚析构函数和纯虚析构函数都要在基类中实现,因为假若基类中有堆区开辟的数据,也是需要实现虚析构函数释放资源的
(4)虚析构函数和纯虚析构函数区别:声明了纯虚析构函数后,该类为抽象类(只要该类中有虚函数,该类就是抽象类),不能实例化对象。而声明虚析构函数不会成为抽象类。