xml
hello,这里是AuroraWanderll。
兴趣方向:C++,算法,Linux系统,游戏客户端开发
欢迎关注,我将更新更多相关内容!
这是类和对象系列的第三篇文章,上篇指引:
类和对象(二)访问限定符-类的实例化与this指针
类和对象(三)-默认成员函数详解与运算符重载
简易目录
- 类的6个默认成员函数概述
- 构造函数详解
- 析构函数详解
1. 类的6个默认成员函数概述
核心概念:当一个类中什么成员都没有时(称为空类),即用户没有显式实现时,编译器会自动生成6个默认成员函数。
class Date {}; // 看似空的类,实际上编译器会生成6个默认成员函数
| 序号 | 默认成员函数 | 基本作用 |
|---|---|---|
| 1 | 构造函数 | 对象创建时自动调用,用于初始化对象 |
| 2 | 析构函数 | 对象销毁时自动调用,用于清理资源 |
| 3 | 拷贝构造函数 | 用同类型的已有对象初始化新对象,如v1(v2) |
| 4 | 拷贝赋值运算符 | 将一个对象的值赋给另一个同类型对象,如v1=v2 |
| 5 | 移动构造函数 (C++11) | 通过"移动"资源来初始化新对象,避免不必要的拷贝 |
| 6 | 移动赋值运算符 (C++11) | 通过"移动"资源来赋值,避免不必要的拷贝 |
| 7 | 取地址重载运算符 |
其中5,6相对比较进阶,本篇不会提及.
2. 构造函数
2.1 构造函数的概念
背景:为什么我们要有构造函数?
答:C语言传统初始化方式繁琐
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// ... 其他成员
};
int main()
{
Date d1;
d1.Init(2022, 7, 5); // 每次创建对象后都要手动调用初始化函数Init,未免太过麻烦
return 0;
}
构造函数定义 :特殊的成员函数,在创建对象时自动调用,用于初始化对象数据成员,整个生命周期只调用一次。
2.2 构造函数的特性
基本特征:
-
函数名与类名相同
-
无返回值
-
对象实例化时自动调用
-
重载(一个对象可以有多个不同的构造函数)
需要注意的是:虽然叫构造函数,但是它并不负责开空间创建对象,它的主要工作是初始化对象
class Date
{
public:
// 1. 无参构造函数
Date()
{}// 2. 带参构造函数(重载) Date(int year, int month, int day) { _year = year; _month = month; _day = day; }private:
int _year;
int _month;
int _day;
};void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参构造函数
// 注意:无参构造不能加括号,否则变成函数声明
Date d3(); // 错误:声明了d3函数,而非创建对象
}
编译器自动生成规则:
-
如果类中没有显式定义构造函数,编译器自动生成无参默认构造函数
-
一旦用户显式定义任何构造函数,编译器不再生成默认构造函数
class Date
{
// 如果用户显式定义构造函数,编译器不再生成默认构造函数
// Date(int year, int month, int day) { ... }private:
int _year;
int _month;
int _day;
};int main()
{
Date d1; // 如果屏蔽自定义构造函数,编译通过;如果放开,编译失败
return 0;
}
默认构造函数的作用:
看起来编译器自动生成的默认构造函数没有作用,例如int类型的参数,默认构造之后依旧是随机值。实际上默认构造函数是会根据类型来进行不同的初始化的
-
对内置类型(int、char等):不处理(C++11前)或使用默认值(C++11后)
-
对自定义类型:调用其默认构造函数
class Time
{
public:
Time() // Time类的构造函数
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};class Date
{
private:
// 内置类型成员,C++11之前不处理
int _year;
int _month;
int _day;// 自定义类型成员 Time _t; // 编译器生成的默认构造函数会调用Time的构造函数};
int main()
{
Date d; // 调用Date的默认构造函数,同时会调用Time的构造函数
return 0;
}
C++11改进:内置类型成员可以在声明时给默认值
class Date
{
private:
// 内置类型成员给默认值,C++11之后,直接初始化成我们给的默认值
int _year = 1970;
int _month = 1;
int _day = 1;
Time _t; // 自定义类型
};
默认构造函数规则:
-
无参构造函数、全缺省构造函数、编译器生成的构造函数都算默认构造函数
-
默认构造函数只能有一个
class Date
{
public:
// 无参构造函数(默认构造函数)
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}// 全缺省构造函数(也是默认构造函数) Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }};
如果我们在类的对象中同时写了超过一个的默认构造,那么它就会报错
编译错误原因:
当执行 Date d1; 时,编译器面临选择困难:
- 可以调用无参构造函数
Date() - 也可以调用全缺省构造函数
Date(1900, 1, 1)(使用默认参数)
两个函数都匹配,编译器无法确定该调用哪一个,因此报编译错误。
正确写法:
方案1:只保留一个默认构造函数
class Date
{
public:
// 只保留全缺省构造函数(推荐)
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
};
方案2:使用不同的参数列表
class Date
{
public:
// 无参构造函数
Date() : _year(1900), _month(1), _day(1) {}
// 带参构造函数(不是全缺省)
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
};
可以简单理解默认构造函数是:调用时不需要传递参数的构造函数
总结要点:
- 空类会自动获得6个默认成员函数
- 构造函数在对象创建时自动调用,用于初始化
- 构造函数可以重载,名称与类名相同且无返回值
- 编译器在特定条件下自动生成默认构造函数
- 默认构造函数对内置类型和自定义类型的处理方式不同
- C++11允许内置类型成员在声明时给默认值
3. 析构函数详解
3.1 析构函数的概念
核心问题:对象是如何被销毁的?
析构函数定义 :与构造函数功能相反,但不是完成对象本身的销毁 (局部对象的销毁由编译器完成),而是在对象销毁时自动调用,完成对象中资源的清理工作。
3.2 析构函数的特性
基本特征:
-
函数名 :类名前加上
~ -
参数和返回值:无参数、无返回值类型
-
唯一性:一个类只能有一个析构函数,不能重载
-
调用时机:对象生命周期结束时自动调用
class Stack
{
public:
// 构造函数:申请资源
Stack(size_t capacity = 3)
{
_array = (int*)malloc(sizeof(int) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}// 析构函数:释放资源 ~Stack() { if (_array) { free(_array); // 释放动态内存 _array = NULL; // 防止野指针 _capacity = 0; // 重置容量 _size = 0; // 重置大小 } } void Push(int data) { _array[_size] = data; _size++; }private:
int* _array;
int _capacity;
int _size;
};void TestStack()
{
Stack s; // 构造函数自动调用
s.Push(1);
s.Push(2);
// 函数结束时,s的析构函数自动调用,释放内存
}
编译器生成的析构函数
重要特性 :编译器生成的默认析构函数会对自定义类型成员调用其析构函数。
class Time
{
public:
~Time()
{
cout << "~Time()" << endl; // 析构时输出信息
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 内置类型成员
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型成员
Time _t; // 包含Time类对象
};
int main()
{
Date d; // 创建Date对象
return 0;
} // d销毁时,输出:~Time()
运行结果解释:
- 虽然
main函数中没有直接创建Time对象 - 但
Date对象d包含Time成员_t - 当
d销毁时,编译器为Date生成的默认析构函数会自动调用Time类的析构函数
析构函数的调用规则
关键原则:
- 创建哪个类的对象,就调用该类的构造函数
- 销毁哪个类的对象,就调用该类的析构函数
- 编译器生成的析构函数会保证所有成员都能正确销毁
3.3 何时需要编写析构函数
不需要编写的情况:
class Date
{
private:
int _year = 1970;
int _month = 1;
int _day = 1;
// 只有内置类型,无动态资源,使用编译器生成的析构函数即可
};
必须编写的情况:
class Stack
{
private:
int* _array; // 动态分配的内存
int _capacity;
int _size;
public:
// 必须编写析构函数来释放动态内存
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
}
}
};
也就是说不是说自定义类型就一定需要写析构函数,关键在于你的类型之中是否动态申请资源。
资源泄漏风险:如果类中申请了资源(动态内存、文件句柄、网络连接等)但没有编写析构函数,会导致资源泄漏。
3.4 实际应用场景
场景1:动态数组管理
class DynamicArray
{
private:
int* _data;
size_t _size;
public:
DynamicArray(size_t size) : _size(size)
{
_data = new int[size]; // 动态分配
}
~DynamicArray()
{
delete[] _data; // 必须释放
_data = nullptr;
}
};
场景2:文件资源管理
class FileHandler
{
private:
FILE* _file;
public:
FileHandler(const char* filename)
{
_file = fopen(filename, "r");
}
~FileHandler()
{
if (_file)
{
fclose(_file); // 必须关闭文件
_file = nullptr;
}
}
};
总结:
- 析构函数作用:对象销毁时自动调用,用于资源清理
- 语法特征 :
~类名(),无参无返回值,不能重载 - 调用时机:对象生命周期结束时自动调用
- 编译器行为:默认生成的析构函数会调用自定义类型成员的析构函数
- 编写原则:有资源申请时必须编写,无资源时可依赖编译器生成
- 资源管理:防止内存泄漏、文件未关闭等资源管理问题
核心思想:谁申请,谁释放;构造函数申请资源,析构函数释放资源,形成完整的资源管理生命周期。
xml
感谢你能够阅读到这里,如果本篇文章对你有帮助,欢迎点赞收藏支持,关注我,
我将更新更多有关C++,Linux系统·网络部分的知识。