1.拷贝构造函数

1.1拷贝构造函数基本形式

就类对象而言,相同类型的类对象是通过拷贝构造函数来在对象初始化期间完成整个复制过程的。

拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类T的拷贝构造函数的形式为T(const T& t)

当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数

一个对象以值传递的方式传入函数体

一个对象以值传递的方式从函数返回

一个对象需要通过另外一个已存在的对象进行初始化。

如果没有自己实现拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,默认的拷贝构造函数完成对象之间的位拷贝,又称浅拷贝。对于任何含有指针变量的类,默认的(浅)拷贝函数注定出错。

浅拷贝只是简单地将一个对象内存的数据复制给另一个对象。在类中含有指针变量状况下,指针变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

下面实现了一个拷贝构造函数的例子:

class Cgoods
{
public:Cgoods(){cout<<"Cgoods()"<<endl;}Cgoods(char *name,int num,double price){cout<<"Cgoods(char *name,int num,double price)"<<endl;if(NULL == name){return;}_name = new char[strlen(name)+1];strncpy(_name,name,strlen(name)+1);this->_num = num;_price = price;}//拷贝构造函数Cgoods(const Cgoods& good){cout<<"Cgoods(Cgoods& good)"<<endl;_name = new char[strlen(good._name)+1];     //深拷贝strncpy(_name,good._name,strlen(good._name)+1);_num = good._num;_price = good._price;}void show(){cout<<"name:"<<_name<<endl;cout<<"num:"<<_num<<endl;cout<<"price:"<<_price<<endl;}~Cgoods(){cout<<"~Cgoods()"<<endl;if(NULL != _name){delete []_name;_name = NULL;}}
private:char *_name;int _num;double _price;
};int main()
{Cgoods good1("shangpin1",10,23.5);Cgoods good2=good1;   //Cgoods good2(good1);return  0;
}

初始化good1时调用了构造函数,初始化good2时调用了拷贝构造函数。

1.2为什么拷贝构造函数必须为引用传递,而不能值传递?

参数传递过程到底发生了什么:

将地址传递和值传递统一起来,归根结底还是传递的是"值"(地址也是值,只不过通过它可以找到另一个值)!

(1)值传递

对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);
对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);

(2)引用传递

无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型)。

因此,如果拷贝构造函数为值传递的话,会造成调用拷贝构造函数的死循环。

2.赋值运算符的重载

2.1赋值运算符的重载函数基本形式

在对象进行赋值操作的时候调用赋值运算符的重载函数,一般来说,类T的赋值运算符重载函数的形式为:

T& operator=(const T &t)

赋值运算符重载函数的参数是函数所在类的const类型的引用。加引用可以避免在函数调用时对实参的一次拷贝,提高了效率。

但const和引用都不是必须的。

赋值运算符重载函数的返回值是被赋值者的引用,即*this。加引用可以在函数返回时避免一次拷贝。最重要的是,通过返回引用值可以实现连续赋值。即类似a=b=c这样。(如果不是返回引用而是返回值类型,那么,执行a=b时,调用赋值运算符重载函数,在函数返回时,由于返回的是值类型,所以要对return后边的“东西”进行一次拷贝,得到一个未命名的副本(有些资料上称之为“匿名对象”),然后将这个副本返回,而这个副本是右值,所以,执行a=b后,得到的是一个右值,再执行=c就会出错。)

这也不是必须的,可以将函数返回值声明为void,什么也不返回,但那样就不能连续赋值了。

下面实现了一个赋值运算符重载函数的例子:

class Cgoods
{
public:Cgoods(){cout<<"Cgoods()"<<endl;_name = NULL;}Cgoods(char *name,int num,double price){cout<<"Cgoods(char *name,int num,double price)"<<endl;if(NULL == name){return;}_name = new char[strlen(name)+1];strncpy(_name,name,strlen(name)+1);this->_num = num;_price = price;}Cgoods(const Cgoods& good){cout<<"Cgoods(Cgoods& good)"<<endl;_name = new char[strlen(good._name)+1];strncpy(_name,good._name,strlen(good._name)+1);_num = good._num;_price = good._price;}void show(){cout<<"name:"<<_name<<endl;cout<<"num:"<<_num<<endl;cout<<"price:"<<_price<<endl;}~Cgoods(){cout<<"~Cgoods()"<<endl;if(NULL != _name){delete []_name;_name = NULL;}}//赋值运算符重载函数Cgoods& operator=(const Cgoods& good){cout<<"Cgoods operator=(const Cgoods& good)"<<endl;if(this != &good)    //避免自赋值{if(NULL != _name)  //避免内存泄漏{delete []_name;}_name = new char[strlen(good._name)+1];strncpy(_name,good._name,strlen(good._name)+1);_num = good._num;_price = good._price;}return *this;}private:char *_name;int _num;double _price;
};int main()
{Cgoods good1("shangpin1",10,23.5);Cgoods good2("shangpin2",20,25.3);Cgoods good3 = good1;good3 = good2 = good1;return 0;
}

