c++(4`OPP,多态泛型、虚表, 构造析构与虚函数)
OPP
面向过程 vs. 面向对象
面向过程:问题分解成一系列的步骤,然后按照顺序执行这些步骤
面向对象:问题分解抽象为一系列的“对象”,通过调用这些对象去解决问题
OPP优势:
使得代码更加模块化、降低耦合(一个小房子拆分成一个个小积木):
- 易复用(可以多次拷贝某个积木去使用)
- 易扩展(添加了某个新家具,并不会影响其他家具)
- 易组合(有很多组合房子样式的方式)
- 提高安全性(如果可以修改物体内部的结构,很有可能无法使用)
- 代码逻辑更清晰,单一设计原则
- 方便调用
- 可维护性,单一功能原则,每一个类都应该有一个单一的功能
三大特性:
- 封装,是将数据和方法封装为独立单元,易复用,易组合
- 继承,允许一个类继承一个现有类,并可以 隐藏/重写/扩展 父类的功能,易复用,易扩展,强化逻辑关系
- 多态,是通过统一的接口操作不同类型的对象,从而产生不同的行为,更灵活
多态
将接口与实现进行分离
静态多态
重载:
1
2
3
4
5
6
7
void print(int i) {
cout << "整数: " << i << endl;
}
void print(double f) {
cout << "浮点数: " << f << endl;
}
展开
- 函数重载:
- 函数名相同,参数列表必须不同,返回值可以相同也可以不同,函数体可以相同也可以不同
1
2
3
4
5
6
7
8
9
10
11
class A {
public:
//重载前置++
A& operator++() {
++x;
++y;
return *this;
}
private:
double x, y;
}
展开
- 运算符重载:
- 在类内定义
- 不能创建自定义运算符,应使用内置中已有的运算符
- 不能改变运算符的操作数数量
- 不能改变运算符的优先级和结合性
- 某些运算符不能被重载(如 ::, .*, ., ?: 等)
- 至少有一个操作数是用户定义类型
动态多态
动态绑定:
注:运行时指执行指令时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A {
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
};
class C: public B {
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
int main() {
A* basePtr = new B();
basePtr->vfunc1();
delete basePtr;
return 0;
}
展开
- 继承体系中的虚函数重写,需要保证函数名、返回值、参数列表都必须相同,派生类是否用virtual都可以(它都是虚函数),函数体可以相同也可以不同
- 用基类的指针/引用去调用函数,基类指针可以指向/绑定派生类对象
泛型
泛型编程是一种编程范式,目的是编写与类型无关的代码,
和多态一样都是相同接口,不同类型的对象,但是产生的是相同的行为
模板:
1
2
3
4
template <typename T>
T maxValue(T a, T b) {
return (a > b) ? a : b;
}
展开
- 函数模板:
1
2
3
4
5
6
7
template <typename T, int size = 10>
class Array {
private:
T arr[size];
public:
int getSize() const { return size; }
};
展开
- 类模板
虚函数表
静态绑定、动态绑定:
静态绑定:构建时就能确定函数调用和具体实现,它在运行时效率更高,但灵活性较差
动态绑定:在运行时根据对象的实际类型确定函数调用和具体实现(运行时候查表去调用函数),它在运行时效率更低,但灵活性较高
定义:
虚函数表是动态绑定的核心,目的是在运行时调用实际类型对象的函数
每个包含虚函数的类都有一个虚表(vtbl),vtbl是属于类的,一个包含虚函数的类有且仅有一个vtbl,vtbl是一个函数指针数组,每个函数指针指向该类的一个虚函数实现
内部自动通过*__vptr指针(指向vtbl的指针)来访问,一个包含虚函数的类有且仅有一个vptr,
这里每个指针都指向一个虚函数的内存地址
注意:
vtbl是在编译阶段生成的,vptr是在运行时进行初始化,也就是当创建一个对象时,自动调用构造函数会负责将对象的vptr初始化为指向该类的vtbl,vptr会存储到内存空间中,在继承体系中vptr是可以被共享访问的
实例:
我们来看下之前多态_动态绑定部分代码 对应的虚表,
A_vtbl: 包含两个函数指针,分别指向A::vfunc1()和A::vfunc2()这两个虚函数
B_vtbl: 包含两个函数指针,分别指向B重写了B::vfunc1()函数,继承了A::vfunc2()这两个虚函数
C_vtbl: 包含两个函数指针,分别指向继承了B::vfunc1()函数,重写了C::vfunc2()这两个虚函数
1
2
3
4
5
6
7
8
9
int main()
{
B bObject;
A *p = & bObject;
p->vfunc1();
p->vfunc2();
delete p;
return 0;
}
展开
再来看一下当调用时如何查表
- 首先创建了B对象,会自动调用B的构造函数,内部自动将vptr指向B_vtbl
- 创建一个基类指针p指向B对象,由于基类指针/引用根据实际的对象查表,并且vptr的共享性,和vtbl独立存在不会消失,那么p是可以访问到B_vtbl
- 使用p来调用vfunc1()函数时,会根据B_vtbl查找vfunc1()函数,也就是B_vtbl中第一个函数指针指向的B::vfunc1()
- 使用p来调用vfunc2()函数时,会根据B_vtbl查找vfunc2()函数,也就是B_vtbl中第二个函数指针指向的A::vfunc2()
构造析构与虚函数
构造函数不能为虚函数:因为调用虚构需要通过vptr去访问vtbl来访问虚构,但是vptr是在构造函数内被初始化的,也就是在调用构造函数前没有初始化(vptr没有指向vtbl),也就无法调用虚构
1
2
A *p = new B;
delete p;
展开
析构函数可以为虚函数,并且当要使用基类指针/引用调用子类时,最好将基类的析构函数声明为虚函数,否则可能存在内存泄露的问题
- 如果类A的析构函数不是虚函数,那么delete p;将会仅仅调用A的析构函数,只释放了B对象中的A部分,而派生出的新的部分未释放掉
- 如果类A的析构函数是虚函数,delete p; 将会先调用B的析构函数,再调用A的析构函数,释放B对象的所有空间
虚函数 vs.纯虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
public:
virtual void vfunc() = 0;
virtual ~A() {} //虚折构
};
class B : public A {
public:
virtual void vfunc(){}
};
class C: public B {
public:
virtual void vfunc(){}
};
int main() {
//A* basePtr0 = new A(); ❌ 错误
A* basePtr1 = new B();
A* basePtr2 = new C();
basePtr1->vfunc();
basePtr2->vfunc();
delete basePtr1;
delete basePtr2;
return 0;
}
展开
是否需要实现:
- 虚函数基类没有实现,没有调用,不会报错,如果调用,会链接错误:undefined reference to `Base::func()’
- 派生类是否实现都可以,如果派生类没有实现(没有定义),就会调用基类的实现
- 声明了纯虚函数的类是一个抽象类,不能创建类的实例,否则有编译错误(cannot instantiate abstract class不能实例化抽象类)
- 基类不能被实现,派生类必须实现,否则编译会报错,它仍然是抽象类
使用场景:
- 虚函数:
- 应用于继承体系中需要重写的行为,它支持多态性,比普通的隐藏方式更灵活
- 纯虚函数:
- 定义接口
- 定义抽象的概念,防止被实例化
override
用于显式标识派生类中重写了基类虚函数,当没有正确重写时(需要保证同名,同参,同返回类型),编译器会报错,因此当我们重写时尽量添加override关键字,从而提高代码安全性和可读性