动态分配一个对象数组

68

我有一个包含动态分配数组的类,比如说

class A
{
    int* myArray;
    A()
    {
        myArray = 0;
    }
    A(int size)
    {
        myArray = new int[size];
    }
    ~A()
    {
        // Note that as per MikeB's helpful style critique, no need to check against 0.
        delete [] myArray;
    }
}

现在我想创建一个动态分配的这些类的数组。这是我的当前代码:

A* arrayOfAs = new A[5];
for (int i = 0; i < 5; ++i)
{
    arrayOfAs[i] = A(3);
}
但这个实现非常糟糕。因为使用A(3)调用创建的新A对象在for循环迭代完成时被析构,这意味着该A实例的内部myArray会被delete[]。

所以我认为我的语法肯定非常错误了?我想有一些修复方法似乎过于复杂,希望能够避免:

  • A创建一个拷贝构造函数。
  • 使用vector<int>vector<A>,这样就不必担心这些问题。
  • arrayOfAs作为A*指针数组,而不是A对象数组。

我认为这只是一些初学者的问题,当尝试动态分配具有内部动态分配的数组时,其语法实际上是有效的。

(另外,由于我已经有一段时间没有写C++了,因此也欢迎对代码风格进行批评。)

未来的读者更新:下面的所有答案都非常有帮助。Martin的答案因为示例代码和有用的“四法则”而被接受,但我真的建议阅读它们。一些是关于问题的好而简洁的陈述,一些正确地指出了为什么vector是一个好方法。

7个回答

136
构建容器时,显然要使用标准容器之一(例如std::vector)。但是,这是你需要考虑的问题的完美例子,当你的对象包含原始指针时。
如果你的对象有一个原始指针,那么你需要记住三个规则(现在是C++11中的五个规则):
- 构造函数 - 析构函数 - 拷贝构造函数 - 赋值运算符 - 移动构造函数(C++11) - 移动赋值运算符(C++11)
这是因为如果未定义,编译器将生成其自己版本的这些方法(请见下文)。编译器生成的版本在处理原始指针时并不总是有用。
拷贝构造函数是难以正确实现的(如果你想提供强异常保证,它就不是简单的操作)。可以使用复制和交换技巧在内部定义赋值运算符。
下面列出了包含整数数组指针的类的绝对最小限度的完整细节。
知道这很难做到正确,你应该考虑使用std::vector而不是整数数组指针。vector易于使用(和扩展),并涵盖了所有与异常相关的问题。将以下类与A的定义进行比较。
class A
{ 
    std::vector<int>   mArray;
    public:
        A(){}
        A(size_t s) :mArray(s)  {}
};

看看你的问题:

A* arrayOfAs = new A[5];
for (int i = 0; i < 5; ++i)
{
    // As you surmised the problem is on this line.
    arrayOfAs[i] = A(3);

    // What is happening:
    // 1) A(3) Build your A object (fine)
    // 2) A::operator=(A const&) is called to assign the value
    //    onto the result of the array access. Because you did
    //    not define this operator the compiler generated one is
    //    used.
}

编译器生成的赋值运算符对于几乎所有情况都是可以的,但当涉及到原始指针时就需要注意了。在您的情况下,由于浅拷贝问题,它正在引起问题。您最终得到了两个包含指向同一块内存的指针的对象。当循环结束时,A(3) 调用其指针上的 delete []。因此,数组中的另一个对象现在包含一个指向已经返回给系统的内存的指针。
编译器生成的复制构造函数通过使用成员的复制构造函数来复制每个成员变量。对于指针,这只意味着指针值从源对象复制到目标对象(因此是浅拷贝)。
编译器生成的赋值运算符通过使用成员的赋值运算符来复制每个成员变量。对于指针,这只意味着指针值从源对象复制到目标对象(因此是浅拷贝)。
因此,包含指针的类的最小要求:
class A
{
    size_t     mSize;
    int*       mArray;
    public:
         // Simple constructor/destructor are obvious.
         A(size_t s = 0) {mSize=s;mArray = new int[mSize];}
        ~A()             {delete [] mArray;}

         // Copy constructor needs more work
         A(A const& copy)
         {
             mSize  = copy.mSize;
             mArray = new int[copy.mSize];

             // Don't need to worry about copying integers.
             // But if the object has a copy constructor then
             // it would also need to worry about throws from the copy constructor.
             std::copy(&copy.mArray[0],&copy.mArray[c.mSize],mArray);

         }

         // Define assignment operator in terms of the copy constructor
         // Modified: There is a slight twist to the copy swap idiom, that you can
         //           Remove the manual copy made by passing the rhs by value thus
         //           providing an implicit copy generated by the compiler.
         A& operator=(A rhs) // Pass by value (thus generating a copy)
         {
             rhs.swap(*this); // Now swap data with the copy.
                              // The rhs parameter will delete the array when it
                              // goes out of scope at the end of the function
             return *this;
         }
         void swap(A& s) noexcept
         {
             using std::swap;
             swap(this.mArray,s.mArray);
             swap(this.mSize ,s.mSize);
         }

