MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++类虚析构函数的调用顺序

2024-07-215.0k 阅读

C++类虚析构函数的调用顺序

在C++编程中,类的继承体系和析构函数的合理使用是非常重要的概念。虚析构函数在处理继承体系中的对象销毁时扮演着关键角色,理解其调用顺序对于编写健壮、无内存泄漏的代码至关重要。

基本概念回顾

析构函数

析构函数是类的一种特殊成员函数,它在对象生命周期结束时自动调用,用于执行清理工作,例如释放对象占用的动态分配内存等资源。析构函数的名称与类名相同,但前面加上波浪线 ~。例如,对于类 MyClass,其析构函数为 ~MyClass()

虚函数

虚函数是在基类中使用 virtual 关键字声明的成员函数。当通过基类指针或引用调用虚函数时,C++ 运行时系统会根据对象的实际类型来决定调用哪个类的虚函数版本,这就是所谓的动态绑定或运行时多态。

虚析构函数

当一个类可能会作为基类,并且通过基类指针删除派生类对象时,就需要将基类的析构函数声明为虚函数。如果基类析构函数不是虚函数,那么通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,这将导致派生类中分配的资源无法正确释放,从而产生内存泄漏。

调用顺序的一般规则

在一个简单的继承体系中,当通过基类指针删除派生类对象时,虚析构函数的调用顺序遵循以下规则:

  1. 首先调用派生类的析构函数:这是因为派生类对象包含了自身特有的数据成员和资源,需要先进行清理。
  2. 然后调用直接基类的析构函数:在派生类析构函数执行完毕后,会调用其直接基类的析构函数,以清理基类部分的资源。
  3. 按照继承层次依次向上调用基类的析构函数:如果存在多层继承,会沿着继承链从最底层的派生类开始,依次调用各个基类的析构函数,直到最顶层的基类。

单一继承体系下的示例

下面通过一个简单的单一继承示例来演示虚析构函数的调用顺序。

#include <iostream>

class Base {
public:
    Base() {
        std::cout << "Base constructor called." << std::endl;
    }
    virtual ~Base() {
        std::cout << "Base destructor called." << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructor called." << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;
    return 0;
}

在上述代码中:

  1. Base:有一个构造函数和一个虚析构函数。构造函数输出 “Base constructor called.”,析构函数输出 “Base destructor called.”。
  2. Derived:从 Base 类公有继承,有自己的构造函数和析构函数。构造函数输出 “Derived constructor called.”,析构函数输出 “Derived destructor called.”。
  3. main 函数:创建一个 Derived 类对象,并通过 Base 类指针指向它,然后使用 delete 操作符删除该指针。

运行上述代码,输出结果如下:

Base constructor called.
Derived constructor called.
Derived destructor called.
Base destructor called.

可以看到,对象创建时,先调用基类构造函数,再调用派生类构造函数;对象销毁时,先调用派生类析构函数,再调用基类析构函数,符合前面提到的一般规则。

多重继承体系下的情况

在多重继承体系中,虚析构函数的调用顺序会稍微复杂一些,但仍然遵循基本的原则。

#include <iostream>

class Base1 {
public:
    Base1() {
        std::cout << "Base1 constructor called." << std::endl;
    }
    virtual ~Base1() {
        std::cout << "Base1 destructor called." << std::endl;
    }
};

class Base2 {
public:
    Base2() {
        std::cout << "Base2 constructor called." << std::endl;
    }
    virtual ~Base2() {
        std::cout << "Base2 destructor called." << std::endl;
    }
};

class Derived : public Base1, public Base2 {
public:
    Derived() {
        std::cout << "Derived constructor called." << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
};

int main() {
    Base1* ptr1 = new Derived();
    delete ptr1;

    Base2* ptr2 = new Derived();
    delete ptr2;

    return 0;
}

在这个多重继承的示例中:

