在进行C++类编写的过程之中,通常会涉及到类的拷贝构造函数与类的赋值函数。初涉类编写的代码,对于两类函数的用法一直是挺让人困惑的内容。这篇文章我们会详细来梳理拷贝构造函数与赋值函数的区别。

1.调用了哪个函数?

上述两种函数的使用和C++之中类的定义紧密相关,所以我们先定义一个类:

class Line {
public:
int getLength( void );
Line( int len ); //简单的构造函数 Line( const Line &obj) { //拷贝构造函数
cout << "调用拷贝构造函数" << endl;
ptr = new int;
*ptr = *obj.ptr;
}; Line& operator=(const Line &obj) { //赋值函数
cout << "调用赋值函数" << endl;
if(this == &obj) {
return *this;
} delete ptr;
ptr = new int;
*ptr = *obj.ptr; return *this; };
~Line(); // 析构函数 private:
int *ptr;
};

这里我们显式声明了拷贝构造函数与赋值构造函数,接下来我们用一小段代码测试一下上面定义的类。(其他函数的定义并不完整,读者可以之行补全)

int main() {
Line l1(10);
Line l2 = l1; Line l3(5);
l3 = l2;
}

输出结果如下:

调用拷贝构造函数
调用赋值函数

看似很相似的两个表达式,却调用了不同的两个函数。初学C++时,这样的结果让我很困惑,所以我们接下来梳理一下这两个函数。

2.拷贝构造函数

上面的代码我们可以看到代码 Line l2 = l1调用了拷贝构造函数。

拷贝构造函数,顾名思义,是一个构造函数,但是它特殊的点就在于在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。所以对于它的使用场合也很简单,只有在构造对象时才会调用到拷贝构造函数,显然Line l2 = l1是一个对象初始化的过程。我们知道每个类都会有构造函数,在对象初始化的过程之中,拷贝构造函数提供了一个通过一个同类型的对象对它进行初始化。

C++支持两种初始化形式:拷贝初始化(int a = 5;)和直接初始化(int a(5);)对于其他类型没有什么区别,对于类类型直接初始化直接调用实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数,也就是说:

A x(2);    //直接初始化,调用构造函数
A y = x;  //复制初始化,调用拷贝构造函数

在C++中,下面几个场景中,拷贝构造函数会被调用:

  • 一个对象需要通过另一个对象进行初始化
  • 一个对象以值传递的方式作为参数传入函数
  • 一个对象以值传递的方式作为返回值从函数返回

如果我们没有显式声明定义对应类的拷贝构造函数,C++编译器会默认生成对应的拷贝构造函数。既然C++编译器会自动生成拷贝构造函数,为什么我们又需要显式的去定义它呢?

因为由C++编译器提供的拷贝构造函数工作方式是浅拷贝。它单纯的使用了=操作符来拷贝类中的成员。但是如果类中用到了需要动态分配内存的成员(如Line类之中的ptr指针),则会出现内存安全的问题。同时对于类的封装性也是一种变相破坏,因为浅拷贝只是单纯拷贝了该成员的内存地址,但所指向的空间内容并没有复制,而是由两个对象共用,就容易出现double free等问题。所以此时就要手动重载拷贝构造函数,实现深拷贝

3.赋值函数

许多文章,博客喜欢把Line& operator=(const Line &obj)重载=操作符的函数称之为赋值构造函数。个人认为其实是不准确的,会产生一个理解误区。其实重载的=操作符就是一个赋值函数。

赋值函数:是把一个新的对象赋值给一个原有的对象,如果原来的对象中有内存分配需要先把内存释放掉。如果我们没有在类之中显式重载对应类的赋值函数,C++编译器也会默认生成对应的赋值函数。生成的规则与拷贝构造函数类似,也是一种浅拷贝的形式。所以我们重载赋值函数的原因也与拷贝构造函数类型,需要实现深度赋值。

由上文的代码也可以看出,赋值函数与拷贝构造函数定义的内容之中,所做的工作大同小异。唯一需要注意的点是:在赋值函数之中需要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。

x = x   //可能会出现赋值给自身的操作,所以需要对赋值对象进行检查

所以现在回头再来看看前文的代码,我们应该可以理解C++编译器调用不同函数的理由了:

int main() {
Line l1(10);
Line l2 = l1; Line l3(5);
l3 = l2;
}

当对象不存在,且用别的对象来初始化,此时调用的便是拷贝构造函数。而当对象已经存在,用别的对象来给它进行赋值操作时,调用的就是赋值函数了。

最后的小Tips:一旦在类之中声明了拷贝构造函数与赋值函数,编译器将不会生成缺省的对应函数。所以我们可以通过将拷贝构造函数和赋值函数声明为私有函数,来阻止编译器生成的缺省函数,在编译阶段来拒绝不安全的浅拷贝。

class Line {
public:
int getLength( void ) {
return *ptr;
};
Line( int len ) {
ptr = new int;
*ptr = len;
} //简单的构造函数
// ~Line(); // 析构函数 private:
int *ptr;
Line( const Line &obj);
Line& operator=(const Line &obj);
}; int main() {
Line l1(10);
Line l2 = l1; //编译器报错,无法通过编译 Line l3(5);
l3 = l2; //同样的的编译器报错,无法通过编译
}

好的,关于拷贝构造函数与赋值函数,就先写到这里。下一篇雾中风景系列,来聊一聊命名空间吧~~

