目录
- 多态的概念
- 概念
- 多态形成的条件
- 虚函数的重写
- 虚函数重写的两个例外
- 多态的题目
- C++11增加的关于多态的关键字
- final
- override
- 多态原理
- 虚函数表指针 vfptr
- 多态的实现
- 静态绑定和动态绑定
- 打印虚函数表
- 补充
- 抽象类
- 概念
- 接口继承和实现继承
多态的概念
概念
具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。-- 这种场景就可以使用多态
多态调用的情况下,是实现重写
多态形成的条件
虚函数的重写
virtual在继承中是虚继承,用这个关键字修饰函数就是虚函数
virtual只能修饰成员函数
父子类中两个虚函数,三同(函数名,参数,返回值),此时构成虚函数的重新
父类的指针或引用去调用虚函数,传的是父类掉父类的,传子类掉子类的(切片)
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student :public Person
{
public:
virtual void BuyTicket() { cout << "买票-75折" << endl; }
};
class Soldier :public Person
{
public:
virtual void BuyTicket() { cout << "买票-免费" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p1;
cout << "Person:";
Func(p1);
Student stu;
cout << "Student:";
Func(stu);
Soldier sol;
cout << "Soldier:";
Func(sol);
return 0;
}
虚函数重写的两个例外
(1)协变:虚函数返回值可以不同,这个返回值要求必须是父子类关系的指针或引用
不满足重写就是隐藏
重载:在同一作用域,函数名相同参数不同,与返回值无关
class A {
};
class B :public A
{
};
class Person
{
public:
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return new A;
}
};
class Student :public Person
{
public:
virtual B* BuyTicket()
{
cout << "买票-75折" << endl;
return new B;
}
};
void f2()
{
Person* p = new Student;
p->BuyTicket();
}
int main()
{
f2();
return 0;
}
(2)析构函数的重写(基类和派生类析构函数的名字不同)
析构函数的名字的特殊处理,及其析构函数建议加virtual
下面这样的析构是正确的
下面这种情况按道理来说应该是
这样会造成p2所指向的内容会有内存泄漏的风险
普通调用:看指针或引用或对象的类型
多态调用:看指针或引用指向的对象
上图的情况适合多态调用,看指针指向的对象去进行delete
(3)虚函数重写时,父类函数加了virtual,子类不加也构成重写
BuyTicket()
在父类中没有virtual,在子类中有virtual。但这样不是重写,只构成了隐藏
父类的BuyTicket()
有virtual,子类的没有,也可以构成重写
函数的接口继承了下来,只重写了实现,与父类形成了重写
多态的题目
1、
C++11增加的关于多态的关键字
final
1、修饰虚函数,表示该虚函数不能再被重写
2、实现一共类,这个类不能被继承
(1)父类构造函数私有化,派生实例化不出对象
只对成员函数等私有化是不可以的,公有继承碰私有,只是不可见,还是可以被继承,只有再下一代才不能被继承
B类对象实例化的时候需要调用A类的构造函数,但A类构造函数私有化
(2)C++11,final修饰的类为最终类,不能被继承
override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
多态原理
虚函数表指针 vfptr
Person
类的成员变量只有int,但其大小确实8?
因为Person
类里面有虚函数,类里面会多一个虚函数表指针(虚表指针)指向虚函数表
虚函数表实际上是个指针数组
多态的实现
对于子类来说,他对BuyTicket函数进行了重写,同时更改了虚函数表里对应的内容
虚函数的重写也叫做覆盖
重写是语法层的概念
覆盖是原理层的概念
多态调用:运行时去指向对象的虚函数表中找函数的地址,进行调用,所以指向父类调的是父类虚函数,指向子类调用的是子类虚函数
普通调用:编译时,符号表找到函数地址,通过调用者类型确定函数地址
静态绑定和动态绑定
1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
打印虚函数表
class Base
{
public:
virtual void func1()
{
cout << "Base::func1()" << endl;
}
virtual void func2()
{
cout << "Base::func2()" << endl;
}
private:
int _i = 1;
};
class Drive :public Base
{
public:
virtual void func1()
{
cout << "Drive::func1()" << endl;
}
virtual void func3()
{
cout << "Drive::func3()" << endl;
}
private:
int _j = 1;
};
typedef void(*VF_ptr)();
void PrintVFT(VF_ptr vft[])
{
printf("%p\n", vft);
for (size_t i = 0; vft[i] != nullptr; i++)
{
printf("[%d]:%p", i, vft[i]);
VF_ptr f = vft[i];
f();
}
}
int main()
{
Base bb;
Drive dd;
Base* pb = &bb;
Drive* pd = ⅆ
PrintVFT((VF_ptr*)(*(int*)pd));
return 0;
}
补充
1、虚函数表存放在哪个区域(栈、堆、静态区/全局数据段、常量区/只读数据段)?常量区
2、虚表是编译时生成的
3、虚表指针构造函数的初始化列表最开始时生成的
Base1里面有个虚表指针,Base2里面有个虚表指针。Drive里面共有两张虚表。
func3()倾向于放在Base1的虚表或者两个虚表都放
抽象类
概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
纯虚函数的使用
1、某些情况下不希望父类生成派生类
2、强制要求派生类对于某个虚函数进行重写
class car
{
public:
virtual void drive() = 0;
};
class BMW :public car
{
public:
virtual void drive() { cout << "BMW->操作" << endl; }
};
class Benz :public car
{
public:
virtual void drive() { cout << "Benz->舒适" << endl; }
};
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。