“深入理解C++类默认成员函数:探索构造、析构与复制“

类的8个默认成员函数

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下8个默认成员
函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
class Date {};

C++类的默认成员函数包括构造函数、析构函数、拷贝构造函数、移动构造函数、赋值运算符和移动赋值运算符;构造函数用于初始化,析构函数用于清理;拷贝构造函数和移动构造函数均可用于同类对象的初始化;赋值运算符和移动赋值运算符则用于将一个对象赋值给另一个对象;取地址运算符主要用于普通对象和const对象取地址,且很少需要自己实现。
在这里插入图片描述


构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任
务并不是开空间创建对象,而是初始化对象。

构造函数特征如下:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
  5. 整个生命周期内只调用一次。

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); // 调用带参的构造函数

	// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
	// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
	// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
	Date d3();
}


如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。
但是,如果你已经定义了一个构造函数(无论是否有参数),编译器就不会再自动生成默认构造函数了。这意味着你需要自己提供所有的构造函数定义,包括无参构造函数。

class Date
{
public:
	/*
	// 如果用户显式定义了构造函数,编译器将不再生成
	Date(int year, int month, int day)
	{
	_year = year;
	_month = month;
	_day = day;
	}
	*/

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
	// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
	// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
		Date d1;
	return 0;
}

关于编译器生成的默认成员函数,很多同学会有疑惑:不实现构造函数的情况下,编译器会
生成默认的构造函数。但是看起来默认构造函数又没什么用?对象调用了编译器生成的默
认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的
默认构造函数并没有什么用??

解答:

C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类
型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员
函数。

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

注意:

C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值。

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;


	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。

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;
	}
private:
	int _year;
	int _month;
	int _day;
};
// 以下测试函数能通过编译吗?
void Test()
{
	Date d1;
}

解答:

  1. 以上程序不能通过编译.因为构造函数冲突,无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。以上程序包含了多个构造函数.

初始化列表
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
在上面的构造函数代码中,我们都是在函数体内初始化的,其实构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值
所有我们就产生了初始化列表

  • 初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式。

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

C++11的缺省值,这个缺省值是给初始化列表的

  1. 如果初始化列表没有显示给值,就用这个缺省值
  2. 如果给值,就不用这个缺省值

初始化列表是每个成员定义的地方,不管你写不写每个成员都要走初始化列表

  • 如果不写初始化列表
    自定义类型调用自己的构造
    内置类型给成随机值
  • 如果显示写初始化列表
    调用给定的参数

注意:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  1. 引用成员变量
  2. const成员变量
  3. 自定义类型成员(且该类没有默认构造函数时)
    4
  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
    一定会先使用初始化列表初始化。

class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
		:_aobj(a)
		, _ref(ref)
		, _n(10)
	{}
private:
	A _aobj;  // 没有默认构造函数
	int& _ref;  // 引用
	const int _n; // const 
};

  1. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后
    次序无关.如果不按该规则可能会出现不必要的麻烦.
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}

	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
	A aa(1);
	aa.Print();
}
A.输出1  1
B.程序崩溃
C.编译不通过
D.输出1  随机值

正确答案是D.编译不通过。

在构造函数中,初始化列表的顺序应该与变量声明的顺序相同,否则会出现不必要的麻烦。在上面的代码中,变量 _a1 和 _a2 的声明顺序是 _a2 和 _a1,但是初始化列表中的顺序是 _a1 和 _a2,这会导致a2初始化成a1没有赋值前的随机值。因此,选项D是正确的。


析构函数

我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

析构函数:

与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
    函数不能重载.
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		_array = new DataType[capacity];
		_capacity = capacity;
		_size = 0;
	}
	//------------------------------------------------------------
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

  1. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器
    生成的默认析构函数,对自定类型成员调用它的析构函数。
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;
};
int main()
{
	Date d;
	return 0;
}

程序运行结束后输出:~Time()

在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?

  • 因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,
    _day三个是 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在 d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。
    但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁, main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
    注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数.

  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
    Date类;有资源申请时,一定要写,否则会造成资源泄漏。

拷贝构造

在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?

拷贝构造函数

只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

特征:

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
    因为会引发无穷递归调用。
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// Date(const Date& d)   // 正确写法
	Date(const Date& d)   // 错误写法:编译报错,会引发无穷递归
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