  1. Base1 类和 Base2:都有构造函数和虚析构函数,分别输出相应的构造和析构信息。
  2. Derived:从 Base1Base2 公有继承,有自己的构造函数和析构函数。
  3. main 函数:分别通过 Base1 类指针和 Base2 类指针创建并删除 Derived 类对象。

运行代码,输出结果如下:

Base1 constructor called.
Base2 constructor called.
Derived constructor called.
Derived destructor called.
Base2 destructor called.
Base1 destructor called.

Base1 constructor called.
Base2 constructor called.
Derived constructor called.
Derived destructor called.
Base2 destructor called.
Base1 destructor called.

可以观察到,对象创建时,按照继承列表中基类的顺序调用构造函数,即先 Base1,再 Base2。对象销毁时,先调用派生类 Derived 的析构函数,然后按照与构造函数相反的顺序调用基类的析构函数,即先 Base2,再 Base1

虚析构函数与内存管理

正确使用虚析构函数对于内存管理至关重要。考虑以下代码示例,其中基类析构函数不是虚函数:

#include <iostream>
#include <cstring>

class Base {
public:
    Base(const char* str) {
        data = new char[strlen(str) + 1];
        std::strcpy(data, str);
        std::cout << "Base constructor called for: " << data << std::endl;
    }
    ~Base() {
        std::cout << "Base destructor called for: " << data << std::endl;
        delete[] data;
    }
private:
    char* data;
};

class Derived : public Base {
public:
    Derived(const char* str1, const char* str2) : Base(str1) {
        extraData = new char[strlen(str2) + 1];
        std::strcpy(extraData, str2);
        std::cout << "Derived constructor called for: " << extraData << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called for: " << extraData << std::endl;
        delete[] extraData;
    }
private:
    char* extraData;
};

int main() {
    Base* ptr = new Derived("Base part", "Derived part");
    delete ptr;
    return 0;
}

在这个例子中,Base 类有一个 char* 类型的数据成员 data,在构造函数中分配内存并在析构函数中释放。Derived 类从 Base 类继承,并添加了自己的 char* 类型数据成员 extraData,同样在构造函数中分配内存并在析构函数中释放。

然而,由于 Base 类的析构函数不是虚函数,当通过 Base 类指针删除 Derived 类对象时,只会调用 Base 类的析构函数,而 Derived 类中为 extraData 分配的内存无法释放,导致内存泄漏。

Base 类的析构函数声明为虚函数:

#include <iostream>
#include <cstring>

class Base {
public:
    Base(const char* str) {
        data = new char[strlen(str) + 1];
        std::strcpy(data, str);
        std::cout << "Base constructor called for: " << data << std::endl;
    }
    virtual ~Base() {
        std::cout << "Base destructor called for: " << data << std::endl;
        delete[] data;
    }
private:
    char* data;
};

class Derived : public Base {
public:
    Derived(const char* str1, const char* str2) : Base(str1) {
        extraData = new char[strlen(str2) + 1];
        std::strcpy(extraData, str2);
        std::cout << "Derived constructor called for: " << extraData << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called for: " << extraData << std::endl;
        delete[] extraData;
    }
private:
    char* extraData;
};

int main() {
    Base* ptr = new Derived("Base part", "Derived part");
    delete ptr;
    return 0;
}

修改后,当通过 Base 类指针删除 Derived 类对象时,会先调用 Derived 类的析构函数,释放 extraData 所占用的内存,然后调用 Base 类的析构函数,释放 data 所占用的内存,从而避免了内存泄漏。

纯虚析构函数

在某些情况下,基类可能希望提供一个纯虚析构函数。纯虚析构函数仍然需要在类外进行定义,即使它没有实际的代码。

#include <iostream>

class Base {
public:
    virtual ~Base() = 0;
};

Base::~Base() {
    std::cout << "Base pure virtual destructor called." << std::endl;
}

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;
    return 0;
}

在上述代码中:

  1. Base:定义了一个纯虚析构函数,并在类外进行了定义。纯虚析构函数的存在使得 Base 类成为抽象类,不能直接实例化。
  2. Derived:从 Base 类继承,并实现了自己的析构函数。
  3. main 函数:创建一个 Derived 类对象,并通过 Base 类指针删除它。

运行代码,输出结果如下:

Derived destructor called.
Base pure virtual destructor called.

可以看到,纯虚析构函数的调用顺序与普通虚析构函数相同,先调用派生类析构函数,再调用基类析构函数。

菱形继承与虚析构函数

菱形继承是多重继承的一种特殊情况,它可能会导致数据冗余和歧义问题。虚析构函数在菱形继承体系中同样起着重要作用。

#include <iostream>

class Top {
public:
    Top() {
        std::cout << "Top constructor called." << std::endl;
    }
    virtual ~Top() {
        std::cout << "Top destructor called." << std::endl;
    }
};

class Middle1 : virtual public Top {
public:
    Middle1() {
        std::cout << "Middle1 constructor called." << std::endl;
    }
    ~Middle1() {
        std::cout << "Middle1 destructor called." << std::endl;
    }
};

class Middle2 : virtual public Top {
public:
    Middle2() {
        std::cout << "Middle2 constructor called." << std::endl;
    }
    ~Middle2() {
        std::cout << "Middle2 destructor called." << std::endl;
    }
};

class Bottom : public Middle1, public Middle2 {
public:
    Bottom() {
        std::cout << "Bottom constructor called." << std::endl;
    }
    ~Bottom() {
        std::cout << "Bottom destructor called." << std::endl;
    }
};

int main() {
    Top* ptr = new Bottom();
    delete ptr;
    return 0;
}

在这个菱形继承示例中:

