欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C++雾中风景6:拷贝构造函数与赋值函数

程序员文章站 2022-03-21 20:32:18
在进行C++类编写的过程之中,通常会涉及到类的拷贝构造函数与类的赋值函数。初涉类编写的代码,对于两类函数的用法一直是挺让人困惑的内容。这篇文章我们会详细来梳理拷贝构造函数与赋值函数的区别。 1.调用了哪个函数? 上述两种函数的使用和C++之中类的定义紧密相关,所以我们先定义一个类: java cla ......

在进行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; //同样的的编译器报错,无法通过编译
}

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