C++拷贝构造函数,析构函数与内存泄漏的那些坑

目录

拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数

class Sales {
public:
    Sales() {
       std::cout<<"Sales()"<<std::endl;
    }
    Sales(const Sales&) {
        std::cout<<"Sales(const Sales&)"<<std::endl;
    }
}
    

如果我们没有定义拷贝构造函数的话,编译器会为我们自动生成一个,即合成拷贝构造函数,合成拷贝构造函数会从原对象中依次将每个非static成员拷贝到正在创建的对象中,内置类型的成员,直接拷贝;其他类类型的成员,会调用该类类型的拷贝构造函数;数组则会逐元素的拷贝,如果数组里的元素是类类型,则会调用元素的拷贝构造函数来进行拷贝。

拷贝初始化

用拷贝构造函数来初始化一个对象,不同于使用构造函数来初始化一个对象,就被称为拷贝初始化,如下代码,第一行就是直接初始化,第 两行就是拷贝初始化

std::string dots(10, '.');
std::string book = "9-9999-999";

除了用一个对象去初始化另一个对象时会调用拷贝构造函数以外,《C++ Primer》中还规定了其他几种情形,会发生拷贝初始化

  • 将一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

另外需要注意的是,当我们初始化标准库容器或是调用其insert或push成员的时候,容器也会对其元素进行拷贝初始化,比如往一个vector容器push一个对象。

为什么拷贝构造函数的参数必须是引用类型

在函数调用的过程中,非引用类型的参数要进行拷贝初始化,即非引用类型的实参对象都会调用其拷贝构造函数,这样就形成了调用拷贝构造函数->复制实参->调用拷贝构造函数的无限循环,无法达成最终拷贝的目的,而如果是引用类型,就不会有这个问题了

拷贝赋值元素符

与类控制其对象如何初始化一样,类也可以控制其对象如何赋值,拷贝赋值运算符就是重载了赋值运算符operator=的函数,赋值元素符通常应该返回一个指向其左侧运算对象的引用

Sales& operator=(const Sales& a) {
        //赋值操作..
        return *this;
}

一个最佳实践就是,大多数类应该定义默认构造函数,拷贝构造函数,和拷贝赋值运算符,无论是隐式的还是显式的

析构函数

析构函数执行与构造函数相反的操作,构造函数初始化对象的非static成员,析构函数释放对象使用的资源,并销毁对象的非static成员。在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中的出现的顺序进行初始化,在一个析构函数中,首先执行函数体,然后销毁成员,且成员按初始化顺序逆序销毁。

值得注意的是,合成析构函数并不会delete一个指针数据成员,也就是说,如果我们在构造函数中动态分配了一块内存,如果不显式定义析构函数,那么那块内存区域在对象销毁是不会自己释放的。

类的拷贝构造函数,拷贝赋值运算符,以及析构函数包括新标准的移动构造函数和移动赋值运算符合称为类的拷贝控制成员(copy control)

不完整定义拷贝控制成员可能带来的问题

同样的还是动态分配内存的问题,举个例子,假设我们在构造函数中动态分配了一块内存区域,并赋给一个指针变量

class Sales {
public:
    Sales() {
        std::cout<<"Sales()"<<std::endl;
        p = new int[10];
    }

    ~Sales() {
        std::cout<<"~Sales()"<<std::endl;
        delete p;
    }

private:
    int *p;
};

由于合成拷贝构造函数或者合成拷贝赋值运算符会简单的拷贝指针成员,所以意味着Sales多个对象可能指向相同的内存,如果发生类似以下调用:

Sales f(Sales a) {  //这里是值传递,所以将被拷贝
    Sales b = a;    //拷贝给b

    return b;       //b和a都将被销毁
}

当函数f返回时,在两个对象上都会调用解析构造函数,会
delete其中的指针成员,但两个对象都指向同一块内存,所以这时候就会发生意外的错误。

阻止拷贝

存在动态内存指针的类中,如果拷贝构造或者拷贝赋值运算符定义不当,也很容易导致野指针问题,造成内存泄漏。

为此,为了避免这类问题,在某些情况下,定义类时必须采用某种机制阻止拷贝或者赋值。不能仅仅通过不定义拷贝控制成员,因为如果我们为定义这类操作,编译器会生成合成版本

比较常见的一些做法比如boost库的不可拷贝类boost::noncopyable,或者我们直接将拷贝构造函数和拷贝赋值运算符定义为私有

private:
    Sales(const Sales&);
    Sales& operator=(const Sales& a);

或者使用C++11 标准的delete关键字,通过将拷贝构造函数和拷贝赋值运算符定位为**删除的函数(deleted function)**来阻止拷贝

struct NoCopy {
    NoCopy() = default;     //使用合成的默认构造函数
    NoCopy(const NoCopy&) = delete;  //阻止拷贝
    NoCopy& operator=(const NoCopy&) = delete ; //阻止赋值
    ~NoCopy() = default;  //使用合成的析构函数
};

后续的文章会分享智能指针和移动构造相关的知识点,欢迎关注~

参考书籍《C++ Primer》

上一篇:执行上下文(execution context)


下一篇:代码分析