多态的定义及实现
通俗的来讲,就是多形态,不同的对象去完成同一个行为会有不同的状态。(比如买票的话,成人票是200,学生可以半价买票,军人可以优先买票)
在编程中,多态就是不同继承关系的类对象,去调用同一个函数,会有不同的行为。
1.多态的构成条件
- 必须用基类的引用或指针来调用这个虚函数
- 被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写
如果不满足多态的话,调用哪个函数跟类型有关,people是哪个类型的,调用的就是哪个类型的成员函数。
如果满足多态的话,调用哪个函数跟对象有关,传的是哪个对象,调用的就是哪个的成员函数。
2.虚函数
被virtual修饰的类成员函数称为虚函数
比如下面这个函数就是虚函数
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
3.虚函数的重写(覆盖)
派生类中有一个跟基类完全相同的虚函数,即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同,这样称之为派生类的虚函数重写(覆盖)了基类的虚函数
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
//重写基类虚函数时,派生类也可以不加上virtual关键字,这样也构成重写,但是这样的写法不规范,不推荐使用
//void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
void Func(Person& people)
{
people.BuyTicket();
}
int main()
{
Person a;
Func(a);
Student b;
Func(b);
return 0;
}
析构函数的重写
如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都与积累的析构函数构成重写,即使基类与派生类的析构函数名字不同。虽然函数名+同,看似违背了重写的规则(函数名必须相同),但其实编译器对析构函数的名字进行了特殊处理,编译后析构函数的名字统一处理成了destructor
问:析构函数能不能被定义成虚函数?
答:可以,并且最好定义成虚函数,因为在某些特殊情况下,如果不定义成虚函数构成多态的话,就不能正确的调用析构函数了,比如下面这种情况
Person* p1 = new Person;
delete p1;
Person* p2 = new Student;
delete p2;
一个基类指针new了一个派生类对象给它,如果析构函数不是虚函数从而没有构成多态的话,调用哪个析构函数就由p的类型决定了,p1p2的类型都是Person,所以就会不会调用派生类的析构函数,如下图
如果析构函数写成虚函数构成多态的话,就能避免这个问题了,运行结果如下图
所以在继承过程中可以不把基类的析构函数写成虚函数,但是最好把基类的析构函数写成虚函数构成多态,这样一定不会错,要不然碰到上面的问题就会发生内存泄露(直接先把基类析构清理了,派生类的还没有进行处理)
C++11中的override和final关键字
1.final:修饰虚函数或者类,表示它们不能再被继承
//修饰虚函数
class Person
{
public:
virtual void BuyTicket() final
{}
};
//修饰类
class Person final
{
public:
virtual void BuyTicket()
{}
};
2.override:检查派生类虚函数是否重写了某个虚函数,如果没有重写的话编译报错
class Student
{
public:
virtual void BuyTicket() override
{}
}
重载、重写(覆盖)、重定义(隐藏)的对比
重载:在同一个作用域内,函数名相同、参数不同。
重写:两个函数分别在基类和派生类作用域中,且函数必须是虚函数,函数名、参数、返回值都必须相同(协变除外)
重定义:两个函数分别在基类和派生类的作用域中,他们的函数名相同。两个基类和派生类的同名函数不构成重写那就构成重定义。
抽象类
1.概念及定义
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写虚函数,派生类才可以实例化出对象。
在虚函数后面加上 =0,就是纯虚函数,如下
class Car
{
public:
virtual void Drive() = 0;
};
纯虚函数规范了派生类必须重写。
2.接口继承与实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现(说白了就是要用函数里面的东西)。
虚函数的继承是一种接口继承,派生类继承的是基类函数的接口,目的是为了进行重写,构成多态,继承的是接口(说白了就是要用函数接口的设计,然后用这个函数接口写不同的内容,如下图)
所以如果不是为了实现多态,就不要把函数定义成虚函数。
多态的原理
1.虚函数表
先看下面代码
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
结果是8(32位下),除了_b成员,还有一个指针,这个指针是虚函数表指针。
一个含有虚函数的类中至少有有一个虚函数表指针,因为虚函数的地址要放到虚函数表中(虚函数表是一个指针数组,放虚函数指针的数组)
注意虚函数表中存的是虚函数的指针,而不是虚函数。虚函数和普通代码一样,都存在代码段(常量区),而虚表在vs编译器下也是存在代码段的。虚表是在编译时产生的。
对下列代码进行测试
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
发现以下几点问题:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放了一个nullptr
- 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
2.多态的原理
根据上面的一堆原理和下面的代码进行分析
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "半价" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person a;
Func(a);
Person b;
Func(b);
}
简单的来说就是,p指向的是基类对象,那它就会去基类对象的虚表里找虚函数。如果指向的是派生类对象,那就是去派生类对象的虚表里找虚函数
3.动态绑定与静态绑定
静态绑定又称为前期绑定,在程序编译期间就确定了程序的行为,也成为静态多态,比如函数重载。
动态绑定又称为后期绑定,在程序运行区间,根据具体得到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。