类的赋值构造函数和复制构造函数

时间:2022-12-17 19:27:53

C++的初学者经常会对复制构造函数一知半解,我曾经对复制构造函数和赋值函数就很是迷茫。闲来无事,整理一下,一个对象的复制构造函数和赋值构造函数。整体的说一下,复制构造函数和赋值构造函数的相同点是: 赋值运算符和复制构造函数都是用已存在的B对象来创建另一个对象A;
最大的不同在于:赋值构造函数处理两个已有对象,即赋值前B应该是存在的;复制构造函数是生成一个全新的对象,即调用复制构造函数之前A不存在。

- 类

首先,介绍一下类:类是数据以及用于操纵该数据的方法(函数)的集合,是逻辑上相关函数与数据的封装。它是对所要处理问题的抽象描述,它把数据(事物的属性)和函数(事物的行为/操纵)封装为一个整体。
类的定义格式如下:

class 类名{
private//私有数据和函数
protect:
//保护数据和函数
public//公有数据和函数
}

C++中面向对象的思想是对对象的属性(成员变量)进行操作,应该通过对象的方法(成员函数)来进行,对象的方法是对象和外部的接口。
这里顺便说一句成员函数的定义通常的格式是:

范围值类型 类名::函数名(参数表)
{
//函数体
}

很多基础比较差的程序员,“::”符号不知道是什么意思(虽然知道也没什么用),只知道怎么用!运算符“::”成为作用域解析运算符,它指出该函数是属于哪一类的成员函数。当然也可以在类的定义中直接定义函数。所以有的时候也不用“::”符号.

对象即类的实例。创建类的对象可以常用的两种方法:
1、直接创建对象,比如CObject类的一个对象 CObject object;(注意这里的object是一个临时对象)
2、采用动态创建类的对象的方法,当然遍历同样也可以动态创建。

- 复制构造函数

上面说到了类,也说到了对象的创建,那么同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制是完全可行的。这个复制的过程只需要把数据成员复制过来即可,而函数成员是可以共用的,同一个类的任何对象都可以共用这个类的函数成员。
在建立对象时可以用同一类的另一个对象来初始化该对象,这时所用的构造函数成为复制构造函数。像X::X(X&),这种形式,且只有一个参数——同类的对象,采用的是引用的方式。不能用X::X(X)这种形式的构造函数,如果把一个真实的类的对象作为参数传递到复制构造函数,会引起无穷的递归。那很多人有不解,平常我创建类的对象的时候也没有写复制构造函数呀,为什么也没有报错呢?原因是如果没有定义的话,那么编译器生成默认复制构造函数;如果定义了地自己的复制构造函数,则默认的复制构造函数就不存在了。(这一点与析构函数不同,即使自己定义了析构函数,默认的析构函数仍然存在)
复制构造函数在以下3中情况下会被调用:

  1. 当用一个对象去初始化同类的另一个对象时。
CObject o2(o1); // CObject o2=o1;
  1. 如果某函数有一个形参是类A的对象,那么该函数被调用时,类A的复制构造函数将被调用。
void Fun(CObject object)
{
    object.a=1;
}
CObject object;
Fun(object);
//CObject 的复制构造函数被调用,生成形参,在内存新建一个局部对象,并把实参复制到新的对象中。
  1. 如果函数的返回值是类CObject的对象,则函数返回时,CObject的复制构造函数被调用。理由也是建立一个临时对象,再返回调用者。
CObject Fun()
{
    CObject object;
    return object; //在此处调用了CObject(object)
}
int main()
{
    CObject object; //此处是一个局部对象,也是一个临时对象
    object = Fun(); 
    return 0;
}

有人可能会问,为什么不直接用要返回的局部对象呢?因为局部对象在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存。所以编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中。所谓返回对象,实际上就是调用复制构造函数把该对象的值复制到临时对象中。

-赋值构造函数

首先看一段代码:

int main()
{
    CObject theObjectOne;
    theObjectOne.Init(100);

    CObject theObjectTwo;
    theObjectTwo.Init(200);

    theObjectTwo = theObjectOne;
    //此操作是对象赋值操作,更深一步的操作时将被复制对象theObjectTwo对象原有的内容清除,并用theObjectOne对象里的内容填充。
    return 0;
}

“=”就是讲一个对象里的内容到另一个对象中去,这其中涉及到对象原有内容的丢弃和新内容的复制。由于对象内容包含指针,将造成不良的后果:指针的值被丢弃了(指针内容丢失了也就是说指针地址丢失了,但是改地址中所存储的内容没有丢失,但指针指向的内容并未释放。指针的值被复制了,但是指针所指内容并未复制,这就要出大事儿了)。所以如果类的对象中有指针的话,此处的“=”绝对不能是我们平常理解的简简单单的等于号,这个“=”必须重载,而这个“=”的重载函数就是我们要讨论的赋值构造函数。

