跳转至

C++ OOP 面试高频知识点 - 04

析构函数,虚析构函数作用、内存泄漏场景

1. 析构函数的基本概念

析构函数(Destructor) 是在对象销毁时自动调用的特殊成员函数,它用于清理对象的资源。析构函数的名称是类名前加 ~,且不能有参数和返回类型。

class MyClass {
public:
    MyClass() {
        cout << "Constructor called!" << endl;
    }

    ~MyClass() {
        cout << "Destructor called!" << endl;
    }
};

int main() {
    MyClass obj; // 调用构造函数
    return 0; // 离开作用域时调用析构函数
}

2. 析构函数的作用

2.1 清理资源

析构函数主要用于清理对象拥有的资源,如: - 动态分配的内存 - 文件句柄 - 网络连接 - 数据库连接等

class FileHandler {
public:
    FileHandler(const string& filename) {
        file = fopen(filename.c_str(), "r");
        if (!file) {
            throw runtime_error("Failed to open file");
        }
    }

    ~FileHandler() {
        if (file) {
            fclose(file);
            cout << "File closed" << endl;
        }
    }

private:
    FILE* file;
};

2.2 防止内存泄漏

如果对象拥有动态分配的内存,在对象销毁时必须释放这些内存,否则会导致内存泄漏。

class MyClass {
public:
    MyClass() {
        data = new int[100];
    }

    ~MyClass() {
        delete[] data; // 必须释放内存
    }

private:
    int* data;
};

3. 虚析构函数

3.1 虚析构函数的作用

在继承关系中,如果基类的析构函数不是虚函数,当通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,导致内存泄漏。

class Base {
public:
    Base() { cout << "Base constructor" << endl; }
    ~Base() { cout << "Base destructor" << endl; } // 不是虚函数
};

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

int main() {
    Base* ptr = new Derived();
    delete ptr; // 只会调用 Base 的析构函数
    return 0;
}

输出:

Base constructor
Derived constructor
Base destructor // 没有调用 Derived 的析构函数!

3.2 如何定义虚析构函数

只需要在基类的析构函数前加 virtual 关键字即可:

class Base {
public:
    Base() { cout << "Base constructor" << endl; }
    virtual ~Base() { cout << "Base destructor" << endl; } // 虚析构函数
};

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

int main() {
    Base* ptr = new Derived();
    delete ptr; // 先调用 Derived 的析构函数,再调用 Base 的析构函数
    return 0;
}

输出:

Base constructor
Derived constructor
Derived destructor // 正确调用了 Derived 的析构函数
Base destructor

4. 内存泄漏场景

4.1 未释放动态分配的内存

void func() {
    int* p = new int[10];
    // 没有 delete[] p;
}

int main() {
    func(); // 内存泄漏!
    return 0;
}

4.2 基类析构函数不是虚函数

class Base {
public:
    virtual void foo() = 0;
    ~Base() { cout << "Base destructor" << endl; }
};

class Derived : public Base {
public:
    Derived() { data = new int[100]; }
    ~Derived() { 
        delete[] data; 
        cout << "Derived destructor" << endl; 
    }
    void foo() override {}

private:
    int* data;
};

int main() {
    Base* ptr = new Derived();
    delete ptr; // 内存泄漏!
    return 0;
}

4.3 异常导致的资源未释放

void func() {
    FileHandler f("test.txt"); // 构造函数成功
    throw runtime_error("Something went wrong");
    // 异常导致析构函数未被调用!
}

4.4 循环引用

在使用智能指针时,如果两个对象相互引用,可能导致循环引用,使得智能指针无法正确释放内存。

class A {
public:
    shared_ptr<B> b;
    ~A() { cout << "A destructor" << endl; }
};

class B {
public:
    shared_ptr<A> a;
    ~B() { cout << "B destructor" << endl; }
};

int main() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    a->b = b;
    b->a = a;
    return 0; // 循环引用导致内存泄漏!
}

5. 如何避免内存泄漏

  1. 使用智能指针:使用 shared_ptrunique_ptr 等智能指针自动管理内存
  2. 确保析构函数正确:基类析构函数应设为虚函数
  3. 异常安全设计:使用 RAII(资源获取即初始化)技术
  4. 工具检测:使用内存泄漏检测工具(如 Valgrind)
  5. 代码审查:定期进行代码审查,确保资源正确释放

6. 常见问题和回答

问题 1:为什么基类析构函数要设为虚函数?

当通过基类指针删除派生类对象时,如果基类析构函数不是虚函数,只会调用基类的析构函数,导致派生类的资源无法正确释放,造成内存泄漏。

问题 2:什么时候不需要虚析构函数?

如果类不会被用作基类,或者类的对象不会被通过基类指针删除,那么就不需要虚析构函数。

问题 3:析构函数可以是纯虚函数吗?

可以,但需要提供定义,否则会导致链接错误。

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

Base::~Base() { cout << "Base destructor" << endl; }

总结

析构函数是 C++ 类中非常重要的成员函数,它负责在对象销毁时清理资源。虚析构函数在继承关系中特别重要,它可以确保派生类的资源正确释放,避免内存泄漏。我们应该始终在基类中定义虚析构函数,除非类明确不会被继承。


练习建议: 1. 设计一个类,包含动态分配的内存 2. 实现正确的构造函数和析构函数 3. 测试在继承关系中的行为 4. 使用智能指针避免手动管理内存