如何在具有指向对象的指针数组的类中创建复制构造函数和析构函数,其中对象本身具有指向整数的指针数组

How to create copy constructor and destructor in a class with an array of pointers to objects where objects itself have an array of pointers to ints

本文关键字:指针 数组 对象 析构函数 整数 创建 复制 构造函数      更新时间:2023-10-16

在下面的代码中,我有一个类A,它有一个动态的整数数组。 我还有另一个类 B,它有一个指向类 A 对象的指针数组,我已经为 A 类编写了复制构造函数。 我需要为 B 类编写一个复制构造函数和析构函数,我已经尝试了各种方法但没有成功。

A类的定义:

class A {
public:
A::A(const A& other){
siz = other.siz;
c = other.c;
s = other.s;
e = other.e;
arr= new int[other.c];
memcpy(arr, other.arr, sizeof(int) * c);
}
A::~A() {
delete [] m_arr; 
}
const A& operator=(const A& rhs){
if(this == &rhs)
return *this; // handling of self assignment
delete[] arr; // freeing previously used memory
arr = new int[rhs.c];
siz = rhs.siz;
c = rhs.c;
e = rhs.e;
s = rhs.s;
memcpy(m_arr, rhs.arr, sizeof(int) * c);
return *this;
}
private :
int *arr ;        
int c ;
int siz ;     
int s ;     
int e ;       
}

B类的定义:

class B {
public:
B::B(const B& other){
// .......need help here
}
B::~B() {
//......need help here
}
private :
static const int constant = 7;
A * array[constant] ;        
int x ;
int y ;     
int z ;           
}

感谢您的帮助

关键是将原始拥有指针包装到 RAII 资源管理器中,并定义组装这些安全RAII构建块的类。然后,C++编译器将能够自动合成复制操作和析构函数(以及移动操作)。

例如,在class A中,您有一个int *arr数据成员,用于存储指向动态分配数组的拥有原始指针。将其替换为 RAII 资源管理器,如标准std::vector容器(例如std::vector<int> arr)。

这样做,无需定义显式析构函数,因为编译器将自动调用矢量数据成员上的std::vector析构函数,并且内存将自动释放(无需调用显式delete[])。

同样,默认的复制构造函数将执行成员级复制,因此std::vector复制构造函数将由C++编译器自动调用,并且从源向量到目标向量的深层复制将自动发生,而无需您使用new[]动态分配、memcpy等"重新发明轮子"。

从 C++11 开始,您可以告诉C++编译器使用此语法合成默认复制构造函数(也可用于默认构造函数、移动构造函数、复制赋值运算符等)

class A {
public:
...
A(const A&) = default;
...
};

class B也是如此,而不是A * array[constant]数据成员,考虑使用vector,例如指向 A 的智能指针向量:

vector<shared_ptr<A>> arrayOfAs;

另一种方法是将std::array用于恒定大小的东西。

无论如何,关键是:考虑使用 RAII 资源管理器作为更复杂的类的构建基块,而不是将原始拥有指针和其他不安全的原始资源作为数据成员。每个原始资源都应安全地包装在其自己的 RAII 资源管理器中,这反过来又应用作更复杂的类的数据成员。


附言作为奖励阅读,请考虑阅读"什么是三法则? ",以及这种跟进。

让我们首先假设对于这个练习,无论出于何种原因,您都不能使用std::vectorstd::array。 假设我们到目前为止遵循您的类的设计,我们可以实现如下的复制构造函数:

B::B(const B &other)
{
for (std::size_t i = 0; i < constant; ++i) {
// Use `new` to allocate memory and also call `A`'s copy constructor.
array[i] = new A(*other.array[i]);
}
}

它的作用,因为array是一个指向As 的指针数组,是为数组中的每个元素分配动态内存,并使用new填充数组中指向这些动态分配的A对象的指针,同时还调用您为A创建的复制构造函数,通过使用语法new A(other_a), 这叫你的A::A(const A &other). 由于other是对A的引用,而不是对A的指针,因此other.array[i]中持有的指针被取消引用,这就是为什么调用是A(*other.array[i]),而不是A(other.array[i])

由于我们在这里分配了动态内存new,析构函数必须为我们对 'new 的每次调用调用delete。 这可以类似地实现:

B::~B()
{
for (std::size_t i = 0; i < constant; ++i) {
// As each `A` has been allocated with `new`, they should now be
// destroyed.
delete array[i];
}
}

因此,我们现在拥有的东西似乎可以按照我们的意愿工作,我们可能会认为这就是它的全部。 但是,事情开始变得复杂,因为如果new执行的其中一个分配失败并引发异常,会发生什么? 或者如果A的构造函数抛出异常怎么办?delete永远不会被要求使用迄今为止已分配new的元素。

为了使我们的复制构造函数异常安全,需要一些稍微复杂的代码。 例如,像这样:

B::B(const B &other)
{
std::size_t i;
try {
for (i = 0; i < constant; ++i) {
array[i] = new A(*other.array[i]);
}
} catch (...) {
// Delete all elements allocated so far
for (std::size_t d = 0; d < i; ++d) {
delete array[i];
}
// Re-throw the exception to the caller
throw;
}
}

像这样的代码很快就会变得无法维护。 为了避免此类问题,一个好的准则是,管理必须创建和销毁的资源的生存期应由仅管理该资源的生存期的类封装。 这很重要,因为如果您开始向类添加更多与此数组类似的构造,那么您的类将负责构造和破坏更多,而不仅仅是此数组,这将使异常安全比现在更加困难。 事实上,构造和析构数组已经相当复杂的原因是因为你的类负责 7 个独立资源(动态分配的对象)的生存期,每个数组元素一个。

考虑到这一点,简化此操作的一种方法是使用一个类来封装具有newdelete的对象动态分配和解除分配。 C++11 包括几个至少封装了释放部分的类,最相关的是std::unique_ptrstd::shared_ptr。 但是,这些类旨在避免复制。unique_ptr是显式不可复制的,复制shared_ptr只会创建对同一资源的新引用,同时保留引用计数。 这意味着您仍然必须手动实现复制。

您可以通过将B中的声明从以下更改来切换到unique_ptr

A *array[constant];

自:

std::unique_ptr<A> array[constant];

然后,您可以使用以下内容填充复制构造函数中的每个成员:

array[i] = std::unique_ptr<A>(new A(*other.array[i]));

使用此方法,您将不再需要担心捕获异常,因为如果在构造函数中的某个位置引发异常,则会自动为数组中的每个unique_ptr调用析构函数。 尚未分配给的unique_ptr默认情况下将保留空指针,并且在销毁它们时将安全地不执行任何操作。

然而,还有另一种方法:根本不使用指针/动态内存。 你已经有一个类(A)负责它自己的资源的生存期。

为此,可以将以下声明更改为B

A *array[constant];

自:

A array[constant];

这意味着您不再需要定义复制构造函数(或复制赋值运算符)。 如果在 C++ 类中未提供复制构造函数,则可以复制该类,就好像它具有一个简单的成员复制构造函数一样,该构造函数也适用于数组,并将为数组中的每个元素调用复制构造函数。 由于数组本身是类的一部分,并且没有指向动态内存的指针,因此不需要使用delete手动释放每个元素。