代码仓库shanchuann/CPP-Learninng

在C++中,构造函数和析构函数是类的特殊成员函数,分别用于对象的初始化和资源清理,它们由编译器自动调用,是面向对象编程中管理对象生命周期的核心机制。

this指针

编译器对类型编译的过程中分为三个部分

  1. **类型解析:**编译器首先对代码中的类型声明进行解析,识别类型的定义、成员结构(如类的成员变量和成员函数)、继承关系等信息
  2. 函数识别:在语义分析阶段,编译器基于类型解析的结果,对程序中的函数返回类型、函数名、参数类型进行识别
  3. **成员方法改写:**无论成员方法是公有私有,他会对非静态成员方法加入常性this指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Pointer {
private:
int row, col;
public:
void SetRow(int r) {
row = r;
}
/* void SetRow(Pointer *const this,int r){
this->row = r;
}
*/
void SetCol(int c) {
col = c;
}
int GetRow() const;
int GetCol() const;
};
int Pointer::GetRow() const{
return row;
}
int Pointer::GetCol() const{
return col;
}
int main() {
Pointer pa,pb,pc,pd;
pa.SetRow(10); //SetRow(&pa,10);
pb.SetRow(20); //SetRow(&pb,20);
pc.SetCol(30); //SetCol(&pc,30);
pd.SetCol(40); //SetCol(&pd,40);
return 0;
}

image-20251021182535279

构造函数(Constructor)

构造函数是创建对象时自动调用的成员函数,主要作用是初始化对象的成员变量(如给成员变量赋初值、分配动态内存等)。

核心特点

  • 名字与类名完全相同:例如类名为Person,构造函数名也必须是Person
  • 无返回值:不能写void或其他返回类型(连return语句都不能带返回值)。
  • 可重载:可以定义多个参数不同的构造函数(参数个数或类型不同),满足不同的初始化需求。
  • 自动调用:创建对象时(如Person p;new Person();)由编译器自动调用,无需手动调用。
  • 一次调用:生存期内构造函数只能被调用一次,但可以重载构造函数。

默认构造函数

如果用户没有定义任何构造函数,编译器会自动生成一个默认构造函数(无参数,函数体为空)。
但如果用户定义了构造函数(无论是否有参数),编译器不会再生成默认构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <string>
using namespace std;

class Person {
private:
string name;
int age;
public:
// 无参构造函数(默认构造的手动实现)
Person() {
name = "Unknown";
age = 0;
cout << "无参构造函数被调用" << endl;
}

// 带参构造函数(重载)
Person(string n, int a) {
name = n;
age = a;
cout << "带参构造函数被调用" << endl;
}

void show() {
cout << "Name: " << name << ", Age: " << age << endl;
}
};

int main() {
Person p1; // 调用无参构造函数
p1.show(); // 输出:Name: Unknown, Age: 0

Person p2("Tom", 18); // 调用带参构造函数
p2.show(); // 输出:Name: Tom, Age: 18

return 0;
}
1
2
3
4
无参构造函数被调用
Name: Unknown, Age: 0
带参构造函数被调用
Name: Tom, Age: 18

析构函数(Destructor)

析构函数是对象生命周期结束时自动调用的成员函数,主要作用是清理对象占用的资源(如释放动态内存、关闭文件句柄等)。

核心特点

  • 名字为~类名:例如类名为Person,析构函数名是~Person
  • 无返回值:同构造函数,不能写返回类型。
  • 无参数:因此不能重载(一个类只能有一个析构函数)。
  • 自动调用:对象销毁时(如局部对象离开作用域、delete动态对象时)由编译器自动调用。

默认析构函数

如果用户没有定义析构函数,编译器会自动生成默认析构函数(函数体为空)。但如果类中使用了动态内存(如new分配的内存),必须手动定义析构函数释放资源,否则会导致内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;

class Array {
private:
int* data; // 动态数组
int size;
public:
// 构造函数:分配动态内存
Array(int s) {
size = s;
data = new int[size]; // 分配内存
cout << "构造函数:分配了" << size << "个int的内存" << endl;
}

// 析构函数:释放动态内存
~Array() {
delete[] data; // 释放内存(必须与new[]对应)
cout << "析构函数:释放了动态内存" << endl;
}

void set(int index, int value) {
if (index >= 0 && index < size) {
data[index] = value;
}
}
};

int main() {
{
Array arr(5); // 创建对象,调用构造函数
arr.set(0, 100);
} // 离开作用域,对象销毁,自动调用析构函数(释放内存)

return 0;
}

输出:

1
2
构造函数:分配了5个int的内存
析构函数:释放了动态内存

构造与析构的调用顺序

  • 构造函数:创建对象时,先调用基类的构造函数,再调用成员对象的构造函数,最后调用自身的构造函数
  • 析构函数:对象销毁时,调用顺序与构造相反:先调用自身的析构函数,再调用成员对象的析构函数,最后调用基类的析构函数

