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. 如何避免内存泄漏
- 使用智能指针:使用
shared_ptr、unique_ptr等智能指针自动管理内存 - 确保析构函数正确:基类析构函数应设为虚函数
- 异常安全设计:使用 RAII(资源获取即初始化)技术
- 工具检测:使用内存泄漏检测工具(如 Valgrind)
- 代码审查:定期进行代码审查,确保资源正确释放
6. 常见问题和回答
问题 1:为什么基类析构函数要设为虚函数?
当通过基类指针删除派生类对象时,如果基类析构函数不是虚函数,只会调用基类的析构函数,导致派生类的资源无法正确释放,造成内存泄漏。
问题 2:什么时候不需要虚析构函数?
如果类不会被用作基类,或者类的对象不会被通过基类指针删除,那么就不需要虚析构函数。
问题 3:析构函数可以是纯虚函数吗?
可以,但需要提供定义,否则会导致链接错误。
class Base {
public:
virtual ~Base() = 0;
};
Base::~Base() { cout << "Base destructor" << endl; }
总结
析构函数是 C++ 类中非常重要的成员函数,它负责在对象销毁时清理资源。虚析构函数在继承关系中特别重要,它可以确保派生类的资源正确释放,避免内存泄漏。我们应该始终在基类中定义虚析构函数,除非类明确不会被继承。
练习建议: 1. 设计一个类,包含动态分配的内存 2. 实现正确的构造函数和析构函数 3. 测试在继承关系中的行为 4. 使用智能指针避免手动管理内存