C++学习笔记---018
C++之多态的知识
前言:
前面篇章学习了C++对于继承的认识,接下来继续学习,C++的另一个特性多态等知识。
/知识点汇总/
封装:
1.数据和方法放在一起,把想给访问的成员,定义为公有,不想被访问的定义为私有或保护。
2.一个类型放在另一个类型里面,通过typedef成员函数调整,封装另一个全新的类型。
继承:
继承机制是面向对象程序设计使得代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产合新的类,称为派生类。
继承呈现了对象面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前接触的复用是函数复用,继承是属于类的复用。
1、C++多态的简单介绍
C++中的多态(Polymorphism)是面向对象编程的一个重要特性,它指的是不同的对象对同一消息做出不同的响应。简单来说,多态允许使用统一的接口来访问不同的对象,而这些对象在响应接口调用时可能会展现出不同的行为。
在C++中,多态性主要通过继承和虚函数来实现。
当一个类从另一个类继承时,它可以重写(override)基类中的虚函数。
当通过基类指针或引用调用虚函数时,实际调用的函数版本将取决于指针或引用所指向的对象的实际类型,这就是所谓的动态绑定或运行时多态。
不同的对象,完成同样的任务,展现不同的形态,结果会不一样。
1.1、多态的分类
静态多态:也称为静态绑定或前期绑定(早绑定)。它主要通过函数重载和运算符重载来实现。在编译期间,编译器根据函数实参的类型(可能会进行隐式类型转换)推断出要调用哪个函数。这种多态在编译时就确定了函数的调用版本,因此效率较高。
动态多态:也称为动态绑定或后期绑定(晚绑定)。它主要通过虚函数来实现。在程序执行期间(非编译期),根据引用或指针所指向的对象的实际类型来确定调用哪个版本的函数。这种多态提供了更高的灵活性和可扩展性,但由于需要在运行时查找虚函数表来确定调用的具体函数,因此可能会带来一定的性能损失。
1.2、多态的构成条件
那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2、虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
2.1、虚函数的重写(覆盖)
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
//注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
//为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
//这样使用
//void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
//Func(&ps);
//Func(&st);
return 0;
}
小结:
C++的多态需要满足两个条件:
1.父子类完成虚函数重写
(虚函数的重写要求三相同:1.返回值 2.参数 3.函数名)
虚函数是子类虚函数重写父类的虚函数。
2.父类的指针或引用去调用虚函数
且调用时,需要父类的指针或引用调用这个虚函数。
注意:这里的virtual void BuyTicket(),虚函数与虚继承的virtual无关
一个关键字virtual两个地方用,且virtual只能修饰成员函数,不能修饰全局函数
指向谁就调用谁
3、虚函数重写的两个例外
3.1、协变:(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。
即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
#include <iostream>
using namespace std;
class A {};
class B : public A {};
class Person
{
public:
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
//返回值不同,要求父子类的指针或引用 -- 协变
virtual B* BuyTicket()
{
cout << "买票-半价" << endl;
return nullptr;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
3.2、析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理, 编译后析构函数的名称统一处理成destructor。
补充:3.派生类重写虚函数可以不加virtual,因为可理解为已经继承父类的了。(不过建议都写上)
#include <iostream>
using namespace std;
class A {};
class B : public A {};
class Person
{
public:
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
//~Person() -- 需要加上virtual,不然容易混乱的调用,因为底层析构函数被统一成destructor
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual B* BuyTicket()
{
cout << "买票-半价" << endl;
return nullptr;
}
// ~Student()
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
//Person ps;
//Student st;
Person* p1 = new Person;
Person* p2 = new Student;
//p1->destructor + operator delete(p1)
// 期望:指向父类的调用父类析构
//p2->destructor + operator delete(p2)
// 期望:指向子类的调用子类析构
//结论:建议在派生类场景使用析构函数时,加上virtual关键字
delete p1;
delete p2;
return 0;
}
4、两个关键字:override 和 final
final:修饰虚函数,表示该虚函数不能再被重写
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
4.1、final
#include <iostream>
using namespace std;
//class Car
//{
//public:
// virtual void Drive() final {}//限制虚函数不能被继承
//};
//class Benz :public Car
//{
//public:
// virtual void Drive()//继承失败
// {
// cout << "Benz-舒适" << endl;
// }
//};
//限制一个类不能被继承
//两个方法:
//1.private
//2.final修饰整个类
//class Car
class Car final//C++11,被·final修饰的类叫最终类,无法被继承
{
public:
private:
//C++98 --- 写成私有
//子类的构造无法生成和实现,导致子类对象无法实例化
Car()
{}
};
class Benz : public Car//被final修饰无法继承
{
public:
};
int main()
{
//方法1:
//派生类的构造必调用父类的构造
//因此满足限制一个类不被继承
//Benz b;
//方法2:用final修饰整个类
return 0;
}
4.2、override
用于派生类中的。检查其虚函数是否完成重写
#include <iostream>
using namespace std;
class Car {
public:
virtual void Drive() {}
};
class Benz :public Car
{
public:
//virtual void Drive(int) override//error --> 未完成重写
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};
int main()
{
Benz b;
return 0;
}
小结
三个概念的对比:重载、重写(覆盖)、隐藏(重定义)
1.重载:
两个函数在同一个作用域,函数名相同与参数类型不同
2.重写(覆盖):
两个函数分别在基类和派生类的作用域;
函数名、参数类型、返回值都必须相同(协变例外);
两个函数必须是虚函数
3.隐藏(重定义):
两个函数分别在基类和派生类的作用域;
函数名相同,变量名不同
两个基类和派生类的同名函数不构成重写就是重定义
注意:隐藏与重写属于包含关系,具有重叠性
5、多态原理层的理解
5.1、虚表指针
#include <iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
char _ch = 'x';
};
int main()
{
cout << sizeof(Base) << endl;//12
//按照之前的内存对齐理解应该是8,为啥是12呢?
//因为只要存在虚函数在底层就有一个虚函数表指针 --》简称虚表指针
//其作用就是实现多态
return 0;
}
5.2、虚函数表
虚函数表,存放所有的虚函数地址 按照声明的顺序存放
#include <iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
void Func3()
{
cout << "Func3()" << endl;
}
private:
int _b = 1;
};
//查看监视创窗口的内存窗口
int main()
{
cout << sizeof(Base) << endl;
Base b;
return 0;
}
5.3、多态的切片原理
如何实现指向谁调用谁? – 切片
#include <iostream>
using namespace std;
class Person {
public:
//不满足多态时,在编译时根据调用的函数类型就找到了BuyTicket地址
//void BuyTicket()
//满足多态时:运行时去指向对象虚函数表中找到BuyTicket的地址
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
private:
int _i = 1;
};
class Student : public Person {
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
private:
int _j = 2;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
//这里执行切片,把子类当中的父类部分切出来
Func(Johnson);
//满足多态条件:
//那么这里调用生成的指令,就会去指向对象的虚表中找对应的虚函数进行调用---》指向谁调用谁(运行时去序表找对应的虚函数地址调用)
//指向父类调用父类的虚函数
//指向子类调用子类的虚函数
//每一个对象都有自己的虚表
//同类型的对象序公用一个虚表,不同类型的对象有各自的虚表
//Person Mike;
//Func(&Mike);
//Person p1;
//Func(&p1);
//Student Johnson;
//Func(&Johnson);
return 0;
}
6、单继承中的虚函数表
6.1、单继承
#include <iostream>
using namespace std;
class Base {
public:
virtual void func1()
{
cout << "Base::func1" << endl;
}
virtual void func2()
{
cout << "Base::func2" << endl;
}
private:
int a = 1;
};
class Derive :public Base {
public:
virtual void func1()
{
cout << "Derive::func1" << endl;
}
virtual void func3()
{
cout << "Derive::func3" << endl;
}
virtual void func4()
{
cout << "Derive::func4" << endl;
}
private:
int b = 2;
};
//实现打印虚表
//虚函数表的本质是一个函数指针数组
typedef void(*VFPTR)();
//virtual function table
//void PrintVFT(VFPTR vft[])
void PrintVFT(VFPTR* vft)
{
for (size_t i = 0; i < 4; i++)
{
printf(" %p -> ", vft[i]);
VFPTR pf = vft[i];
//函数指针的调用
(*pf)();
}
}
int main()
{
Base b;
Derive d;
//继承下来了func1和func2,func1重写了,func2没重写
//func3和func4
//函数指针
//void (*p)();
//VFPTR p1;
//函数指针数组
//void (*p[10])();
//VFPTR p2[10];
//任意自由转
VFPTR* ptr = (VFPTR*)(*((int*) & d));
PrintVFT(ptr);
return 0;
}
6.2、多继承
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
cout << sizeof(d) << endl;//20-->Base1 8 + Base2 8 + 4
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
6.3、单继承的虚函数,放在哪里呢?
答:虚表是运行时就加载到常量区的,而初始化是构造函数的初始化列表的时候完成的
#include <iostream>
using namespace std;
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
Derive()
{}
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b = 0;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x->", i, vTable[i]);
VFPTR f = vTable[i];
f(); //多继承中的虚函数表
}
cout << endl;
}
int main()
{
Base b;
Derive d;
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
7、抽象类
7.1、概念
在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
#include <iostream>
using namespace std;
//包含纯虚函数的类称为抽象类
//抽象类不能直接实例化对象;
//并且间接强制派生类重写虚函数
//override是已经重写的,用于检查是否重写,目的是检查语法问题
class Car
{
public:
//纯虚函数
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
//继承并完成重写
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
int main()
{
//Car c;抽象类不能直接实例化对象
//抽象类强制了派生类去执行重写 -->重写的是实现
Benz b1;
BMW b2;
//可以利用指针实现实例化
Car* ptr = &b1;
ptr->Drive();
Car* ptr = &b2;
ptr->Drive();
return 0;
}
7.2、接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所以如果不实现多态,不要把函数定义成虚函数。
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
//多态调用:指向父类调用父类的虚函数,指向子类调用子类的虚函数
//void Func(Person p)//不是父类的指针或引用,那么就不满足多态,则属于普通调用,就编译时找函数的地址
//多态中的虚表:虚函数表,存虚函数的地址,目的是为了实现多态
//虚继承中的虚表:存的是当前位置距离虚基类部分的位置的偏移量,本质是解决菱形继承中的数据二义性。
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。 不满足多态的函数调用时编译时确认好的。
虚函数存放在哪里?
答:虚函数跟普通函数一样都存在内存的代码段,不是存放在虚表中的,虚表存放的是虚函数的起始地址。
虚表存放在哪个区域?
答:也是代码段
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Person p;
Student s;
Person* p3 = &p;
Student* p4 = &s;
printf("Person虚表地址p3:%p\n", *(int*)p3);
printf("Student虚表地址p4:%p\n", *(int*)p4);
//说明虚表存放在常量区/代码段的
return 0;
}
8、总结
C++多态的优点包括:
灵活性:多态允许使用基类指针或引用调用派生类的方法,从而提供了更高的灵活性和可扩展性。
可维护性:多态性使得代码更易于维护和修改,因为新增一个派生类不需要修改已有的代码,只需要添加新的派生类即可。
可读性:多态性可以使代码更简洁和易读,因为派生类的特定实现只出现在派生类的定义中,而不是在整个代码中分散出现。
C++多态也存在一些缺点:
性能损失:运行时的动态绑定会导致一定的性能损失,因为需要在运行时查找虚函数表来确定调用的具体函数。
调试困难:由于动态多态涉及到运行时类型的确定和虚函数表的查找,因此可能会增加调试的难度。
总之,C++中的多态是一种强大的特性,它允许程序员以统一的方式处理不同的对象类型,并提供了更高的灵活性和可扩展性。然而,在使用多态时需要注意其可能带来的性能损失和调试困难等问题。