关键区别

特性 构造函数 析构函数
作用 初始化对象(分配资源) 清理对象(释放资源)
名字 与类名相同 ~类名
参数 可以有(可重载) 无(不可重载)
调用时机 对象创建时 对象销毁时
默认版本 无自定义时自动生成 无自定义时自动生成

移动构造函数

移动构造函数是 C++11 引入的重要特性,其核心目的是通过 “资源转移” 替代 “资源复制”,解决临时对象(或即将销毁的对象)在传递过程中的性能开销问题。与拷贝构造函数不同,移动构造函数不复制对象的底层资源,而是 “窃取” 源对象的资源所有权,从而避免不必要的内存分配与数据拷贝。

C++中不存在缺省的移动构造,C++11 及以后标准中存在默认移动构造函数,但它的生成有严格条件

  • 类中没有显式定义拷贝构造函数、拷贝赋值运算符(operator=)、移动赋值运算符(移动版本operator=)或析构函数;
  • 类中没有显式定义移动构造函数(若显式定义,默认版本则不会生成)。

编译器会在满足以下条件时,才自动生成默认移动构造函数。

默认移动构造函数的行为是 “逐成员移动”:对对象的每个非静态成员,调用其对应的移动构造函数(若成员是类类型且有移动构造);对于基本类型(如int、指针等),则直接按位复制(类似浅拷贝,但后续通过 “置空源对象” 避免资源冲突)。

尽管存在默认移动构造函数,但多数场景下(尤其是包含动态资源的类),需要显式定义移动构造函数

  1. 默认移动构造可能被抑制

    若类中显式定义了拷贝构造函数、拷贝赋值运算符或析构函数(常见于需要手动管理动态内存的场景),编译器会主动抑制默认移动构造函数的生成。此时若使用std::move试图触发移动语义,编译器会退而求其次调用拷贝构造函数(若存在),导致本可避免的深拷贝开销。

    例如,一个包含动态数组的类若定义了析构函数(用于释放内存),默认移动构造不会生成,std::move会触发拷贝而非移动:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class MyClass {
    private:
    int* data;
    int size;
    public:
    MyClass(int s) : size(s), data(new int[s]) {}
    ~MyClass() { delete[] data; } // 显式定义析构函数,抑制默认移动构造
    MyClass(const MyClass& other); // 显式定义拷贝构造,进一步抑制
    };

    MyClass a(10);
    MyClass b = std::move(a); // 此处调用拷贝构造,而非移动构造(因默认移动被抑制)
  2. 默认移动构造的 “浅移动” 不安全

    即使默认移动构造被生成,对于包含指针(或动态资源)的类,其 “逐成员移动” 本质是 “复制指针地址”(类似浅拷贝),但不会自动将源对象的指针置空。这会导致源对象与目标对象的指针指向同一块内存,当源对象析构时,目标对象的指针会成为野指针,引发内存访问错误。

    例如,默认移动构造对指针的处理:

    1
    2
    3
    // 编译器生成的默认移动构造(伪代码)
    MyClass::MyClass(MyClass&& other)
    : size(other.size), data(other.data) {} // 仅复制指针地址,未置空源对象

    此时若源对象other析构(释放data指向的内存),目标对象的data会变为野指针。

显式定义移动构造函数时,需遵循以下规则:

参数必须是右值引用语法为MyClass(MyClass&& other),右值引用(&&)专门用于匹配右值(如临时对象、std::move转换的左值),确保仅对 “即将销毁的对象” 进行资源窃取。

核心操作:转移资源并置空源对象移动构造的核心是 “窃取” 源对象的资源(如动态内存、文件句柄等),并将源对象的资源指针置空,避免源对象析构时释放已转移的资源。

**通常标记为noexcept**移动构造函数应尽可能标记为noexcept(不抛出异常),这是因为标准容器(如std::vector)在扩容时,若元素的移动构造函数是noexcept,会使用移动而非拷贝来转移元素,大幅提升性能;若可能抛异常,容器会退化为拷贝以保证异常安全。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class MyClass {
private:
int* data;
int size;
public:
// 构造函数:分配动态资源
MyClass(int s) : size(s), data(new int[s]) {}

// 析构函数:释放资源(显式定义,会抑制默认移动构造)
~MyClass() { delete[] data; }

// 拷贝构造(深拷贝,确保资源独立)
MyClass(const MyClass& other) : size(other.size), data(new int[other.size]) {
for (int i = 0; i < size; ++i) {
data[i] = other.data[i];
}
}

// 显式定义移动构造函数
MyClass(MyClass&& other) noexcept
: size(other.size), data(other.data) // 窃取源对象的资源
{
// 置空源对象,避免其析构时释放资源
other.data = nullptr;
other.size = 0;
}
};