【问题标题】:c++ adding objects to vector destroys earlier objectsc ++将对象添加到向量会破坏早期的对象
【发布时间】:2016-10-29 23:55:30
【问题描述】:

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

#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 也得到了相同的行为。

【问题讨论】:

  • vector 在需要增加分配的存储空间时复制/移动现有元素,并且您的类不遵循 Rule of Three
  • 为什么要使用动态数组?在 A 类中使用向量。
  • 哇,这些都是非常奇怪的假设。这段代码也同样奇怪。

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


【解决方案1】:

您的A 类不遵循Rule of Three

三法则(也称为三巨头法则或三巨头)是 C++(C++11 之前)中的一条经验法则,它声称如果一个类定义了以下一项(或多项)它可能应该明确定义所有三项:

  • 析构函数
  • 复制构造函数
  • 复制赋值运算符

这三个函数是特殊的成员函数。如果使用其中一个函数而没有首先由程序员声明,它将由编译器隐式实现,默认语义是对类的所有成员执行上述操作。

  • 析构函数 – 调用所有对象类类型成员的析构函数
  • 复制构造函数 – 从复制构造函数参数的相应成员构造对象的所有成员,调用对象的类类型成员的复制构造函数,并对所有非类类型(例如, int 或指针) 数据成员
  • 复制赋值操作符——从赋值操作符参数的对应成员中赋值对象的所有成员,调用对象类类型成员的复制赋值操作符,对所有非类类型进行普通赋值(例如 int 或指针)数据成员。

三法则声称,如果其中之一必须由程序员定义,则意味着编译器生成的版本在一种情况下不适合类的需求,并且可能不适合其他情况也是如此。“三法则”一词是 Marshall Cline 在 1991 年创造的。

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

由于隐式生成的构造函数和赋值运算符只是简单地复制所有类数据成员(“浅拷贝”),因此应该为封装复杂数据结构或具有外部引用的类定义显式复制构造函数和复制赋值运算符,例如指针,如果您需要复制类成员指向的对象。 如果默认行为(“浅拷贝”)实际上是预期行为,那么显式定义虽然是多余的,但将是“自记录代码”,表明这是一个意图而不是疏忽。

你需要添加一个拷贝构造函数和一个拷贝赋值运算符(你的析构函数需要使用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 实现了移动语义,允许目标对象从临时对象中获取(或窃取)数据。以下示例还显示了新的移动成员:移动构造函数和移动赋值运算符。因此,对于五人法则,我们有以下特殊成员:

  • 析构函数
  • 复制构造函数
  • 移动构造函数
  • 复制赋值运算符
  • 移动赋值运算符

存在类可能需要析构函数但不能明智地实现复制和移动构造函数以及复制和移动赋值运算符的情况。例如,当基类不支持后面的四大成员时,就会发生这种情况,但派生类的构造函数分配内存供自己使用。[需要引用]在 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++ 的 0 规则(主要用于 C++11 及更高版本)。 0 规则规定,如果您指定任何默认成员,则您的类必须专门处理单个资源。此外,它必须定义所有默认成员来处理该资源(或酌情删除默认成员)。因此,这些类必须遵循上述 5 规则。资源可以是任何东西:分配的内存、文件描述符、数据库事务等。

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

由于大多数类不将所有权作为他们唯一关心的问题,因此大多数类可以省略默认成员。这就是 0 规则得名的地方。

完全消除您的手动数组并改用std::vector

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

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

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

【讨论】:

  • 谢谢!你用教科书的清晰度解释了它。但是当我尝试使用复制构造函数的代码时,在添加对象 8 之后,构造函数被调用一次,然后复制构造函数被调用 9 次,而析构函数又被调用了 9 次。在对象 7 处,复制构造函数只被调用了一次。在对象 4 处,它被调用了五次。这不是非常低效吗?为什么会这样?
  • vector 添加项目时,如果新的size() 将超过当前的capacity(),则vector 通过分配新数组、复制/移动现有元素来增长并释放旧数组,然后将新项目插入新数组。 Praetorian 在他之前的评论中提到了这一点。 capacity() 以块的形式增长,因此不会在每次插入时都执行重新分配。似乎您的 vector 一次以 4 个为一组增长。如果您提前知道要添加多少项,您可以reserve() 数组大小以避免这些重新分配。
  • 好的,我想我明白了。向量在添加对象 0 后从一个元素开始,然后每当添加一个不适合向量的新元素时,都会创建一个大小为两倍的新向量,将所有现有对象复制到其中,然后旧向量被破坏。因此,大量的构造函数调用以 2 的幂为索引。
  • 它不会增长到 两倍 的大小,这可能会随着时间的推移使内存不堪重负。最初的 capacitysize 是 0。你添加了第一个对象,所以 capacity 增长到 4,没有复制任何内容,插入了对象,size 变成了 1。你添加了 3 个对象,递增 @ 987654345@ 到 4。你添加了第 5 个对象,所以 capacity 增长到 8,复制了 4 个对象,插入了第 5 个对象,size 变成了 5。你又添加了 3 个对象,将 size 增加到 8。你添加了第 9 个对象,所以容量再次增长,复制 8 个对象,插入第 9 个,size 变成了 9 个。依此类推...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-15
  • 1970-01-01
  • 1970-01-01
  • 2017-11-07
  • 1970-01-01
相关资源
最近更新 更多