c++ - 引用指针 - 指针传递引用传递区别




什么是三项规则? (6)

我什么时候需要自己申报?

三规则规定,如果你声明了任何一个

  1. 复制构造函数
  2. 复制赋值运算符
  3. 析构函数

那么你应该宣布所有三个。 它源于这样一种观察,即需要接管复制操作的意义几乎总是源自于执行某种资源管理的类,而且几乎总是暗示

  • 无论在一次复制操作中进行的资源管理可能需要在另一次复制操作中完成,

  • 类的析构函数也会参与资源的管理(通常释放它)。 要管理的经典资源是内存,这就是为什么所有管理内存的标准库类(例如,执行动态内存管理的STL容器)都声明“三大”:复制操作和析构函数。

三法则的结果是,用户声明的析构函数的存在表明简单的成员智能副本不太可能适用于类中的复制操作。 反过来,这表明如果一个类声明了一个析构函数,复制操作可能不应该自动生成,因为它们不会做正确的事情。 在C ++ 98被采用的时候,这条推理的重要性并没有得到充分的理解,所以在C ++ 98中,用户声明的析构函数的存在不会影响编译器生成复制操作的意愿。 在C ++ 11中仍然如此,但仅仅是因为限制生成复制操作的条件会破坏太多的遗留代码。

我怎样才能防止我的对象被复制?

将复制构造函数和复制赋值运算符声明为私有访问说明符。

class MemoryBlock
{
public:

//code here

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"copy constructor"<<endl;
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

在C ++ 11以后,你也可以声明拷贝构造函数和赋值操作符被删除

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

复制对象意味着什么? 什么是复制构造函数复制赋值运算符 ? 我什么时候需要自己申报? 我怎样才能防止我的对象被复制?


介绍

C ++用值语义处理用户定义类型的变量。 这意味着对象被隐式复制到各种上下文中,我们应该理解“复制对象”实际上意味着什么。

让我们考虑一个简单的例子:

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

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

(如果您对name(name), age(age)部分感到困惑,这称为成员初始化程序列表 。)

特殊会员功能

复制person对象意味着什么? main功能显示两种不同的复制方案。 初始化person b(a);复制构造函数执行。 它的工作是根据现有对象的状态构建一个新的对象。 赋值b = a复制赋值操作符执行 。 它的工作通常稍微复杂一点,因为目标对象已经处于某种需要处理的有效状态。

由于我们自己并没有声明拷贝构造函数和赋值运算符(也不是析构函数),所以这些都是为我们隐式定义的。 标准报价:

复制构造函数和复制赋值运算符,以及析构函数都是特殊的成员函数。 [ 注意当程序没有明确声明它们时,实现会隐式地为一些类类型声明这些成员函数。 如果使用它们,实现将隐含地定义它们。 [...] 结束说明 ] [n3126.pdf第12节§1]

默认情况下,复制对象意味着复制其成员:

非联合类X的隐式定义的复制构造函数执行其子对象的成员副本。 [n3126.pdf第12.8节§16]

非联合类X的隐式定义的复制赋值运算符执行其子对象的成员复制分配。 [n3126.pdf第12.8节30节]

隐式定义

隐式定义的特殊成员函数如下所示:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}

在这种情况下,成员复制正是我们想要的:复制nameage ,因此我们得到一个独立的独立person对象。 隐式定义的析构函数总是空的。 在这种情况下这也很好,因为我们没有在构造函数中获取任何资源。 成员的析构函数在析构函数完成后隐式调用:

在执行析构函数的主体并销毁在主体内分配的任何自动对象之后,类X的析构函数调用X的直接成员的析构函数[n3126.pdf 12.4§6]

管理资源

那么我们何时应该明确地声明这些特殊的成员函数呢? 当我们的班级管理资源时 ,也就是说,班级的某个对象负责该资源时。 这通常意味着资源在构造函数中获得 (或传入构造函数)并在析构函数中释放

