【c++随笔03】构造函数、析构函数、拷贝构造函数、移动构造函数
- 一、构造函数
- 1、为何要有构造函数?
- 2、构造函数定义
- 3、无参构造、带参构造
- 4、构造函数注意事项
- 4.1 构造函数是特殊的,不是常规的成员函数,不能直接调d1.Date() 。
- 4.2 如果通过无参构造函数创建对象,对象后面不用跟括号,否则就成了函数声明。
- 4.3 这里如果调用带参构造函数,我们需要传递三个参数(这里我们没设缺省) 。
- 4.4 如果你没有自己定义构造函数(类中未显式定义),C++ 编译器会自动生成一个无参的默认构造函数。当然,如果你自己定义了,编译器就不会帮你生成了。
- 4.5 默认构造函数:包括无参构造函数、全缺省构造函数
- 4.6 任何一个类的默认构造函数,只有三种:
- 二、析构函数
- 1、概念
- 2、demo
- 3、编译器自动生成的析构函数
- 三、拷贝构造函数、拷贝赋值运算符
- 1、为何要有拷贝构造函数
- 2、拷贝构造函数特点
- 3、demo(拷贝构造函数、拷贝赋值运算符)
- 4、浅拷贝、深拷贝
- 5、浅拷贝
- 6、深拷贝
- 四、移动构造函数、移动构造赋值运算符
- 1、概念
- 2、定义
- 3、demo(移动构造函数、移动构造赋值运算符)
- 4、noexcept
一、构造函数
1、为何要有构造函数?
我们先看一段代码
#include <iostream>
class Date {
public:
void setDate(int year, int month, int day)
{
m_year = year;
m_month = month;
m_day = day;
}
void print()
{
printf("%d-%d-%d\n", m_year, m_month, m_day);
}
private:
int m_year;
int m_month;
int m_day;
};
int main(void)
{
Date d1;
d1.setDate(2022, 3, 8);
d1.print();
return 0;
}
问题:每次创建Date类的对象d1,可以通过setDate函数设置对象d1每个成员的值,但是每创建一个对象,都通过成员函数setDate来给对象d1赋值,是不是有些麻烦?
- 是否可以在创建对象d1时就给每个成员赋值呢?
答案:可以,通过构造函数。
2、构造函数定义
构造函数的概念
-
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用。
-
能够保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数的意义:
- 能够保证对象被初始化。
- 构造函数是特殊的成员函数,主要任务是初始化,而不是开空间。(虽然构造函数的名字叫构造)
构造函数的特性,构造函数是特殊的成员函数,主要特征如下:
- 构造函数的函数名和类名是相同的
- 构造函数无返回值
- 构造函数可以重载
- 会在对象实例化时自动调用对象定义出来。
3、无参构造、带参构造
不给参数时就会调用无参构造函数,给参数则会调用带参构造函数。
#include <iostream>
class Date {
public:
/* 无参构造函数 */
Date() {
m_year = 0;
m_month = 1;
m_day = 1;
}
/* 带参构造函数 */
Date(int year, int month, int day) {
m_year = year;
m_month = month;
m_day = day;
}
void print()
{
printf("%d-%d-%d\n", m_year, m_month, m_day);
}
private:
int m_year;
int m_month;
int m_day;
};
int main(void)
{
Date d1; // 对象实例化,此时触发构造,调用无参构造函数
d1.print();
Date d2(2022, 3, 9); // 对象实例化,此时触发构造,调用带参构造函数
d2.print();
return 0;
}
输出
0-1-1
2022-3-9
4、构造函数注意事项
4.1 构造函数是特殊的,不是常规的成员函数,不能直接调d1.Date() 。
Date d1;
d1.Date(); // 不能这么去调,构造函数是特殊的,不是常规的成员函数!
4.2 如果通过无参构造函数创建对象,对象后面不用跟括号,否则就成了函数声明。
Date d2(2022, 3, 9); //正确
Date d3(); //错误
4.3 这里如果调用带参构造函数,我们需要传递三个参数(这里我们没设缺省) 。
Date d2(2022, 3, 9); // 正确:对象实例化,此时触发构造,调用带参构造函数
Date d3(2022, 3) //错误,需要传递三个参数(这里我们没设缺省) 。
4.4 如果你没有自己定义构造函数(类中未显式定义),C++ 编译器会自动生成一个无参的默认构造函数。当然,如果你自己定义了,编译器就不会帮你生成了。
#include <iostream>
class Date {
public:
void print() {
printf("%d-%d-%d\n", m_year, m_month, m_day);
}
private:
int m_year;
int m_month;
int m_day;
};
int main(void)
{
Date d1; // 对象实例化,此时触发构造,调用无参构造函数
d1.print();
return 0;
}
输出
-858993460–858993460–858993460
4.5 默认构造函数:包括无参构造函数、全缺省构造函数
- 无参构造函数、
- 全缺省构造函数
无参的构造函数和全缺省的构造函数都成为默认构造函数,并且默认构造参数只能有一个,语法上他们两个可以同时存在,但是如果有对象定义去调用就会报错。
#include <iostream>
class Date {
public:
// 无参的构造函数
Date() {
m_year = 1970;
m_month = 1;
m_day = 1;
}
// 全缺省的构造函数
Date(int year = 1970, int month = 1, int day = 1) {
m_year = year;
m_month = month;
m_day = day;
}
void print()
{
printf("%d-%d-%d\n", m_year, m_month, m_day);
}
private:
int m_year;
int m_month;
int m_day;
};
int main(void)
{
Date d1;
// 无参的构造函数和全缺省的构造函数都成为默认构造函数,并且默认构造参数只能有一个,
// 语法上他们两个可以同时存在,但是如果有对象定义去调用就会报错。
return 0;
}
4.6 任何一个类的默认构造函数,只有三种:
-
无参的构造函数
-
全缺省的构造函数
-
我们不写,编译器自己生成的构造函数
C++ 规定:
- 我们不写编译器默认生成构造函数,对于内置类型的成员变量,不做初始化处理。
- 但是对于自定义类型的成员变量会去调用它的默认构造函数(不用参数就可以调的)初始化。
- 如果没有默认构造函数(不用参数就可以调用的构造函数)就会报错!
#include <iostream>
using namespace std;
class A {
public:
// 如果没有默认的构造函数,会报错。
A(int a) { // 故意给个参
cout << " A() " << endl;
_a = 0;
}
private:
int _a;
};
class Date {
public:
void print()
{
printf("%d-%d-%d\n", m_year, m_month, m_day);
}
private:
int m_year;
int m_month;
int m_day;
A m_a;
};
int main(void)
{
Date d1; // 无参的构造函数和全缺省的构造函数都成为默认构造函数,并且默认构造参数只能有一个,语法上他们两个可以同时存在,但是如果有对象定义去调用就会报错。
return 0;
}
报错如下
从报错可知:当调用Date的默认构造函数时,会自动调用A的构造函数,但是由于A定义了构造函数,所以A就没有默认的构造函数(准确说是,编译器没有给A准备默认构造函数)
把上面A的构造函数删除就可以正常执行了,因为A不写构造函数,编译器就会给准备一个默认的构造函数,不带参数的默认构造函数。
二、析构函数
1、概念
- 概念:对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作。
构造函数是特殊的成员函数,主要特征如下:
- 析构函数名是在类名前面加上字符
- 析构函数既没有参数也没有返回值(因为没有参数,所以也不会构成重载问题)
- 一个类的析构函数有且仅有一个(如果不写系统会默认生成一个析构函数)
- 析构函数在对象生命周期结束后,会自动调用。
2、demo
#include <iostream>
using namespace std;
class Date {
public:
// 全缺省的构造函数
Date(int year = 1970, int month = 1, int day = 1) {
m_year = new int(year);
m_month = month;
m_day = day;
}
// 拷贝构造函数 //以下两种拷贝构造函数写法的效果相同
~Date()
{
if (nullptr != m_year)
{
m_year = nullptr;
delete m_year;
cout << "调用了 析构函数" << endl;
}
}
void print() {
printf("%d-%d-%d\n", *m_year, m_month, m_day);
// cout << "m_year地址 = " << m_year << endl;
}
private:
int *m_year;
int m_month;
int m_day;
};
int main(void)
{
Date d1(2023);
Date d2(d1); // 调用拷贝构造函数
d2.print();
Date d3;
d3 = d1; // 调用拷贝赋值运算符
d3.print();
return 0;
}
输出如下
2023-1-1
2023-1-1
调用了 析构函数
调用了 析构函数
调用了 析构函数
3、编译器自动生成的析构函数
如果不自己写构造函数,让编译器自动生成,那么这个自动生成的 默认构造函数:
- 对于 “内置类型” 的成员变量:不会做初始化处理。
- 对于 “自定义类型” 的成员变量:会调用它的默认构造函数(不用参数就可以调的)初始化,如果没有默认构造函数(不用参数就可以调用的构造函数)就会报错!
而我们的析构函数也是这样的,一个德行!
如果我们不自己写析构函数,让编译器自动生成,那么这个 默认析构函数:
- 对于 “内置类型” 的成员变量:不作处理 (不会帮你清理的.)
- 对于 “自定义类型” 的成员变量:会调用它对应的析构函数 (已经仁至义尽了) 。
三、拷贝构造函数、拷贝赋值运算符
1、为何要有拷贝构造函数
Date d1(2023);
Date d2(d1); // 调用拷贝构造函数
d2.print();
Date d3;
d3 = d1; // 调用拷贝赋值运算符
d3.print();
- 问题:我们在创建对象的时候,能不能创建一个与某一个对象一模一样的新对象呢?
- 答案:可以,这时我们就可以用拷贝构造函数。
2、拷贝构造函数特点
- 拷贝构造函数是构造函数的一个重载形式。函数名和类名相同,没有返回值。
- 拷贝构造函数的参数只有一个,并且必须要使用引用传参!
拷贝构造函数用于使用一个对象初始化另一个对象的时候,系统会默认为我们声明实现一个拷贝构造函数,这种默认的拷贝构造函数为浅拷贝
3、demo(拷贝构造函数、拷贝赋值运算符)
#include <iostream>
using namespace std;
class Date {
public:
// 全缺省的构造函数
Date(int year = 1970, int month = 1, int day = 1) {
m_year = year;
m_month = month;
m_day = day;
}
// 拷贝构造函数 //以下两种拷贝构造函数写法的效果相同
/* Date(const Date& other) :m_year(other.m_year), m_month(other.m_month), m_day(other.m_day)
{
cout << "调用了 拷贝构造函数" << endl;
}*/
Date(const Date& other) {
m_year = other.m_year;
m_month = other.m_month;
m_day = other.m_day;
cout << "调用了 拷贝构造函数" << endl;
}
// 拷贝赋值运算符
Date& operator=(const Date& other) {
if (this != &other) {
m_year = other.m_year;
m_month = other.m_month;
m_day = other.m_day;
}
cout << "调用了 拷贝赋值运算符" << endl;
return *this;
}
void print() {
printf("%d-%d-%d\n", m_year, m_month, m_day);
}
private:
int m_year;
int m_month;
int m_day;
};
int main(void)
{
Date d1(2023);
Date d2(d1); // 调用拷贝构造函数
d2.print();
Date d3;
d3 = d1; // 调用拷贝赋值运算符
d3.print();
return 0;
}
输出
调用了 拷贝构造函数
2023-1-1
调用了 拷贝赋值运算符
2023-1-1
4、浅拷贝、深拷贝
浅拷贝(Shallow Copy)是指新对象只复制了原对象的地址,而没有复制实际的数据。简单来说,浅拷贝只是将指针指向了原始数据的存储位置,多个指针指向同一块内存,修改其中一个对象的数据会影响其他对象。
深拷贝(Deep Copy)是指新对象不仅复制了原对象的数据,还复制了数据所在的内存空间。换句话说,深拷贝将重新分配内存并将原始数据复制到新的内存位置。这样每个对象都有自己独立的内存空间,修改一个对象的数据不会影响其他对象。
C++提供的拷贝构造函数只是简单的将成员变量赋值。如果类里有一个指针,并且申请了一个堆空间,那么在使用默认拷贝构造函数时会使得两个对象指向同一个堆空间。重复释放会导致进程崩溃。
5、浅拷贝
#include <iostream>
using namespace std;
class Date {
public:
// 全缺省的构造函数
Date(int year = 1970, int month = 1, int day = 1) {
m_year = new int(year);
m_month = month;
m_day = day;
}
// 拷贝构造函数 //以下两种拷贝构造函数写法的效果相同
/* Date(const Date& other) :m_year(other.m_year), m_month(other.m_month), m_day(other.m_day)
{
cout << "调用了 拷贝构造函数" << endl;
}*/
Date(const Date& other) {
m_year = other.m_year;
m_month = other.m_month;
m_day = other.m_day;
cout << "调用了 拷贝构造函数" << endl;
}
// 拷贝赋值运算符
Date& operator=(const Date& other) {
if (this != &other) {
m_year = other.m_year;
m_month = other.m_month;
m_day = other.m_day;
}
cout << "调用了 拷贝赋值运算符" << endl;
return *this;
}
void print() {
printf("%d-%d-%d\n", *m_year, m_month, m_day);
cout << "m_year地址 = " << m_year << endl;
}
private:
int *m_year;
int m_month;
int m_day;
};
int main(void)
{
Date d1(2023);
Date d2(d1); // 调用拷贝构造函数
d2.print();
Date d3;
d3 = d1; // 调用拷贝赋值运算符
d3.print();
return 0;
}
输出,可以看到year的地址相同
调用了 拷贝构造函数
2023-1-1
m_year地址 = 000001C88A4E67E0
调用了 拷贝赋值运算符
2023-1-1
m_year地址 = 000001C88A4E67E0
6、深拷贝
#include <iostream>
using namespace std;
class Date {
public:
// 全缺省的构造函数
Date(int year = 1970, int month = 1, int day = 1) {
m_year = new int(year);
m_month = month;
m_day = day;
}
// 拷贝构造函数 //以下两种拷贝构造函数写法的效果相同
/* Date(const Date& other) :m_year(other.m_year), m_month(other.m_month), m_day(other.m_day)
{
cout << "调用了 拷贝构造函数" << endl;
}*/
Date(const Date& other) {
m_year = new int(*other.m_year);
m_month = other.m_month;
m_day = other.m_day;
cout << "调用了 拷贝构造函数" << endl;
}
// 拷贝赋值运算符
Date& operator=(const Date& other) {
if (this != &other) {
m_year = new int(*other.m_year);
m_month = other.m_month;
m_day = other.m_day;
}
cout << "调用了 拷贝赋值运算符" << endl;
return *this;
}
void print() {
printf("%d-%d-%d\n", *m_year, m_month, m_day);
cout << "m_year地址 = " << m_year << endl;
}
private:
int *m_year;
int m_month;
int m_day;
};
int main(void)
{
Date d1(2023);
Date d2(d1); // 调用拷贝构造函数
d2.print();
Date d3;
d3 = d1; // 调用拷贝赋值运算符
d3.print();
return 0;
}
调用了 拷贝构造函数
2023-1-1
m_year地址 = 0000016E1CE96210
调用了 拷贝赋值运算符
2023-1-1
m_year地址 = 0000016E1CE961D0
四、移动构造函数、移动构造赋值运算符
1、概念
当一个函数的参数按值传递时,这就会进行拷贝,而对于我自己定义的类型,我们也需要提供拷贝构造函数。
但是,不得不说,拷贝的代价是昂贵的。
所以,我们需要寻找一个避免不必要拷贝的方法,
- 移动构造函数和移动赋值运算符是C++11引入的特性,用于支持对临时对象的高效转移语义。它们可以大幅提升对象的构造和赋值性能,特别是当对象包含大量资源需要进行拷贝或赋值操作时。
2、定义
移动构造函数(Move Constructor):
移动构造函数用于从一个临时对象"偷取"资源而不进行深拷贝,从而提高效率。其函数签名如下:
ClassName(ClassName&& other);
在移动构造函数中,other是一个右值引用,表示一个即将被销毁的临时对象。移动构造函数应该将资源(例如指针、文件句柄等)从other转移到新创建的对象中,并将other中的资源置为无效状态。
- 移动构造函数,可以避免内存的重新分配,——右值引用提供了一个暂时的对象。
3、demo(移动构造函数、移动构造赋值运算符)
#include <iostream>
using namespace std;
class Date {
public:
// 全缺省的构造函数
Date(int year = 1970, int month = 1, int day = 1) {
m_year = new int(year);
m_month = month;
m_day = day;
}
// 拷贝构造函数 //以下两种拷贝构造函数写法的效果相同
Date(Date&& other) noexcept {
if (this != &other) {
//delete m_year;
m_year = other.m_year;
m_month = other.m_month;
m_day = other.m_day;
other.m_year = nullptr;
other.m_month = 0;
other.m_day = 0;
}
cout << "调用了 移动构造函数" << endl;
}
// 拷贝赋值运算符
Date& operator=(Date&& other) noexcept {
if (this != &other) {
// delete m_year;
m_year = other.m_year;
m_month = other.m_month;
m_day = other.m_day;
other.m_year = nullptr;
other.m_month = 0;
other.m_day = 0;
}
cout << "调用了 移动构赋值算符" << endl;
return *this;
}
~Date()
{
if (nullptr != m_year)
{
m_year = nullptr;
delete m_year;
cout << "调用了 析构函数" << endl;
}
}
void print() {
printf("%d-%d-%d\n", *m_year, m_month, m_day);
// cout << "m_year地址 = " << m_year << endl;
}
private:
int *m_year;
int m_month;
int m_day;
};
int main(void)
{
Date d1(2023);
Date d2(std::move(d1)); // 调用拷贝构造函数
//d1.print();//会报错的
d2.print();
Date d3(2025);
Date d4 = std::move(d3); // 调用拷贝赋值运算符
// d3.print(); //会报错的
d4.print();
return 0;
}
输出
调用了 移动构造函数
2023-1-1
调用了 移动构造函数
2025-1-1
调用了 析构函数
调用了 析构函数
4、noexcept
对于移动构造函数和移动赋值运算符,通常建议将它们标记为 noexcept。noexcept 是 C++11 引入的异常规格说明符,用于明确指定函数是否可能引发异常。
将移动操作标记为 noexcept 有以下几个好处:
-
提高性能:当一个函数被标记为 noexcept 时,编译器可以进行一些优化,以提高代码的执行效率。
-
支持类型的移动语义:std::vector、std::unordered_map 等容器在执行元素的插入、删除等操作时使用移动语义,如果移动操作可能抛出异常,这些容器就无法利用移动操作来提升性能。
-
资源管理:在资源管理类(如智能指针)中,移动操作通常用于转移资源的所有权。如果移动操作可能抛出异常,可能会导致资源未正确释放,从而引发资源泄漏。
因此,对于移动构造函数和移动赋值运算符,如果能够确保其不会抛出异常,建议将它们标记为 noexcept。例如:
在上述示例中,我们使用 noexcept 关键字将移动构造函数和移动赋值运算符标记为不会抛出异常。
需要注意的是,如果移动操作调用了可能会抛出异常的函数(如动态内存分配函数 new),则这些移动操作不能标记为 noexcept。
总之,将移动构造函数和移动赋值运算符标记为 noexcept 可以提高代码的性能,并且能更好地支持类型的移动语义和资源管理。但请注意在确保移动操作不会引发异常的情况下使用 noexcept。
参考
1、https://blog.csdn.net/weixin_50502862/article/details/123359614
2、https://blog.csdn.net/qq_34799070/article/details/121298249