为啥继承时基类的析构一般声明为虚函数

本文通过实例解释了为何在继承中需要将基类的析构函数声明为虚函数,以确保派生类资源正确释放,避免内存泄漏。同时探讨了在不确定抽象类哪些成员为纯虚函数时,如何通过声明析构函数为纯虚函数来解决问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、为啥继承时基类的析构函数声明为虚函数?

文字描述太抽象了,直接用代码给出答案。

(1)第一段代码:

#include<iostream>
using  namespace std ;

class Base
{
public:
	Base()
	{
		
		cout << "基类的构造函数被调用了" << endl;
		
	}
	 ~Base()
	{
		
		cout << "基类的析构函数被执行了" <<endl;
	}

};

class Derived :public Base
{
public:
	Derived(char *str = "")
	{
		if (str == NULL)
		{
			_Dstr = new char[1];
			*_Dstr = '\0';
		}
		else
		{
			_Dstr = new char [strlen(str) + 1];
			strcpy(_Dstr, str);
		}
		cout << "派生类的构造函数被调用了" << endl;

	}
	~Derived()
	{
		if (_Dstr != NULL)
		{
			delete [] _Dstr;
			_Dstr = NULL;
		}
		cout << "派生类的析构函数被执行了" <<endl;
	}
private:
	char *_Dstr;
};


void test()
{
	Base B1;
	Base & b = B1;   //基类的引用或指针指向基类对象。
}


int main()
{
	test();

	cout << "hello..." <<endl;
	system("pause");
	return 0;
}


(2)第二段代码

#include<iostream>
using  namespace std ;

class Base
{
public:
	Base()
	{
		
		cout << "基类的构造函数被调用了" << endl;
		
	}
	 ~Base()
	{
		
		cout << "基类的析构函数被执行了" <<endl;
	}

};

class Derived :public Base
{
public:
	Derived(char *str = "")
	{
		if (str == NULL)
		{
			_Dstr = new char[1];
			*_Dstr = '\0';
		}
		else
		{
			_Dstr = new char [strlen(str) + 1];
			strcpy(_Dstr, str);
		}
		cout << "派生类的构造函数被调用了" << endl;

	}
	~Derived()
	{
		if (_Dstr != NULL)
		{
			delete [] _Dstr;
			_Dstr = NULL;
		}
		cout << "派生类的析构函数被执行了" <<endl;
	}
private:
	char *_Dstr;
};


void test()
{

	Base * b = new Derived("str");   //基类的引用或指针指向派生类对象。
	delete b;
}


int main()
{
	test();

	cout << "hello..." <<endl;
	system("pause");
	return 0;
}


//分析答案发现析构时,只调用了基类的析构函数,没有调用派生类的析构函数,导致派生类内存泄露,


(3)第三段代码

#include<iostream>
using  namespace std ;

class Base
{
public:
	Base()
	{
		
		cout << "基类的构造函数被调用了" << endl;
		
	}
	 virtual ~Base()  //将基类的析构函数声明为虚函数。 
	{
		
		cout << "基类的析构函数被执行了" <<endl;
	}

};

class Derived :public Base
{
public:
	Derived(char *str = "")
	{
		if (str == NULL)
		{
			_Dstr = new char[1];
			*_Dstr = '\0';
		}
		else
		{
			_Dstr = new char [strlen(str) + 1];
			strcpy(_Dstr, str);
		}
		cout << "派生类的构造函数被调用了" << endl;

	}
	~Derived()
	{
		if (_Dstr != NULL)
		{
			delete [] _Dstr;
			_Dstr = NULL;
		}
		cout << "派生类的析构函数被执行了" <<endl;
	}
private:
	char *_Dstr;
};


void test()
{

	Base * b = new Derived("str");   //基类的引用或指针指向派生类对象。
	delete b;
}


int main()
{
	test();

	cout << "hello..." <<endl;
	system("pause");
	return 0;
}


//答案分析:析构时,先调用了派生类的析构函数,再调用基类的析构函数,不存在内存泄露。反过来看源代码只是发现只是将基类的析构函数声明为虚函数。


结论: 继承时,如果派生类有资源需要自己手动释放,最好将基类的析构函数声明为虚函数,这样我们可以通过基类的指针或引用去释放子类的资源。防止内存泄露。


2.当我们想要继承一个抽象类的时候,但是还不能确定抽象类的那些成员是纯虚函数,我们可以将抽象类的析构函数声明为纯虚函数。

