是结构体中的复制构造函数,该结构体包含动态分配的数组,总是必需的

Is a copy constructor within a struct which contains a dynamically allocated array always necessary?

本文关键字:结构体 数组 动态分配 包含 复制 构造函数      更新时间:2023-10-16

我在不同的网站上阅读了一些关于复制构造函数的教程,维基百科,也浏览了"复制构造函数"搜索结果的前5页,但我仍然没有得到它:

我有两个问题:
1. 复制构造函数到底是做什么的?
2. 复制构造函数总是必需的(在包含动态分配数组的结构中)?

我将试着用一个例子来解释是什么困扰着我,以及我实际上在问什么:

假设我不想使用c++字符串,而是创建我自己的"智能"字符串。我只提供问题所需的代码:

#include <iostream>
#include <cstdlib>
using namespace std;
struct MyString
{
    char* str;
    int n , i;
    MyString(int x, char c)
    {
        str=(char*)malloc(x+1);
        for(n=0; n<x; ++n) str[n] = c;
        str[n] = '';
    }
    void print()
    {
        cout << str << endl;
    }
    void append(MyString second)
    {
        str=(char*)realloc(str, (n + second.n + 1) * sizeof(char));
        for(i=0; i < second.n; ++i) str[n+i] = second.str[i];
        n += second.n;
        str[n] = '';
    }
    ~MyString()
    {
        cout << "destructor!" << endl;
        free(str);
    }
};
int main()
{
    MyString A(5, '$'), B(5, '#');
    A.print();
    B.print();
    A.append(B);
    A.print();
    B.print();
    A.append(B);
    A.print();
    B.print();
}

为了简单起见,该结构体只包含一个构造函数,对于给定的整数"n"和给定的字符"c",该构造函数产生一个字符串,该字符串包含字符"c"重复"n"次。示例:MyString A(5, '$');表示A是字符串"$$$$$"。print函数将字符串打印到屏幕上,append函数将一个字符串附加到另一个字符串上。例如:A.append (B);意味着= A + B或在这个例子 "$$$$$" + "#####" = "$$$$$#####".

有几件事需要注意:
1. MyString包含一个动态分配的数组。2. 函数append的形参是"MyString"。
3.我没有包含复制构造函数。

append函数声明如下:

void append(MyString second)

通常这意味着函数append接收到MyString类型对象的副本,但由于MyString包含一个动态分配的对象,该函数将接收到指向原始对象的指针的副本(如果我是正确的?)并将其视为本地副本,这意味着在执行追加操作后,将调用该指针的析构函数并销毁该对象,因此查看我的原始main函数:

int main()
{
    MyString A(5, '$'), B(5, '#');
    A.print();
    B.print();
    A.append(B);
    // B doesn't exist anymore
    A.print();  // OK
    B.print();  // ???
    A.append(B); // ???
    A.print();   // ???
    B.print();   // ???   
}

为了解决这个问题,我可以写一个复制构造函数,但是我真的需要吗?我可以这样写append函数:

void append(MyString const& second)

还是

void append(MyString& second)

,这是有效的,但我被告知,每次我遇到一个对象,涉及动态分配+一个函数,有对象类型作为参数,我应该写一个复制构造函数,只是为了安全。但会出什么问题呢?如果我不添加复制构造函数,用户会做些什么把事情搞砸呢?

我可以这样写一个复制构造函数:

 MyString(MyString const& second)
    {
        n = second.n;
        str = (char*)malloc(n);
        for(i=0; i<n; ++i) str[i] = second.str[i];
    }

然后我可以让函数append保持原来的形式

void append(MyString second)

但是当执行下面这行代码时到底发生了什么?

A.append(B);

我被告知构造函数没有返回值。因此,如果B在函数append(在A内)执行之前调用了他的复制构造函数,那么B究竟是如何"告诉"A去哪里寻找B的副本的呢?

现在我看到这个问题已经太大了:(所以我现在就到此为止。欢迎任何编辑、建议、评论和回答!提前感谢!

复制构造函数包含从另一个MyString对象生成MyString对象所需的指令。

在这种情况下,你绝对需要一个复制构造函数,否则类型MyString的语义将是非标准的,并且很容易出错。

编译器会自动为你实现一个复制构造函数。不幸的是,它很可能是错误的,因为对象有指针。这个自动实现将只是复制指针的值到另一个MyString。然后,您将拥有两个认为它们拥有相同内存的MyString对象。其中一个将被销毁(它的析构函数将运行),而另一个将留下一个不再有效的指针。考虑以下内容:

MyString first(10, 'a');
{ //This is here to create a new scope for the second myString
    MyString second(first); //The copy constructor that the compiler made for you runs here
    //. . . some other stuff
} //Right here, second goes out of scope and it's destructor runs.  This calls delete on str
  //Now you're in trouble - first's str pointer now points to unallocated memory
first.print(); //Uh-oh - undefined behavior.

或:

//Declaration:
void SomeFunctionThatPassesByValue(MyString anotherMyString);
. . .
MyString first(10, 'a');
SomeFunctionThatPassesByValue(first); //The copy-constructor can run here too
/* Inside SomeFunctionThatPassesByValue, there will be a copy of first named
anotherMyString which will have its destructor run and call delete and de-allocate
your memory out from under you */
first.print(); //Uh-oh again!

现在,你可以说"任何使用MyString的人:都要非常小心,不要按值复制它,否则事情会搞砸"- 但这不是很现实。你应该显式地禁用复制构造函数(这是Hans Passant在评论中的建议),或者你应该正确地实现它。

你还需要正确地实现(或禁用)复制赋值操作符,否则你会遇到同样的问题。

MyString second(10, 'b');
second = first; //The copy-assign operator that the compiler made for you is also wrong!

对于如何"正确"实现复制构造函数和复制赋值操作符,您有一些选项。最简单的方法就是分配更多的内存并复制指向的内存。如果指向的内存是不可变的(它不是在您的示例中),那么您可以共享指针和计数引用,并仅在使用该指针的最后一个MyString被销毁时调用delete,但这很难得到正确的