【C/C++ 虚函数以及替代方案】C++ 虚函数的使用开销以及替代方案(二)

简介: 【C/C++ 虚函数以及替代方案】C++ 虚函数的使用开销以及替代方案

【C/C++ 虚函数以及替代方案】C++ 虚函数的使用开销以及替代方案(一)https://developer.aliyun.com/article/1464356


4. C++11/14/17/20中虚函数的新性与应用 (New Features and Applications of Virtual Functions in C++11/14/17/20)

4.1 覆盖与最终修饰符的使用 (Use of Override and Final Modifiers)

在C++11及其后续版本中,引入了两个新的关键字:overridefinal,它们为虚函数的使用提供了更多的控制力。

4.1.1 override关键字

override关键字用于显式地声明一个虚函数覆盖了基类中的同名虚函数。这个关键字可以帮助我们在编译时期就发现潜在的错误。例如,如果派生类中的函数并没有正确地覆盖基类中的虚函数(可能是因为函数签名不匹配),编译器就会发出警告。

让我们通过一个例子来理解这个概念:

class Base {
public:
    virtual void foo(int) {}
};
class Derived : public Base {
public:
    void foo(int) override {}  // 正确覆盖了基类的虚函数
    void foo(double) override {}  // 编译错误:没有匹配的基类虚函数可以覆盖
};

在这个例子中,Derived类中的第二个foo函数试图覆盖一个接受double参数的基类虚函数,但是这样的函数在Base类中并不存在,因此编译器会报错。

4.1.2 final关键字

final关键字可以用于阻止虚函数的进一步覆盖,或者阻止类的进一步派生。如果一个虚函数被声明为final,那么任何试图覆盖这个函数的派生类都会在编译时期引发错误。

下面是一个使用final关键字的例子:

class Base {
public:
    virtual void foo() final {}
};
class Derived : public Base {
public:
    void foo() {}  // 编译错误:试图覆盖一个final虚函数
};

在这个例子中,Derived类试图覆盖Base类中的foo函数,但是由于foo函数被声明为final,这个覆盖操作在编译时期就被阻止了。

通过使用overridefinal关键字,我们可以更好地控制虚函数的覆盖行为,从而提高代码的可读性和可维护性。

4.2 默认虚函数的应用 (Application of Default Virtual Functions)

C++11引入了= default= delete两个新的函数修饰符,它们可以用于控制类的默认函数(如构造函数、析构函数、拷贝构造函数、拷贝赋值运算符等)的行为。这两个修饰符也可以用于虚函数。

4.2.1 = default修饰符

= default修饰符用于显式地要求编译器生成一个函数的默认版本。这在我们希望类具有某些默认行为,但又需要声明为虚函数以支持多态性时,非常有用。

例如,我们可以声明一个虚析构函数并使用= default修饰符:

class Base {
public:
    virtual ~Base() = default;  // 虚析构函数,使用默认实现
};
class Derived : public Base {
public:
    // 派生类会自动获得一个默认的虚析构函数
};

在这个例子中,Base类的析构函数被声明为虚函数,并使用= default修饰符,这意味着它将具有默认的析构行为(即什么都不做)。Derived类作为Base的派生类,会自动获得一个虚析构函数,这保证了当我们通过Base类的指针删除一个Derived类的对象时,Derived类的析构函数能够被正确调用。

4.2.2 = delete修饰符

= delete修饰符用于禁止编译器生成函数的默认版本。如果我们不希望类具有某些默认行为,就可以使用这个修饰符。

例如,我们可以禁止拷贝构造函数和拷贝赋值运算符的默认实现:

class NonCopyable {
public:
    NonCopyable(const NonCopyable&) = delete;  // 禁止拷贝构造函数
    NonCopyable& operator=(const NonCopyable&) = delete;  // 禁止拷贝赋值运算符
};

在这个例子中,NonCopyable类的拷贝构造函数和拷贝赋值运算符都被声明为= delete,这意味着我们不能创建NonCopyable类的拷贝。

通过使用= default= delete修饰符,我们可以更精确地控制类的行为,使其更符合我们的需求。

4.3 虚函数与智能指针的结合 (Combination of Virtual Functions and Smart Pointers)

