1. 多态的概念
多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果。
-
比如买车票这件事
-
普通人买票->正常买票
-
学生买票->半价买票
-
军人买票->优先买票
-
2. 多态定义及实现
多态: 指向谁调用谁
2.1 多态构成的两个条件:
- 虚函数重写
- 父类指针或者引用去调用虚函数
2.2 虚函数的理解
class Person
{
public:
//被virtual修饰的类成员函数
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
-
只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual。
- 虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。虚函数这里的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性。
2.2 虚函数重写的两个条件:
//父类
class Person
{
public:
//父类的虚函数
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
//子类
class Student : public Person
{
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
//子类
class Soldier : public Person
{
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
{
cout << "优先-买票" << endl;
}
};
- 这个函数是虚函数,
- 三同:函数名,参数,返回值都要相同
2.3 满足虚函数重写的两大特例
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
// 虚函数重写/覆盖条件 : 虚函数 + 三同(函数名、参数、返回值)
// 特例1:子类虚函数不加virtual,依旧构成重写 (实际最好加上)
//void BuyTicket() { cout << "买票-半价" << endl; }
virtual void BuyTicket() { cout << "买票-半价" << endl; }
//重写的协变:返回值可以不同, 但是要求返回值必须是父子关系的指针或者引用
//.....
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "优先买票" << endl; }
};
// 多态两个条件:
// 1、虚函数重写
// 2、父类指针或者引用去调用虚函数
// void Func(Person* p) // 父类指针或者引用去调用虚函数
void Func(Person& p)// 父类指针或者引用去调用虚函数
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
- 子类虚函数不加virtual,依旧构成重写(实际中最好加上)
- 重写的协变:返回值可以不同,但是要求返回值必须是父子关系的指针或者引用
3. 经典面试题: 下面程序的输出结果是?
#include <iostream>
using namespace std;
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0){ std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
- 首先test不构成多态,func构成多态
- p的类型是B*,指向的是B,p->test(A*this),这里会发生切片:子类指针传给父类指针
- 父类里的test中的this->func,会去调用子类B的func,程序的结果是B->0吗
- 这里有两点需要注意:
- 虚函数func重写的是接口继承,重写实现
- 普通函数继承是实现继承
- 虚函数func重写,这里的子类B中func会去调用自己的func实现,会继承父类的接口
virtual void func(int val = 1)接口继承
{ std::cout << "B->" << val << std::endl; };重写实现 - 这段程序的运行结果是B->1
4. 经典面试题: 下面程序的输出结果是?
#include <iostream>
using namespace std;
class A
{
public:
virtual void func(int val) { std::cout << "A->" << val << std::endl; }
void test() { func(1); }
};
class B : public A
{
public:
void func(int val) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
A*p = new B;
p->test();
return 0;
}
- 这段代码中的p虽然是A*的指针,但是这里发生了切片操作,p指向的是B的那一大段空间
- 发生了多态,与指针类型无关
5. 多态的原理
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void Func() { cout << "Func" << endl; }
int _a = 0;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
int _b = 0;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
- vfptr是一张虚函数的表,本质就是一个函数指针数组
- 多态调用:运行时是去指向对象的虚表中找到函数地址,并进行调用(在符合多态的两个条件时,)
- 普通调用:编译链接时就确认了函数地址,运行时直接调用
5.1 动态绑定和静态绑定
静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载。
动态绑定: 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
//父类
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
//子类
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
6. 在继承中把析构函数定义成虚函数
#include <iostream>
using namespace std;
//建议在继承中析构函数定义成虚函数
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
//int* _ptr;
};
class Student : public Person {
public:
// 析构函数名会被处理成destructor,所以这里析构函数完成虚函数重写
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* ptr1 = new Person;
delete ptr1;
Person* ptr2 = new Student;
delete ptr2;
return 0;
}
- 这段代码在不同的编译器中,结果可能不一样,
- 析构函数名会被处理成destructor,所以这里析构函数完成虚函数重写
- 发生了多态,与指针类型无关
7. 关于重载.覆盖(重写),隐藏(重定义)的对比
- 重载 : a. 两个函数在同一作用域,b.函数名相同,参数不同(类型,顺序,个数)
- 重写(覆盖):
两个函数分别在基类和派生类的作用域
函数名/参数/返回值都必须相同(协变除外) -> 简称三同
特殊点1: 两个函数都必须是虚函数(其实子类可以不用加virtual)
特殊点2: 如果返回值不同,则必须是父类的指针或引用 - 重定义(隐藏)
两个函数分别在基类和派生类的作用域
函数名相同
两个基类和派生类的同名函数不构成重写,那么就是重定义(隐藏)
8. C++11 override 和 final(使该类不可被子类继承)
8.1 final(使该类不可被子类继承) -> 加在父类上
- 在一个父类的成员函数的后面+final使其变成不可被子类继承
8.2 override(判读子类是否发生了虚函数的重写) ->加在子类上
#include <iostream>
using namespace std;
class Car{
public:
virtual void Drive1() {}
void Drive2(){}
};
class Benz :public Car {
public:
// 检查子类虚函数是否完成重写
virtual void Drive1() override { cout << "Benz-舒适" << endl; }
virtual void Drive2() override { cout << "Benz-舒适" << endl; }
};
int main()
{
Benz A;
return 0;
}
- 在子类后面+override,判断这个子类的虚函数是否发生了虚函数的重写
9. 抽象类 && 纯虚函数
- 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类 不能实例化出对象 。
10. 多继承中的多态
#include <iostream>
using namespace std;
class Car
{
public:
virtual void Drive() = 0;
};
class BMW : public Car
{
public:
virtual void Drive()
{
cout << "操控-好开" << endl;
}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-豪华舒适" << endl;
}
};
int main()
{
//Car c;
//BMW b;
Car* ptr = new BMW;
ptr->Drive();
ptr = new Benz;
ptr->Drive();
return 0;
}
- 其中如果不需要用到父类的虚函数,仅仅是为了实现多态,
- 将父类的虚函数写成纯虚函数,将父类定义成抽象类
11. 理解虚表
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person::买票-全价" << endl;
}
virtual void Func1()
{
cout << "Person::Func1()" << endl;
}
};
class Student : public Person {
public:
// 这里只用BuyTicket()完成了虚函数的重写
virtual void BuyTicket()
{
cout << "Student::买票-半价" << endl;
}
virtual void Func2()
{
cout << "Student::Func2()" << endl;
}
};
int main()
{
// 同一个类型的对象共用一个虚表
Person p1;
Person p2;
// vs下 不管是否完成重写,子类虚表跟父类虚表都不是同一个
Student s1;
Student s2;
return 0;
}
- 同一个类型的对象共用一个虚表
- vs下 不管是否完成重写,子类虚表跟父类虚表都不是同一个
11. 1父类的虚表
#include <iostream>
using namespace std;
typedef void(*VFPTR)();
// 打印虚函数表中的虚函数地址,并且调用虚函数
void PrintVFTable(VFPTR* table)
{
for (size_t i = 0; table[i] != nullptr; ++i)
{
printf("vft[%d]:%p->", i, table[i]);
VFPTR pf = table[i];
pf();
}
cout << endl;
}
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1 = 1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2 = 2;
};
class Derive : public Base1, public Base2 {
public:
//这里只重写了 func1
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d = 3;
};
int main()
{
Derive d;
PrintVFTable((VFPTR*)(*(int*)&d));
PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
return 0;
}
- 这里子类的虚函数func3是没有发生重写的,也没有发生隐藏,就是一个普通的虚函数,
- 虽然在监视窗口中没有看到func3,但是在打印出父类的虚表Base1和Base2中,发现这个普通的虚函数存在第一个父类的虚表中
12. 问答题
12.1 什么是多态?
-
就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
12.2 什么是重载、重写(覆盖)、重定义(隐藏)?
- 重载 : a. 两个函数在同一作用域,b.函数名相同,参数不同(类型,顺序,个数)
- 重写(覆盖):
两个函数分别在基类和派生类的作用域
函数名/参数/返回值都必须相同(协变除外) -> 简称三同
特殊点1: 两个函数都必须是虚函数(其实子类可以不用加virtual)
特殊点2: 如果返回值不同,则必须是父类的指针或引用 - 重定义(隐藏)
两个函数分别在基类和派生类的作用域
函数名相同
两个基类和派生类的同名函数不构成重写,那么就是重定义(隐藏)
12.3 多态的实现原理?
- 每一个类中都有一个vfptr,它是一张虚函数的表,本质就是一个函数指针数组
- 多态调用:运行时去指向对象的虚表中找到函数地址,并进行调用(在符合多态的两个条件时,)
12.4 inline函数可以是虚函数吗?
- 不可以,但是inline只是一个建议,当一个函数是虚函数以后,多态调用中,inline会直接失效
12.5 静态成员可以是虚函数吗?
-
不可以 ,因为 静态成员函数没有this指针 ,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
12.6 构造函数可以是虚函数吗
- 不可以,virtual函数是为了实现多态,运行时去虚表中找对应虚函数进行调用
- 对象中虚表指针都是构造函数初始化列表阶段才初始化的
- 构造函数的虚函数是没有意义的
12.7 析构函数可以是虚函数吗
- 可以,建议基类的析构函数定义成虚函数
- 析构函数名都会被处理成destructor,所以这里析构函数完成了虚函数重写
- 构成多态与指针类型就无关了
12.8 拷贝构造 和 operator=可以是虚函数?
-
拷贝构造不可以,拷贝构造也是构造函数,同上
-
operator=可以但是没有什么实际价值
12.9 对象访问普通函数快还是虚函数更快?
-
不构成多态, 是一样快的。
-
构成多态,则调用的普通函数快,
因为构成多态,运行时调用虚函数需要到虚函数表中去查找
12.10 虚函数是在什么阶段生成的,存在哪的?
- 编译阶段就生成好的,存在代码段(常量区)
12. 11什么是抽象类?抽象类的作用?
- 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类
-
抽象类强制重写了虚函数,另外抽象类体现出了 接口继承关系 。