可以看到调用了两次构造函数,一次拷贝构造函数,和两次赋值运算符的重载函数。

2.2赋值运算符重载函数要避免自赋值

在赋值运算符重载函数的实现部分可以看到,我们首先判断是否是自赋值,避免自赋值情况的发生。一般地,我们通过比较赋值者与被赋值者的地址是否相同来判断两者是否是同一对象,正如if(this!=&good)这一句。

为什么要避免自赋值?

①为了效率。显然,自己给自己赋值完全是毫无意义的无用功,如果我们一旦判定是自赋值,就立即return *this,会避免对其它函数的调用。

②如果类的数据成员中含有指针,自赋值有时会导致灾难性的后果。对于指针间的赋值(这里假设用_p给p赋值),先要将p所指向的空间delete掉(为什么要这么做呢?因为指针p所指的空间通常是new来的,如果在为p重新分配空间前没有将p原来的空间delete掉,会造成内存泄露),然后再为p重新分配空间,将_p所指的内容拷贝到p所指的空间。如果是自赋值,那么p和_p是同一指针,在赋值操作前对p的delete操作,将导致p所指的数据同时被销毁。那么重新赋值时,拿什么来赋?

所以,对于赋值运算符重载函数,一定要先检查是否是自赋值,如果是,直接return *this

2.3默认的赋值运算符重载函数

当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个浅拷贝的赋值运算符重载函数。也就是说只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本。

看一个例子:

#include<iostream>
#include<string>
using namespace std;class Data
{
private:int data;
public:Data() {};Data(int _data):data(_data){cout << "constructor" << endl;}Data& operator=(const int _data)    //参数类型不是本类或本类的引用{cout << "operator=(int _data)" << endl;data = _data;return *this;}
};int main()
{Data data1(1);Data data2,data3;cout << "=====================" << endl;data2 = 1;cout << "=====================" << endl;data3 = data2;return 0;
}

我们提供了一个带int型参数的赋值运算符重载函数,data2 = 1;一句调用了该函数,如果编译器不再提供默认的赋值运算符重载函数,那么,data3 = data2;一句将不会编译通过,但我们看到事实并非如此。说明此种情况下,系统提供了一个默认的赋值运算符重载函数。

2.4区分拷贝构造函数和赋值运算符重载的一些调用形式

class Cgoods
{
public:Cgoods(){cout<<"Cgoods()"<<endl;_name = NULL;}Cgoods(char *name,int num = 10,double price = 2.5){cout<<"Cgoods(char *name,int num,double price)"<<endl;if(NULL == name){return;}_name = new char[strlen(name)+1];strncpy(_name,name,strlen(name)+1);this->_num = num;_price = price;}Cgoods(const Cgoods& good){cout<<"Cgoods(Cgoods& good)"<<endl;_name = new char[strlen(good._name)+1];strncpy(_name,good._name,strlen(good._name)+1);_num = good._num;_price = good._price;}void show(){cout<<"name:"<<_name<<endl;cout<<"num:"<<_num<<endl;cout<<"price:"<<_price<<endl;}~Cgoods(){cout<<"~Cgoods()"<<endl;if(NULL != _name){delete []_name;_name = NULL;}}Cgoods& operator=(const Cgoods& good){cout<<"Cgoods operator=(const Cgoods& good)"<<endl;if(this != &good){if(NULL != _name){delete []_name;}_name = new char[strlen(good._name)+1];strncpy(_name,good._name,strlen(good._name)+1);_num = good._num;_price = good._price;}return *this;}private:char *_name;int _num;double _price;
};int main()
{Cgoods good1;  //Cgoods()Cgoods good2("shangpin2",2,12.5);   //Cgoods(char *name,int num,double price)Cgoods good3("shangpin3");   //Cgoods(char *name,int num,double price)Cgoods good4 = good3; //Cgoods(Cgoods& good)Cgoods good5 = Cgoods("shangpin5");        //Cgoods(char *name,int num,double price)//构造临时对象    -》     用临时拷贝构造   -》析构临时对象 //编译器优化流程为:直接构造good5Cgoods Cgood6 = "shangpin6";   //Cgoods(char *name,int num,double price)//构造good6Cgoods good7;good7 = Cgoods("shangpin7");//构造临时对象  -》  运算符重载  -》析构临时对象//Cgoods()//Cgoods(char *name,int num,double price)//Cgoods operator=(const Cgoods& good)Cgoods good8  = (Cgoods)"shangpin";//~Cgoods()//Cgoods(char *name,int num,double price)return 0;
}