C++11引入了智能指针,如std::unique_ptrstd::shared_ptrstd::weak_ptr,它们可以自动管理内存,避免内存泄漏。智能指针与虚函数结合使用,可以更好地支持动态多态。

4.3.1 智能指针与虚函数

智能指针可以安全地管理动态分配的对象,当智能指针的生命周期结束时,它会自动删除它所管理的对象。如果这个对象是一个派生类对象,并且通过基类指针进行管理,那么我们需要确保基类的析构函数是虚函数,以便正确地调用派生类的析构函数。

例如:

class Base {
public:
    virtual ~Base() = default;  // 虚析构函数
    virtual void foo() = 0;  // 纯虚函数
};
class Derived : public Base {
public:
    void foo() override {
        // 实现函数
    }
};
std::unique_ptr<Base> ptr = std::make_unique<Derived>();
ptr->foo();  // 通过基类指针调用派生类的虚函数

在这个例子中,Base类的析构函数是虚函数,Derived类覆盖了Base类的虚函数foo。我们通过std::unique_ptr管理一个Derived对象,并通过基类指针调用派生类的虚函数。

4.3.2 智能指针的类型转换

智能指针也支持动态类型转换,如std::dynamic_pointer_cast,它可以将基类的智能指针转换为派生类的智能指针。这在处理复杂的类层次结构时非常有用。

例如:

std::shared_ptr<Base> basePtr = std::make_shared<Derived>();
std::shared_ptr<Derived> derivedPtr = std::dynamic_pointer_cast<Derived>(basePtr);

在这个例子中,我们首先创建了一个Derived对象,并通过std::shared_ptr进行管理。然后,我们使用std::dynamic_pointer_castbasePtr转换为std::shared_ptr

通过智能指针和虚函数的结合,我们可以更好地支持动态多态,同时避免内存泄漏和类型转换的错误。

5. Qt中虚函数的使用与优化 (Use and Optimization of Virtual Functions in Qt)

5.1 Qt事件处理机制中的虚函数 (Virtual Functions in Qt Event Handling Mechanism)

在Qt中,事件处理机制是一个核心的部分,它允许我们在用户交互或者系统事件发生时,进行相应的处理。在这个机制中,虚函数扮演了一个重要的角色。

5.1.1 事件处理机制的基本概念 (Basic Concepts of Event Handling Mechanism)

在Qt中,事件(Event)是由系统或者用户产生的一种动作或者发生的一种情况,例如鼠标点击、键盘按键、定时器超时等。事件处理就是对这些事件进行相应的处理。

在Qt的事件处理机制中,我们通常会重写某个类的虚函数来处理特定的事件。例如,如果我们想要处理鼠标点击事件,我们可以重写QWidget类的mousePressEvent()虚函数。

class MyWidget : public QWidget
{
protected:
    void mousePressEvent(QMouseEvent *event) override
    {
        // 处理鼠标点击事件的代码
    }
};

在这个例子中,mousePressEvent()就是一个虚函数。当鼠标点击事件发生时,Qt会自动调用这个函数。

5.1.2 虚函数在事件处理中的作用 (Role of Virtual Functions in Event Handling)

虚函数在Qt的事件处理机制中起到了核心的作用。通过重写虚函数,我们可以自定义事件的处理方式,实现更复杂的功能。

在Qt中,每个事件都对应一个虚函数,例如mousePressEvent()对应鼠标点击事件,keyPressEvent()对应键盘按键事件等。当事件发生时,Qt会自动调用对应的虚函数。如果我们重写了这个虚函数,那么Qt就会调用我们的函数,而不是默认的函数。

这就是虚函数的多态性。通过虚函数,我们可以在不修改原有代码的情况下,改变事件的处理方式,增加新的功能。

5.1.3 虚函数的使用注意事项 (Notes on Using Virtual Functions)

虽然虚函数在Qt的事件处理机制中非常有用,但是在使用时还是需要注意一些问题。

首先,虚函数的重写必须遵循一定的规则。例如,函数的参数列表必须完全相同,返回类型也必须相同。此外,虚函数的访问权限通常是protected,这意味着只有子类和友元类可以访问。