         // C++11
         A(A&& src) noexcept
             : mSize(0)
             , mArray(NULL)
         {
             src.swap(*this);
         }
         A& operator=(A&& src) noexcept
         {
             src.swap(*this);     // You are moving the state of the src object
                                  // into this one. The state of the src object
                                  // after the move must be valid but indeterminate.
                                  //
                                  // The easiest way to do this is to swap the states
                                  // of the two objects.
                                  //
                                  // Note: Doing any operation on src after a move 
                                  // is risky (apart from destroy) until you put it 
                                  // into a specific state. Your object should have
                                  // appropriate methods for this.
                                  // 
                                  // Example: Assignment (operator = should work).
                                  //          std::vector() has clear() which sets
                                  //          a specific state without needing to
                                  //          know the current state.
             return *this;
         }   
 }

您有关于您所提到的异常问题的文章链接吗? - shoosh
为什么你要将“raw”大写?它肯定不是任何缩写,只是指“原始”的意思,即未修改的、简单的,不是智能指针或其他类型的包装器。 - jalf
2
@jalf 这些叫做“恐慌引号” :) - joshperry
为什么移动赋值运算符不返回任何内容? - Daniele
@Daniele:因为那是一个bug。正在修复中。 - Martin York

12

我建议使用std::vector:如下所示

typedef std::vector<int> A;
typedef std::vector<A> AS;

STL略微过度使用并没有什么问题,这样你就可以花更多时间实现你的应用程序的特定功能,而不是重复造轮子。


8
你的A对象的构造函数动态分配了另一个对象,并将指向该动态分配对象的指针存储在一个裸指针中。
对于这种情况,你必须定义自己的复制构造函数、赋值运算符和析构函数。编译器生成的那些函数不会正确工作。(这是“三大法则”的一个推论:具有任何一个析构函数、赋值运算符或复制构造函数的类通常都需要这三个函数)。
你已经定义了自己的析构函数(并提到创建了一个复制构造函数),但你需要定义另外两个“三大法则”的函数。
另一种选择是将指向你动态分配的int[]的指针存储在其他对象中,让它来为你处理这些事情。比如像vector(正如你所提到的)或者boost::shared_array<>。
简而言之,为了充分利用RAII,你应该尽量避免使用裸指针。
既然你要求进行其他风格修正,还有一个小问题就是当你删除裸指针时,你不需要在调用delete之前检查是否为0——delete通过不执行任何操作来处理这种情况,因此你不必在代码中添加这些检查。

有很多非常好的答案;我真的想接受它们中的大部分,包括你的答案,作为“最佳答案”。非常感谢。还要感谢你对风格的批评。 - Domenic
这是“规则4”,它需要一个普通构造函数。如果您不初始化指针,则它们具有随机值。 - Martin York
@Martin - 你说得对。我一直听说构造函数是一个“已知的”,所以它被包含在“三个原则”中。但我认为明确地将其包含在规则中是更好的方式。 - Michael Burr

4
  1. 仅当对象具有默认构造函数和复制构造函数时,才使用数组或常见容器。

  2. 否则,请存储指针(或智能指针,但在这种情况下可能会遇到一些问题)。

PS:始终定义自己的默认构造函数和复制构造函数,否则将使用自动生成的构造函数。


2
使用new运算符的放置特性,您可以在原地创建对象并避免复制:

放置 (3) :void* operator new (std::size_t size, void* ptr) noexcept;

仅返回ptr(不分配存储空间)。 注意,如果该函数由new表达式调用,则将执行适当的初始化(对于类对象,这包括调用其默认构造函数)。

我建议采取以下措施:
A* arrayOfAs = new A[5]; //Allocate a block of memory for 5 objects
for (int i = 0; i < 5; ++i)
{
    //Do not allocate memory,
    //initialize an object in memory address provided by the pointer
    new (&arrayOfAs[i]) A(3);
}

2

您需要一个赋值运算符,以便:

arrayOfAs[i] = A(3);

正常工作。


实际上,这里使用的是赋值运算符而不是拷贝构造函数。左侧已经完全构造完成。 - Martin York
1
很遗憾,不行。因为原始的A(3)和数组arrayofAs[i]都包含指向堆上同一区域的成员myArray。先离开作用域的那个将删除对象。后离开作用域的也会删除它,这就导致了问题。 - Martin York

2
为什么不添加一个setSize方法呢?该方法可以设置元素的大小。
A* arrayOfAs = new A[5];
for (int i = 0; i < 5; ++i)
{
    arrayOfAs[i].SetSize(3);
}

我喜欢“复制”,但在这种情况下,默认构造函数实际上没有做任何事情。 如果原始的m_array存在,SetSize可以将数据从中复制出来。为此,您必须在类内部存储数组的大小。
或者
SetSize可以删除原始的m_array。

void SetSize(unsigned int p_newSize)
{
    //I don't care if it's null because delete is smart enough to deal with that.
    delete myArray;
    myArray = new int[p_newSize];
    ASSERT(myArray);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接