在进行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. Winform开发框架中工作流模块之审批会签操作

    在前面介绍了框架中工作流的几个开发过程,本篇随笔重点介绍一下日常审批环节中的具体处理过程,从开始创建表单,以及各个审批.会签过程的流转过程,希望大家对其中流程的处理有一个大概的印象. 1.请假申请表单 ...

  2. 使用Mkdocs构建你的项目文档

    使用Mkdocs构建你的项目文档 环境搭建 安装必需软件 作者是在windows下安装的,如果是linux或mac用户,官网有更详细的安装说明. windows 10 x64 当然还有广大的windo ...

  3. js复制链接

    需求:js复制一个并非当前页面的链接 实现: html <button class="btn btn-primary btn_copyurl">复制课程链接</b ...

  4. 「mysql优化专题」高可用性、负载均衡的mysql集群解决方案(12)

    一.为什么需要mysql集群? 一个庞大的分布式系统的性能瓶颈中,最脆弱的就是连接.连接有两个,一个是客户端与后端的连接,另一个是后端与数据库的连接.简单如图下两个蓝色框框(其实,这张图是我在悟空问答 ...

  5. fatal: Authentication failed for “someurl”

    一.前言 我们在公司做项目,很多时候会遇到这个问题:Git failed with a fatal error. Authentication failed for ‘ http// xxx..... ...

  6. APP的线程安全

    一般来说iOS中两个就够了,但是安卓中的第三个,iOS也是要注意的: 第一:网络方面,别人以为做数据请求用post会比get请求安全,但是这是错的,post请求虽然看起来你的请求是在请求体上,不像ge ...

  7. STM32基础分析——USART的DMA模式

    有关USART的DMA传输模式,其基本的概念和配置,网上有很多博客和教程都有,这里不再赘述,只是记录一下比较容易忽视而造成调试不通的问题. 1. 串口发送和接收分属两个DMA通道 一般方式操作串口时, ...

  8. 一口一口吃掉Volley(四)

    欢迎访问我的个人博客转发请注明出处:http://www.wensibo.top/2017/02/17/一口一口吃掉Volley(四)/ 非常感谢你能够坚持看到第四篇,同时这也是这个Volley系列教 ...

  9. 查看内存和cpu

    top: 主要参数 d:指定更新的间隔,以秒计算. q:没有任何延迟的更新.如果使用者有超级用户,则top命令将会以最高的优先序执行. c:显示进程完整的路径与名称. S:累积模式,会将己完成或消失的 ...

  10. ElasticSearch 学习记录之ES如何操作Lucene段

    近实时搜索 提交(Commiting)一个新的段到磁盘需要一个 fsync 来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据.但是每次提交的一个新的段都fsync 这样操作代价过大.可以使用 ...