【C++】多态
文章目录
概念
通俗来讲,多态就是指多种形态,不同对象去做同一件事,产生的状态也不同
例如买票,普通人买票,全价买票;学生买票,半价买票
定义
多态的实现是建立在继承的基础上的。拥有不同继承关系的类对象,调用同一函数,产生不同的结果。
例如,Student
继承了Person
,Student
半价买票,而Person
全价买票
构成
多态构成由两个必要条件:
- 必须通过父类的指针或引用去调用虚函数
- 虚函数的重写:子类必须对父类的虚函数进行重写,父类和子类的虚函数要求三个相同(函数名,参数,返回值)
虚函数
虚函数就是用关键字virtual
修饰的类成员函数。注:这里的virtual
和虚继承的virtual
无任何关联
class Person
{
public:
virtual void BuyTickets()
{
cout << "Person->全价" << endl;
}
};
重写(覆盖)
虚函数的重写,子类中有一个与父类完全相同的虚函数(三同,函数名、参数、返回值相同),子类就会将父类的虚函数进行重写
class Person
{
public:
virtual void BuyTickets()
{
cout << "Person->全价" << endl;
}
};
class Student : public Person
{
public:
// 对父类的虚函数进行了重写
virtual void BuyTickets()
{
cout << "Student->半价" << endl;
}
};
调用的时候必须用父类的指针或者引用
// 父类指针调用
void func1(Person* p)
{
p->BuyTickets();
}
// 父类引用调用
void func2(Person& p)
{
p.BuyTickets();
}
int main()
{
Person p;
Student s;
func1(&p);
func1(&s);
func2(p);
func2(s);
}
运行结果:
虚函数重写的例外
虚函数重写要求子类和父类虚函数三同(函数名、参数、返回值),但是有三个例外
1.协变(子类和父类虚函数的返回值类型不同)
子类和父类虚函数的返回值类型可以不同,可以返回父子类关系的指针或引用。即父类虚函数返回父类的指针或引用,子类虚函数返回子类的指针或引用
class Person
{
public:
// 返回父类引用
virtual Person& BuyTickets()
{
cout << "Person->全价" << endl;
return *this;
}
};
class Student : public Person
{
public:
// 返回子类引用
virtual Student& BuyTickets()
{
cout << "Student->半价" << endl;
return *this;
}
};
另外,返回其他父子类关系的指针或引用,也可以构成协变
// AB父子类关系
class A {};
class B : public A {};
class Person
{
public:
// 返回A类引用
virtual A& BuyTickets()
{
cout << "Person->全价" << endl;
}
};
class Student : public Person
{
public:
// 返回B类引用
virtual B& BuyTickets()
{
cout << "Student->半价" << endl;
}
};
2.析构函数的重写(子类与父类的析构函数名字不同)
在学习继承时我们就知道,析构函数表面上的名字无论是什么,最终都会被改为destructor
。所以在析构函数重写时,子类与父类的析构函数名可以不同
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
所以我们现在知道了,析构函数的名字暗中被改为destructor
,就是为了实现虚函数重写,为了实现多态。
可是为什么析构函数一定要实现多态呢?来看一下这种情况:
父类和子类中的析构函数没有构成重写,子类中开辟了空间,子类析构函数将开辟的空间清理掉
class Person
{
public:
virtual void BuyTickets()
{
cout << "Person->全价" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTickets()
{
cout << "Student->半价" << endl;
}
~Student()
{
// 清理开辟的空间
cout << "delete[]" << _ptr << endl;
delete[] _ptr;
cout << "~Student()" << endl;
}
protected:
// 开辟空间
int* _ptr = new int[10];
};
实例化一个Person
对象和一个Student
对象,将他们的指针赋给Person
指针,再delete
这两个对象
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
}
看看运行结果:
我们知道,delete
自定义类型对象时,会调用相应的析构函数,而上面的运行结果显示p1
和p2
走的都是Person
类的析构,p2
指向的Student
对象没有析构,可能导致内存泄漏
不满足多态时,调用函数时看的是指针类型,指针是什么类型就调用谁的。如p1
和p2
都是Person
类型,所以调用的都是Person
的析构函数
满足多态时,要看指针指向的类型,指向的是什么类型就调用谁的
在上面的情况中,我们期望的是:指向父类,那就调用父类的析构;指向子类,那就调用子类的析构。所以就需要用到多态,需要子类与父类的析构函数构成虚函数重写。这就是为什么析构函数的名字会被改为destructor
class Person
{
public:
virtual void BuyTickets()
{
cout << "Person->全价" << endl;
}
// 析构函数重写
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTickets()
{
cout << "Student->半价" << endl;
}
// 析构函数重写
virtual ~Student()
{
// 清理开辟的空间
cout << "delete[]" << _ptr << endl;
delete[] _ptr;
cout << "~Student()" << endl;
}
protected:
// 开辟空间
int* _ptr = new int[10];
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
}
由上我们可以做出总结:在继承中,析构函数尽量写为虚函数,防止发生内存泄露
3.子类的虚函数可以不加virtual
只要父类的虚函数加了virtual
,子类虚函数可以不加virtual
,如下
class Person
{
public:
// 父类虚函数一定加 virtual
virtual void BuyTickets()
{
cout << "Person->全价" << endl;
}
};
class Student : public Person
{
public:
// 子类虚函数可以不加 virtual
void BuyTickets()
{
cout << "Student->半价" << endl;
}
};
但是实际中为了代码可读性,还是建议加上
C++11 final与override
C++11 提供了两个关于重载的关键字:final
和override
被final修饰的虚函数,无法被重写
class Car
{
public:
// 被final修饰,无法被重写
virtual void drive() final
{
cout << "Car->drive()" << endl;
}
};
class Benz : public Car
{
public:
virtual void drive()
{
cout << "Benz->drive()" << endl;
}
};
此外,被final修饰的类,无法被继承
class Car final // final修饰类
{
public:
virtual void drive() final
{
cout << "Car->drive()" << endl;
}
};
class Benz : public Car
{
public:
virtual void drive()
{
cout << "Benz->drive()" << endl;
}
};
override可以检查子类虚函数是否重写了父类的某个虚函数,没有重写就报错
class Car
{
public:
virtual void drive()
{
cout << "Car->drive()" << endl;
}
};
class Benz : public Car
{
public:
// 参数不同,无法构成重写
virtual void drive(int i) override
{
cout << "Benz->drive()" << endl;
}
};
重载、重写(覆盖)、重定义(隐藏)的对比
重载
- 两个函数在同一作用域
- 函数名相同,参数不同
重写(覆盖)
- 一个函数在父类作用域,一个函数在子类作用域
- 函数三同:函数名、参数、返回值相同。协变(返回值不同)、析构函数(函数名不同)例外
- 必须都是虚函数
重定义(隐藏)
- 一个函数在父类作用域,一个函数在子类作用域
- 函数名相同
- 父类和子类的同名函数不构成重写就是重定义
抽象类
在虚函数的后面加上=0
,这个虚函数就变为了纯虚函数,包含纯虚函数的类称为抽象类(接口类),抽象类不能实例化对象。继承了抽象类的子类也不可以实例化对象,只有将父类的纯虚函数重写,子类才可以实例化对象。
也就是说,纯虚函数规定子类必须进行重写,同时也体现了接口继承
class Car // 抽象类
{
public:
// 纯虚函数
virtual void drive() = 0
{
cout << "Car->drive()" << endl;
}
};
int main()
{
Car c; // 尝试实例化
}
抽象类无法实例化:
class Benz : public Car // 继承了抽象类
{
};
int main()
{
Benz b; // 尝试实例化
}
子类也无法实例化:
子类将父类的纯虚函数重写
class Benz : public Car
{
public:
virtual void drive()
{
cout << "Benz->drive()" << endl;
}
};
int main()
{
Benz b; // 尝试实例化
b.drive();
}
可以实例化:
抽象类虽然不可以实例化,但是可以作为指针接收子类对象
实现继承与接口继承
实现继承
普通函数的继承就是一种实现继承,子类继承了父类的函数,可以使用函数,继承的是父类函数的实现
接口继承
虚函数的继承是一种接口继承,子类继承的是父类函数的接口,对父类函数的实现进行重写,覆盖了父类函数的实现。继承接口的目的是为了实现多态,如果不实现多态,那就不要将函数写成虚函数
多态的原理
虚表
此时有以下类
class Base
{
public:
virtual void func()
{
cout << "func()" << endl;
}
protected:
int _b = 1;
};
常见笔试问题:sizeof(Base)
是多少?
是4
吗?既然这个题都这样问了,大概率不是4
,直接来看结果:
Base
的大小除了int
的4
字节,还多出来4
字节,我们自然就联想到这与虚函数有关
我们通过调试来看一下:
可以看到,在对象的前面,多了一个__vfptr
指针,我们称这个指针为虚函数表指针,虚函数表中存的是虚函数的地址,虚函数表简称虚表
我们再改造一下上面的代码:将虚函数func
改名为func1
,添加虚函数func2
,添加普通函数func3
;添加子类Derive
继承Base
,并重写func1
class Base
{
public:
virtual void func1()
{
cout << "Base::func1()" << endl;
}
virtual void func2()
{
cout << "Base::func2()" << endl;
}
// 普通函数
void func3()
{
cout << "Base::func3()" << endl;
}
protected:
int _b = 1;
};
class Derive : public Base
{
public:
// 重写
virtual void func1()
{
cout << "Derive::func1()" << endl;
}
protected:
int _d = 2;
};
通过上图,func1
、func2
进入虚表,而func3
没有进入虚表,可以得知普通函数的地址并不会放入虚表
再来看子类内部的情况:
可以得知:
- 子类继承了父类的虚函数表,但是这个虚表又和父类的虚表不是同一张虚表,是父类虚表的拷贝。这点从两个
__vfptr
的值不同可以看出 - func1完成了重写,所以d的虚表中存的是重写的Derive::func1,所以虚函数的重写也叫覆盖,覆盖的是虚表中的虚函数。重写是语法层面的的叫法,覆盖是原理层面的叫法
- 子类的普通函数和继承父类的普通函数不会进入虚表,例如
func3
此外,子类自己的虚函数会按照声明顺序放到虚表的最后
class Derive : public Base
{
public:
virtual void func1()
{
cout << "Derive::func1()" << endl;
}
// 子类自己的虚函数
virtual void func4()
{
cout << "Drive::func4()" << endl;
}
protected:
int _d = 2;
};
不过在vs下的监视窗口并不显示func4
进入了虚表
我们可以用内存窗口查看:
我们知道前两个是func1
和func2
,第三个也不知道是不是func4
虚函数表的本质是一个函数指针数组,一般情况这个数组最后面放了一个nullptr
。我们可以将虚表内的指针拿出来调用,这样就可以验证func4
了
我们先将函数指针typedef
一下,要不然写起来有点麻烦
typedef void(*VFPTR)();// 将函数指针重命名为VFPTR
接着写一个打印函数指针数组的函数
void PrintVFPTR(VFPTR* vf)// 传递的是函数指针数组的首地址,即虚表指针
{
for (int i = 0; i < 3; i++)
{
// 打印函数的地址
printf("%p->", vf[i]);
// 取出函数地址,调用函数
VFPTR pvf = vf[i];
(*pvf)();
}
}
现在的问题就是,如何取到虚表指针。虚表指针是对象的第一个成员,指针大小是4
字节,也就是我们要取到对象的前4
字节
直接将对象强转为int
可不可以?
显然不可以,有关联的类型才可以相互转换。但是,指针可以随便相互转换
我们可以将Derive*
转换为int*
VFPTR* ptr = (int*)&d;
再将int*
解引用,这样我们就得到了d的4
个字节
VFPTR* ptr = *((int*)&d);
还有一点:整型和指针可以相互转换,因为整型表示大小,4
个字节;指针表示编号,也是4
个字节。将得到的4
字节转为我们需要的VFPTR*
就搞定了
VFPTR* ptr = (VFPTR*)(*((int*)&d));
下面直接测试:
可以看到,子类对象d
的虚表中确实有自己的虚函数func4
多态的原理
了解了前置知识,我们可以看多态的原理了
既然我们花了很多篇幅来讲解虚表,那就说明虚表是实现多态的关键
依然是Person
和Student
买票的例子
class Person
{
public:
virtual void BuyTickets()
{
cout << "Person->全价" << endl;
}
protected:
int _p = 1;
};
class Student : public Person
{
public:
void BuyTickets()
{
cout << "Student->半价" << endl;
}
protected:
int _s = 2;
};
void func(Person& p)
{
p.BuyTickets();
}
int main()
{
Person p;
func(p);
Student s;
func(s);
return 0;
}
满足多态时,调用函数会根据指针或者引用找到指向的对象,然后到对象的虚表中找对应的虚函数进行调用
核心就是:指向谁,就调用谁的虚函数
指向父类,调用父类的虚函数
指向子类,调用子类的虚函数
不满足多态,也就是普通的函数调用,在编译阶段就确定了函数的地址
满足多态的条件时,函数地址就不是在编译阶段确定了,而是在运行阶段。运行时,到对象的虚表找对应虚函数的地址
int main()
{
Student s;
// 多态调用
func(s);
// 普通函数调用
s.BuyTickets();
return 0;
}
从call
可以看出,多态和普通函数调用确实是不同的
多继承的虚函数表
涉及到多继承就让人头大,接下来我们看一下多继承中的虚表是什么样的
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;
};
可以预测,Derive
对象会继承Base1
和Base2
的虚表,有两张虚表,重写的虚函数会覆盖虚表中的虚函数。那么子类自己的虚函数func3
会怎样呢?是在两张表中都放一个,还是只放一张表?可以用打印虚表数组的方式查看
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;
// 查看d中的Base1
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
// 查看d中的Base2
Base2* pf = &d;
VFPTR* vTableb2 = (VFPTR*)(*(int*)pf);
PrintVTable(vTableb2);
return 0;
}
结果:
通过结果可以看出:多继承子类的未重写虚函数会放在第一个继承父类部分的虚表中
本来还有菱形继承和菱形虚拟继承的部分,但是写到这里实在不想写了,这部分太恶心人了。而且实际中不建议也不会用菱形继承和菱形虚拟继承,所以就到这里吧
结束,再见 😄