  1. Top:是顶层基类,有构造函数和虚析构函数。
  2. Middle1Middle2:从 Top 类虚继承,分别有自己的构造函数和析构函数。
  3. Bottom:从 Middle1Middle2 类继承,有自己的构造函数和析构函数。
  4. main 函数:通过 Top 类指针创建并删除 Bottom 类对象。

运行代码,输出结果如下:

Top constructor called.
Middle1 constructor called.
Middle2 constructor called.
Bottom constructor called.
Bottom destructor called.
Middle2 destructor called.
Middle1 destructor called.
Top destructor called.

可以观察到,在菱形继承体系中,虚析构函数的调用顺序依然遵循从最底层派生类开始,依次向上调用各个基类析构函数的规则。同时,虚继承保证了 Top 类的子对象在 Bottom 类中只有一份实例,避免了数据冗余。

虚析构函数与异常处理

在处理异常时,虚析构函数的调用顺序也需要特别关注。当在构造函数或成员函数中抛出异常时,C++ 会自动调用已构造对象的析构函数,以确保资源的正确释放。

#include <iostream>
#include <stdexcept>

class Base {
public:
    Base() {
        std::cout << "Base constructor called." << std::endl;
    }
    virtual ~Base() {
        std::cout << "Base destructor called." << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructor called. Throwing exception..." << std::endl;
        throw std::runtime_error("Exception in Derived constructor");
    }
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
};

int main() {
    try {
        Base* ptr = new Derived();
        delete ptr;
    } catch (const std::runtime_error& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中,Derived 类的构造函数抛出一个异常。当异常抛出时,Derived 类的析构函数不会被调用,因为对象的构造尚未完成。然而,Base 类的析构函数会被调用,因为 Base 类部分已经成功构造。

运行代码,输出结果如下:

Base constructor called.
Derived constructor called. Throwing exception...
Base destructor called.
Caught exception: Exception in Derived constructor

如果在 Derived 类的成员函数中抛出异常,并且对象已经完全构造,那么虚析构函数的调用顺序将遵循正常的规则。

#include <iostream>
#include <stdexcept>

class Base {
public:
    Base() {
        std::cout << "Base constructor called." << std::endl;
    }
    virtual ~Base() {
        std::cout << "Base destructor called." << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructor called." << std::endl;
    }
    void throwException() {
        std::cout << "Throwing exception from Derived member function." << std::endl;
        throw std::runtime_error("Exception in Derived member function");
    }
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
};

int main() {
    try {
        Base* ptr = new Derived();
        ptr->throwException();
        delete ptr;
    } catch (const std::runtime_error& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

运行上述代码,输出结果如下:

Base constructor called.
Derived constructor called.
Throwing exception from Derived member function.
Derived destructor called.
Base destructor called.
Caught exception: Exception in Derived member function

可以看到,当在 Derived 类的成员函数中抛出异常时,由于对象已经完全构造,先调用 Derived 类的析构函数,再调用 Base 类的析构函数。

总结虚析构函数调用顺序的本质

虚析构函数调用顺序的本质在于 C++ 语言对对象生命周期管理和多态性的支持。在继承体系中,对象的创建是从基类逐步向派生类进行,而对象的销毁则是逆向进行,这符合对象构造和析构的逻辑。虚析构函数通过动态绑定机制,确保在通过基类指针删除派生类对象时,能够正确调用到派生类的析构函数,从而实现资源的完整清理。

在复杂的继承体系如多重继承、菱形继承中,虚析构函数的调用顺序依然遵循从最底层派生类到顶层基类的顺序,这保证了在不同继承结构下资源释放的一致性和正确性。同时,结合异常处理机制,虚析构函数能在异常发生时合理地释放已构造对象的资源,增强了程序的健壮性。

深入理解虚析构函数的调用顺序,对于编写高质量、可靠的 C++ 代码,尤其是涉及到复杂继承和资源管理的场景,具有重要的指导意义。开发人员可以通过遵循这些规则,避免内存泄漏等常见问题,提高程序的稳定性和可维护性。

在实际编程中,需要根据具体的业务需求和继承结构,仔细设计和实现类的析构函数,确保虚析构函数的正确使用,以达到高效、安全的资源管理目标。同时,通过不断实践和分析不同继承体系下虚析构函数的行为,可以进一步加深对 C++ 语言特性的理解和掌握。

希望通过以上详细的讲解和丰富的代码示例,能帮助读者全面深入地理解 C++ 类虚析构函数的调用顺序及其本质,从而在实际项目中能够更加熟练、准确地运用这一重要概念。