有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符和析构函数。而且,在新标准下,一个类还可以定义一个移动构造函数和一个移动赋值运算符。这些操作通常应该被看作一个整体。通常,只需要其中一个操作,而不需要定义所有操作的情况是很少见的。 

       需要析构函数的类也需要拷贝和赋值操作。当我们决定一个类是否需要定义它自己版本的拷贝控制成员时,一个基本原则是首先确定这个类是否需要一个析构函数。如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。 
       需要拷贝操作的类也需要赋值操作,反之亦然。 
       如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。反之亦然——如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。

1. 需要析构函数的类也需要拷贝构造函数和拷贝赋值函数。

       通常,若一个类需要析构函数,则代表其默认的析构函数不足以释放类所拥有的资源,其中最典型的就是指针成员(析构时需要手动去释放指针指向的内存)。所以,若存在自定义(且正确)的析构函数,但使用默认的拷贝构造函数,那么拷贝过去的也只是指针,此时两个对象的指针变量同时指向同一块内存,指向同一块内存的后果很有可能是在两个对象中的析构函数中先后被释放两次。所以需要额外的拷贝控制函数去控制相应资源的拷贝。

        所以这类例子的共同点就是:一个对象拥有额外的资源(指针指向的内存),但另一个对象使用默认的拷贝构造函数也同时拥有这块资源。当一方对象被销毁后,析构函数释放了资源,这时另一个对象便失去了这块资源(但程序员还不知道)。

class person
{
public:
    std::string *name;
    int age;


    person(const char* the_name, int the_age)
    {
        name = new std::string(the_name);
        age = the_age;
    }


    ~person()
    {
        delete name;
    }
};


int main(void)
{
    person a("me", 20);
    person b(a);
    std::cout << *b.name << std::endl;

    return 0;
}


       在上面的代码中对象b使用默认的拷贝构造函数拷贝对象a的值,这个程序没有什么实际意义。在main函数返回时,a,b变量会分别被析构,它们的成员name指向同一块内存,所以在程序结束时便会发生错误。

2. 需要拷贝操作的类也需要赋值操作,反之亦然。需要拷贝操作代表这个类在拷贝时需要进行一些额外的操作。赋值操作=先析构+拷贝,所以拷贝需要的赋值也需要。反之亦然。

3. 析构函数不能是私有的。如果类的析构函数是私有的,那么成员便无法销毁。所以在程序中不能定义这个类的对象。可以动态分配该对象并获得其指针,但无法销毁这个动态分配的对象(delete 失效)。若上面的类的定义是

class person
{
public:
    std::string *name;
    int age;


    person(const char* the_name, int the_age)
    {
        name = new std::string(the_name);
        age = the_age;
    }
private:
    ~person()
    {
        delete name;
    }
};

则在main函数中定义变量a,b就会发生编译错误,然而,这样的定义却可以通过编译 

person *p; 
p = new person("me", 20) 

但是,这样动态分配的变量是不能被释放的,在调用 delete p 会发生编译错误, 内存泄露就这样发生了。


4. 如果一个类有私有的或不可访问的析构函数,那么其默认和拷贝构造函数会被定义为私有的。

        如果没有这条规则,可能会创造出无法被私有的对象。 理论上来说,当析构函数不能被访问时,任何静态定义的对象都不能通过编译器的编译,所以这种情况只会出现在与动态分配有关的拷贝/默认构造函数身上。

5. 如果一个类有const或引用成员,则不能使用默认的拷贝赋值操作。

原因很简单,const或引用成员只能在初始化时被赋值一次,而默认的拷贝赋值操作会对所有成员都进行赋值。显然,它不能赋值const和引用成员,所以默认的拷贝构造函数不能被使用,即会被定义为私有的。

C++ 三/五法则(总结的真棒)

当定义一个类时,我们显式地或隐式地指定了此类型的对象在拷贝、赋值和销毁时做什么。一个类通过定义三种特殊的成员函数来控制这些操作:拷贝构造函数拷贝赋值运算符析构函数

拷贝构造函数定义了当用同类型的另一个对象初始化新对象时做什么,拷贝赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么,析构函数定义了此类型的对象销毁时做什么。我们将这些操作称为拷贝控制操作


  由于拷贝控制操作是由三个特殊的成员函数来完成的,所以我们称此为“C++三法则”。在较新的 C++11 标准中,为了支持移动语义,
  又增加了移动构造函数和移动赋值运算符,这样共有五个特殊的成员函数,所以又称为“C++五法则”。
  也就是说,“三法则”是针对较旧的 C++89 标准说的,“五法则”是针对较新的 C++11 标准说的。
  为了统一称呼,后来人们把它叫做“C++ 三/五法则”。


“需要析构函数的类也需要拷贝和赋值操作”
  从“需要析构函数”可知,类中必然出现了指针类型的成员(否则不需要我们写析构函数,默认的析构函数就够了),所以,我们需要自己写析构函数来释放给指针所分配的内存来防止内存泄漏。
  那么为什么说“也需要拷贝构造函数和赋值操作”呢?原因是:类中出现了指针类型的成员,必须防止浅拷贝问题。所以需要自己书写拷贝构造函数和拷贝赋值运算符,而不能使用默认的拷贝构造函数和默认的拷贝赋值运算符。

“需要拷贝操作的类也需要赋值操作,反之亦然”

“析构函数不能是删除的成员”
  如果析构函数是删除的,那么无法销毁此类型的对象。对于一个删除了析构函数的类型,编译器不允许定义该类型的变量或创建该类的临时对象。而且,如果一个类有某个成员的类型删除了析构函数,我们也不能定义该类的变量或临时对象。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