其次,虚函数的调用可能会有一些性能开销。因为虚函数的调用需要通过虚函数表进行,这会比直接的函数调用慢一些。因此,如果一个函数被频繁调用,那么可能需要考虑其他的设计方案。

虚函数在Qt的事件处理中的工作流程可以通过下图进行理解:

在这个流程中:

  1. 当一个Qt事件(如鼠标点击、键盘按键等)被触发时,它会被发送到对应的QWidget对象。
  2. QWidget对象会调用对应的虚函数(如mousePressEvent()keyPressEvent()等)来处理这个事件。
  3. 如果我们在自定义的MyWidget类中重写了这个虚函数,那么Qt会调用我们的函数,而不是QWidget的函数。
  4. 在我们的函数中,我们可以编写自定义的事件处理代码,实现我们想要的功能。

这就是虚函数在Qt事件处理中的作用。通过重写虚函数,我们可以自定义事件的处理方式,实现更复杂的功能。

在使用虚函数时,需要注意一些问题。虚函数的重写必须遵循一定的规则,例如函数的参数列表和返回类型必须完全相同。虚函数的调用可能会有一些性能开销,因为虚函数的调用需要通过虚函数表进行,这会比直接的函数调用慢一些。因此,如果一个函数被频繁调用,那么可能需要考虑其他的设计方案。最后,虚函数的使用需要谨慎,不恰当的使用可能会导致程序的错误或者性能问题。

5.2 Qt中虚函数与信号槽机制的结合 (Combination of Virtual Functions and Signal-Slot Mechanism in Qt)

在Qt中,信号槽机制是一种非常重要的事件处理方式。它允许我们在某个事件发生时,自动调用一个函数。这个函数可以是一个普通函数,也可以是一个虚函数。在这一节中,我们将探讨如何在信号槽机制中使用虚函数。

5.2.1 信号槽机制的基本概念 (Basic Concepts of Signal-Slot Mechanism)

在Qt中,信号(Signal)是由某个对象发出的一种通知,表示某个事件已经发生。槽(Slot)是一个函数,它可以响应某个信号。当信号发出时,与之相连的槽函数会被自动调用。

信号槽机制的一个重要特点是,信号和槽可以跨对象、跨线程进行连接。这使得我们可以在不同的对象或者线程中处理事件,提高了程序的灵活性和响应速度。

5.2.2 虚函数在信号槽机制中的应用 (Application of Virtual Functions in Signal-Slot Mechanism)

虚函数可以作为槽函数在信号槽机制中使用。这意味着,我们可以在虚函数中处理信号,实现更复杂的功能。

例如,我们可以在QWidget的子类中,重写resizeEvent()虚函数,并将它作为槽函数连接到窗口大小改变的信号上。

class MyWidget : public QWidget
{
    Q_OBJECT
protected:
    void resizeEvent(QResizeEvent *event) override
    {
        // 处理窗口大小改变的代码
    }
public slots:
    void onWindowResized()
    {
        // 调用虚函数
        resizeEvent(nullptr);
    }
};
// 在某个地方
connect(myWidget, SIGNAL(windowResized()), myWidget, SLOT(onWindowResized()));

在这个例子中,当窗口大小改变时,onWindowResized()槽函数会被自动调用,然后它会调用resizeEvent()虚函数来处理这个事件。

5.2.3 虚函数在信号槽机制中的优点与缺点 (Advantages and Disadvantages of Virtual Functions in Signal-Slot Mechanism)

虚函数在信号槽机制中有一些优点。首先,通过虚函数,我们可以在不修改原有代码的情况下,改变事件的处理方式,增加新的功能。其次,虚函数的多态性使得我们可以在子类中实现不同的事件处理方式。

然而,虚函数在信号槽机制中也有一些缺点。虚函数的调用需要通过虚函数表进行,这会比直接的函数调用慢

虚函数在Qt的信号槽机制中的工作流程可以通过下图进行理解:

在这个流程中:

  1. 当一个Qt信号(如窗口大小改变等)被触发时,它会被发送到对应的QObject对象。
  2. QObject对象会调用对应的槽函数(如onWindowResized()等)来处理这个事件。
  3. 如果我们在槽函数中调用了虚函数(如resizeEvent()等),那么Qt会调用我们的函数,而不是QWidget的函数。
  4. 在我们的函数中,我们可以编写自定义的事件处理代码,实现我们想要的功能。

