C++ 拷贝构造函数与赋值函数

news/2024/5/19 2:01:50 标签: c++, 构造函数, 赋值函数

这里我们用类String 来介绍这两个函数:

拷贝构造函数是一种特殊构造函数具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用拷贝构造函数。为啥形参必须是对该类型的引用呢?试想一下,假如形参是该类的一个实例,由于是传值参数,我们把形参复制到实参会调用拷贝构造函数,如果允许拷贝构造函数传值,就会在拷贝构造函数内调用拷贝构造函数,从而形成无休止的递归调用导致栈溢出。

string(const string &s);
//类成员,无返回值

赋值函数,也是赋值操作符重载,因为赋值必须作为类成员,那么它的第一个操作数隐式绑定到 this 指针,也就是 this 绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为const 引用传递。

string& operator=(const string &s);
//类成员,返回对同一类类型(左操作数)的引用

拷贝构造函数赋值函数并非每个对象都会使用,另外如果不主动编写的话,编译器将以“位拷贝”的方式自动生成缺省的函数。在类的设计当中,“位拷贝”是应当防止的。倘若类中含有指针变量,那么这两个缺省的函数就会发生错误。这就涉及到深复制和浅复制的问题了。 
拷贝有两种:深拷贝,浅拷贝 
当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。 
但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。 
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。指向不同的内存空间,但内容是一样的 
简而言之,当数据成员中有指针时,必须要用深拷贝。

class A{
    char * c;
}a, b;

//浅复制不会重新分配内存
//将a 赋给 b,缺省赋值函数的“位拷贝”意味着执行
a.c = b.c;
//从这行代码可以看出
//b.c 原有的内存没有释放
//a.c 和 b.c 指向同一块内存,任何一方的变动都会影响到另一方
//对象析构的时候,c 被释放了两次(a.c == b.c 指针一样)

//深复制需要自己处理里面的指针
class A{
    char *c;
    A& operator =(const A &b)
    {
        //隐含 this 指针
        if (this == &b)
            return *this;
        delete c;//释放原有内存资源

        //分配新的内存资源
        int length = strlen(b.c);
        c = new char[length + 1];
        strcpy(c, b.c);

        return *this;
    }
}a, b;
//这个是深复制,它有自定义的复制函数,赋值时,对指针动态分配了内存

这里再总结一下深复制和浅复制的具体区别:

  1. 当拷贝对象状态中包含其他对象的引用时,如果需要复制的是引用对象指向的内容,而不是引用内存地址,则是深复制,否则是浅复制。
  2. 浅复制就是成员数据之间的赋值,当值拷贝时,两个对象就有共同的资源。而深拷贝是先将资源复制一份,是对象拥有不同的资源(内存区域),但资源内容(内存里面的数据)是相同的。
  3. 与浅复制不同,深复制在处理引用时,如果改变新对象内容将不会影响到原对象内容
  4. 与深复制不同,浅复制资源后释放资源时可能会产生资源归属不清楚的情况(含指针时,释放一方的资源,其实另一方的资源也随之释放了),从而导致程序运行出错

深复制和浅复制还有个区别就是执行的时候,浅复制是直接复制内存地址的,而深复制需要重新开辟同样大小的内存区域,然后复制整个资源。

好,有了前面的铺垫,下面开始讲讲拷贝构造函数赋值函数,其实前面第一部分也已经介绍了许多

这里以string 类为例来进行说明

class String
{
public:
    String(const char *str = NULL);
    String(const String &rhs);
    String& operator=(const String &rhs);
    ~String(void){
        delete[] m_data;
    }

private:
    char *m_data;
};

//构造函数
String::String(const char* str)
{
    if (NULL == str)
    {
        m_data = new char[1];
        *m_data = '\0';
    }
    else
    {
        m_data = new char[strlen(str) + 1];
        strcpy(m_data, str);
    }
}

//拷贝构造函数,无需检验参数的有效性
String::String(const String &rhs)
{
    m_data = new char[strlen(rhs.m_data) + 1];
    strcpy(m_data, rhs.m_data);
}

//赋值函数
String& String::operator=(const String &rhs)
{
    if (this == &rhs)
        return *this;

    delete[] m_data; m_data = NULL;
    m_data = new char[strlen(rhs.m_data) + 1];
    strcpy(m_data, rhs.m_data);

    return *this;
}

类String 拷贝构造函数与普通构造函数的区别是:在函数入口处无需与 NULL 进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。(这是引用与指针的一个重要区别)。然后需要注意的就是深复制了。 
相比而言,对于类String 的赋值函数则要复杂的多:

1、首先需要执行检查自赋值

