【C++修行之道】类和对象(三)拷贝构造函数

【C++修行之道】类和对象(三)拷贝构造函数

码农世界 2024-06-18 后端 107 次浏览 0个评论

目录

一、 概念

二、特征

 正确的拷贝构造函数写法:

拷贝函数的另一种写法 

三、若未显式定义,编译器会生成默认的拷贝构造函数。

四、编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?

深拷贝的写法:

五、拷贝构造函数典型调用场景:

六、总结:


一、 概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。

那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

【C++修行之道】类和对象(三)拷贝构造函数

二、特征

拷贝构造函数也是特殊的成员函数,其特征如下: 

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
  4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
  5. 拷贝构造函数典型调用场景:
  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象
    class Date
    {
    public:
    	Date(int year = 1900, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
        Date(const Date& d) // 正确写法
        {
    	    // this->_year = d._year;
    	    _year = d._year;
        	_month = d._month;
    	    _day = d._day;
        }
    	Date(Date& d)// 错误写法: 它不能用于从常量对象或临时对象进行拷贝构造
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    	Date(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;
    }
    

     正确的拷贝构造函数写法:

    Date(const Date& d) // 正确写法
    {
    	// this->_year = d._year;
    	_year = d._year;
    	_month = d._month;
    	_day = d._day;
    }

    这是正确的拷贝构造函数写法。它接受一个对Date类型的常量引用作为参数,这意味着它可以用于从常量对象、非常量对象甚至是临时对象进行拷贝构造。由于它的灵活性,这是最常用的拷贝构造函数定义方式。

    	Date(Date& d)
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    • 不能接收常量对象:这个构造函数只接受非常量引用(Date&),这意味着你不能使用它来拷贝一个常量对象。如果试图这样做,编译器会报错,因为常量对象不能被非常量引用所绑定。
    • 不能接收临时对象:在C++中,临时对象(也称为右值)经常出现在表达式中,例如函数返回值或者类型转换的结果。由于这个拷贝构造函数不接受右值引用或常量引用,因此它不能用于拷贝这些临时对象。
    • 发生错误操作时没有报错:赋值反了

      【C++修行之道】类和对象(三)拷贝构造函数

          Date(Date d)// 错误写法:编译报错,会引发无穷递归
      	{
      		_year = d._year;
      		_month = d._month;
      		_day = d._day;
      	}

      这个构造函数是错误的,会引发无穷递归。原因在于,当试图用这个构造函数创建一个Date对象时,它会尝试以值传递的方式接收一个Date对象作为参数。为了构造这个参数对象d,又需要调用拷贝构造函数,这会导致无限递归调用,最终耗尽栈空间并导致程序崩溃。 

      【C++修行之道】类和对象(三)拷贝构造函数

      【C++修行之道】类和对象(三)拷贝构造函数

      【C++修行之道】类和对象(三)拷贝构造函数

      class Date
      {
      public:
      	Date(int year, int month, int day)
      	{
      		_year = year;
      		_month = month;
      		_day = day;
      	}
      	/*Date(Date& d)
      	{
      		d._year = _year;
      		d._month = _month;
      		d._day = _day;
      	}*/
      	// Date d3(d2);
      	//Date(const Date& d)
      	//{
      	//	// this->_year = d._year;
      	//	_year = d._year;
      	//	_month = d._month;
      	//	_day = d._day;
      	//}
      	Date(Date* d)
      	{
      		_year = d->_year;
      		_month = d->_month;
      		_day = d->_day;
      	}
      	void Print()
      	{
      		cout << _year << "-" << _month << "-" << _day << endl;
      	}
      private:
      	// 给缺省值
      	int _year = 1;
      	int _month = 1;
      	int _day = 1;
      };
      typedef int DataType;
      class Stack
      {
      public:
      	Stack(size_t capacity = 3)
      	{
      		cout << "Stack(size_t capacity = 3)" << endl;
      		_array = (DataType*)malloc(sizeof(DataType) * capacity);
      		if (NULL == _array)
      		{
      			perror("malloc申请空间失败!!!");
      			return;
      		}
      		_capacity = capacity;
      		_size = 0;
      	}
      	// Stack st2 = st1;
      	Stack(const Stack& st)
      	{
      		_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
      		if (NULL == _array)
      		{
      			perror("malloc申请空间失败!!!");
      			return;
      		}
      		memcpy(_array, st._array, sizeof(DataType) * st._size);
      		_size = st._size;
      		_capacity = st._capacity;
      	}
      	void Push(DataType data)
      	{
      		// CheckCapacity();
      		_array[_size] = data;
      		_size++;
      	}
      	bool  Empty()
      	{
      		return _size == 0;
      	}
      	DataType Top()
      	{
      		return _array[_size - 1];
      	}
      	
      	void Pop()
      	{
      		--_size;
      	}
      	// 其他方法...
      	~Stack()
      	{
      		cout << "~Stack()" << endl;
      		if (_array)
      		{
      			free(_array);
      			_array = NULL;
      			_capacity = 0;
      			_size = 0;
      		}
      	}
      private:
      	DataType* _array;
      	int _capacity;
      	int _size;
      };

      拷贝函数的另一种写法 

      int main()
      {
      	Date d2(2024, 4, 9);
      	// 下面这两种写法是等价的
      	Date d3(d2);
      	Date d4 = d2; // 这也是拷贝构造
      	d2.Print();
      	d3.Print();
      	//func(d2);
      	/*Date d3(&d2);
      	d3.Print();*/
      	return 0;
      }

      【C++修行之道】类和对象(三)拷贝构造函数

      【C++修行之道】类和对象(三)拷贝构造函数

      三、若未显式定义,编译器会生成默认的拷贝构造函数。

      默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

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

      【C++修行之道】类和对象(三)拷贝构造函数

      在上述代码中,Date 类并没有显式定义拷贝构造函数。当代码中尝试通过已有的 Date 对象 d1 来拷贝构造一个新的 Date 对象 d2 时,由于没有找到用户定义的拷贝构造函数,编译器会自动为 Date 类生成一个默认的拷贝构造函数。

      这个默认生成的拷贝构造函数会完成以下任务:

      1. 对于基本数据类型成员:直接拷贝其值。在 Date 类中,_year、_month 和 _day 这三个整型成员变量会直接被赋值,即新对象 d2 的这些成员会获得与 d1 相同的值。
      2. 对于自定义类型成员:调用该类型的拷贝构造函数。在 Date 类中,_t 是 Time 类型的成员变量。当默认拷贝构造函数被调用时,它会进一步调用 Time 类的拷贝构造函数来初始化新对象 d2 中的 _t 成员。这就是为什么在上述代码中,即使没有显式编写拷贝操作,仍然可以看到 Time 类的拷贝构造函数被调用的输出。

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

      四、编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?

      尽管编译器生成的默认拷贝构造函数可以完成字节序的值拷贝,但在某些情况下,仍然需要自己显式实现拷贝构造函数。这主要是因为默认拷贝构造函数执行的是浅拷贝,它会拷贝对象的所有成员变量,但如果对象中包含指针或动态分配的资源(如使用 new 或 malloc 分配的内存),浅拷贝可能会导致问题。

      当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

      // 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
      typedef int DataType;
      class Stack
      {
      public:
      	Stack(size_t capacity = 10)
      	{
      		_array = (DataType*)malloc(capacity * sizeof(DataType));
      		if (nullptr == _array)
      		{
      			perror("malloc申请空间失败");
      			return;
      		}
      		_size = 0;
      		_capacity = capacity;
      	}
      	void Push(const DataType& data)
      	{
      		// CheckCapacity();
      		_array[_size] = data;
      		_size++;
      	}
      	~Stack()
      	{
      		if (_array)
      		{
      			free(_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;
      }
      

      【C++修行之道】类和对象(三)拷贝构造函数

      Stack s2(s1);
      • 这里,试图用s1来初始化s2。由于没有为Stack类提供自定义的拷贝构造函数,编译器会使用默认的拷贝构造函数。这个默认的拷贝构造函数将s1的_array指针值直接拷贝给s2的_array,这意味着s1和s2的_array成员现在指向同一块内存地址。
      • 当s1和s2的生命周期结束时,它们的析构函数都会被调用,并试图释放同一块内存,这会导致未定义行为,通常是程序崩溃,因为同一块内存被释放了两次(double free)。

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

        【C++修行之道】类和对象(三)拷贝构造函数

        深拷贝的写法:

        class Date
        {
        public:
        	Date(int year, int month, int day)
        	{
        		_year = year;
        		_month = month;
        		_day = day;
        	}
        	/*Date(Date& d)
        	{
        		d._year = _year;
        		d._month = _month;
        		d._day = _day;
        	}*/
        	// Date d3(d2);
        	//Date(const Date& d)
        	//{
        	//	// this->_year = d._year;
        	//	_year = d._year;
        	//	_month = d._month;
        	//	_day = d._day;
        	//}
        	Date(Date* d)
        	{
        		_year = d->_year;
        		_month = d->_month;
        		_day = d->_day;
        	}
        	void Print()
        	{
        		cout << _year << "-" << _month << "-" << _day << endl;
        	}
        private:
        	// 给缺省值
        	int _year = 1;
        	int _month = 1;
        	int _day = 1;
        };
        typedef int DataType;
        class Stack
        {
        public:
        	Stack(size_t capacity = 3)
        	{
        		cout << "Stack(size_t capacity = 3)" << endl;
        		_array = (DataType*)malloc(sizeof(DataType) * capacity);
        		if (NULL == _array)
        		{
        			perror("malloc申请空间失败!!!");
        			return;
        		}
        		_capacity = capacity;
        		_size = 0; 
        	}
        	// Stack st2 = st1;
        	Stack(const Stack& st)
        	{
        		_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
        		if (NULL == _array)
        		{
        			perror("malloc申请空间失败!!!");
        			return;
        		}
        		memcpy(_array, st._array, sizeof(DataType) * st._size);
        		_size = st._size;
        		_capacity = st._capacity;
        	}
        	void Push(DataType data)
        	{
        		// CheckCapacity();
        		_array[_size] = data;
        		_size++;
        	}
        	bool  Empty()
        	{
        		return _size == 0;
        	}
        	DataType Top()
        	{
        		return _array[_size - 1];
        	}
        	
        	void Pop()
        	{
        		--_size;
        	}
        	// 其他方法...
        	~Stack()
        	{
        		cout << "~Stack()" << endl;
        		if (_array)
        		{
        			free(_array);
        			_array = NULL;
        			_capacity = 0;
        			_size = 0;
        		}
        	}
        private:
        	DataType* _array;
        	int _capacity;
        	int _size;
        };
        void func(Date& d)
        {
        	d.Print();
        }
        class MyQueue 
        {
        private:
        	Stack _st1;
        	Stack _st2;
        	int _size = 0;
        };
        int main()
        {
        	Date d2(2024, 4, 9);
        	Date d4 = d2; 
        	d4.Print();
        	Stack st1(10);
        	st1.Push(1);
        	st1.Push(1);
        	st1.Push(1);
        	Stack st2 = st1;
        	st2.Push(2);
        	st2.Push(2);
        	while (!st2.Empty())
        	{
        		cout << st2.Top() << " ";
        		st2.Pop();
        	}
        	cout << endl;
        	while (!st1.Empty())
        	{
        		cout << st1.Top() << " ";
        		st1.Pop();
        	}
        	cout << endl;
        	MyQueue q1;
        	MyQueue q2(q1);
        	return 0;
        }
        • 拷贝构造函数创建的对象:为新对象分配足够的内存空间来存储栈中的元素。内存的大小是根据原始对象的容量(_capacity)来计算的。使用memcpy函数将原始对象栈中的元素复制到新分配的内存中。将新对象的_size和_capacity设置为与原始对象相同的值。 
        • MyQueue类中的成员变量:每个MyQueue对象都包含两个Stack对象,因此当q1和q2销毁时,它们的四个Stack成员变量(q1._st1, q1._st2, q2._st1, q2._st2)也会被销毁,每个Stack成员的析构函数都会被调用。这里增加了4次析构函数的调用。
        • 有一点需要注意:如果MyQueue类没有定义拷贝构造函数,并且默认使用了浅拷贝(即只拷贝成员变量的值,而不是它们所指向的内容),那么q2中的_st1和_st2实际上只是q1中对应成员的简单复制(指针或引用的复制)。在这种情况下,析构函数的调用次数可能会少于6次,因为多个对象可能共享相同的资源。但在上述代码中,我们假设MyQueue的拷贝构造函数进行了深拷贝,即创建了Stack对象的独立副本,因此每个Stack对象都有自己的生命周期,并在结束时调用析构函数。

          【C++修行之道】类和对象(三)拷贝构造函数

          五、拷贝构造函数典型调用场景:

          • 使用已存在对象创建新对象
          • 函数参数类型为类类型对象
          • 函数返回值类型为类类型对象
            class Date
            {
            public:
            	Date(int year, int month, 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;
            }

            【C++修行之道】类和对象(三)拷贝构造函数

             构造函数 Date(int year, int month, int day) 

                Date(int year, int month, int day)
            	{
            		cout << "Date(int,int,int):" << this << endl;
            	}
            

            拷贝构造函数 Date(const Date& d)

            当创建一个已存在Date对象的副本时会调用它。同样,当这个构造函数被调用时,会打印一条消息和当前对象的地址。

            Date()

            	~Date()
            	{
            		cout << "~Date():" << this << endl;
            	}

            为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

            六、总结:

            【C++修行之道】类和对象(三)拷贝构造函数

            今天就先到这了!!!

            【C++修行之道】类和对象(三)拷贝构造函数

            看到这里了还不给博主扣个:

            ⛳️ 点赞☀️收藏 ⭐️ 关注!

            你们的点赞就是博主更新最大的动力!

            有问题可以评论或者私信呢秒回哦。

转载请注明来自码农世界,本文标题:《【C++修行之道】类和对象(三)拷贝构造函数》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,107人围观)参与讨论

还没有评论,来说两句吧...

Top