9

我正在学习 c++,我最近学习了(在堆栈溢出中)关于复制和交换习语的知识,我对此有一些疑问。因此,假设我有以下类使用复制和交换习语,例如:

class Foo {
private:
  int * foo;
  int size;

public:
  Foo(size_t size) : size(size) { foo = new int[size](); }
  ~Foo(){delete foo;}

  Foo(Foo const& other){
    size = other.size;
    foo = new int[size];
    copy(other.foo, other.foo + size, foo);
  }

  void swap(Foo& other) { 
    std::swap(foo,  other.foo);  
    std::swap(size, other.size); 
  }

  Foo& operator=(Foo g) { 
    g.swap(*this); 
    return *this; 
  }

  int& operator[] (const int idx) {return foo[idx];}
};

我的问题是,假设我有另一个类,它有一个 Foo 对象作为数据但没有指针或其他可能需要自定义复制或分配的资源:

class Bar {
private:
  Foo bar;
public:
  Bar(Foo foo) : bar(foo) {};
  ~Bar(){};
  Bar(Bar const& other) : bar(other.bar) {}; 
  Bar& operator=(Bar other) {bar = other.bar;}
};

现在我有一系列问题:

  1. 上面为Bar类实现的方法和构造函数是否安全?使用了复制和交换来Foo确保在分配或复制时不会造成任何伤害Bar

  2. 在复制构造函数和交换中通过引用传递参数是强制性的吗?

  3. 是否可以说,当 的参数operator=按值传递时,为此参数调用复制构造函数以生成对象的临时副本,然后与该副本交换*this?如果我通过引用传递,operator=我会有一个大问题,对吧?

  4. 是否存在这种习语无法在复制和分配时提供完全安全的情况Foo

4

3 回答 3

8

你应该尽可能地在初始化列表中初始化你的类的成员。这也将解决我在评论中告诉你的错误。考虑到这一点,您的代码变为:

class Foo {
private:
  int size;
  int * foo;

public:
  Foo(size_t size) : size(size), foo(new int[size]) {}
  ~Foo(){delete[] foo;} // note operator delete[], not delete

  Foo(Foo const& other) : size(other.size), foo(new int[other.size]) {
    copy(other.foo, other.foo + size, foo);
  }

  Foo& swap(Foo& other) { 
    std::swap(foo,  other.foo);  
    std::swap(size, other.size); 
    return *this;
  }

  Foo& operator=(Foo g) { 
    return swap(g); 
  }

  int& operator[] (const int idx) {return foo[idx];}
};

class Bar {
private:
  Foo bar;
public:
  Bar(Foo foo) : bar(foo) {};
  ~Bar(){};
  Bar(Bar const& other) : bar(other.bar) { }
  Bar& swap(Bar &other) { bar.swap(other.bar); return *this; }
  Bar& operator=(Bar other) { return swap(other); }
}

它始终使用相同的成语

笔记

正如对问题的评论中提到的那样,Bar自定义复制构造函数等是不必要的,但我们假设Bar还有其他东西:-)

第二个问题

需要通过引用传递,swap因为这两个实例都已更改。

需要通过引用传递到复制构造函数,因为如果通过值传递,则需要调用复制构造函数

第三个问题

是的

第四个问题

不,但这并不总是最有效的做事方式

于 2011-05-06T00:38:03.320 回答
5

1 - 上面为 Bar 类实现的方法和构造函数是否安全?对 Foo 使用了复制和交换功能后,我确保在分配或复制 Bar 时不会造成任何伤害?

关于copy-ctor:那总是安全的(全有或全无)。它要么完成(全部),要么抛出异常(无)。

如果您的类仅由一个成员组成(即也没有基类),则赋值运算符将与成员的类一样安全。如果您有多个成员,则赋值运算符将不再是“全有或全无”。第二个成员的赋值运算符可能会抛出异常,在这种情况下,对象将被“中途”赋值。这意味着您需要在新类中再次实现复制和交换以获得“全有或全无”分配。

但是,它仍然是“安全的”,因为您不会泄漏任何资源。当然,每个成员的状态都是一致的——只是新类的状态不会一致,因为分配了一个成员而另一个没有。

2 - 在复制构造函数和交换中通过引用传递参数是强制性的吗?

是的,通过引用传递是强制性的。复制构造函数是复制对象的构造函数,因此它不能按值获取参数,因为这意味着必须复制参数。这将导致无限递归。(将为参数调用 copy-ctor,这意味着为参数调用 copy-ctor,这意味着......)。对于交换,原因是另一个:如果您要按值传递参数,则永远无法使用交换来真正交换两个对象的内容-交换的“目标”将是最初传入的对象的副本,这将立即被销毁。

3 - 当operator=的参数按值传递时,该参数调用复制构造函数以生成对象的临时副本,然后与*this交换的就是这个副本,是否正确?如果我在 operator= 中通过引用传递,我会有一个大问题,对吧?

是的,这是正确的。然而,通过引用到常量来获取参数也很常见,构造一个本地副本,然后与本地副本交换。然而,通过引用到常量的方式有一些缺点(它禁用了一些优化)。如果你没有实现复制和交换,你可能应该通过引用到常量。

4 - 在复制和分配 Foo 时,是否存在这种习惯用法无法提供完全安全性的情况?

据我所知没有。当然,多线程总是可以使事情失败(如果没有正确同步),但这应该是显而易见的。

于 2011-05-06T00:58:09.630 回答
2

这是一个典型的例子,说明遵循习语会导致不必要的性能损失(过早悲观化)。这不是你的错。复制和交换的成语被夸大了。这一个很好的成语。但不能盲目跟风。

注意: 您可以在计算机上做的最昂贵的事情之一就是分配和取消分配内存。在实际可行时避免这样做是值得的。

注意: 在您的示例中,复制和交换习惯用法始终执行一次释放,并且通常(当赋值的 rhs 是左值时)也执行一次分配。

观察:当 时size() == rhs.size(),不需要进行释放或分配。您所要做的就是一个copy. 这要快得多

Foo& operator=(const Foo& g) {
    if (size != g.size)
        Foo(g).swap(*this); 
    else
       copy(other.foo, other.foo + size, foo);
    return *this; 
}

即检查您可以首先回收资源的情况。如果你不能回收你的资源,那就复制和交换(或其他)。

注意: 我的评论与其他人给出的好的答案不矛盾,也没有任何正确性问题。我的评论只是关于显着的性能损失。

于 2011-05-06T01:47:44.120 回答