c ++将对象添加到向量中会破坏早期对象

时间:2016-06-27 20:45:21

标签: c++ vector crash destructor push-back

我需要将相同类的对象添加到向量:

#include <vector>
#include <cstdio>

class A {
    int *array;
    int size;
public:
    A(int s) { 
       array = new int[size = s];
       fprintf(stderr, "Allocated %p\n", (void*)array);
    }
   ~A()      { 
       fprintf(stderr, "Deleting %p\n", (void*)array);
       delete array; 
    }
};

int main() {
    std::vector<A> v;

    for (int n = 0; n < 10; n++) {
        fprintf(stderr, "Adding object %d\n", n);
        v.push_back(A(10 * n));
        //v.emplace_back(10 * n);
    }
    return 0;
}   

当我运行此程序时,它会在产生以下输出后崩溃:

Adding object 0
Allocated 0x146f010
Deleting 0x146f010
Adding object 1
Allocated 0x146f050
Deleting 0x146f010
*** glibc detected *** ./a.out: double free or corruption (fasttop): 0x000000000146f010 ***

似乎在添加第一个对象时调用第0个对象的析构函数。更奇怪的是当我使用emplace_back而不是push_back时:

Adding object 0
Allocated 0x1644030
Adding object 1
Allocated 0x1644080
Deleting 0x1644030
Adding object 2
Allocated 0x1644100
Deleting 0x1644030
Deleting 0x1644080
Adding object 3
Allocated 0x1644160
Adding object 4
Allocated 0x1644270
Deleting 0x1644030
*** glibc detected *** ./a.out: double free or corruption (fasttop): 0x0000000001644030 ***

有人可以解释为什么会发生这种情况,以及正确的方法吗?在Linux下使用的编译器是g ++ 4.7.2,但在Mac OS X下我也得到了与clang 7.3.0相同的行为。

1 个答案:

答案 0 :(得分:4)

您的A课程不遵循Rule of Three

  

三个规则(也称为三巨头或三巨头的法则)是C ++(在C ++ 11之前)的经验法则,声称如果一个类定义下面的一个(或多个)它应该明确定义所有三个:

     
      
  •   
  • 复制构造函数
  •   
  • 复制分配操作员
  •   
     

这三个功能是特殊的成员功能。如果在没有首先由程序员声明的情况下使用其中一个函数,则编译器将使用默认语义对该类的所有成员执行所述操作来隐式实现这些函数。

     
      
  • 析构函数 - 调用所有对象的类类成员的析构函数
  •   
  • 复制构造函数 - 从复制构造函数参数的相应成员构造所有对象的成员,调用对象类类的复制构造函数成员,并对所有非类类型(例如,int或指针)数据成员进行简单分配
  •   
  • 复制赋值运算符 - 从赋值运算符参数的相应成员中分配所有对象的成员,调用对象类的复制赋值运算符-type成员,并对所有非类类型(例如int或pointer)数据成员进行简单赋值。
  •   
     

三条规则声称如果其中一个必须由程序员定义,则意味着编译器生成的版本在一种情况下不适合该类的需要,并且它可能不适合其他情况也是。术语&#34;三法则&#34;是由马歇尔·克莱因于1991年创造的。

     

对此规则的修订是,如果类的设计方式是资源获取初始化(RAII)用于其所有(非平凡)成员,则析构函数可能未定义(也称为大二)。这种方法的一个现成例子是使用智能指针而不是普通指针。

     

