CD61.【C++ Dev】多态(1)
目录
1.形象地理解多态
2.使用virtual关键字实现多态
虚函数的定义
示例代码
3.虚函数重写
4.多态核心要点(考频高!)
满足多态的严格条件
多态调用和普通对象调用的区别
虚函数重写的例外1:协变
虚函数重写的例外2: 析构函数的重写
为什么析构函数需要重写?
析构函数需要重写的特殊场景
5.override和final关键字
override
final
练习题
1.形象地理解多态
多态指的是多种形态或状态
看一个形象的例子:不同人买票的票价不同,非VIP会员全价买票,而VIP会员是有优惠价的
2.使用virtual关键字实现多态
例如以下代码的VIP_member由non_VIP_member继承而来,它们有相同的成员函数,前面都用了virtual关键字修饰来实现多态
虚函数的定义
使用virtual修饰的成员函数为虚函数,具体应用参见多态
示例代码
注:菱形继承也使用virtual,但虚继承和虚函数没有任何关系
class non_VIP_member
{
public:virtual void price() const{cout << "Full price" << endl;}
};class VIP_member:public non_VIP_member
{
public:virtual void price() const{cout << "Discounted price" << endl;}
};
测试代码:
void test(non_VIP_member& member)
{member.price();
}int main()
{non_VIP_member mem1;VIP_member mem2;test(mem1);test(mem2);return 0;
}
运行结果:
3.虚函数重写
虚函数的重写(覆盖):
1.子类中有一个跟父类完全相同的虚函数(即子类虚函数与父类虚函数的
返回值类型、函数名字、参数列表完全相同,简称三同),称子类的虚函数重写或覆盖了父类的虚函数
2.虚函数重写的是实现,使用父类形参的定义,不使用子类形参的定义
注: 只有成员函数才能变成虚函数
上方两个price函数其实是虚函数的重写,VIP_member和non_VIP_member的price的返回类型都为void,参数列表都只有隐藏的this指针
4.多态核心要点(考频高!)
满足多态的严格条件
1.调用的函数必须是重写的虚函数
2.参数是父类的指针或引用
3.父类成员函数必须加virtual,子类相同的成员函数可以不写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性,建议子类也写上virtual)
如果去掉virtual,就算是指针或引用,也不是多态
class non_VIP_member
{
public:void price() const{cout << "Full price" << endl;}
};class VIP_member:public non_VIP_member
{
public:void price() const{cout << "Discounted price" << endl;}
};void test(non_VIP_member& member)
{member.price();
}int main()
{non_VIP_member mem1;VIP_member mem2;test(mem1);//mem1是non_VIP_member类型,调用它的price()test(mem2);//mem1是VIP_member类型,调用它的price()return 0;
}
运行结果:
多态调用和普通对象调用的区别
因此可以推出:
1. 多态调用看的是“指针或引用”实际指向的对象类型(运行时决定,动态绑定)
2. 普通对象调用看的是"当前静态类型" (编译期决定,静态绑定)
多态调用:
使用引用:
void test(non_VIP_member& member)
{member.price();
}int main()
{non_VIP_member mem1;VIP_member mem2;test(mem1);//mem1是non_VIP_member类型,调用它的price()test(mem2);//mem1是VIP_member类型,调用它的price()return 0;
}
运行结果:
使用指针:
void test(non_VIP_member* member)
{member->price();
}int main()
{non_VIP_member mem1;VIP_member mem2;test(mem1);//mem1是non_VIP_member类型,调用它的price()test(mem2);//mem1是VIP_member类型,调用它的price()return 0;
}
运行结果:
普通对象调用:
#include <iostream>
using namespace std;
class non_VIP_member
{
public:void price() const{cout << "Full price" << endl;}
};class VIP_member:public non_VIP_member
{
public:void price() const{cout << "Discounted price" << endl;}
};void test(non_VIP_member member)//非引用也非指针
{member.price();//一律调用non_VIP_member的price(),静态绑定
}int main()
{non_VIP_member mem1;VIP_member mem2;test(mem1);test(mem2);return 0;
}
运行结果:
虚函数重写的例外1:协变
C++11引入了协变:指重写的虚函数的返回的值可以不同,但是返回值必须是父子关系指针或引用,而且返回值必须同时是指针或者同时是引用
例如下面这样就不是协变:
这样就是协变:
class non_VIP_member
{
public:virtual non_VIP_member& price() {cout << "Full price" << endl;return *this;}
};class VIP_member:public non_VIP_member
{
public:virtual VIP_member& price(){cout << "Discounted price" << endl;return *this;}
};
当然其他符合要求的写法也可以:
虚函数重写的例外2: 析构函数的重写
虽然父类与子类析构函数名字不同,看起来违反了虚函数重写的规则,但是如果条件合适也能成虚函数重写
如果父类的析构函数为虚函数,子类析构函数只要定义,无论子类析构函数是否加virtual关键字,都与基类的析构函数构成重写
这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
为什么析构函数需要重写?
class non_VIP_member
{
public:virtual ~non_VIP_member(){cout << "~non_VIP_member()" << endl;}
};class VIP_member:public non_VIP_member
{
public:virtual ~VIP_member(){cout << " ~VIP_member()" << endl;}
};
测试代码:
int main()
{non_VIP_member mem1;VIP_member mem2;return 0;
}
运行结果:
发现VIP_member析构了一次,non_VIP_member析构了两次
如果不加virtual,运行结果是一样的
上面的例子看不出问题,记下面这个特殊场景:
析构函数需要重写的特殊场景
例如这个场景没有重写:
#include <iostream>
using namespace std;
class non_VIP_member
{
public:~non_VIP_member(){cout << "~non_VIP_member()" << endl;}
};class VIP_member:public non_VIP_member
{
public:~VIP_member(){cout << " ~VIP_member()" << endl;delete[] area;}
protected:int* area = new int[100];
};int main()
{non_VIP_member* ptr=new non_VIP_member;//ptr是父类的指针delete ptr;ptr = new VIP_member;delete ptr;return 0;
}
VS上没有报内存泄漏,使用Linux下的valgrind内存泄漏检测工具,输入以下命令:
valgrind --tool=memcheck --leak-check=full ./a.out
(有关valgrind的介绍参见CD32.【C++ Dev】类和对象(22) 内存管理(下)文章)
运行结果:
==1212205== Memcheck, a memory error detector
==1212205== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==1212205== Using Valgrind-3.22.0 and LibVEX; rerun with -h for copyright info
==1212205== Command: ./a.out
==1212205==
~non_VIP_member()
~non_VIP_member()
==1212205== Mismatched new/delete size value: 1
==1212205== at 0x484A5B9: operator delete(void*, unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1212205== by 0x109262: main (2.cpp:29)
==1212205== Address 0x4e21510 is 0 bytes inside a block of size 8 alloc'd
==1212205== at 0x4846FA3: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1212205== by 0x10922F: main (2.cpp:28)
==1212205==
==1212205==
==1212205== HEAP SUMMARY:
==1212205== in use at exit: 400 bytes in 1 blocks
==1212205== total heap usage: 5 allocs, 4 frees, 75,161 bytes allocated
==1212205==
==1212205== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==1212205== at 0x48485C3: operator new[](unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1212205== by 0x1092F2: VIP_member::VIP_member() (2.cpp:12)
==1212205== by 0x109240: main (2.cpp:28)
==1212205==
==1212205== LEAK SUMMARY:
==1212205== definitely lost: 400 bytes in 1 blocks
==1212205== indirectly lost: 0 bytes in 0 blocks
==1212205== possibly lost: 0 bytes in 0 blocks
==1212205== still reachable: 0 bytes in 0 blocks
==1212205== suppressed: 0 bytes in 0 blocks
==1212205==
==1212205== For lists of detected and suppressed errors, rerun with: -s
==1212205== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
内存泄漏了400字节(指int* ptr = new int[100];)
分析原因:
1.析构函数没有加virtual,因此不是多态
2.ptr=new VIP_member;是切片,ptr指向子对象中父类的那部分,不包含子类的area指针指向的内存区域,因此delete ptr时,是普通对象的调用,只析构子对象中属于父类的那部分,属于子类部分的400字节的空间没办法释放,导致内存泄漏
看看VS的反汇编:只调用了父类的析构函数,因为ptr的类型是non_VIP_member*
希望ptr指向谁就调用谁,那么就要加上virtual,将析构函数处理成destructor()
#include <iostream>
using namespace std;
class non_VIP_member
{
public:virtual ~non_VIP_member(){cout << "~non_VIP_member()" << endl;}
};class VIP_member:public non_VIP_member
{
public:virtual ~VIP_member(){cout << " ~VIP_member()" << endl;delete area;}
protected:int* area = new int[100];
};int main()
{non_VIP_member* ptr=new non_VIP_member;//ptr是父类的指针delete ptr;ptr = new VIP_member;delete ptr;return 0;
}
使用valgrind再次检测:没有内存泄漏
修改后的代码满足多态的所有条件,多态调用看的是“指针或引用”实际指向的对象类型,那么ptr = new VIP_member;的ptr实际指向的对象类型是VIP_member,因此会调用VIP_member的析构函数,不看ptr原来定义的类型
可以发现是VIP_member的地址存放在eax寄存器中,是动态取得的
而eax指向的是跳转至VIP_member析构函数的指令
结论:析构函数需要重写,统一处理成destructor()
C++的库函数也是这样做的,例如exception:
5.override和final关键字
C++11引入了override和final关键字,用来帮助用户检测是否重写
override
作用:修饰子类虚函数,检查子类虚函数是否重写了父类某个虚函数,如果没有重写编译报错
final
作用:修饰父类的虚函数,表示该虚函数不能再被重写
练习题
1.下面代码编译会报错吗?
class non_VIP_member
{
public:virtual void func() final { }
};class VIP_member:public non_VIP_member
{
public:virtual void func(int) { }
};int main(){ }
解析:
final表示该虚函数不能再被重写,而且这两个func()不构成虚函数的重写(参数列表不一样)
因此不会报错,但func()构成隐藏,因为函数名相同
2.下面代码编译会报错吗?
class non_VIP_member
{
public:virtual void func(){ }
};class VIP_member:public non_VIP_member
{
public:virtual void func(int) override { }
};int main(){ }
解析:
虽然func()不构成虚函数的重写,但是override是检查子类虚函数是否重写了父类某个虚函数,如果没有重写编译报错
显然父类non_VIP_member没有func(int),所以会报错