让我们回顾一下预标准的C ++。 没有std::string这样的东西,程序员也爱上了指针。 person类可能看起来像这样:

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};

即使在今天,人们仍然以这种风格编写课程并陷入困境:“ 我把一个人推到了一个向量中,现在我发现了疯狂的内存错误! ”请记住,默认情况下,复制对象意味着复制它的成员,但复制name成员只是复制一个指针, 而不是它指向的字符数组! 这有几个不愉快的效果:

  1. 通过a变化可以通过b观察。
  2. 一旦b被销毁, a.name就是一个悬挂指针。
  3. 如果a被销毁,删除悬挂指针会产生未定义的行为
  4. 由于作业没有考虑作业前指定的name ,迟早你会在整个地方发生内存泄漏。

明确的定义

由于成员复制不具备所需的效果,因此我们必须明确定义复制构造函数和复制赋值运算符以制作字符数组的深层副本:

// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

请注意初始化和赋值之间的区别:在分配name以防止内存泄漏之前,我们必须拆除旧状态。 此外,我们必须防止x = x形式的自我分配。 没有这个检查, delete[] name会删除包含字符串的数组,因为当你写x = xthis->namethat.name都包含相同的指针。

例外安全

不幸的是,如果由于内存耗尽导致new char[...]抛出异常,则此解决方案将失败。 一种可能的解决方案是引入一个局部变量并对语句进行重新排序:

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

这也可以在没有明确检查的情况下自行分配。 这个问题的更强大的解决方案是copy-and-swap成语 ,但我不会在这里详细讨论异常安全。 我只提到例外来说明以下几点: 编写管理资源的类很困难。

非复制资源

一些资源不能或不应该被复制,例如文件句柄或互斥体。 在这种情况下,只需将复制构造函数和复制赋值运算符声明为private而不给出定义:

private:

    person(const person& that);
    person& operator=(const person& that);

或者,您可以从boost::noncopyable继承或将它们声明为已删除(C ++ 0x):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

三条规则

有时你需要实现一个管理资源的类。 (永远不要管理单个类中的多个资源,这只会导致痛苦。)在这种情况下,请记住三条规则

如果您需要自己显式声明析构函数,复制构造函数或复制赋值运算符,则可能需要明确声明它们中的全部三个。

(不幸的是,这个“规则”不是由C ++标准或我知道的任何编译器强制执行的。)

忠告

大多数情况下,你不需要自己管理资源,因为现有的类如std::string已经为你做了。 只需将使用std::string成员的简单代码与使用char*的错综复杂且容易出错的替代方法进行比较,就可以确信。 只要你远离原始指针成员,三条规则不太可能涉及你自己的代码。


三条法则是C ++的基本原则,基本上是这样说的

如果你的课程需要任何

  • 一个拷贝构造函数
  • 一个赋值操作符
  • 析构函数

明确地定义,那么它可能需要全部三个

原因是他们三个人通常都用来管理资源,如果你的班级管理资源,通常需要管理复制和释放资源。

如果没有良好的语义来复制类的管理资源,则考虑通过将复制构造函数和赋值运算符声明(不defining )为private来禁止复制。

(请注意,即将推出的新版本的C ++标准(即C ++ 11)将语义添加到C ++中,这可能会改变三项规则。但是,我对此知之甚少,无法编写C ++ 11部分关于三的规则。)


三巨头的法律如上所述。

简单的英语就是一个简单的例子,它解决了这种问题:

非默认析构函数

你在你的构造函数中分配了内存,所以你需要编写一个析构函数来删除它。 否则你会造成内存泄漏。

你可能会认为这是工作完成。

问题在于,如果复制是由对象组成的,则副本将指向与原始对象相同的内存。

一旦它们中的一个在其析构函数中删除了内存,另一个将会有一个指向无效内存的指针(这称为一个悬挂指针),当它试图使用它时,事情会变得多毛。