我们再来理解两个概念:值拷贝和位拷贝,位拷贝拷贝的是地址;值拷贝拷贝的是内容。理解了这两个概念我们再看一段代码:
定义一个string类,但是不实现其他的成员函数

Class String
{
public:
           String(const char *ch = NULL); //默认构造函数
           String(const String &str); //复制构造函数
           ~String();
           String &operator=(const String &str);//赋值构造函数
 privat:
            char *m_data;
}

int main()
{
    String strA;
    strA.m_data=L"Windows";
    String strB;
    strB.m_data=L"Linux";

    strB.m_data = strA.m_data;
}

如果“=”未重写赋值构造函数的话,将strA赋给strB;则编译器会默认进行位拷贝,即strB.m_data = strA.m_data;
则strA.m_data和strB.m_data都指向了同一块区域,虽然strA.m_data指向的内容会改变为“Linux”,但是会出现这样的问题:
(1)strB.m_data原来指向的内存区域未释放,造成内存泄露。
(2)strB.m_data和strA.m_data指向同一块区域,任何一方改变都会影响另一方。
(3)当对象被析构时,strA.m_data被释放两次。

如果“=”重写了复制构造函数后,strB.m_data = strA.m_data;进行的是值拷贝,会将strA.m_data的内容赋给strB.m_data,strB.m_data还是指向原来的内存区域,但是其内容改变。

所以在我理解起来,赋值构造函数其实就是对“=”的重载。缺省的赋值构造函数采用的是“位拷贝”而非“值拷贝”的方式实现。如果类中含有指针变量,那么赋值构造函数如果不重载的话肯定会出错。理解到这里,我们再回过头看一下复制构造函数,其实也是一样的道理,缺省的复制构造函数采用的也是“位拷贝”的方式实现,所以如果类中含有指针变量的话我们也需要重写复制构造函数。
总而言之一句话,什么把你弄的这么头疼来看这篇文章,归根到底就是——指针。

最后附上一段不错的理解复制构造函数和赋值构造函数的代码:

#include <iostream>
#include <cstring>
using namespace std;

class String  
{
    public:
        String(const char *str);
        String(const String &other);
        String & operator=(const String &other);
        ~String(void); 
    private:
        char *m_data;
};

String::String(const char *str)
{
    cout << "自定义构造函数" << endl;
    if (str == NULL)
    {
        m_data = new char[1];
        *m_data = '\0';
    }
    else
    {
        int length = strlen(str);
        m_data = new char[length + 1];
        strcpy(m_data, str);
    }
}

String::String(const String &other)
{
    cout << "自定义复制构造函数" << endl;
    int length = strlen(other.m_data);
    m_data = new char[length + 1];
    strcpy(m_data, other.m_data);
}

String & String::operator=(const String &other)
{
    cout << "自定义赋值函数" << endl; 

    if (this == &other)
    {
        return *this;
    }
    else
    {
        delete [] m_data;
        int length = strlen(other.m_data);
        m_data = new char[length + 1];
        strcpy(m_data, other.m_data);
        return *this;
    }
}

String::~String(void)
{
    cout << "自定义析构函数" << endl; 
    delete [] m_data;
}
int main()
{
    cout << "a(\"abc\")" << endl;
    String a("abc");

    cout << "b(\"cde\")" << endl;
    String b("cde");

    cout << " d = a" << endl;
    String d = a;

    cout << "c(b)" << endl;
    String c(b);

    cout << "c = a" << endl;
    c = a;

    cout << endl;

执行结果

a(“abc”)
执行自定义构造函数
b(“ced”)
执行自定义构造函数
d=a
执行自定义复制构造函数
c(b)
执行自定义复制构造函数
c=a
执行自定义赋值函数

执行自定义析构函数
执行自定义析构函数
执行自定义析构函数
执行自定义析构函数

说明几点

  1. 赋值函数中,上来比较 this == &other 是很必要的,因为防止自复制,这是很危险的,因为下面有delete []m_data,如果提前把m_data给释放了,指针已成野指针,再赋值就错了

  2. 赋值函数中,接着要释放掉m_data,否则就没机会了(下边又有新指向了)

  3. 拷贝构造函数是对象被创建时调用,赋值函数只能被已经存在了的对象调用

    注意:String a(“hello”); String b(“world”); 调用自定义构造函数

         String c=a;调用拷贝构造函数,因为c一开始不存在,最好写成String c(a);