在这里插入图片描述


  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
    字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time(const Time& t)
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
		cout << "Time::Time(const Time&)" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;

	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	return 0;
}

注意

在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。


  1. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
    当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

typedef int DataType;
class Stack
{
public:
	/*注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
	时,则拷贝构造函数是一定要写的,否则就是浅拷贝。*/
	Stack(size_t capacity = 10)
	{

		_array = new DataType[capacity];
			_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			delete _array;
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

![在这里插入图片描述](https://img-blog.csdnimg.cn/cfc8cabb65744002abb5d8a905db0c2c.png

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。


  1. 拷贝构造函数典型调用场景:
  1. 使用已存在对象创建新对象
  2. 函数参数类型为类类型对象
  3. 函数返回值类型为类类型对象
class Date
{
public:
	Date(int year, int minute, int day)
	{
		cout << "Date(int,int,int):" << this << endl;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d):" << this << endl;
	}
	~Date()
	{
		cout << "~Date():" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date Test(Date d)
{
	Date temp(d);
	return temp;
}
int main()
{
	Date d1(2022, 1, 13);
	Test(d1);
	return 0;
}

在这里插入图片描述
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用。


移动构造

如果临时对象即将消亡,并且它里面的资源需要被再利用的时候,就可以触发移动构造。
什么时候该触发移动构造?临时对象即将消亡,且它里面的资源需要被再利用(有可被利用的临时对象)。

它用于将资源从一个对象转移到另一个对象,而不需要进行复制操作。这在处理大型对象或具有大量资源的对象时特别有用,因为它可以避免不必要的内存分配和复制操作,从而提高程序的性能。

Date(Date&& d)
  1. 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
    意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类
    型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
    如果实现了就调用移动构造,没有实现就调用拷贝构造。
  2. 拷贝对象需要深拷贝时,自己写移动构造.
  • 移动构造函数的语法如下:
class MyClass {  
public:  
    MyClass(MyClass&& other) {  
        // 移动资源  
    }  
};

其中,MyClass&& other是一个右值引用,表示要被移动的对象。在移动构造函数中,你可以将其他对象的资源转移到新对象中,而不必复制这些资源。


下面是一个简单的示例,展示了如何使用移动构造函数

#include<string>
#include<iostream>
using namespace std;
namespace XmLife
{
	class string
	{
	public:
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(100)
		{
			cout << "string() --构造" << endl;
			_str = new char[_capacity];
			strcpy(_str, str);
		}

		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 资源转移" << endl;
			swap(s); //转移资源
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		void push_back(char ch)
		{
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

XmLife::string get_string()
{
	XmLife::string str;
	str.push_back(1);
	str.push_back(2);
	str.push_back(3);
	return str;
}

// 拷贝构造和移动构造
int main()
{
	XmLife::string ret = get_string();  //移动构造

	XmLife::string s1("1111111"); //构造
	XmLife::string s2(s1); //拷贝构造

	return 0;
}

get_string()函数返回值是 string,如果大型对象或具有大量资源的对象时,如果使用string传值返回会调用拷贝构造,浪费了内存分配和复制操作,但又因为str是局部对象,出了作用域就销毁了,又不能传引用返回。这时我们的移动构造的特点就来了.他相当于把你之前的资源盗取过来,在把空的资源给你销毁掉。

  1. 因为str是即将消亡,且它里面的资源需要被再利用,在你return str时候会调用移动构造。
  2. 在移动构造里面他会把你str里面的值和自己的内容交换,交换完成后str是空值了,并且返回str时候会销毁.
  3. 注意: 如果没有移动构造会调用拷贝构造来完成,从而性能会下降。

移动构造函数的作用是避免不必要的内存分配和复制操作,从而提高程序的性能。它可以将一个对象的资源直接转移给另一个对象,而不是通过复制操作创建新的对象。移动构造函数通常使用右值引用作为参数,接收一个将要被移动的对象,并且在移动过程中将原对象的资源设置为空或无效,同时将资源交给新对象。这种机制可以避免不必要的内存分配和复制操作,从而提高程序的性能。特别是在处理大型对象或具有大量资源的对象时,移动构造函数的作用更加显著。


赋值运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似.

函数名字为:

关键字operator后面接需要重载的运算符符号。

函数原型:

返回值类型 operator操作符(参数列表)

注意:

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型参数
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
    藏的this
  5. .* :: sizeof ?: . 注意以上5个运算符不能重载。
  • 赋值运算符重载

赋值运算符重载格式

  1. 参数类型:const T&,传递引用可以提高传参效率
  2. 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  3. 检测是否自己给自己赋值
  4. 返回*this :要复合连续赋值的含义

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};


  1. 赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
	if (&left != &right)
	{
		left._year = right._year;
		left._month = right._month;
		left._day = right._day;
	}
	return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。

我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。


  1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注
    意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
    重载完成赋值。

当然跟上面拷贝构造一样,如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。


移动赋值运算符重载

移动赋值运算符的作用是将一个对象的资源转移到另一个对象中,并使原对象处于有效状态。

移动赋值运算符的重载语法如下:

class MyClass {  
public:  
    MyClass& operator=(MyClass&& other) {  
        // 移动资源  
        return *this;  
    }  
};

其中,MyClass&& other是一个右值引用,表示要被移动的右操作数对象。在移动赋值运算符中,我们需要将其他对象的资源转移到当前对象中,并使原对象处于有效状态。


  1. 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
    的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内
    置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
    值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
    完全类似)
  2. 移动赋值对象需要深拷贝时,自己写移动赋值重载函数.

移动赋值操作通过移动语义实现对象的转移,避免了不必要的数据拷贝,提高了性能。移动赋值会在以下情况下被触发:

  1. 当右值(临时对象或具有右值引用的对象)与左值(具有左值引用的对象)进行赋值操作时。
  2. 当使用std::move函数将左值转换为右值引用后进行赋值操作时。
  3. 当返回右值引用的函数的结果被赋值给目标对象时。
  4. 当涉及到内置类型时,移动赋值并不适用,因为内置类型的对象直接存储其值,不涉及资源的转移和释放。
  • 内置类型
int main()
{
	//内置类型
	int a = 10;
	int b = std::move(a); // b 现在拥有 a 的值

	int* p = new int(10);
	int* q = std::move(p); // q 现在拥有 p 所指向的内存
	*p = 30;

	cout << *p  <<  " ||| " << *q << endl; //30 ||| 30
	
	std::vector<int> vec1;
	vec1.push_back(1);
	std::vector<int> vec2 = std::move(vec1); // vec2 现在拥有 vec1 的所有权,vec1 是空的
}

  • 自定义类型
#include <iostream>
#include <string>

class MyClass {
private:
    std::string data;

public:
    // 构造函数
    MyClass(const std::string& value) : data(value) {
        std::cout << "构造函数,值为 " << data << std::endl;
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept : data(std::move(other.data)) {
        std::cout << "移动构造函数,值为 " << data << std::endl;
    }

    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            data = std::move(other.data);
            std::cout << "移动赋值运算符,新值为 " << data << std::endl;
        }
        return *this;
    }

    // 析构函数
    ~MyClass() {
        std::cout << "析构函数,值为 " << data << std::endl;
    }

    // 输出值
    void print() const {
        std::cout << "值为 " << data << std::endl;
    }
};

int main() {
    MyClass obj1("Hello");
    MyClass obj2("World");

    obj1 = std::move(obj2);

    obj1.print();
    obj2.print();

    return 0;
}

输出结果::
构造函数,值为 Hello
构造函数,值为 World
移动赋值运算符,新值为 World
值为 World
值为
析构函数,值为
析构函数,值为 World

使用 std::move 将 obj2 移动赋值给 obj1,从而触发了移动赋值操作。在移动赋值运算符中,我们通过使用 std::move 将 other 的数据成员的值转移到了 data 中,实现了资源的转移。最后,我们分别输出了 obj1 和 obj2 的值,在 obj1 中打印出了新值 “World”,而 obj2 的值为空。


取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class Date
{
public:
    Date* operator&()
    {
        return this;
    }
    const Date* operator&()const
    {
        return this;
    }
private:
    int _year; // 年
    int _month; // 月
    int _day; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!


强制生成默认函数的关键字default

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原
因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以
使用default关键字显示指定移动构造生成。

class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}
    Person(const Person& p)
        :_name(p._name)
        , _age(p._age)
    {}
    Person(Person&& p) = default;
private:
    string _name;
    int _age;
};
int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);
    return 0;
}

禁止生成默认函数的关键字delete

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}
    Person(const Person& p) = delete;
private:
    string _name;
    int _age;
};
int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);
    return 0;
}
  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值