这就是虚函数在Qt信号槽机制中的作用。通过虚函数,我们可以在信号槽机制中实现更复杂的功能。

在使用虚函数时,需要注意一些问题。虚函数的重写必须遵循一定的规则,例如函数的参数列表和返回类型必须完全相同。虚函数的调用可能会有一些性能开销,因为虚函数的调用需要通过虚函数表进行,这会比直接的函数调用慢一些。因此,如果一个函数被频繁调用,那么可能需要考虑其他的设计方案。最后,虚函数的使用需要谨慎,不恰当的使用可能会导致程序的错误或者性能问题。

5.3 Qt中虚函数的性能优化策略 (Performance Optimization Strategies for Virtual Functions in Qt)

虽然虚函数在Qt中有着广泛的应用,但是它们的使用也会带来一些性能开销。这主要是因为虚函数的调用需要通过虚函数表进行,这会比直接的函数调用慢一些。因此,如果一个虚函数被频繁调用,那么可能需要考虑一些优化策略。

5.3.1 避免不必要的虚函数调用 (Avoid Unnecessary Virtual Function Calls)

首先,我们应该尽量避免不必要的虚函数调用。如果一个函数并不需要使用到虚函数的多态性,那么就没有必要将它声明为虚函数。例如,如果一个函数只是进行一些简单的计算或者数据访问,那么就可以直接声明为普通函数。

5.3.2 使用内联函数 (Use Inline Functions)

内联函数是一种可以提高程序性能的技术。当一个函数被声明为内联函数时,编译器会尽可能地将这个函数的代码直接嵌入到调用它的地方,从而避免函数调用的开销。

虽然虚函数不能直接声明为内联函数,但是我们可以通过一些技巧来实现类似的效果。例如,我们可以在虚函数中调用一个内联函数,这样虚函数的大部分代码就可以被直接嵌入到调用它的地方。

5.3.3 使用最终修饰符 (Use Final Modifier)

在C++11中,引入了一个新的关键字final,它可以用来修饰虚函数。当一个虚函数被声明为final时,它就不能在子类中被重写。这样,编译器就可以更好地优化这个函数的调用,提高程序的性能。

例如,我们可以这样声明一个虚函数:

class MyBaseClass
{
public:
    virtual void myFunction() final
    {
        // 函数的代码
    }
};

在这个例子中,myFunction()就是一个最终虚函数。它在MyBaseClass中被声明为final,所以不能在子类中被重写。

虽然这些优化策略可以提高程序的性能,但是它们也可能会带来一些问题。例如,内联函数可能会增加程序的大小,最终修饰符可能会限制程序的灵活性。因此,在使用这些策略时,我们需要根据具体的情况进行权衡。

6. C++名著中的虚函数解析 (Analysis of Virtual Functions in C++ Masterpieces)

6.1 《C++ Primer》中的虚函数解析 (Analysis of Virtual Functions in “C++ Primer”)

《C++ Primer》是一本深受广大C++程序员喜爱的经典教材,其中对虚函数的解析深入浅出,让我们一起来探索一下。

6.1.1 虚函数的定义与作用

在《C++ Primer》中,虚函数被定义为在基类中声明的,用于在派生类中被重写的函数。它的主要作用是实现多态性,即允许我们使用基类的指针或引用来操作派生类对象。

让我们用一个表格来更直观地理解虚函数的定义和作用:

术语 定义 作用
虚函数 在基类中声明的,用于在派生类中被重写的函数 实现多态性

6.1.2 虚函数的使用场景

《C++ Primer》中提到,虚函数主要用于以下两种场景:

  1. 当我们希望通过基类的指针或引用来操作派生类对象时,可以使用虚函数。
  2. 当我们希望在派生类中改变基类的行为时,可以通过重写虚函数来实现。

这就像是我们在生活中的角色扮演,每个人都有不同的角色,如父亲、儿子、老师、学生等,虽然我们都是人,但在不同的角色中,我们的行为会有所不同。这就是多态性的体现,虚函数就是实现这种多态性的工具。

6.1.3 虚函数的注意事项

