目录
八、多继承派生类的未重写的虚函数放在第一个被继承的基类部分的虚函数中
一、多态的概念
通俗来讲的,就是多种形态,去完成某个行为,不同的对象完成时会产生不同的状态
例:
二、多态的定义和实现
1.多态的构成条件:
多态实在不同继承关系的类对象去调用同一个函数产生不同行为
例如Student继承了Person,Person买票是全价,Student类中买票就是半价
在继承中,构成多态要满足这两个条件:
1.必须通过基类的指针或者引用(这个指针指向谁调用谁:例如:A* p = new B,在p中调用虚函数的时候还是调用B类中的)调用虚函数
2.被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写
2.虚函数
虚函数就是virtual修饰的函数,这个virtual和虚拟继承那里的virtual是一个关键字,但是两者之间没什么关系(一关键字多用)。
注意:virtual只能修饰成员函数。
虚函数格式:
class Player
{
public:
virtual void shoot()
{
cout<<"命中"<<endl;
}
};
class Bao : public Player
{
public:
virtual void shoot()
{
cout<<"打铁"<<endl;//完成了虚函数的重写
}
};
int main()
{
Bao A;
void func(Player& p)//必须是父类的指针或者引用
{
p.shoot();
}
func(A);//切割
return 0;
}
注意:把父类的“virtual”去掉的话,那就不是虚函数了
把子类的“virtual”去掉的话,还是虚函数(继承)
**3.虚函数的重写(覆盖)
虚函数的重写(覆盖)条件: 它是个虚函数+ 三同(函数名、参数、返回值)
注意:重写不是隐藏,函数名相同的并且分别在父类和子类的作用域的情况下,不符合重写的就是隐藏
对于上面的重写条件,也会有几个特例:
特例1:子类虚函数不加virtual,依旧构成重写(最好加上)
这一点可以理解为在父类中定义为了虚函数,那么在所有子类中都把这个函数认定为虚函数。
特例2:重写的 “协变”。当基类对象中返回的是基类的指针或者引用,派生类对象中返回的是派生类对象的指针或者引用,此时是可以认为他们完成重写的。
例:
class A
{};
class B : public A
{};
class Person
{
public:
virtual A* f() //返回一个父类指针
{
return new A;
}
};
class Student : public Person
{
public:
virtual B* f() //返回一个子类指针
{
return new B;
}
};
这里只要是一个父类指针或引用和一个它对应的子类指针或引用即可,不要求是哪对父子(只要是父子就行),但是父类指针必须在父类中返回,子类指针必须在子类中返回。
特例3:如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。
虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
例:
class Person
{
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person
{
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
我们是建议在继承中将析构函数定义为虚函数的(在父类的析构函数前面加个virtual就好了)
因为有这种情况:
class Person
{
//...
};
class Student : public Person
{
//...
};
int main()
{
Person* ptr = new Student;//如果不用多态定义所有的虚函数,那么只会调用~Person(),
//对于Student开辟的多的空间就会导致内存泄漏
delete ptr;
return 0;
}
因为子类的析构函数会自动调用父类的析构函数,而继承中所有的析构函数又被编译器统一处理成了destructor(),所以把析构函数定义为虚函数百利而无一害。
注意:虚函数重写是接口继承,普通函数继承是实现继承。
接口继承就是继承它的 主体架子 (函数名、参数、返回值),对于内部的实现不会继承
这意味这虚函数重写会继承父类的参数列表(也就是说父类的缺省参数有效,重写的缺省参数无效)
注意:多态中,被指针/引用原来是指向谁的,就由谁来调用虚函数
三、多态的底层原理
其实和虚拟继承中冗余的父类成员一样,虚函数会进虚表(存放虚函数地址的表,也就是个函数指针数组,里面存放所有虚函数的指针),每个类中的虚函数都会以一个函数指针(_vfptr))形式存在类中,指向对应的虚表(每个类中只有一个虚表,来存储继承关系中所有虚函数的地址,也就是说每个类中所有的虚函数全化为了一个函数指针来指向这个类中对应的虚表)
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
//答案:8 bytes —— 虚函数表指针 和 int
如果发生重写,在该类中指针对应的虚表中,重写的虚函数的地址会发生改变。
因为每个子类和父类中的虚表都是独特的,所以多态的实现其实就是找对应虚表中的对应函数地址并且调用。(这意味这即使在子类指针在传参过程中被切割成了父类指针,他所对应的虚表也是和父类不同的)。
总计一下派生类的虚表生成:
1.先将基类中的虚表内容拷贝一份到派生类虚表中
2.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
3.派生类自己的新增加的虚函数按其在派生类中的声明次序增加到派生类的最后
4.一般情况下最后会在虚表中放个nullptr
问题:虚函数存在哪里?虚表存在哪里?
答:在虚表中;在对象中 很显然这是错误的,因为虚表中存的是虚函数的指针。
虚函数其实和普通的函数一样,是存在代码段的,只不过它的指针被存到了虚表中了。
另外,虚表也不是存在对象中的,对象只是存的是虚表的指针。虚表也是存在代码段的。
四、C++11中的override 和 final
1.final:用来修饰虚函数,表示该虚函数不能重写(前面我们已经学过final修饰类代表这个类不能被继承)
例:
class Car
{
public:
virtual void Drive() final
{}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;//这里就会报错
}
};
2.override:检查子类虚函数是否完成了重写,没完成重写会报错
例:
class Car
{
public:
virtual void Drive()
{}
};
class Benz :public Car
{
public:
virtual void Drive(int i) override //由于不满足重写的条件(三同),所以会报错
{}
};
注意:override是写在 子类 的虚函数后面的。
五、重载、覆盖(重写)、隐藏(重定义)对比
重载:1.两个函数在同一个作用域 2.函数名相同、函数参数不同(个数、类型、顺序)
重写(覆盖):1.两虚函数分别在基类和派生类的作用域 2.函数名/参数/返回值都必须相同(协变例外)3.两个函数必须都是虚函数
隐藏(重定义):1.两个函数分别在基类和派生类的作用域 2.函数名相同 3.对于基类和派生类中的同名函数,不是重写的情况就是隐藏
六、抽象类
在虚函数后面写上一个=0,那么这个虚函数就变成一个 纯虚函数,包含纯虚函数的类叫做抽象类,抽象类无法实例化对象。
例:
class Car
{
public:
virtual void Drive() = 0;//没有了具体实体,对应“载具”在现实中的抽象概念
};
子类继承之后还是无法实例化出对象,只有重写纯虚函数,子类才能实例化出对象(相当于强制你去重写纯虚函数)
例:
class Benz:public Car
{
public:
virtual void Drive()
{
cout<<"舒服"<<endl;
}
};
对于纯虚函数的重写,也体现了接口继承(只继承接口,不继承实现)。
七、多态的原理
对于上面这个多态关系,我们传Person就会调用Person::BuyTicket(),传Student就会调用Student::BuyTicket()。
这就引申到了静态绑定和动态绑定:
1.静态绑定(前期绑定):在编译期间确定了程序的行为(比如:函数重载)
2.动态绑定(后期绑定):在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。