• “深入理解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();
    }
    
    
    • 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

    如果类中没有显式定义构造函数,则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;
    }
    
    • 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

    关于编译器生成的默认成员函数,很多同学会有疑惑:不实现构造函数的情况下,编译器会
    生成默认的构造函数。但是看起来默认构造函数又没什么用?对象调用了编译器生成的默
    认构造函数,但是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;
    }
    
    • 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

    注意:

    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;
    }
    
    • 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

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

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    解答:

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

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

    • 初始化列表

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

    class Date
    {
    public:
    	Date(int year, int month, int day)
    		: _year(year)
    		, _month(month)
    		, _day(day)
    	{}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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  随机值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    正确答案是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
    • 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

    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;
    }
    
    • 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

    程序运行结束后输出:~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
    • 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

    在这里插入图片描述


    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
    • 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
    • 38
    • 39
    • 40

    注意

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


    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;
    }
    
    • 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
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    ![在这里插入图片描述](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;
    }
    
    • 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

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


    移动构造

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

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

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

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


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

    #include
    #include
    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;
    }
    
    • 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
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    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
    • 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

    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
    • 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

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

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


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

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


    移动赋值运算符重载

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

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

    class MyClass {  
    public:  
        MyClass& operator=(MyClass&& other) {  
            // 移动资源  
            return *this;  
        }  
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其中,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 是空的
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 自定义类型
    #include 
    #include 
    
    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
    
    • 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
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    使用 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; // 日
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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


    强制生成默认函数的关键字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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    禁止生成默认函数的关键字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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  • 相关阅读:
    MIP精确算法的关键——确定界 篇一
    [nodejs]NVM使用指南
    Vue3+elementplus搭建通用管理系统实例三:登录流程实现
    网络安全中等保有几个级别?
    (Python)使用Matplotlib将x轴移动到绘图顶部
    查询三级数据结构sql语句
    如何用Claude 3 Sonnet Artifacts实现对数据文件的可视化分析?
    advanced installer 做包教程
    Centos7的系统管理(systemctl、系统运行级别、关机)
    Databend 集群部署 | 新手篇(2)
  • 原文地址:https://blog.csdn.net/m0_74173186/article/details/132640136