虽然虚函数为我们提供了强大的功能,但在使用时也需要注意一些问题。《C++ Primer》中提到,虚函数在构造函数和析构函数中不会表现出虚的特性,这是因为在构造函数和析构函数中,对象的类型是固定的,不会发生多态。

这就像我们在装修房子时,无论房子最后会变成什么样,在装修过程中,它始终是一个未完成的工程,我们不能对它进行装饰。同样,虚函数在构造和析构过程中,无法表现出其多态的特性。

以上就是《C++ Primer》中关于虚函数的一些基本解析,希望能帮助你更好地理解和使用虚函数。

6.2 《Effective C++》中的虚函数解析 (Analysis of Virtual Functions in “Effective C++”)

在Scott Meyers的《Effective C++》一书中,虚函数的使用和理解被赋予了极高的重要性。以下是从该书中提取的一些关于虚函数的关键观点和建议。

6.2.1 避免遮掩继承来的名称 (Avoid Hiding Inherited Names)

在C++中,如果派生类中的函数与基类中的函数同名,但参数不同,那么基类中的函数在派生类中会被遮掩。这可能会导致一些意想不到的结果。例如,如果基类有一个虚函数,而派生类有一个同名但参数不同的函数,那么在派生类对象上调用该函数时,可能不会调用到预期的虚函数。因此,Meyers建议使用using声明或者函数重载来避免这种情况。

class Base {
public:
    virtual void foo(int);
};
class Derived : public Base {
public:
    using Base::foo;  // 使用using声明来避免遮掩
    virtual void foo(double);
};

6.2.2 理解虚函数的调用成本 (Understand the Cost of Virtual Function Calls)

虚函数调用的成本主要体现在两个方面:运行时性能和内存使用。虚函数调用需要通过虚函数表进行,这比直接函数调用要慢一些。此外,每个有虚函数的对象都需要存储一个指向虚函数表的指针,这会增加对象的内存占用。因此,如果性能和内存使用是关键考虑因素,那么可能需要考虑其他设计方案,如模板和泛型编程。

6.2.3 虚析构函数的重要性 (Importance of Virtual Destructors)

如果基类的析构函数不是虚函数,那么通过基类指针删除派生类对象时,可能不会调用派生类的析构函数,导致资源泄漏。因此,Meyers强调,如果一个类有任何虚函数,那么它的析构函数通常也应该是虚函数。

class Base {
public:
    virtual ~Base() {}  // 虚析构函数
};
class Derived : public Base {
public:
    ~Derived() override {
        // 清理派生类的资源
    }
};

6.3 《More Effective C++》中的虚函数解析 (Analysis of Virtual Functions in “More Effective C++”)

在Scott Meyers的《More Effective C++》一书中,虚函数的使用和理解被进一步深化。以下是从该书中提取的一些关于虚函数的关键观点和建议。

6.3.1 虚函数与构造/析构函数 (Virtual Functions and Constructors/Destructors)

在C++中,构造函数和析构函数中调用虚函数可能不会产生预期的结果。因为在构造和析构过程中,对象的类型会逐级变化。例如,在基类的构造函数中,对象的类型是基类,因此调用虚函数会调用基类的版本,而不是派生类的版本。这可能会导致一些意想不到的结果。因此,Meyers建议避免在构造函数和析构函数中调用虚函数。

6.3.2 虚函数与运算符重载 (Virtual Functions and Operator Overloading)

在C++中,运算符重载通常不应该是虚函数。因为运算符重载通常需要确保类型的一致性,而虚函数的动态绑定可能会破坏这种一致性。例如,如果一个类重载了operator+并将其声明为虚函数,那么在派生类中重载operator+时,可能会导致类型不一致的问题。因此,Meyers建议避免将运算符重载声明为虚函数。

6.3.3 虚函数与异常处理 (Virtual Functions and Exception Handling)

在C++中,虚函数可以抛出异常,但在派生类中重载虚函数时,需要注意异常规格(exception specification)。如果派生类的虚函数抛出的异常不在基类虚函数的异常规格中,那么可能会导致未定义行为。因此,Meyers建议在设计虚函数的异常规格时,要考虑到派生类可能抛出的异常。