【C/C++】拷贝构造函数 赋值运算符的重载相关推荐

  1. c++复习(2)拷贝构造函数与运算符重载

    目录 前言 拷贝构造函数 函数定义 调用 缺省(默认)的拷贝构造函数 -- 浅拷贝 涉及指针或者内存操作 用char * 用char[] 用string 自己写的拷贝构造函数 类中数据含有指针 类中含 ...

  2. C++ 类和对象(二):构造函数、析构函数、拷贝构造函数、运算符重载

    构造函数 析构函数 拷贝构造函数 运算符重载 class Date {}; 可以看到,上面那个类没有任何成员,是一个空类,但是它真的什么都没有吗?其实一个类在我们不写的情况下,都会生成6个默认的成员函 ...

  3. C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解

    目录 类的6个默认成员函数 一.构造函数 1.概念 2.特征如下: (1) 函数名与类名相同. (2)无返回值. (3)对象实例化时编译器自动调用对应的构造函数. (4)构造函数可以重载. (5)如果 ...

  4. 拷贝构造函数与赋值重载

    目录 前言 1.拷贝构造函数

  5. 类与对象:类的6个默认成员函数: 构造函数、析构函数、拷贝构造函数、赋值操作符重载、默认拷贝构造与赋值运算符重载的问题、const成员函数、 取地址及const取地址操作符重载

    1.类的6个默认成员函数 如果一个类中什么成员都没有,简称为空类.任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数. 构造函数 析构函数 拷贝构造函数 赋值操作符重载 const成员函数 ...

  6. C++编程思想:指针,引用,拷贝构造函数,赋值运算符

    文章目录 对指针的引用和指针的指针到底是怎么一回事? 函数返回引用到底返回了什么? 拷贝构造函数和默认拷贝构造函数 自定义拷贝构造函数 使用默认拷贝构造函数 =赋值运算符重载 对指针的引用和指针的指针 ...

  7. C++“拷贝构造函数”和“重载 = 运算符”

    文章目录 一:拷贝构造函数和缺省拷贝构造函数 (一)拷贝构造函数 1.功能介绍 2.定义方法 3.特点 (二)缺省拷贝构造函数 1.功能 2.定义(系统自动生成) 3.特点 (三)"浅&qu ...

  8. C++、构造函数与拷贝构造函数

    1. 初始化构造函数 构造函数的函数体内属于数据成员的赋值,初始化发生在函数体语句之前,所以需要显式初始化的成员必须在初始化列表中完成初始化,包括: 包含 const 或引用成员,且没有提供默认初始化 ...

  9. 什么是拷贝构造函数?拷贝构造函数何时被调用

    1.什么是拷贝构造函数: CA(const CA& C)就是我们自定义的拷贝构造函数.可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变 ...

最新文章

  1. 8个实用而有趣Bash命令提示行
  2. 每天研究一个产品,阿德老师“手摸手”带你写产品分析报告 |
  3. 如何用 ajax 连接mysql数据库,并且获取从中返回的数据。ajax获取从mysql返回的数据。responseXML分别输出不同数据的方法。...
  4. Vue 实例生命周期
  5. 1022 D进制的A+B (20 分)(c语言)
  6. 复旦大学把衣服变成了显示器,能聊天能导航,水洗弯折都不怕
  7. 【CodeForces - 616C 】The Labyrinth点石成金(并查集,dfs)
  8. Android WebView常见问题及解决方案汇总【很全很实用】
  9. DriverMessageBean配置详解
  10. windows下的文件遍历(使用CFindFile)
  11. Java 十大必读经典书籍推荐
  12. linux/ubuntu16.04系统上snowboy swig源码安装及使用全记录和遇到的错误
  13. 两篇文章的相似度比较
  14. 2022-03-27 screenX和clientX的区别以及offsetX和pageX的区别
  15. Python发送验证码短信
  16. Anaconda4.5.1+tensorflow2.1.0+keras2.3.1+theano+Mingw+python3.6安装总结
  17. 【JavaScript实现十进制转换成二进制】
  18. 百度-视觉技术部招聘计算机视觉相关算法实习生
  19. 爬虫手机App——数据采集小攻略
  20. 心跳服务1.0(Heart Rate Service 1.0)

热门文章

  1. 细说嵌入式Linux文件系统的制作方法
  2. 《XNA游戏开发》简介
  3. python对XML 操作
  4. 蓝桥杯 BASIC-3 基础练习 字母图形
  5. 日期上午下午怎么用date存_Java12都要出来了,你还在使用Date操作日期吗?
  6. configure 包,出现error: no acceptable C compiler found in $PATH 问题
  7. echarts 统计图如何实现打印导出
  8. 订单中有订单详细实体类。保存订单
  9. android 关于提高第三方app的service优先级
  10. 关于单页面应用一些随想