(1)第一段代码

#include<iostream>
using  namespace std ;

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

class Derived: public Abstract
{
public:
	Derived()
	{
		cout << "派生类的构造函数被执行了" <<endl;
	}
	~Derived()
	{
		cout << "派生类的析构函数被执行了" << endl;
	}
};

void test()
{
	Derived d;
}

int main()
{
	test();

	cout << "hello..." <<endl;
	system("pause");
	return 0;
}

如果我们单纯的直接这么写,我们发现编译器报错,报下列的错误:


抽象类的析构函数.obj : error LNK2019: 无法解析的外部符号 "public: virtual __thiscall Abstract::~Abstract(void)" (??1Abstract@@UAE@XZ),该符号在函数 __unwindfunclet$??0Derived@@QAE@XZ


通过分析:我们发现定义派生类对象时,会先调用抽象类的构造函数,抽象类的构造函数没有显示的给出,编译器合成一个默认的构造函数,再调用派生类的构造函数,析构时,先调用派生类的析构函数,最后调用抽象类的析构函数,但是抽象类的析构函数已经显示的声明了,因此会调用程序员自己写的,但是这个析构函数没有函数体,所以编译器报错,说无法解析的外部符号,

解决方案:给抽象类的析构函数定义。看下面的代码:

(2)第二段代码:

#include<iostream>
using  namespace std ;

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

 Abstract:: ~Abstract()
{
	cout << "抽象类的析构函数被执行了" <<endl;
};

class Derived: public Abstract
{
public:
	Derived()
	{
		cout << "派生类的构造函数被执行了" <<endl;
	}
	~Derived()
	{
		cout << "派生类的析构函数被执行了" << endl;
	}
};

void test()
{
	Derived d;
}

int main()
{
	test();

	cout << "hello..." <<endl;
	system("pause");
	return 0;
}