以上就是《More Effective C++》中关于虚函数的一些主要观点。这些观点不仅提供了关于如何正确使用虚函数的实用建议,还揭示了虚函数在C++对象模型中的核心地位。

6.4 《The C++ Programming Language》中的虚函数解析 (Analysis of Virtual Functions in “The C++ Programming Language”)

《The C++ Programming Language》是由C++的创造者Bjarne Stroustrup亲自撰写的一本书,被誉为C++的圣经。在这本书中,虚函数的解析非常详细,让我们一起来看看。

6.4.1 虚函数的基本概念

在《The C++ Programming Language》中,虚函数被描述为一种机制,它允许在派生类中改变基类的行为。这种机制是通过在基类中声明一个函数为虚函数,然后在派生类中重写这个函数来实现的。

这就像我们在生活中的角色扮演,每个人都有不同的角色,如父亲、儿子、老师、学生等,虽然我们都是人,但在不同的角色中,我们的行为会有所不同。这就是多态性的体现,虚函数就是实现这种多态性的工具。

6.4.2 虚函数的使用场景

在《The C++ Programming Language》中,Stroustrup强调了虚函数的一个重要使用场景:当我们希望通过基类的指针或引用来操作派生类对象时,可以使用虚函数。这种机制允许我们在运行时动态地改变对象的行为,这是实现多态性的关键。

使用场景 描述
通过基类的指针或引用操作派生类对象 虚函数允许我们在运行时动态地改变对象的行为,这是实现多态性的关键

6.4.3 虚函数的注意事项

在《The C++ Programming Language》中,Stroustrup特别提醒我们,虽然虚函数提供了强大的功能,但在使用时也需要注意一些问题。例如,虚函数在构造函数和析构函数中不会表现出虚的特性,这是因为在构造函数和析构函数中,对象的类型是固定的,不会发生多态。

这就像我们在装修房子时,无论房子最后会变成什么样,在装修过程中,它始终是一个未完成的工程,我们不能对它进行装饰。同样,虚函数在构造和析构过程中,无法表现出其多态的特性。

以上就是《The C++ Programming Language》中关于虚函数的一些基本解析,希望能帮助你更好地理解和使用虚函数。


【C/C++ 虚函数以及替代方案】C++ 虚函数的使用开销以及替代方案(三)https://developer.aliyun.com/article/1464359


目录
相关文章
|
2月前
|
C++
9. C++虚函数与多态
9. C++虚函数与多态
32 0
|
2月前
|
设计模式 编解码 算法
【C/C++ 虚函数以及替代方案】C++ 虚函数的使用开销以及替代方案(三)
【C/C++ 虚函数以及替代方案】C++ 虚函数的使用开销以及替代方案
43 0
|
2月前
|
算法 安全 编译器
【C++ 关键字 override】C++ 重写关键字override(强制编译器检查该函数是否覆盖已存在的虚函数)
【C++ 关键字 override】C++ 重写关键字override(强制编译器检查该函数是否覆盖已存在的虚函数)
27 0
|
2月前
|
算法 Java 编译器
【C++ 关键字 virtual 】C++ virtual 关键字(将成员函数声明为虚函数实现多态
【C++ 关键字 virtual 】C++ virtual 关键字(将成员函数声明为虚函数实现多态
27 0
|
2月前
|
存储 编译器 C++
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
|
7天前
|
C++
C++虚函数学习笔记
C++虚函数学习笔记
11 0
|
1月前
|
C++
C++示例(电脑组装)展现C++多态的优势以及虚函数抽象类的应用
C++示例(电脑组装)展现C++多态的优势以及虚函数抽象类的应用
|
2月前
|
存储 程序员 编译器
【C++ 模板类与虚函数】解析C++中的多态与泛型
【C++ 模板类与虚函数】解析C++中的多态与泛型
47 0
|
2月前
|
存储 算法 编译器
【C++入门到精通】C++入门 —— 多态(抽象类和虚函数的魅力)
多态是面向对象编程中的一个重要概念,指的是同一个消息被不同类型的对象接收时产生不同的行为。通俗来说,**就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态**。
44 0
|
1天前
|
编译器 C语言 C++
c++初阶------类和对象(六大默认构造函数的揭破)-3
c++初阶------类和对象(六大默认构造函数的揭破)