C++ 构造函数与析构函数详解:语法、调用规则与拷贝构造

阿澈
阿澈
发布时间:2026-10-16| 3人浏览过
原创

在 C++ 面向对象编程中,对象的初始化和清理是确保程序安全的关键环节。本文详细讲解构造函数与析构函数的语法特性、调用时机、四种分类方式及其实际应用场景,重点分析拷贝构造函数的触发条件与编译器默认规则,帮助开发者规范代码实现。

构造函数与析构函数的核心概念

类与对象的封装解决了数据访问权限问题,而对象的初始化和清理则涉及生命周期管理。若对象未初始化即使用,或使用后未及时清理,均可能导致不可预知的安全隐患。C++ 通过编译器自动调用的构造函数和析构函数来解决这一问题。即使开发者不显式定义,编译器也会提供空实现的构造函数和析构函数,这是编译器强制要求的行为。

基本语法与规则

构造函数用于对象创建时的初始化,其核心特征如下:

  • 名称必须与类名完全相同。
  • 没有返回值类型,甚至不能写 void。
  • 可以接受参数,支持函数重载。
  • 在对象创建时由编译器自动调用,无需手动触发,每个对象仅调用一次。

析构函数用于对象销毁前的资源清理,规则如下:

  • 名称格式为 ~类名,即在类名前加上波浪号 ~。
  • 同样没有返回值,不写 void。
  • 不可以有参数,因此不支持重载。
  • 在对象销毁前由系统自动调用,每个对象仅调用一次。

构造函数与析构函数的完整实现示例

以下代码展示了如何在类中定义构造函数和析构函数,并观察其在栈上对象的生命周期中的调用顺序。

#include <iostream>
using namespace std;

class Person
{
public:
	// 构造函数
	Person()
	{
		cout << "Person构造函数的调用" << endl;
	}
	// 析构函数
	~Person()
	{
		cout << "~Person析构函数的调用" << endl;
	}
};

void test01()
{
	Person p; // 栈上的对象,离开作用域后编译器自动释放
}

int main()
{
	test01();
	return 0;
}

在上述逻辑中,当在 test01 中创建 Person 对象 p 时,立即触发构造函数;当该函数执行完毕、对象超出作用域时,析构函数被自动调用以清理内存空间。

构造函数的分类与拷贝构造

根据参数是否存在,构造函数分为无参构造和有参构造;根据功能类型,可分为普通构造和拷贝构造。

拷贝构造函数是一种特殊的构造函数,用于使用一个已存在的对象来初始化另一个新对象。其标准格式如下:

Person(const Person &p) 
{
	age = p.age; // 将传入对象的属性拷贝到当前对象
	cout << "Person的拷贝构造函数调用" << endl;
}

关键在于参数必须是常量引用(const Person &)。这能避免无限递归调用(如果传值会再次触发拷贝构造),同时保证源对象不被修改。虽然两个对象属性相同,但它们在内存中的地址是不同的。

对象实例化的三种调用方式

为了清晰展示不同构造方式的差异,先构建包含所有类型构造函数的类结构:

class Person
{
public:
	Person() { cout << "无参构造" << endl; }
	Person(int a) { age = a; cout << "有参构造" << endl; }
	Person(const Person &p) { age = p.age; cout << "拷贝构造" << endl; }
	~Person() { cout << "析构" << endl; }
	int age;
};

1. 括号法(推荐)

这是最直观的实例化方式,但在调用默认构造函数时需特别注意:Person p1() 会被编译器视为函数声明而非对象创建,应写作 Person p1。

Person p;      // 调用无参构造
Person p2(10); // 调用有参构造
Person p3(p2); // 调用拷贝构造

2. 显示法

明确指定类型进行构造,常用于复杂初始化或匿名对象场景。需要注意的是,不要尝试用拷贝构造去初始化匿名对象(如 Person p(Person(p2)) 这种写法在某些语境下可能引发重定义或歧义错误),且匿名对象在当前语句执行完毕后会被立即回收。

Person p2 = Person(10); // 有参构造
Person p3 = Person(p2); // 拷贝构造
Person(100);            // 匿名对象,即时回收

3. 隐式转换法(不推荐)

利用赋值操作符触发构造,虽然可行但代码意图不明显,建议优先使用上述两种显式方法。

Person p2 = 10;   // 隐式转换为有参构造
Person p3 = p2;   // 隐式转换为拷贝构造

拷贝构造函数的实际调用场景

理解何时触发拷贝构造对于性能优化至关重要。以下是三种典型场景:

1. 用一个对象初始化另一个对象

这是最直接的场景。主函数中创建一个对象后,使用它去初始化一个新对象,编译器会自动调用拷贝构造函数。

void test01()
{
	Person p1(20);
	Person p2(p1); // 触发拷贝构造
	cout << "p2的年龄:" << p2.m_age << endl;
}

2. 值传递方式作为函数参数

当对象以值传递(Pass by Value)的方式传入函数时,形参会接收实参的一个副本,从而触发拷贝构造。

void doWork(Person p) { } // 形参 p 是实参的副本

void test02()
{
	Person p; // 调用无参构造
	doWork(p); // 进入函数时,触发拷贝构造生成形参
}              // 函数结束,形参析构,随后 p 析构

3. 函数返回值返回局部对象

当函数以值返回一个局部对象时,需要将局部对象的内容拷贝到调用者的栈帧中(尽管现代编译器通常通过 RVO/NRVO 进行优化,但从语言语义角度看,触发了拷贝构造的逻辑过程)。

Person doWork2()
{
	Person p1;
	cout << (int)&p1<< " 1 " << endl;
	return p1; // 语义上:拷贝构造临时对象,然后 p1 析构
}

void test03()
{
	Person p = doWork2(); // 调用者接收副本
	cout << (int)&p<< " 2 " << endl;
}

注意观察输出结果,局部变量 p1 的地址与最终 p 的地址不同,证明它们是两个独立的对象,中间经历了值的拷贝。

编译器提供的默认构造函数规则

C++ 编译器会在特定条件下向类中注入三个关键成员函数:

  1. 默认构造函数(空实现)
  2. 析构函数(空实现)
  3. 拷贝构造函数(执行浅拷贝/值拷贝)

重要规则:

  • 自定义有参构造:如果你定义了有参数的构造函数,编译器将不再提供默认的无参构造函数。此时若尝试使用 Person p1; 将会报错,因为找不到匹配的无参函数。
  • 自定义拷贝构造:如果你显式定义了拷贝构造函数,编译器将不再提供其他的普通构造函数(包括默认构造和有参构造,具体取决于编译器标准和实现细节,通常意味着你需要自行补充其他必要的构造需求)。

这意味着,只要手动编写了任何非默认的构造函数,就必须确保覆盖了所有预期的构造场景,否则会导致编译错误。

综上所述,掌握构造与析构函数的自动调用机制,并合理处理拷贝构造,是编写健壮的 C++ 面向对象代码的基础。

以上就是 C++ 构造函数与析构函数用法及调用规则的详细内容,更多关于 C++ 对象生命周期管理的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。