这是防止自复制以及间接复制,如 b = a; c = b; a = c;之类,如果不进行自检的话,那么后面的 delete 将会进行自杀操作,后面随之的拷贝操作也会出错,所以这是关键的一步。还需要注意的是,自检是检查地址,而不是内容,内存地址是唯一的。必须是 if(this == &rhs)

2、释放原有的内存资源

必须要用 delete 释放掉原有的内存资源,如果此时不释放,该变量指向的内存地址将不再是原有内存地址,也就无法进行内存释放,造成内存泄露。

3、分配新的内存资源,并复制资源

这样变量指向的内存地址变了,但是里面的资源是一样的

4、返回本对象的引用

这样的目的是为了实现像 a = b = c; 这样的链式表达,注意返回的是 *this 。

但仔细一想,上面的程序没有考虑到异常安全性,我们在分配内存之前用delete 释放了原有实例的内存,如果后面new 出现内存不足抛出异常,那么之前delete 的 m_data 将是一个空指针,这样很容易引起程序崩溃,所以我们可以调换下顺序,即先 new 一个实例内存,成功后再用 delete 释放原有内存空间,最后用 m_data 赋值为new后的指针。

接下来说说拷贝构造函数赋值函数之间的区别。

拷贝构造函数赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建是调用的,而赋值函数只能在已经存在了的对象调用。看下面代码:

    String a("hello");
    String b("world");

    String c = a;//这里c对象被创建调用的是拷贝构造函数
                 //一般是写成 c(a);这里是与后面比较
    c = b;//前面c对象已经创建,所以这里是赋值函数

上面说明出现“=”的地方未必调用的都是赋值函数(算术符重载函数),也有可能拷贝构造函数,那么什么时候是调用拷贝构造函数,什么时候是调用赋值函数你?判断的标准其实很简单:如果临时变量是第一次出现,那么调用的只能是拷贝构造函数,反之如果变量已经存在,那么调用的就是赋值函数 
参考资料:《Effective C++》、《高质量C++&C编程指南》

 

转自: https://blog.csdn.net/zhoucheng05_13/article/details/80937775


http://www.niftyadmin.cn/n/618115.html

相关文章

jQuery选择器之内容筛选选择器

背景: 基本筛选选择器针对的都是元素DOM节点,如果我们要通过内容来过滤,jQuery也提供了一组内容筛选选择器,当然其规则也会体现在它所包含的子元素或者文本内容上。 选择器描述$(“:contains(text)”)选择所有包含指定文本的元素…

Linux 查看与修改mtu值

MTU:通信术语 最大传输单元(Maximum Transmission Unit)是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位)。 我们在使用互联网时进行的各种网络操作,都是通过一个又一个“数据包”传…

并行-问题拾遗

背景: 正做并行设计的时候,报错: terminate called after throwing an instance of ‘std::bad_alloc’ 查看代码,发现是在做并行计算任务id(字符串)获取之后,对id做一次按照\t的分割,并将结…

C11标准和C++11标准

1、 C语言的C11 标准有哪些改进的地方&#xff1f;会对各类C项目起到哪些帮助&#xff1f; 1. 对齐处理操作符 alignof&#xff0c;函数 aligned_alloc()&#xff0c;以及 头文件 <stdalign.h>。 2. _Noreturn 函数标记&#xff0c;类似于 gcc 的 __attribute__((nore…

python学习笔记-半角字符和全角之间的转换

背景&#xff1a; 在做日文文本的处理时候&#xff0c;统一半全角字符。 分析&#xff1a; 说明: 全角字符unicode编码从65281~65374 &#xff08;十六进制 0xFF01 ~ 0xFF5E&#xff09; 半角字符unicode编码从33~126 &#xff08;十六进制 0x21~ 0x7E&#xff09; 特例&…

linux磁盘异常占用

背景&#xff1a; 某服务器报警&#xff0c;/根目录下磁盘超设定阈值。 分析&#xff1a; 1&#xff1a;查看磁盘占用情况 df -h 可以看出根目录已经占用了86%: 从上述信息可以看出根目录的文件系统对应的设备是/dev/sda2。 注&#xff1a;disk free(df&#xff0c;检…

理解ffmpeg 中的pts,dts,time_base

PTS&#xff1a;Presentation Time Stamp。PTS主要用于度量解码后的视频帧什么时候被显示出来 DTS&#xff1a;Decode Time Stamp。DTS主要是标识读入内存中的&#xff42;&#xff49;&#xff54;流在什么时候开始送入解码器中进行解码 也就是pts反映帧什么时候开始显示&am…

交叉编译流量监测工具nload

一、编译安装ncurses 下载&#xff1a;wget http://ftp.gnu.org/gnu/ncurses/ncurses-6.0.tar.gz 配置&#xff1a;./configure --prefix/home/install/ncurses --hostarm-hisiv300-linux --with-normal 要生成动态库加--with-shared 编译安装&#xff1a;make &…