C++雾中风景6:拷贝构造函数与赋值函数的更多相关文章

  1. 关于C++中的拷贝构造函数和赋值函数

    如果类定义的数据成员中存在指针或引用,那么最好重载这两个函数. 1.     定义 拷贝构造函数的定义格式:构造函数名(const 源类名& 引用对象形参名){} 赋值函数定义格式:源类名 & ...

  2. C++中:默认构造函数、析构函数、拷贝构造函数和赋值函数——转

    对于一个空类,编译器默认产生4个成员函数:默认构造函数.析构函数.拷贝构造函数和赋值函数.1.构造函数:构造函数是一种特殊的类成员,是当创建一个类的时候,它被调用来对类的数据成员进行初始化和分配内存. ...

  3. CPP_类默认函数:构造函数,拷贝构造函数,赋值函数和析构函数

    类默认函数:构造函数,拷贝构造函数,赋值函数和析构函数 // person.h #ifndef _PERSON_H_ #define _PERSON_H_ class Person{ public : ...

  4. C++中构造函数,拷贝构造函数和赋值函数的区别和实现

    C++中一般创建对象,拷贝或赋值的方式有构造函数,拷贝构造函数,赋值函数这三种方法.下面就详细比较下三者之间的区别以及它们的具体实现 1.构造函数 构造函数是一种特殊的类成员函数,是当创建一个类的对象 ...

  5. C++中的构造函数,拷贝构造函数,赋值函数

    C++中一般创建对象,拷贝或赋值的方式有构造函数,拷贝构造函数,赋值函数这三种方法.下面就详细比较下三者之间的区别以及它们的具体实现 1.构造函数 构造函数是一种特殊的类成员函数,是当创建一个类的对象 ...

  6. C++ 拷贝构造函数与赋值函数的区别(很严谨和全面)

    这里我们用类String 来介绍这两个函数: 拷贝构造函数是一种特殊构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用.当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式 ...

  7. 《剑指offer》面试题1:为类CMyString添加赋值运算符函数——C++拷贝构造函数与赋值函数

    题中已给出CMyString的类定义,要求写赋值运算符函数. #include<iostream> #include<cstring> using namespace std; ...

  8. String类的构造函数,析构函数、拷贝构造函数和赋值函数

    (1)构造函数 String::String(const char *str) { if(str==NULL) { m_data = new char[1]; *m_data = ‘\0’; } el ...

  9. C++类中的一些细节(重载、重写、覆盖、隐藏,构造函数、析构函数、拷贝构造函数、赋值函数在继承时的一些问题)

    1 函数的重载.重写(重定义).函数覆盖及隐藏 其实函数重载与函数重写.函数覆盖和函数隐藏不是一个层面上的概念.前者是同一个类内,或者同一个函数作用域内,同名不同参数列表的函数之间的关系.而后三者是基 ...

随机推荐

  1. 六.Spring与RabbitMQ集成--stock trading(股票交易系统)

    周末继续写博客,算起来,关于rabbitMQ这个中间件的研究已经持续至两个星期了,上一篇文章使用sring amqp实现了同步和异步的消息接收功能.这一节继续实用spring amqp实现一个股票交易 ...

  2. Material使用08 MdDialogModule、MdAutocompleteModule

    1 MatDialog 1.1 简要描述 MdDialog是一个服务,可以利用它来打开一个具有material风格和动画效果的对话框 技巧01:虽然已经在模块级别导入了MdDialogModule但是 ...

  3. JavaScript循环实例

    几个经典的循环案例: 1.一张纸的厚度是0.0001米,将纸对折,对折多少次厚度超过珠峰高度8848米 var i=0; var h=0.0001; while(true){ i++; h=h*2; ...

  4. 【USACO】玉米实验(单调队列)

    Description 约翰决定培育新的玉米品种以提高奶牛的产奶效率.约翰所有的土地被分成 N ×N 块,其中第 r行第 c 列的玉米质量为 Ar,c.他打算找一块质量比较均匀的土地开始自己的实验.经 ...

  5. S7-200以太网通信

    一.西门子网络系统 二.s7-200通过以太网模块接入以太网 三.S7-200可以接入的以太网系统 四.S7-200以太网通讯实验 五.实验硬件系统组成 六.S7-200作为服务器的配置 1.进入以太 ...

  6. bzoj 3712: [PA2014]Fiolki

    Description 化学家吉丽想要配置一种神奇的药水来拯救世界.吉丽有n种不同的液体物质,和n个药瓶(均从1到n编号).初始时,第i个瓶内装着g[i]克的第i种物质.吉丽需要执行一定的步骤来配置药 ...

  7. ES6 对象的扩展(下)

    属性的可枚举性 对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为.Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象. var ob ...

  8. 10 Easy Steps to a Complete Understanding of SQL

    原文出处:http://tech.pro/tutorial/1555/10-easy-steps-to-a-complete-understanding-of-sql(已经失效,现在收集如下) Too ...

  9. 《精通Spring 4.X企业应用开发实战》读书笔记1-1(IoC容器和Bean)

    很长一段时间关注在Java Web开发的方向上,提及到Jave Web开发就绕不开Spring全家桶系列,使用面向百度,谷歌的编程方法能够完成大部分的工作.但是这种不系统的了解总觉得自己的知识有所欠缺 ...

  10. Ubuntu16.04 添加 Docker用户组

    Ubuntu16.04 添加 Docker用户组 将用户添加到docker用户组就不用每次都 sudo了. ### 首先创建用户组 sudo groupadd docker 将用户加如组 sudo g ...