Bootstrap

C++拷贝构造器之浅拷贝与内存重析构

一般构造器、析构器、拷贝构造器会被称之为构造函数、析构函数与复制构造函数,但是其与函数还是有一些区别,所以我们以“器”来称呼之。

1、构造器、析构器与拷贝构造器:

(1)、构造器简析:

constructor 构造器
* 与类名相同A(),(A为类名),无返回值,生成对象时系统自行调用,相当于初始化
* 可以有参数,能够重载、以及设置默认参数
* 如果不自定义任何构造器,则系统会有一个默认的无参构造器生成,若自定义了一个构造器,则系统不再生成构造器
* 注意:无参数构造器与有参数但是有默认参数的构造器,定义对象时可能会产生二义性
* 所以重载与默认参数不要在同一个函数名中同时出现

(2)、析构器简析:

destructor 析构器
* 格式:~A(),无参数、无返回值、用于对象销毁时的内存处理工作(对象消失时,自动被调用)
* 所谓消失,指的是跳出其作用空间,且以后不会再使用
* 若无自定义析构器,则系统默认生成一个析构器
* 对于系统自行析构的,先创建的后析构、后创建的先析构
* 由于析构器无参数,所以不存在重载的问题

(3)、拷贝构造器:

copy constructor 拷贝构造器
* 格式:A(const A &);
* 若不自定义,则采用系统默认的拷贝构造器(类似于构造器)
* 系统提供的拷贝构造器,默认是一个等位拷贝,即江湖上传闻的“浅拷贝”,浅拷贝可能会导致内存重析构(double free),但是内存重析构在有些系统上可能不会表现出来(如Windows)。
* 在有些情况(对象含有堆空间的时候),要自定义实现拷贝构造器
* 拷贝构造是一个从无到有的过程(用一个已有的对象,完成另一个对象从无到有并初始化的过程),而构造是一个从有到初始化的过程

eg:(注意一下几种写法的区别)
string a("China");//构造
string b = a;//拷贝构造
string c(a);//拷贝构造

string d;//构造
d = a;//赋值运算符重载

2、拷贝构造之浅拷贝与内存重析构:

所谓内存重析构,其实就是同一块堆内存被释放了两次,第一次释放没有什么问题,但是第二次属于free(NULL);的操作,这种内存重析构(重复析构)存在逻辑性的致命错误。
而浅拷贝由于只存在一份堆空间,却存在两个对象拥有指向该空间的指针,所以析构时,两个指针指向的空间都会被回收,但是第二次回收,指针NULL无指向、无效。但深拷贝析构时,各自对象有各自的堆内存,就不会存在这种重析构出错的情况(如下图)。(浅拷贝存在的这种问题在有的平台上或许不会变现出来)

这里写图片描述

以实例(string类的拷贝构造器)说明拷贝构造器的浅拷贝与内存重析构:

/*mystring.h*/
#ifndef MYSTRING_H
#define MYSTRING_H

#include <stdio.h>
#include <string.h>
class MyString
{
public:
    MyString(const char *p = NULL);//构造器
    ~MyString();//析构器
    MyString(const MyString & another);//拷贝构造器
    char * c_str();//返回堆中字符串的首地址
private:
    char * _str;
};

#endif
/*mystring.cpp*/
#include "mystring.h"

MyString::MyString(const char *p)
{
    if(p == NULL){
        _str = new char[1];/*定义成数组,与else对应,方便析构*/
        *_str = '\0';
    }else{
        int len = strlen(p);
        _str = new char[len];
        strcpy(_str, p);
    }
}
MyString::~MyString()
{
    delete []_str;
}
MyString::MyString(const MyString & another)
{
#if 0
    _str = another._str;//浅拷贝,与系统默认的一样
#endif
#if 1
    /*深拷贝*/
    int len = strlen(another._str);
    _str = new char[len];
    _str = another._str;
#endif
}
char * MyString::c_str(){
    return _str;
}
/*main.cpp*/
#include <iostream>
#include "mystring.h"
using namespace std;

int main()
{
    string s1;
    string s2("string s2");
    string s3(s2);

    cout<<"s1:"<<s1.c_str()<<endl;
    cout<<"s2:"<<s2.c_str()<<endl;
    cout<<"s3:"<<s3.c_str()<<endl;

    cout<<"\n*****************************\n"<<endl;

    MyString S1;//由默认参数构造器完成
    MyString S2("String S2");//由非默认参数参构造器完成
    MyString S3(S2);//由拷贝构造器实现,而不是有参数的构造器实现

    /*由于<<没有重载,所以暂时用c_str()来输出字符串数组*/
    cout<<"S1:"<<s1.c_str()<<endl;
    cout<<"S2:"<<s2.c_str()<<endl;
    cout<<"S3:"<<s3.c_str()<<endl;
    return 0;
}

我们先采用浅拷贝的方式来编译运行(运行时出错提示:double free):

这里写图片描述
再将浅拷贝注释掉,用深拷贝来编译运行(结果正常):

这里写图片描述

3、默认赋值运算符重载——穿着马甲的浅拷贝:

其实每一个类中,系统不但默认存在以上三种“器”,还存在默认的赋值运算符重载:

(默认)赋值运算符重载:
* 格式:A& operator=(A&);
* 系统/编译器提供默认重载,
* 默认赋值运算符重载也是一种等位赋值(浅赋值),若是自定义,编译器不再提供
* 浅复制不但会导致重析构,还会导致自身内存泄漏;
* 对于自赋值需要特别注意。

为什么说它是穿着马甲的浅拷贝?又为什么说默认赋值运算符重载会导致内存重析构与内存泄漏呢?我们画张图来分析:
这里写图片描述

/*系统默认的类似于如下所示*/
MyString& MyString::operator=(const MyString & another)
{
    this->_str = another._str;//只是复制了字符串的地址
    return *this;
}

首先默认的赋值运算符重载是浅拷贝才会导致内存泄露,其次对象析构时会产生重析构。对于赋值运算符重载,在对象含有堆空间的申请与释放时,也需要自行定义,不能够使用系统默认的。对于我们MyString的例子来说,其非默认实现如下:

MyString& MyString::operator=(const MyString & another)
{
    if(this == &another)
        return *this;//自赋值,不会删除自身空间,直接返回,排除自赋值逻辑错误
    delete []this->_str;//先删除自身,否则会导致内存泄露
    int len = strlen(another._str);//求赋值对象的长度
    this->_str = new char[len+1];//根据赋值对象为被赋值对象创建新的空间,防止重析构。
    strcpy(this->_str, another._str);//复制

    return *this;//返回this以实现连等:S1 = S2 = S3 = ...;
}

上面所说自赋值逻辑错误即:如果S1 = S1;那么默认按(先删除自身->再求长度->创建新空间->复制)的步骤走肯定是不对的,因为第一步删除S1后,后面的步骤丢失掉了依赖,即所谓逻辑出错。

4、声明隐患:

即使是浅拷贝这种致命的逻辑错误,在Windows下也可能不会有异常:

这里写图片描述

但是对于这种潜在的隐患,我们一定要注意,在对象有堆空间时,一定要用深拷贝,而不能用浅拷贝。对于C++的在这种浅拷贝的问题是因为char*引起来的,也就是为了兼容C而引起的。在纯C++语法编写的程序中,由于不会用到char数组、char*等,就不会有浅拷贝深拷贝的问题了。

;