目录
一、多态的概念
二、静态的多态
三、动态的多态
1、虚函数
2、虚函数的重写(覆盖)
3、利用虚函数的重写实现多态
4、虚函数重写的例外
4.1协变(返回值分别为构成父子关系的指针或引用,也构成重写)
4.2析构函数是虚函数的场景(动态申请的子类对象,交给父类指针管理,需要virtual修饰析构函数)
四、C++11中的final和override
1、C++98防止一个类被继承的方法
2、C++11的final关键字
2.1final修饰类,防止该类被继承
2.2final修饰虚函数,防止该虚函数被重写
3、C++11的override关键字
override修饰子类重写的虚函数,检查是否完成重写
五、重载、重写、隐藏的区别
六、抽象类(接口类)
1、抽象类的概念
2、实现继承和接口继承
七、多态的实现原理
1、虚函数表
2、多态的原理
2.1形成多态的原因
2.2为什么一定要传入从子类对象切片而来的父类对象的指针/引用才能引发多态?为什么不能直接传入切片的父类对象?
2.3多态与非多态的成员函数调用区别
八、单继承和多继承中子类虚函数表
九、关于多态的问答题
1、什么是多态?
2、重载、重写(覆盖)、重定义(隐藏)的区别?
3、多态的实现原理?
4、inline可以是虚函数吗?
5、静态成员可以是虚函数吗?
6、构造函数可以是虚函数吗?
7、析构函数可以是虚函数吗?
8、对象调用普通成员函数快还是虚函数快?
9、什么是抽象类?抽象类的作用?
多态指多种形态。不同的对象完成同一件事情,但是结果不同。例如公交刷卡行为:成人刷卡全价,学生刷卡半价。亦或是不同的客户来消费,金卡会员8折,银卡会员9折,普通会员无折扣。
静态的多态是在编译时产生不同。例如函数重载就是一种静态的多态行为。看上去是在调用同一个函数,但是会产生不同的行为。
int main()
{
int a=1;
double b=2.3;
std::cout<
动态的多态是在运行时产生不同。
构成多态的条件:缺一不可,否则就不构成多态。
1、必须通过父类对象的引用或指针当做形参调用虚函数。(仅限引用和指针是原因见下文)
2、子类必须完成对父类虚函数的重写且被调用的函数是虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout<<"买票-半价"<
被virtual关键字修饰的类的非静态成员函数称为虚函数。
注意虚函数和虚继承虽然都使用了virtual关键字,但是它们没有关系。
1、子类重写父类虚函数时,子类“三同”函数不写virtual也构成重写,但是不规范。C++设计者的初衷是父类写了virtual,即使子类不写,也构成多态,那就不会出现内存泄漏的情况了。(设计项目时可能父类和子类并不是同一个人写的,那么子类程序员没写virtual的概率极大)
2、可以这样理解:虽然子类对应函数没写virtual,但他先继承了父类中虚函数的“虚”属性,再完成重写。注意:类作用限定符也会被子类继承,如果父类虚函数是public,即使子类重写函数是private,也会变成public
如果父类中存在虚函数,并且子类拥有“三同”成员函数:返回值类型、函数名称、参数列表均相同。那么子类的虚函数就是对父类虚函数的重写。例如上面例子中Student::BuyTicket()是Person::BuyTicket() ,它们构成重写,而不是构成隐藏。
重写还有一个叫法是覆盖。子类重写父类的虚函数意为子类重写的函数会覆盖父类的这个虚函数。(下文会细讲)
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout<<"买票-半价"<
先看构不构成多态,构成多态,那么和对象类型无关,传入哪个对象的引用/指针就使用谁的虚函数。
如果不构成多态,均调用p类型的函数。
构成重写需要成员函数返回值相同,但是存在例外,当返回值是构成父子关系的指针或引用时,它们也构成重写。这种重写叫做协变。不过父类返回值一定要用父指针/父引用,子必须用子指针/子引用,不能颠倒。
class Person
{
public:
virtual ~Person()
{}
};
class Student : public Person
{
public:
virtual ~Student()
{}
};
析构函数也可重写,虽然父子析构函数的函数名表面上不一样,但其实所有析构函数的函数名都会被处理成destructor。
下面看个场景:
各new一个父类/子类的对象,交给父类指针管理,这没问题。但是在析构的时候,因为是父类指针,所以p1p2都会去调用父类的析构函数,但别忘了起初我们可是new了一个子类对象,如果子类对象中存在资源,那么就会导致内存泄漏!
为了避免这种情况,就需要使用virtual关键字修饰析构函数:
父子构造函数构成多态,那就不看p1p2的类型了,p1p2指向哪个对象,就调用哪个对象的析构函数。
class Person
{
public:
static Person CreateObj()
{
//new Person;
return Person();
}
private:
Person()
{}
};
class Student : public Person
{
public:
};
int main()
{
Person p=Person::CreateObj();
return 0;
}
C++98通过把构造函数变为私有的方式,让子类继承后根本构造不出父类对象。
但是父类却可以通过静态的“偷家”函数构造对象。(这里必须静态,静态成员函数调用无需借助对象。如果是非静态成员函数,则需要对象才能调用,但是生成对象必须通过这个函数······无限循环了)
class Person final
{
public:
};
Person被final修饰后将不能被继承,
为了防止程序员在子类进行重写时,函数名拼写出现错误,这就造成了重写的函数和父类被重写的函数对不上。这是个很严重的问题,因为这种情况并不违反语法规则,编译期间编译器是不会报错的,只有在程序运行时发现结果不对,回去debug时才能发现问题。
C++很贴心的增加了override关键字,成员函数被修饰后,编译器会帮忙检查是否重写成功。
在虚函数的后面加上=0,则这个函数被称为纯虚函数。包含纯虚函数的类被称为抽象类(接口类)。一个类型,在现实世界中没有对应的实物,就可以定义为抽象类。例如职能类、Person类等。
抽象类不能实例化出对象。
class Person
{
public:
virtual void Func()=0
{
//纯虚函数一般只声明,不实现。因为没有对象
}
};
子类继承了父类的纯虚函数,子类也变成了抽象类,同样不能实例化出对象。
除非子类重写纯虚函数,子类才能实例化出对象。
抽象类的作用是强制子类进行重写。
子类继承父类的普通函数,是为了使用该函数的具体实现,这是实现继承。
而虚函数是为了让子类进行重写,实现多态,子类只继承了函数名,并不继承具体实现,这是接口继承,接口继承会继承父类的类作用限定符和缺省参数。
所以不准备实现多态的话,就不要用virtual去修饰成员函数了。
注意:虚函数表指针并不一定放在所有成员变量的最前面,有的编译器会放在最后面。虚函数表指针指向的虚函数表本质是存放虚函数指针的函数指针数组,vs下会在这个数组最后放一个nullptr,而Linux不会。
虚函数表存放于常量区(代码段)。
vs中虚函数表中存放的并不是虚函数的地址,而是一句jump指令的地址,通过该jump指令找到对应的虚函数。
再看一段代码:
p能调用Func1是因为Func1并不存放在类中,而是在代码区,所以p->Func1并不是解引用,而是将p当做形参传递给this。
p调不了Func2是因为虚函数需要通过类对象中的虚函数表指针找到对应的虚函数进行调用,所以它是一个解引用行为,p是nullptr,对空指针的解引用行为引发程序崩溃。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
private:
int _a;
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
private:
int _b;
};
void Func(Person& p)//子类传入会被切片,所以可以不用const/构成多态,跟p类型无关,传子调子,传父调父
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
//传入父类对象,调用父类的BuyTicket();传入子类对象将调用子类的BuyTicket();
Func(p);
Func(s);//这里的s会被切片传参
}
从监视窗口可以看到,子类中的虚函数表指针存放于继承于父类的那部分,但是父子对象中虚函数表指针及指向的虚函数并不是同一个。切片后得到的父类对象的虚函数表指向重写的虚函数,这就解释了传入一个父类对象的指针/引用就调用父类对象的虚函数,传入一个从子类切片而来的父类对象的指针/引用就会去调用重写的虚函数。
注:同类型的对象,虚表指针是相同的,均指向同一张虚函数表。
因为指针/引用切片出来的父类对象能获得重写的虚函数,值切片出来的父类对象中的虚函数必须源自于父类。(想一想如果值切片的父类对象中的虚函数拷贝自子类重写的虚函数,那不是乱套了。例如虚函数是析构函数,值切片后的父类中的析构函数如果是子类的析构函数,那么父类对象在析构的时候要出大问题)
函数调用满足多态,需要在程序运行时去对象中的虚函数表中找虚函数进行调用;不满足多态的成员函数,编译器在编译时确定调用的地址。
1、单继承中,子类中非重写虚函数将放在同一张虚函数表中。
当然对子类进行切片时,切片得到的父类对象是不会得到func3和func4的。
2、多继承中,子类中非重写的虚函数将被存放于第一个继承的父类部分的虚函数表中。
多态指多种形态。不同的对象完成同一件事情,但是结果不同。例如公交刷卡行为:成人刷卡全价,学生刷卡半价。亦或是不同的客户来消费,金卡会员8折,银卡会员9折,普通会员无折扣。
函数重载:1、两个函数在同一作用域2、函数名相同,参数列表不同,返回值没有要求;
重写:1、两个函数必须位于子类和父类中2、函数名、参数列表、返回值必须相同(协变除外)3、两个函数均为虚函数;
隐藏:1、两个函数必须位于子类和父类中2、函数名相同3、不构成重写,就构成隐藏。
对于多态的实现原理,必须先从构成多态的条件说起:1、必须通过父类对象的引用或指针当做形参调用虚函数。2、子类必须完成对父类虚函数的重写且被调用的函数是虚函数。
子类和父类的虚函数表指针、虚函数表、重写的虚函数的地址均不相同,我们传入一个父类对象,它使用的是源自父类的虚函数,传入一个从子类切片而来的父类对象,这个对象中的虚函数是子类重写的虚函数。虽说这两个都是父类对象,但是对象体内的虚函数并不是同一个,所以会产生不同的行为,这便是多态的原理。
inline可以是虚函数。调用时,如果不构成多态,这个函数就保持inline属性。如果构成多态,就不具备inline属性,因为多态是要在运行时去对象的虚函数表里面找虚函数,所以在编译时,不能使用inline进行展开。
静态成员不能是虚函数。因为静态成员没有this指针,在外部可以直接使用类名::成员函数的方式对静态成员函数进行调用,但是调用虚函数需要通过对象才能找到虚函数表,所以静态成员不能是虚函数。
构造函数不能是虚函数。因为对象的虚函数表指针是在构造函数的初始化列表中进行初始化。(先有鸡还是先有蛋的问题)
析构函数可以是虚函数,用于处理子类对象交给父类的指针管理的情况。
如果不构成多态,即使是虚函数,也是在编译阶段确定调用地址,速度一样快;但是一旦构成多态,编译器在运行时通过对象去虚函数表中确定虚函数的调用地址,这个时候就是普通函数快了。
抽象类又称接口类。包含纯虚函数的类被称为抽象类,在虚函数后边加个=0,这个虚函数就被叫做纯虚函数。抽象类不能实例化出对象。在现实世界中没有对应的实物,就可以定义为抽象类。例如职能类、Person类等。
抽象类体现接口继承的关系。子类继承抽象类后,也变成了抽象类。这就强制用户对纯虚函数进行重写,对虚函数的重写是一种接口继承,子类会继承虚函数的函数名及缺省值,但不会继承实现。