文章目录
- 1.C++的三大特性
- 1.1封装
- 1.2继承
- 1.3多态
- 1.3.1 虚函数
- 1.3.2 多态代码+反汇编分析。
- 反汇编分析1——基类指针指向子类对象,构造过程。
- 反汇编分析2——基类指针指向子类对象,调用虚函数getPrice()过程。
- 反汇编分析3——基类对象,调用虚函数getPrice()过程。
- 反汇编分析4——基类指针指向子类对象,析构过程。
- 反汇编分析5——基类指针指向子类对象,析构过程,基类析构函数不是虚函数时。
- 1.3.3 静态多态vs动态多态
1.C++的三大特性
1.1封装
将对象的属性和方法封装起来。为了模块化,便于使用者操作,同时可以隔离外部使用者对内部数据的干扰,提高了安全性。
类成员的三种属性:
private:本类使用。设置get和set方法,因为通过接口来访问和修改数据是可控的,相对安全。
protected:本类和子类使用。
public:公开的,都可以访问。
1.2继承
允许通过继承原有类的某些特性或全部特性而产生新的类,原有的类称为基类(父类),产生的类称为派生类(子类)。为了扩展和重用,减少重复代码。
1.3多态
发生在继承关系中,不同的对象,对于相同的方法有不同的操作逻辑。为了接口重用,提高代码的可复用性、可维护性和可扩充性。
多态的实现机制为虚函数。
1.3.1 虚函数
关键字:virtual
作用:允许在子类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和子类的同名函数。
最常见的用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。
实现原理:虚表+虚表指针。
当类存在虚函数时,编译器会为类创建一个虚表,虚表是一个数组,数组的元素存放的是虚函数地址。同时为每个类对象添加一个隐藏数据成员,即虚表指针,它被定义在对象首地址处。
【注意】 虚表只有一份,而有多少个对象,就有多少个虚表指针。
1.3.2 多态代码+反汇编分析。
1.定义了一个基类,图形类Shape,数据成员分别为价格price和面积area,两个虚成员函数分别为获取图形描述getDescription()和获取图形价格getPrice(),虚析构函数;
2.定义了一个子类,圆形类Circle,继承Shape,数据成员为半径radius,重写父类方法getDescription()和getPrice();
3.定义了一个子类,矩形类Rectangle,继承Shape,数据成员分别为长度length和宽度width,重写父类方法getDescription()和getPrice();
4.main()里,基类指针s1指向Circle类对象,基类指针s2指向Rectangle类对象,基类对象s3。
// ConsoleApplication5.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include<iostream>
#include<string>
using namespace std;
class Shape
{
protected:
double price;
double area;
public:
Shape() :price(100),area(0) {}
virtual ~Shape() { printf("%s\n", "Delete shape"); }
virtual void getDescription() { printf("%s\n", "Base shape");}
virtual void getPrice() { printf("%s%f\n", "Shape price ",price); }
};
class Circle : public Shape
{
private:
double radius;
public:
Circle(double r) : radius(r) { area = 3.14 * radius * radius; price = 100 + area * 6; }
~Circle() { printf("%s%f\n", "Delete circle with radius ",radius); }
void getDescription() { printf("%s%f\n", "Circle with radius ",radius);}
void getPrice(){ printf("%s%f%s%f\n", "Circle with area ", area," price ",price); }
};
class Rectangle : public Shape {
private:
double length;
double width;
public:
Rectangle(double l, double w) : length(l), width(w) { area = length * width; price = 100 + area * 6; }
~Rectangle() { printf("%s%f%s%f\n", "Delete rectangle with length ", length," and width ",width); }
void getDescription() { printf("%s%f%s%f\n","Rectangle with length ", length, " and width ", width);}
void getPrice() { printf("%s%f%s%f\n", "Rectangle with area ", area, " price ", price); }
};
int main() {
Shape* s1 = new Circle(5.0);
Shape* s2 = new Rectangle(4.0, 6.0);
s1->getPrice();
s2->getPrice();
Shape s3;
s3.getPrice();
s3.getDescription();
delete s1;
delete s2;
_CrtDumpMemoryLeaks();
return 0;
}
程序执行结果,如下图。
反汇编分析1——基类指针指向子类对象,构造过程。
1.调用子类构造函数(Rectangle),如下图。
2.调用父类构造函数(Shape),如下图。
看寄存器eax,对象s2地址0x014E5AB8;
虚表指针0x00DC9E10;
length地址eax+18h,0x0014E5AD0;
width地址eax+20h,0x0014E5AD8;
area=length * width,area地址eax+10h,0x0014E5AC8。
price=100 + area * 6,price地址eax+8h,0x0014E5AC0,如下图。
基类数据成员声明顺序price、area。
子类数据成员声明顺序length、width,如下图。
反汇编分析2——基类指针指向子类对象,调用虚函数getPrice()过程。
1.取对象s2地址,存放进eax,0x014E5AB8。
2.取虚表指针,存放进edx,0x00DC9E10。
3.取虚函数getPrice()地址,0x00DC14D3,并调用,如下图。
反汇编分析3——基类对象,调用虚函数getPrice()过程。
基类对象调用自身虚函数时,没有构成多态性,所以没必要查表访问,编译器使用了直接调用方式,如下图。
基类对象s3地址0x0118FE90。
虚表指针0x00DC9F64。
虚析构函数地址0x00DC14BF。
虚函数getDescription()地址0x00DC10CD。
虚函数getPrice()地址0x00DC14CE。
虚函数声明顺序:析构函数、getDescription()、getPrice()。
反汇编分析4——基类指针指向子类对象,析构过程。
1.调用子类析构函数(Circle),如下图。
2.释放子类资源后,调用父类析构函数(Shape),如下图。
反汇编分析5——基类指针指向子类对象,析构过程,基类析构函数不是虚函数时。
只调用基类析构函数,如下图。
【注意】 由于子类析构函数不会被调用,子类资源没有正确释放,造成内存泄漏。
1.3.3 静态多态vs动态多态
1.静态多态在编译期完成,由模板实现;而动态多态在运行期完成,由继承、虚函数实现。
2.静态多态中接口是隐式的,以有效表达式为中心;而动态多态中接口是显式的,以函数签名为中心。
3.【注意】 虚函数表在编译的时候就确定了,而类对象的虚函数指针是在运行阶段确定的,这是实现多态的关键!