文章

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,

1743085914667

这里每个指针都指向一个虚函数的内存地址

注意:

vtbl是在编译阶段生成的,vptr是在运行时进行初始化,也就是当创建一个对象时,自动调用构造函数会负责将对象的vptr初始化为指向该类的vtbl,vptr会存储到内存空间中,在继承体系中vptr是可以被共享访问的

实例:

我们来看下之前多态_动态绑定部分代码 对应的虚表,

1743090299152

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关键字,从而提高代码安全性和可读性

本文由作者按照 CC BY 4.0 进行授权
本页总访问量