以下是 C ++ Primer 5th Edition 的练习:
练习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
指向新地址。我想知道这种微妙的差异是否有任何重大意义?在类似情况下,我是否应始终将指针更改为新地址?为什么呢?
答案 0 :(得分:2)
您的版本对于自行分配不安全。
delete ps;
ps = new std::string(*hp.ps);
这里,如果进行自我赋值,你可能会删除源和目标中的ps,使其在新语句中的使用错误(在大多数情况下它可能看似起作用)。
您可以直接将new的值分配到您的成员变量中,但在知道是否需要之前,您不能随意删除ps
。
如果您在执行代码之前测试了自我分配,例如if (this!=&hp)
,那么它会更好,但仍然不理想(请参阅其他地方的异常安全评论)。
答案 1 :(得分:2)
您的代码在自我分配和异常方面存在问题:假设内存分配引发std::bad_alloc
异常。在编码时,你应该总是假设内存分配可以出错,尽管实际上很少。在代码中
delete ps;
ps = new std::string(*hp.ps);
当第二行代码抛出异常时, ps
将指向陈旧成员。顺便说一句,如果你最终自我分配对象,你实际上只有delete
才能访问它之前的内存。因此,首先复制右侧的内容,然后将事情放到适当位置,最后释放资源是个好主意。
碰巧,这些正是
的操作swap()
操作利用这三种操作的方式称为复制和交换习语:
T& T::operator=(T other) {
this->swap(other);
return *this;
}
答案 2 :(得分: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
仍指向有效字符串,因此您没有格式错误的对象。
问题有多少取决于一些不同的事情,但通常更好的做法是避免任何形容错误的对象。
答案 3 :(得分: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;
}