【问题标题】:Array class member initialization in C++C++中的数组类成员初始化
【发布时间】:2016-04-26 09:25:53
【问题描述】:

我有以下代码sn-p:

#include <iostream>
using namespace std;
class A {
    int* data;
    int size;
public:
    A(int s):size(s)
    {
        data = new int[size];
    }
    A() {
        data = nullptr;
    }
    ~A() {
        if (data) delete [] data;
    }
};
class B {
    A a[2];
public:
    B() {
        a[0] = A(10);
        a[1] = A(11); 
    }
};
int main(int argc, char *argv[]) {
    B b;
}

在上面的 C++ 代码中,我有 class A,它有一个 array 成员 int* data,并且 (de) 内存分配由 (de) 处理构造函数。我创建了 class B,它有一个固定长度的 class Aarray 作为数据成员。

我的问题是:如何优雅地初始化成员A a[2]?在上面的代码中,A(10)A(11)是在栈上创建的,当跳出作用域时,会调用它们的析构函数,因此数据无效。跳转main函数作用域时,a[2]持有的指针会被释放两次,导致报错:

pointer being freed was not allocated

一种可能的解决方案是仔细设计copy constructormove constructor,这样上述编码范例就可以工作。

我尝试过的另一个解决方案是在class B初始化列表中初始化数组:

B() : a { A(10), A(11) }

这个解决方案有效,我并没有真正告诉初始化列表的底层机制。我想它一定和简单的constructcopy有很大的不同。我真的希望一些专家可以对这种机制进行详细的解释。当然,这个解决方案是丑陋的硬编码并且不灵活。

所以我想知道 C++ 中是否有一些编程范式来解决这个设计问题?

【问题讨论】:

  • 在我们跳入任何复杂的东西之前,深入了解if (data) delete [] data; 怎么样——你查过delete [] 做了什么吗?
  • 有关范例,请参阅rule of five。如需解决方案,请参阅std::vector

标签: c++ arrays initialization


【解决方案1】:

在上面的代码中,A(10) 和 A(11) 是在堆栈上创建的

它们是临时对象。没有指定在哪里创建它们或是否创建它们。

当跳出作用域时,会调用它们的析构函数

每个temporary的析构函数会在对应的move赋值语句结束后被调用。

一个可能的解决方案是仔细设计一个复制构造函数和一个移动构造函数,这样上面的编码范式就可以工作了。

还有 {copy,move} 赋值运算符。当隐含声明的那些没有做正确的事情时,你应该总是这样做。如果您删除析构函数中的某些内容,它们永远不会做正确的事情。

我尝试过的另一个解决方案是在 B 类的初始化列表中初始化数组

这个解决方案有效,我并没有真正告诉初始化列表的底层机制。我想它一定和简单的构造和复制有很大的不同。

原始代码中的错误是A 的移动赋值运算符表现不佳。由于初始化列表永远不会从临时位置移动分配,因此它永远不会触发错误。

这实际上是构建您要求的a 的更优雅的方式。不是因为它避免了错误,而是因为避免不必要的移动本质上是一件好事。

所以我想知道 C++ 中是否有一些编程范式来解决这个设计问题?

是的。 RAII Single responsibility principle。除非你的班级除了管理data 指向的内存之外什么都不做,它不应该管理内存。相反,它应该将内存管理委托给 RAII 对象。在这种情况下,您应该使用std::vector 成员。

class A {
    std::vector<int> data;
public:
    A(int s):data(s) {}
    A() = default;
};

【讨论】:

    【解决方案2】:

    使用初始化列表构造 B::a,如下所示:

    class B {
        A a[2];
    public:
        B() : a({10, 11}){
        }
    };
    

    【讨论】:

      【解决方案3】:

      理想的答案是强制A 使用移动而不是副本,或者在副本上为该项目分配新空间。在这两者中,效率最高的是前者,因此我将在下面对其进行扩展:

      强制移动可以通过两种方式完成:

      • 删除复制构造函数并复制operator=,实现自己的移动构造函数和operator=
      • 始终使用std::movestd::swap

      其中,前者的优势在于您不会意外复制课程,但使用后者,您正在移动的事实会更加明显。

      要删除默认复制方法,请执行以下操作:

      class A {
          A( const A& a ) = delete;
          A& operator =( const A& a ) = delete;
      }
      

      【讨论】:

      • 我编辑了你的答案,我希望现在更好(如果你不这么认为,请回复)。另外,据我所知,声明移动操作(构造函数和/或赋值操作符)会自动删除复制的操作——对吗?
      • @anatolyg 感谢您的编辑,我不确定声明是否删除了复制方法。我个人仍然会删除复制方法,以便我的意图明确
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-07-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多