<think>好的,我现在需要帮助用户理解为什么基类析构函数必须声明虚函数,以及它在多态和内存管理中的作用。用户已经提供了一些引用内容,我需要结合这些引用来组织一个清晰且详细的回答。 首先,我要回忆一下用户之前的问题和我的回答。用户之前询问了虚函数表的位置、多态的底层逻辑,以及模板的使用。现在他们聚焦在析构函数虚函数声明上,特别是基类的情况。根据引用,用户提供的资料提到了几个关键点: 1. 虚析构函数确保通过基类指针删除派生类对象,正确调用派生类的析构函数,避免资源泄漏。 2. 造函数不能是虚函数,而析构函数可以且应该声明虚函数。 3. 纯虚析构函数需要有定义,否则可能导致运行错误。 接下来,我需要将这些点整合成一个逻辑连贯的回答。首先,解释为什么基类析构函数需要是虚函数。这里的关键是多态中的对象销毁问题。当使用基类指针指向派生类对象,如果基类析构函数不是虚的,那么删除该指针只会调用基类析构函数,而不会调用派生类的,导致派生类分配的资源未被释放,引发内存泄漏。 然后,需要说明内存泄漏的具体例子。比如,基类可能有动态分配的资源,如果析构函数不是虚的,派生类中的资源不会被正确清理。这里可以举一个具体的代码示例,对比虚和非虚的不同行为。 接着,讨论纯虚析构函数的情况。根据引用[3],纯虚析构函数必须要有定义,否则会导致链接错误或运行崩溃。这是因为即使析构函数是纯虚的,派生类仍需要调用基类析构函数,所以必须提供实现。 然后,引用[4]提到造函数不能是虚函数,因为对象造的顺序是从基类到派生类,虚函数机制在对象造期间尚未建立。这可以作为一个对比点,帮助用户理解析构函数造函数在虚函数机制中的不同。 最后,总结基类析构函数声明为虚的重要性,强调其在多态中的必要性,以及如何正确使用虚析构函数来确保资源安全释放。同,可以提到一些设计原则,比如如果一个类被设计为基类(尤其是多态基类),其析构函数必须为虚函数,即使它没有其他虚函数。 在组织回答,需要确保结清晰,分点说明,每个部分都结合引用中的信息,并给出代码示例和对比,帮助用户直观理解。同,要注意使用用户提到的引用编号,如[^1]、[^3]等,以增强回答的可信度。 可能还需要考虑用户的知识水平,避免过于技术化的术语,或者适当解释术语,比如虚函数表、多态、内存泄漏等,确保用户能够理解。此外,用户可能对纯虚析构函数的实现感到困惑,需要详细说明为什么纯虚析构函数需要定义,而其他纯虚函数不需要。 最后,检查是否有遗漏的关键点,比如多重继承下的析构函数处理,但根据用户的问题和引用内容,这可能不是当前的重点,可以暂不涉及,保持回答的针对性。</think>### 基类析构函数必须声明虚函数的原因及重要性 #### 一、核心问题:多态对象销毁的资源泄漏 当通过**基类指针**删除**派生类对象**,若基类析构函数非虚,会导致**仅调用基类析构函数**,而**派生类的析构函数不会执行**,进而引发资源泄漏。这是多态内存管理的核心陷阱[^1][^4]。 **关键验证示例**: ```cpp class Base { public: ~Base() { cout << "~Base"; } // 非虚析构函数 }; class Derived : public Base { public: int* buffer = new int[1024]; // 动态分配资源 ~Derived() { delete[] buffer; // 资源释放 cout << "~Derived"; } }; int main() { Base* obj = new Derived(); delete obj; // 仅输出 ~Base → Derived的buffer泄漏! } ``` 输出结果:`~Base` **后果**:Derived类中的`buffer`未被释放,造成内存泄漏[^1][^3] --- #### 二、解决方案:虚析构函数的作用 将基类析构函数声明为`virtual`,确保**动态绑定**正确发生: ```cpp class Base { public: virtual ~Base() { ... } // 虚析构函数 }; ``` 此执行`delete obj`的完整过程: 1. 通过虚表定位到`Derived::~Derived()` 2. 执行派生类析构函数 → 释放`buffer` 3. **自动调用基类析构函数**(通过析构函数链式调用机制) 输出结果:`~Derived ~Base` 资源得到完整释放[^1][^4] --- #### 三、技术原理深度解 1. **虚函数调用机制** - 虚析构函数与其他虚函数共享**虚函数表**机制 - 通过虚表指针(vptr)找到正确的析构函数实现[^3] 2. **析构函数链式调用** ```cpp // 编译器自动生成的调用顺序 Derived::~Derived() { // 用户编写的代码 delete[] buffer; // 编译器插入基类调用 Base::~Base(); } ``` **关键特性**:即使基类析构函数是纯虚的,也必须提供实现(否则链接错误)[^3] --- #### 四、设计原则与最佳实践 1. **黄金法则** - 任何**可能被继承的类**(尤其是多态基类),必须声明析构函数[^4] - 即使基类没有其他虚函数,只要可能通过基类指针删除对象,就必须遵守此规则 2. **纯虚析构函数特殊情况** ```cpp class AbstractBase { public: virtual ~AbstractBase() = 0; // 纯虚声明 }; // 必须提供实现(否则派生类会崩溃) AbstractBase::~AbstractBase() {} ``` **意义**:强制要求派生类实现逻辑,同保证基类可执行 3. **错误场景对比** | 基类类型 | 行为表现 | 内存安全 | |--------------|----------------------------|----------| | 非虚 | 仅调用基类 | ❌ 泄漏 | | 虚 | 完整调用派生→基类 | ✅ 安全 | | 纯虚未实现 | 编译/链接错误或运行崩溃 | ❌ 危险 | --- #### 五、底层实现视角 1. **虚析构函数的特殊处理** - 编译器会为虚析构函数生成**两个版本**: - `Derived::~Derived()`(实际执行) - `Derived::_delete()`(处理operator delete的包装函数) 2. **对象销毁过程** ```mermaid graph TD A[delete基类指针] --> B[通过虚表定位派生类析构函数] B --> C[执行派生类析构函数体] C --> D[自动调用基类析构函数] D --> E[释放对象内存] ``` --- ### 总结:必须使用虚析构函数的场景 1. **多态基类**:任何通过基类指针操作派生类对象的场景 2. **资源管理类**:涉及文件句柄、网络连接、内存管理等资源的类层次结 3. **接口类**:包含纯虚函数的抽象基类(需同声明纯虚并提供实现) **最终建议**:在C++核心指南中,明确建议将多态基类析构函数声明为`virtual`,这是编写安全面向对象程序的基础要求[^1][^4]。 ### 相关问题 1. 如何检测程序中是否存在非虚析构函数导致的内存泄漏? 2. 多重继承场景下虚析构函数的工作机制有何不同? 3. C++中的`= default`语法对虚析构函数有什么影响? 4. 智能指针(如`std::unique_ptr`)如何与虚析构函数配合使用?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值