组合类型中成员对象的构造在类构造函数体执行前按声明顺序完成,析构则在其后按逆序自动进行,初始化列表是确保正确高效的唯一方式。

在C++中处理组合类型(即一个类包含其他类的对象作为其成员)时,构造函数和析构函数的行为是理解对象生命周期的核心。简单来说,成员对象的构造总是发生在包含它的类的构造函数体执行之前,并且遵循它们在类中声明的顺序。而成员对象的析构则是在包含它的类的析构函数体执行之后,以与构造顺序完全相反的顺序进行。理解这一点,对于编写健壮、高效且无内存泄漏的C++代码至关重要。
组合类型,顾名思义,就是一个类由其他类(或基本类型)的实例“组合”而成。比如一个
Car
Engine
Tire
当一个组合类型的对象被创建时,其构造过程并非简单地从上到下执行。实际上,它遵循一个严格的顺序:
这个顺序非常重要,特别是第二点。这意味着,当你的
Car
Engine
Tire
立即学习“C++免费学习笔记(深入)”;
析构过程则恰好相反:
这种机制确保了在包含对象被销毁前,其成员对象有机会清理自己的资源,并且在基类被销毁前,派生类和其成员对象已经完成清理。
#include <iostream>
#include <string>
#include <vector> // 引入vector,作为组合成员示例
class Engine {
public:
Engine(const std::string& type) : type_(type) {
std::cout << " Engine(" << type_ << ") constructed." << std::endl;
}
~Engine() {
std::cout << " Engine(" << type_ << ") destructed." << std::endl;
}
void start() {
std::cout << " Engine " << type_ << " starting..." << std::endl;
}
private:
std::string type_;
};
class Tire {
public:
Tire(int size) : size_(size) {
std::cout << " Tire(" << size_ << " inches) constructed." << std::endl;
}
~Tire() {
std::cout << " Tire(" << size_ << " inches) destructed." << std::endl;
}
private:
int size_;
};
class Car {
public:
// 注意这里初始化列表的使用
Car(const std::string& model, const std::string& engineType, int tireSize)
: model_(model), // 成员model_初始化
engine_(engineType), // 成员engine_初始化
frontLeftTire_(tireSize), // 成员frontLeftTire_初始化
frontRightTire_(tireSize),
rearLeftTire_(tireSize),
rearRightTire_(tireSize) // 成员rearRightTire_初始化
{
std::cout << "Car(" << model_ << ") constructed. Ready to go!" << std::endl;
engine_.start(); // 此时engine_已完全构造,可以安全使用
}
~Car() {
std::cout << "Car(" << model_ << ") destructed. Goodbye!" << std::endl;
// 成员析构函数会自动调用
}
private:
std::string model_;
Engine engine_; // 声明顺序:engine_
Tire frontLeftTire_; // 声明顺序:frontLeftTire_
Tire frontRightTire_;
Tire rearLeftTire_;
Tire rearRightTire_;
};
int main() {
std::cout << "--- Creating a Car object ---" << std::endl;
Car myCar("Sedan", "V6", 18);
std::cout << "--- Car object created ---" << std::endl;
// myCar在这里的生命周期内
std::cout << "--- Destroying the Car object ---" << std::endl;
return 0; // myCar在main函数结束时被销毁
}运行上述代码,你会清晰地看到构造和析构的顺序。
Engine
Tire
Car
Car
这是一个非常关键的问题,也是很多C++初学者容易困惑的地方。为什么我们不能在构造函数体内部像普通变量赋值那样去初始化成员对象呢?
原因在于C++对对象生命周期的严格管理。当一个类的对象被创建时,它的所有非静态成员对象必须在包含它的类的构造函数体执行之前就完成构造。这意味着,如果你在构造函数体内部写
engine_ = Engine("V6");Engine
engine_
Engine
Engine("V6")engine_
Engine
初始化列表(initializer list)正是为了解决这个问题而存在的。它提供了一个在成员对象被构造时,直接调用其特定构造函数的机制。通过
:
Car
engine_
class MyComplexMember {
public:
// 只有一个带参数的构造函数,没有默认构造函数
MyComplexMember(int value) : value_(value) {
std::cout << " MyComplexMember(" << value_ << ") constructed." << std::endl;
}
// ... 其他成员函数
private:
int value_;
};
class Container {
public:
// 必须使用初始化列表来构造 MyComplexMember
Container(int memberValue) : member_(memberValue) {
std::cout << "Container constructed." << std::endl;
}
// 如果尝试这样:
// Container(int memberValue) {
// member_ = MyComplexMember(memberValue); // 编译错误!MyComplexMember没有默认构造函数
// std::cout << "Container constructed." << std::endl;
// }
private:
MyComplexMember member_;
};在
Container
MyComplexMember
const
当我们谈论组合类型的析构函数时,其实更多的是在理解其自动化的机制。与构造函数需要你明确指定如何初始化成员不同,成员对象的析构是自动发生的,你通常不需要在包含类的析构函数中显式地去调用它们的析构函数。
正如前面提到的,析构顺序是与构造顺序完全相反的。这意味着:
Car
Car
Car
Car
rearRightTire_
rearLeftTire_
frontRightTire_
frontLeftTire_
engine_
Car
这个自动调用的机制是C++ RAII(Resource Acquisition Is Initialization)原则的体现。当一个对象超出其作用域时,其析构函数会被自动调用,从而确保该对象所持有的资源(无论是内存、文件句柄、网络连接还是其他)能够被正确释放。对于成员对象,这个原则同样适用。只要成员对象本身正确地管理了其内部资源(例如,
std::string
std::vector
// 假设有一个类需要手动管理资源,比如文件句柄
class FileHandle {
public:
FileHandle(const std::string& filename) : filename_(filename) {
// 模拟打开文件
std::cout << " FileHandle for " << filename_ << " opened." << std::endl;
}
~FileHandle() {
// 模拟关闭文件
std::cout << " FileHandle for " << filename_ << " closed." << std::endl;
}
private:
std::string filename_;
};
class Document {
public:
Document(const std::string& docName, const std::string& logFileName)
: name_(docName), logFile_(logFileName) { // logFile_ 作为成员对象
std::cout << " Document(" << name_ << ") constructed." << std::endl;
}
~Document() {
std::cout << " Document(" << name_ << ") destructed." << std::endl;
// 注意:这里不需要手动调用 logFile_.close() 或 delete logFile_
// FileHandle的析构函数会在Document析构函数体执行后自动调用
}
private:
std::string name_;
FileHandle logFile_; // 成员对象,其析构函数会自动调用
};
int main_doc() {
std::cout << "--- Creating a Document object ---" << std::endl;
Document myDoc("Report_Q1", "report_log.txt");
std::cout << "--- Document object created ---" << std::endl;
// myDoc在这里的生命周期内
std::cout << "--- Destroying the Document object ---" << std::endl;
return 0;
}通过
main_doc
FileHandle
Document
T*
delete
std::unique_ptr
std::shared_ptr
在组合类型的构造与析构中,确实有一些常见的陷阱,它们可能导致性能问题、未定义行为甚至程序崩溃。
陷阱一:忘记或忽视初始化列表的重要性,尤其是在成员对象没有默认构造函数时。 前面已经详细讨论过,这是最常见也是最直接的错误。如果你有一个成员类
MemberClass
MemberClass
class RequiredParam {
public:
RequiredParam(int id) : id_(id) {}
private:
int id_;
};
class BadContainer {
public:
BadContainer() {
// member_ = RequiredParam(1); // 错误:RequiredParam没有默认构造函数
// 如果RequiredParam有默认构造函数,这里会先默认构造,再赋值,效率低
}
private:
RequiredParam member_;
};正确的做法是始终使用初始化列表:
GoodContainer() : member_(1) {}陷阱二:在构造函数体中使用尚未完全初始化的成员。 虽然成员对象在构造函数体开始执行时就已经构造完成,但它们的初始化顺序是按照声明顺序来的。如果一个成员A依赖于成员B,而成员B在类中声明的位置晚于A,那么在A的构造函数中(或在A的初始化列表中)使用B,可能会导致未定义行为,因为B可能还没有被初始化。
class DependentMember {
public:
DependentMember(int val) : value_(val) { std::cout << " DependentMember(" << value_ << ") constructed." << std::endl; }
private:
int value_;
};
class OrderProblem {
public:
OrderProblem(int a_val, int b_val)
: b_(b_val), // b_先被初始化
a_(b_.value_ + a_val) // 错误!b_.value_在这里是未定义的,因为b_尚未初始化
// 实际上,这里是a_先被声明,所以a_会先被初始化
// 正确的顺序应该是b_先声明,a_后声明
{
std::cout << "OrderProblem constructed." << std::endl;
}
private:
DependentMember a_; // 声明在b_之前
DependentMember b_;
};
// 正确的声明顺序应该是:
class OrderCorrect {
public:
OrderCorrect(int a_val, int b_val)
: b_(b_val),
a_(b_.value_ + a_val) // 现在b_在a_之前声明,所以b_会在a_之前初始化,这里使用b_.value_是安全的
{
std::cout << "OrderCorrect constructed." << std::endl;
}
private:
DependentMember b_; // b_先声明
DependentMember a_; // a_后声明
};这个陷阱强调了成员声明顺序的重要性,它决定了成员的构造顺序。
陷阱三:手动管理原始指针成员,却忘记了“三/五/零法则”。 如果一个组合类型包含原始指针成员,并且这个指针指向的是动态分配的内存,那么你必须手动管理这块内存。这意味着:
delete
class RiskyContainer {
public:
RiskyContainer(int size) : data_(new int[size]), size_(size) {
std::cout << " RiskyContainer constructed, allocated " << size_ << " ints." << std::endl;
}
~RiskyContainer() {
delete[] data_; // 必须手动释放内存
std::cout << " RiskyContainer destructed, freed memory." << std::endl;
}
// 缺少拷贝构造函数和拷贝赋值运算符会导致浅拷贝问题
// 缺少移动构造函数和移动赋值运算符会导致效率低下或不正确
private:
int* data_;
int size_;
};
// 推荐使用智能指针:
#include <memory>
class SafeContainer {
public:
SafeContainer(int size) : data_(std::make_unique<int[]>(size)), size_(size) {
std::cout << " SafeContainer constructed, allocated " << size_ << " ints with unique_ptr." << std::endl;
}
~SafeContainer() {
std::cout << " SafeContainer destructed." << std::endl;
// unique_ptr 会自动释放内存,无需手动 delete[]
}
private:
std::unique_ptr<int[]> data_;
int size_;
};使用
std::unique_ptr
std::shared_ptr
陷阱四:构造函数中抛出异常。 如果一个成员对象的构造函数在初始化列表中抛出异常,或者包含类的构造函数体中抛出异常,那么包含类的构造函数将不会完成。C++标准规定,在这种情况下,所有已经成功构造的成员对象(以及基类子对象)都会被正确地析构。这是C++的异常安全保证之一。虽然这通常是期望的行为,但理解其机制可以帮助你设计
以上就是C++组合类型构造函数与析构函数使用方法的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号