C++对象的构造与析构
前言:学习C++,对象(类)是我们绕不开的话题。对象的构造(Construction)与析构(Destruction)是对象生命周期的两个端点,也是 C++ 资源管理的核心机制。
构造是对象的"出生证明",析构是对象的"死亡清理"
目录
一、构造函数
1.1 概念及特征
构造函数(Constructor)是类的一个特殊的成员函数主要用于在创建对象时自动调用,完成对象的初始化工作(为对象的数据成员分配内存并设置初始值)。
C++构造函数的基本特点主要是:函数名称与类名相同,无返回值,自动调用,支持重载,可由编译器自动生成。例如:
class AA{
public:
// 默认构造函数
AA() {
std::cout << "默认构造\n";
}
// 带参构造函数
AA(int id) : id_(id) {
std::cout << "带参构造: " << id_ << "\n";
}
private:
int id_ = 0;
};
1.2 构造类型
根据构造方式的不同,C++的构造函数可以分为多种类型:
1) 默认构造函数:
默认构造函数不接受任何参数。如果我们没有在类中没有显式定义任何构造函数,编译器会自动生成一个默认的构造函数。默认构造函数通常对类的每个非静态数据成员执行默认初始化。
class BB{
public:
BB() { m_value = 0; } // 默认构造函数
private:
int m_value;
};
2) 带参构造函数:
带参构造函数接受一个或多个参数,根据提供的参数值进行对象的初始化。带参构造函数可以重载,以支持不同数量或类型的参数组合。
class CC{
public:
CC(int a, float b) : m_a(a), m_b(b) {
// 带参构造函数体:使用参数初始化成员变量
}
private:
int m_a;
float m_b;
};
int main() {
CC c(3, 1.1f); // 使用带参构造函数创建对象
return 0;
}
3) 拷贝构造函数:
拷贝构造函数接受一个同类型对象的引用作为参数,用于创建一个与给定对象内容相同的新对象。其作用是用已存在对象的状态来初始化新对象,进行成员的逐个拷贝。
class DD {
public:
DD(const DD& other) {
m_real = other.m_real;
m_imag = other.m_imag;
}
private:
double m_real;
double m_imag;
};
int main() {
DD d1(1.0, 2.0);
DD d2 = c1; // 调用拷贝构造函数,用d1初始化d2
return 0;
}
使用拷贝构造函数,需要我们理解两个概念:深拷贝与浅拷贝。大家可以参考作者之前的文章:C++ 深拷贝与浅拷贝_浅拷贝和深拷贝 c++-CSDN博客
特别需要注意的是:如果我们没有显示地定义拷贝构造函数,那么系统默认生成的拷贝构造函数是浅拷贝,如果这时候类中存在指针变量,浅拷贝仅会复制指针的值,并没有副本的拷贝,多个指针指向了同一块内存,存在重复释放的问题。通常,我们需要自定义拷贝构造函数来实现深拷贝,避免该问题。
4) 移动构造函数:
移动构造函数接受一个同类型对象的右值引用,其作用是"直接窃取"源对象的资源,减少不必要的拷贝开销。将新对象的指针指向源对象原本的资源,并将源对象的指针成员置为nullptr,从而实现高效的资源转移。
class EE {
public:
// 移动构造函数
EE(EE&& other) noexcept : m_data(other.m_data), m_rows(other.m_rows) {
other.m_data = nullptr; // 将源对象的指针置空,防止析构时释放资源
other.m_rows = 0;
}
~EE() {
delete[] m_data;
}
private:
double* m_data;
size_t m_rows;
};
二、析构函数
2.1 概念及特征
析构函数(Destructor)是类的一个特殊成员函数,它的主要作用是在对象生命周期结束时自动调用,执行清理工作(释放对象占用的资源)。
C++析构函数的主要特点是:函数名为 ~类名,没有任何参数,也没有返回值;自动调用,不支持重载,可由编译器自动生成。例如:
class FF {
FILE* fp_;
public:
FF(const char* path) {
fp_ = fopen(path, "r");
}
~FF() {
if (fp_) {
fclose(fp_);
std::cout << "文件关闭,资源释放\n";
}
}
};
2.2 何时调用
析构函数一般在以下几种情况下会被自动调用:
1) 当对象离开作用域时,如 栈上的局部对象离开作用域时;
2) 显示delete,在堆上用new分配的对象,当执行delete操作时;
3) 临时对象的销毁,如 函数返回的临时对象,使用完毕后会自动调用析构函数
2.3 虚析构函数
需要特别注意的一点是:如果某个类是一个基类,我们需要将其析构函数声明为虚函数(virtual)。目的是,通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,确保资源的正确释放。
例如:
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor called\n";
}
};
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destructor called\n";
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 由于Base的析构函数是虚函数,delete ptr会正确调用Derived的析构函数
return 0;
}
三、构造与析构顺序
对于存在继承关系的类,基类与派生类的构造与析构的顺序是怎样的呢?记住一个原则:构造时先基后派,析构时先派后基。
构造的时候,先调用基类的构造函数,再调用派生类的构造函数;析构的时候相反,先调用派生类的析构函数,再调用基类的析构函数。
例如:
#include <iostream>
class Base {
public:
Base() { std::cout << "Base 构造函数\n"; }
virtual ~Base() { std::cout << "Base 析构函数\n"; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived 构造函数\n"; }
~Derived() { std::cout << "Derived 析构函数\n"; }
};
int main() {
Derived d;
return 0;
}
输出结果如下:
Base 构造函数
Derived 构造函数
Derived 析构函数
Base 析构函数更多推荐
所有评论(0)