因为隐式生成的构造函数和赋值运算符只是复制所有类数据成员(&#34;浅复制&#34;),所以应该为封装复杂数据结构的类定义显式复制构造函数和复制赋值运算符。如果需要复制类成员指向的对象,则有指针等外部引用。如果默认行为(&#34;浅副本&#34;)实际上是预期的行为,则显式定义虽然多余,但它将是一个自我记录的代码&#34;表明这是一种意图而非疏忽。

您需要添加复制构造函数和复制赋值运算符(并且您的析构函数需要使用delete[]而不是delete):

class A
{
private:
    int *array;
    int size;

public:
    A(int s) 
        : size(s), array(new int[s])
    { 
        fprintf(stderr, "Allocated %p\n", array);
    }

    A(const A &src)
        : size(src.size), array(new int[src.size])
    { 
        std::copy(src.array, src.array + src.size, array);
        fprintf(stderr, "Allocated %p, Copied from %p\n", array, src.array);
    }

    ~A()
    { 
        fprintf(stderr, "Deleting %p\n", array);
        delete[] array; 
    }

    A& operator=(const A &rhs)
    { 
       A tmp(rhs);
       std::swap(array, tmp.array);
       std::swap(size, tmp.size);
       return *this;
    }
};

由于您提到emplace_back(),这意味着您正在使用C ++ 11或更高版本,这意味着您还应该处理Rule of Five的移动语义:

  

随着C ++ 11的出现,C ++ 11实现了移动语义,允许目标对象从临时对象中抓取(或窃取)数据,因此可以将规则扩展为五。以下示例还显示了新的移动成员:移动构造函数和移动赋值运算符。因此,对于五的规则,我们有以下特殊成员:

     
      
  •   
  • 复制构造函数
  •   
  • 移动构造函数
  •   
  • 复制分配操作员
  •   
  • 移动赋值运算符
  •   
     

存在类可能需要析构函数但存在无法合理地实现复制和移动构造函数以及复制和移动赋值运算符的情况。例如,当基类不支持后面的Big Four成员时,会发生这种情况,但派生类的构造函数会为自己的用途分配内存。[citation needed]在C ++ 11中,这可以简化为明确指定五个成员为默认成员。

您应该将移动构造函数和移动赋值运算符添加到上面的代码中:

class A
{
private:
    int *array;
    int size;

public:
    A(int s) 
        : size(s), array(new int[s])
    { 
        fprintf(stderr, "Allocated %p\n", array);
    }

    A(const A &src)
        : size(src.size), array(new int[src.size])
    { 
        std::copy(src.array, src.array + src.size, array);
        fprintf(stderr, "Allocated %p, Copied from %p\n", array, src.array);
    }

    A(A &&src)
        : size(0), array(nullptr)
    { 
        std::swap(array, src.array);
        std::swap(size, src.size);
        fprintf(stderr, "Moved %p, Replaced with %p\n", array, src.array);
    }

    ~A()
    { 
        fprintf(stderr, "Deleting %p\n", array);
        delete[] array; 
    }

    A& operator=(const A &rhs)
    { 
       A tmp(rhs);
       std::swap(array, tmp.array);
       std::swap(size, tmp.size);
       return *this;
    }

    A& operator=(A &&rhs)
    { 
       std::swap(array, rhs.array);
       std::swap(size, rhs.size);
       return *this;
    }
};

否则,您应该努力争取Rule of Zero

  

R. Martinho Fernandes提出了将上述所有内容简化为C ++规则(主要是C ++ 11及更新版本)的提案。规则0表示如果指定任何默认成员,那么您的类必须专门处理单个资源。此外,它必须定义所有默认成员来处理该资源(或根据需要删除默认成员)。因此,这些类必须遵循上述规则5。资源可以是任何东西:分配的内存,文件描述符,数据库事务等。

     

任何其他类不得直接分配任何资源。此外,他们必须省略默认成员(或通过= default明确地将所有成员分配为默认成员)。 应使用单资源类作为成员/局部变量间接使用任何资源。这使得这些类从成员变量的并集继承默认成员,从而自动转发所有底层资源的并集的可移动性/可复制性。由于1资源的所有权仅由1个成员变量拥有,因此构造函数中的异常不会因RAII而泄漏资源。完全初始化的变量将其析构函数称为&amp;未初始化的变量不能拥有任何资源。

     

由于大多数课程都不会将所有权作为唯一关注点,因此大多数课程都可以省略默认成员。这就是规则0的名称。

完全删除您的手动数组并改为使用std::vector

class A
{
private:
    std::vector<int> array;

public:
    A(int s) 
        : array(s)
    { 
    }
};

无需显式定义复制/移动构造函数,复制/移动赋值运算符或析构函数,因为编译器提供的默认实现将自动为您调用vector的相应功能。