因此,你写了一个拷贝构造函数,以便为它分配新的对象自己的内存块来销毁。

赋值运算符和复制构造函数

您将构造函数中的内存分配给您的类的成员指针。 当您复制此类的对象时,默认赋值运算符和复制构造函数会将此成员指针的值复制到新对象。

这意味着新对象和旧对象将指向同一片内存,因此当您在一个对象中更改它时,它也会针对其他对象进行更改。 如果一个对象删除了这个内存,另一个将继续尝试使用它 - eek。

为了解决这个问题,你需要编写自己的拷贝构造函数和赋值操作符。 您的版本为新对象分配单独的内存,并复制第一个指针所指向的值而不是其地址。


复制对象意味着什么? 有几种方法可以复制对象 - 让我们来谈谈您最可能引用的两种 - 深层复制和浅层复制。

由于我们使用面向对象的语言(或者至少假定是这样),所以假设你分配了一块内存。 由于它是一种OO语言,我们可以很容易地引用我们分配的内存块,因为它们通常是我们定义的原始变量(int,chars,bytes)或类,它们是由我们自己的类型和基元组成的。 假设我们有一类Car,如下所示:

class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;

public changePaint(String newColor)
{
   this.sPrintColor = newColor;
}

public Car(String model, String make, String color) //Constructor
{
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}

public Car(const Car &other) // Copy Constructor
{
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
   if(this != &other)
   {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return *this;
}

}

如果我们声明一个对象,然后创建一个完全独立的对象副本,那么深层副本就是我们最终得到2个完全集合的内存中的2个对象。

Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.

现在让我们做一些奇怪的事情。 比方说car2要么编程错误,要么意图分享car1的实​​际内存。 (这样做通常是一个错误,而在课堂上通常是讨论下的毯子。)假装每次询问car2时,都确实解析了car1内存空间的指针......这或多或少是浅拷贝是。

//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.

 Car car1 = new Car("ford", "mustang", "red"); 
 Car car2 = car1; 
 car2.changePaint("green");//car1 is also now green 
 delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
 the address of where car2 exists and delete the memory...which is also
 the memory associated with your car.*/
 car1.changePaint("red");/*program will likely crash because this area is
 no longer allocated to the program.*/

因此,无论您使用哪种语言编写,在复制对象时都要非常小心,因为大部分时间您都需要进行深层复制。

什么是复制构造函数和复制赋值运算符? 我已经在上面使用过它们。 当您输入代码时会调用复制构造函数,如Car car2 = car1; 本质上,如果你声明一个变量并将其分配在一行中,那就是调用复制构造函数的时候。 赋值运算符是当您使用等号时发生的情况 - car2 = car1; 。 注意car2没有在同一个语句中声明。 您为这些操作编写的两段代码可能非常相似。 实际上,典型的设计模式还有另一个功能,您可以调用它来设置所有内容,只要您满意,初始复制/分配是合法的 - 如果您查看我编写的代码,函数几乎完全相同。

我什么时候需要自己申报? 如果您不是在编写代码来共享或以某种方式进行生产,您只需要在需要时声明它们。 如果您选择“无意中”使用它,并且没有创建一个程序语言,那么您需要了解程序语言的功能,即编译器默认。 我很少使用复制构造函数作为实例,但赋值运算符覆盖很常见。 你知道你可以重写什么加法,减法等等吗?

我怎样才能防止我的对象被复制? 覆盖所有允许使用私有函数为对象分配内存的方式是一个合理的开始。 如果你真的不希望人们复制它们,你可以通过抛出异常并且不复制对象来公开并提醒程序员。


许多现有的答案已经触及了拷贝构造函数,赋值运算符和析构函数。 然而,在C ++ 11之后,移动语义的引入可能会扩展到3以上。

最近Michael Claisse发表了一个涉及这个主题的演讲: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class ://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class





rule-of-three