1

以下是C++ Primer 第 5 版的练习:

练习 13.22:假设我们希望 HasPtr 表现得像一个值。也就是说,每个对象都应该有自己的对象指向的字符串副本。我们将在下一节展示复制控制成员的定义。但是,您已经知道实现这些成员所需的一切。在继续阅读之前编写 HasPtr 复制构造函数和复制赋值运算符。(第 511 页)

类代码HasPtr

class HasPtr
{
public:
    //! default constructor
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0) { }

    //! copy constructor
    HasPtr(const HasPtr& hp) : ps(new std::string(*hp.ps)), i(hp.i) { }

    HasPtr&
    operator = (const HasPtr& hp);

    ~HasPtr()
    {
        delete ps;
    }

private:
    std::string *ps;
    int    i;
};

我的这个复制赋值运算符的代码:

HasPtr&
HasPtr::operator = (const HasPtr &hp)
{
    delete ps;
    ps = new std::string(*hp.ps);

    i  = hp.i;

    return *this;
}

本书以下部分中提供的代码:

HasPtr& 
HasPtr::operator = (const HasPtr &rhs)
{
    auto newp = new string(*rhs.ps);   // copy the underlying string
    delete ps;       // free the old memory
    ps = newp;       // copy data from rhs into this object
    i = rhs.i;
    return *this;    // return this object
}

通过一步一步的执行,我发现两个代码之间有细微的差别。在我的代码中,它不会更改ps指向的地址,而书中的代码ps指向一个新地址。我想知道这种细微的差异是否有任何重要意义?在类似情况下,我是否应该始终将指针更改为新地址?为什么?

4

4 回答 4

2

您的代码在自分配和异常方面存在问题:假设内存分配引发std::bad_alloc异常。编码时,您应该始终假设内存分配可能会出错,尽管实际上很少发生。在代码中

delete ps;
ps = new std::string(*hp.ps);

ps当第二行代码抛出异常时,将指向 stale member。顺便说一句,如果您最终自行分配对象,那么您实际上delete是唯一在访问它之前的内存。因此,最好先复制右边的内容,然后放东西,最后释放资源。

碰巧,这些正是

  1. 复制构造函数
  2. swap()您通常希望对任何类型持有资源的操作
  3. 析构函数

利用这三个操作的方法称为复制和交换习语:

T& T::operator=(T other) {
    this->swap(other);
    return *this;
}
于 2014-01-01T02:07:19.593 回答
1

您的版本对于自分配是不安全的。

delete ps;
ps = new std::string(*hp.ps);

在这里,如果进行自赋值,您可能会同时删除源和目标中的 ps,从而使其在新语句中的使用错误(但在大多数情况下它可能具有欺骗性)。

您可以将 new 的值直接分配给您的成员变量,但您不能ps在知道是否需要之前随意删除它。

如果您测试了自我分配,例如。if (this!=&hp)在编写代码之前,它会更好,但仍然不理想(请参阅其他地方的异常安全评论)。

于 2014-01-01T01:54:01.963 回答
1

从功能上讲,我只能看到一个区别。

    delete ps;
    ps = new std::string(*hp.ps);

如果内存不足,则调用new std::string可能会引发异常。在您的情况下,ps仍然有旧的已删除字符串的地址 - 所以它格式错误。如果您从异常中恢复,则可能会取消引用ps并且会发生不好的事情。

    auto newp = new string(*rhs.ps);   // copy the underlying string
    delete ps;       // free the old memory
    ps = newp;       // copy data from rhs into this object

在教科书代码中,ps直到分配新字符串后才会删除。在异常情况下,ps仍指向有效字符串,因此您没有格式错误的对象。

有多少问题取决于几个不同的事情,但通常更好的做法是避免出现任何格式错误的对象。

于 2014-01-01T02:09:26.897 回答
1

您的代码中实际上存在两个问题:

  • 自我分配
  • 异常安全

您通常希望您的成员函数提供强大的异常保证,即,当分配失败时(在您的情况下,它可以在operator new或 的复制构造函数中string),程序状态不会改变。

我认为现代做法是提供一个swap函数并使赋值调用复制构造函数。就像是:

void HasPtr::swap(HasPtr& rhs)
{
    std::swap(this.ps, rhs.ps);
    std::swap(this.i, rhs.i);
}

HasPtr(const HasPtr& rhs)
{
    ps = new string(*rhs.ps);
    i = rhs.i;
}

HasPtr& operator=(const HasPtr& rhs)
{
    HasPtr temp(rhs);
    this.swap(temp);
    return *this;
}
于 2014-01-01T02:20:24.893 回答