文章目录
- 1.C++的三大特性
- 1.1封装
- 1.2继承
- 1.3多态
- 1.3.1 虚函数
- 1.3.1.1.【注意】当类存在虚函数时,编译器会为类创建一个虚表,虚表是一个数组,数组的元素存放的是虚函数地址。即虚表在编译的时候就确定了,且只有一份。同时为每个类对象添加一个隐藏数据成员,即虚表指针,它是在运行阶段确定的,有多少个对象,就有多少个虚表指针。另外,虚表指针被定义在对象首地址处。
- 1.3.1.2.【注意】派生类会生成兼容基类的虚表,如果子类重写了相应的虚函数,那么子类虚表中相应的虚函数地址就会改变,指向自身的虚函数实现,否则指向的是基类的虚函数实现。另外,如果子类也有虚函数,那么子类虚表就会添加该虚函数地址。
- 1.3.1.3.【注意】每个类都有虚表时,单继承的子类维护一张虚表,子类对象有一个虚表指针。多继承的子类维护多张虚表,子类对象包含多个虚表指针。
- 1.3.1.4. 为什么构造函数通常不定义为虚函数?
- 1.3.1.5. 引用是否能实现动态绑定?
- 1.3.2 多态代码+反汇编分析。
- 1.3.3 静态多态vs动态多态
1.C++的三大特性
1.1封装
将对象的属性和方法封装起来。为了模块化,便于使用者操作,同时可以隔离外部使用者对内部数据的干扰,提高了安全性。
类成员的三种属性:
private:本类使用。设置get和set方法,因为通过接口来访问和修改数据是可控的,相对安全。
protected:本类和子类使用。
public:公开的,都可以访问。
1.2继承
允许通过继承原有类的某些特性或全部特性而产生新的类,原有的类称为基类(父类),产生的类称为派生类(子类)。为了扩展和重用,减少重复代码。
1.3多态
发生在继承关系中,不同的对象,对于相同的方法有不同的操作逻辑。为了接口重用,提高代码的可复用性、可维护性和可扩充性。
多态的实现机制为虚函数。
1.3.1 虚函数
关键字:virtual
作用:允许在子类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和子类的同名函数。
最常见的用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。
实现原理:虚表+虚表指针。
1.3.1.1.【注意】当类存在虚函数时,编译器会为类创建一个虚表,虚表是一个数组,数组的元素存放的是虚函数地址。即虚表在编译的时候就确定了,且只有一份。同时为每个类对象添加一个隐藏数据成员,即虚表指针,它是在运行阶段确定的,有多少个对象,就有多少个虚表指针。另外,虚表指针被定义在对象首地址处。
1.3.1.2.【注意】派生类会生成兼容基类的虚表,如果子类重写了相应的虚函数,那么子类虚表中相应的虚函数地址就会改变,指向自身的虚函数实现,否则指向的是基类的虚函数实现。另外,如果子类也有虚函数,那么子类虚表就会添加该虚函数地址。
1.3.1.3.【注意】每个类都有虚表时,单继承的子类维护一张虚表,子类对象有一个虚表指针。多继承的子类维护多张虚表,子类对象包含多个虚表指针。
1.3.1.4. 为什么构造函数通常不定义为虚函数?
- 虚函数的调用需要虚表指针,而该指针存放在对象的内存空间中。由于初始时,对象还未创建,还没有内存空间,更没有虚表指针用来调用虚构造函数。
1.3.1.5. 引用是否能实现动态绑定?
- 可以,因为引用或指针既可以指向基类对象也可以指向子类对象,所以它是在运行时根据所指对象,调用相应的虚函数。
1.3.2 多态代码+反汇编分析。
1.基类Shape,数据成员为price和area,虚析构函数,两个虚函数为getDescribe()和getPrice(),;
2.子类Circle,继承Shape,数据成员为radius,重写父类方法getPrice();
3.main()里,基类指针s1指向Circle类对象,基类对象s2。代码如下。
#include<iostream>
using namespace std;
class Shape
{
protected:
double price;
double area;
public:
Shape() :price(100), area(2) {}
virtual ~Shape() { printf("%s\n", "Delete shape"); }
virtual void getDescribe() { 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 getPrice() { printf("%s%f%s%f\n", "Circle with area ", area, " price ", price); }
};
int main() {
Shape* s1 = new Circle(5.0);
s1->getPrice();
delete s1;
Shape s2;
s2.getPrice();
return 0;
}
程序执行结果,如下图。
反汇编分析1——基类指针指向子类对象,构造过程。
- ecx保存了子类对象地址0x00DD1A38,如下图。
-
调用完父类构造函数,如下图。
- 子类对象保存的数据内容分别是基类虚表指针,price,area(按基类数据成员的声明顺序)。
- 基类虚表保存的虚函数地址分别是基类析构函数,getDescribe(),getPrice()(按基类虚函数声明顺序)。
-
设置子类虚表指针后,如下图。
- 子类对象首地址处保存的基类虚表指针替换成子类虚表指针。
- 子类虚表保存的虚函数地址分别是子类析构函数,getDescribe(),getPrice()。
- 因为子类没有重写getDescribe(),所以地址不变,指向基类的getDescribe()实现。
- 因为子类重写getPrice(),所以地址改变,指向子类的getPrice()实现。
- 紧接着构造子类的数据成员,如下图。
- 最后执行子类的构造代码,先修改area值,再修改price值,如下图。
- 总结:先构造基类,再构造子类。
反汇编分析2——基类指针指向子类对象,调用虚函数getPrice()过程。
- 取子类对象地址。
- 取子类虚表指针地址。
- 取子类虚函数getPrice()地址,然后调用,如下图。
反汇编分析3——基类对象,调用虚函数getPrice()过程。
- 基类对象调用自身虚函数时,没有构成多态性,所以没必要查表访问,编译器使用了直接调用方式,如下图。
反汇编分析4——基类指针指向子类对象,析构过程。
- 子类对象保存的数据内容,如下图。
- 执行子类的析构代码,如下图。
- 紧接着执行基类的析构代码,如下图。
- 最后释放指针内存。
- 总结:先析构子类,再析构基类。
反汇编分析5——基类指针指向子类对象,析构过程,基类析构函数不是虚函数时。
- 只调用基类析构函数,如下图。
- 【注意】 由于子类析构函数不会被调用,子类资源没有正确释放,造成内存泄漏。
1.3.3 静态多态vs动态多态
1.静态多态在编译期完成,由模板实现;而动态多态在运行期完成,由继承、虚函数实现。
2.静态多态中接口是隐式的,以有效表达式为中心;而动态多态中接口是显式的,以函数签名为中心。
3. 另外,静态多态代码如下。
template <typename T>
class Calculator
{
public:
T add(T a, T b) {
return a + b;
}
};
int main() {
Calculator<int> intCalculator;
cout << intCalculator.add(5, 10) << endl;
Calculator<double> doubleCalculator;
cout << doubleCalculator.add(3.5, 2.5) << endl;
return 0;
}