2

我为我创建的一个类编写了一个复制赋值运算符,我使用以前的帖子作为指导: 什么是三法则?

我对这个人解释的一个方面有点困惑。

这是他们的课程:

class person
{
    char* name;
    int age;
};

这是我用作参考的复制赋值运算符定义(至少提供了 2 个):

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

我发现令人困惑的是,为什么它们包括这条线delete[] name;

这是他们提供的另一个示例:

person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

我立即回避了这个,因为我不明白为什么该函数会检查if(this != &that)然后在似乎尚未生成的数组上运行 delete (delete[] name;)。调用赋值运算符时,是否在调用复制赋值运算符函数之前立即调用常规构造函数?因此意味着我们必须删除由类构造函数生成的数组,因为它只能充满垃圾数据?

为什么我不能只写: name = that.name

name = new char[that.name.size()];
for (int i = 0; i < that.name.size(); i++)`
{
  name[i] = that.name[i]
}

这可能是非常基本的,我应该只实现帖子的建议,但我的实际用例涉及具有多个成员的 s 数组,struct所以我只是有点难以理解我到底需要做什么。

我意识到这里有 2.5 个问题。任何见解将不胜感激。

这是我第一次实现自定义复制构造函数和复制赋值运算符,我相信在我完成几次之后它会看起来很容易。

提前致谢。

4

3 回答 3

7

所有这些都是确保正确管理内存所必需的。

如果我理解正确,您需要回答何时operator=实际调用?
好吧,operator=总是在存在两个有效对象时调用。其中一个是分配给的,第二个是数据源。在此操作之后,两个对象必须仍然保持有效

这意味着比在内部operator=this有为字符串分配内存的对象(在其中一个构造函数中分配)和that对象,也为另一个字符串分配了内存。

我发现令人困惑的是,为什么它们包括这条线delete[] name;

我们必须首先清理当前驻留在thisobject 中的内存,否则在分配新的 momry 给它之后我们会丢失这个指针。

调用赋值运算符时,是否在调用复制赋值运算符函数之前立即调用常规构造函数?

不,对象已经存在,这operator=就是调用(而不是复制构造函数)的原因。

因此意味着我们必须删除由类构造函数生成的数组,因为它只能充满垃圾数据?

它充满了有效数据。您的对象已构建,其中包含一些数据,然后您将其他内容分配给该对象。


附录:什么时候调用复制构造函数,什么时候operator=调用?(有关更多详细信息,请参阅此问题):

class A{};

int main()
{
    A a1; //default constructor 
    A a2 = a1; //copy-constructor, not assignment operator! New object is needed

    A a3; 
    a3 = a1; //we have valid and existing object on the left side, assignment operator is used
}
于 2019-12-16T21:16:39.600 回答
4

我发现令人困惑的是,为什么它们包含行 delete[] name;?

当使用newandnew[]来管理内存时,经验法则是它们应该有一个匹配的deleteor delete[]。据推测,name之前是用new[](在构造函数中或通过先前的赋值)分配的,所以在下一行用 重新分配它之前name = local_name;,我们需要delete[]它。

我不明白为什么函数会检查 if(this != &that)

此检查是健全性检查。如果您将对象分配给自身,则无需执行任何操作。实际上,它可能会导致问题,因为如果您delete[] name;指向this同一个对象作为that引用,那么您将无法再复制that.name,因为它的内存已被释放。

为什么我不能只写:name = that.name 或

这将name在两个不同的实例中指向同一个内存。在某些情况下,这样的共享内存不仅有用而且是理想的。但是,您必须小心,因为如果其中一个实例delete[]以其名称执行,那么name在另一个实例中将不再有效。

于 2019-12-16T21:03:05.573 回答
3

让我们从这个代码片段的问题开始。

name = new char[that.name.size()];
for (int i = 0; i < that.name.size(); i++)`
{
  name[i] = that.name[i]
}

数据成员名称的类型为 char *。那就是它是一个指针。指针和指针指向的字符都没有像size.

所以这个代码片段是无效的,不会编译。

你可以使用这样的方法

name = that.name;

如果数据成员具有 type std::string

为什么我不能只写:name = that.name

在这种情况下,该类的两个对象将包含指向相同内存范围的指针。因此,如果一个对象将被删除,那么另一个对象的指针将无效,并且使用它来删除分配的内存会导致未定义的行为。

调用赋值运算符时,是否在调用复制赋值运算符函数之前立即调用常规构造函数?

复制赋值运算符只能应用于已构造的对象。那就是类的对象必须已经存在,并且使用的构造函数应nullptr通过分配的内存地址或通过分配的内存地址初始化其数据成员名称。

我立即回避了这个,因为我不明白为什么该函数会检查 if(this != &that)

此检查允许避免对象的自分配。也就是说,当一个对象被分配给它自己时,分配和删除内存的代码将是多余的。

我发现令人困惑的是,为什么它们包含行 delete[] name;?

因为对象已经被构造并且它的数据成员名称可以指向包含名称字符串的分配内存。

考虑到所有所说的,复制赋值运算符看起来像

person& operator =( const person &that )
{
    if ( this != &that )
    {
        char *local_name = new char[strlen(that.name) + 1];
        // If the above statement throws,
        // the object is still in the same state as before.
        // None of the following statements will throw an exception :)
        strcpy(local_name, that.name);
        delete[] name;
        name = local_name;
        age = that.age;
    }

    return *this;
}
于 2019-12-16T21:22:50.770 回答