如何在C++中使用基类的构造函数和赋值运算符?

110

我有一个类B,它拥有一组构造函数和一个赋值运算符。

这是它的定义:

class B
{
 public:
  B();
  B(const string& s);
  B(const B& b) { (*this) = b; }
  B& operator=(const B & b);

 private:
  virtual void foo();
  // and other private member variables and functions
};

我想创建一个继承类 D,只需重写函数foo(),不需要进行其他更改。

但是,我希望D具有与B相同的构造函数集,包括复制构造函数和赋值运算符:

D(const D& d) { (*this) = d; }
D& operator=(const D& d);

我是否必须用 D 重写它们所有,或者有没有一种方法可以使用 B 的构造函数和运算符?我特别想避免重写赋值运算符,因为它必须访问 B 的所有私有成员变量。


如果您只想覆盖 foo 方法,可以使用 using B::operator=; 来继承赋值运算符,但是复制和移动构造函数无法被继承:https://dev59.com/hqnka4cB1Zd3GeqPOHHM - anton_rh
5个回答

146
您可以显式地调用构造函数和赋值运算符:
class Base {
//...
public:
    Base(const Base&) { /*...*/ }
    Base& operator=(const Base&) { /*...*/ }
};

class Derived : public Base
{
    int additional_;
public:
    Derived(const Derived& d)
        : Base(d) // dispatch to base copy constructor
        , additional_(d.additional_)
    {
    }

    Derived& operator=(const Derived& d)
    {
        Base::operator=(d);
        additional_ = d.additional_;
        return *this;
    }
};
有趣的是,即使您没有显式定义这些函数(它使用编译器生成的函数),这也可以正常工作。
class ImplicitBase { 
    int value_; 
    // No operator=() defined
};

class Derived : public ImplicitBase {
    const char* name_;
public:
    Derived& operator=(const Derived& d)
    {
         ImplicitBase::operator=(d); // Call compiler generated operator=
         name_ = strdup(d.name_);
         return *this;
    }
};  

2
@CravingSpirit 这是一个拷贝构造函数(省略了参数名)。 - Motti
2
@CravingSpirit 他们在不同的情境中使用,这是基础的C++。我建议你多读一些相关资料。 - Motti
1
@qed 复制构造函数用于初始化,而赋值运算符用于赋值表达式。 - Bin
@Motti 1. 我们可以通过派生类的任何成员函数显式调用基类的构造函数,例如 base::base(),实际上会创建一个临时对象,并在语句结束时销毁。同样地,我们也可以重载=运算符。2. 但是我们不能通过派生类的成员函数显式调用复制构造函数,我们只能通过将一个基类对象复制到另一个基类对象来间接调用它,例如在成员函数中使用 base b2=b1。如果我尝试像这样做 base::base(b1),会出现错误。 - Abhishek Mane
1
@MackieMesser这里只有在处理模板(特别是转发引用)时才需要使用std::forward(),而这个情况并非如此。无论如何,这个答案是在2009年之前的,也就是在C++11和std::forward()出现之前。(参见https://en.cppreference.com/w/cpp/utility/forward) - Motti
显示剩余4条评论

20

简短回答:是的,你需要在D中重复这项工作。

长话短说:

如果您的派生类'D'不包含任何新成员变量,则默认版本(由编译器生成)应该可以正常工作。默认的复制构造函数将调用父级复制构造函数,而默认的赋值运算符将调用父级赋值运算符。

但是,如果您的类'D'包含资源,则需要进行一些工作。

我认为您的复制构造函数有些奇怪:

B(const B& b){(*this) = b;}

D(const D& d){(*this) = d;}

通常,复制构造函数是按照从基类到派生类的顺序链式调用的。但由于您调用了赋值运算符,因此复制构造函数必须先调用默认构造函数以从下向上进行对象的默认初始化,然后再使用赋值运算符向下操作。这似乎效率不高。

现在,如果你执行一个赋值操作,你就会从底部向上(或从顶部向下)复制,但很难为你提供强异常保证。如果在任何时候资源无法复制并且您抛出异常,则该对象将处于不确定状态(这是一件坏事)。

通常我看到的方式正好相反。赋值运算符是根据复制构造函数和交换函数定义的。这是因为这样做可以更容易地提供强异常保证。我认为通过这种方式可能无法提供强保证(我可能错了)。

class X
{
    // If your class has no resources then use the default version.
    // Dynamically allocated memory is a resource.
    // If any members have a constructor that throws then you will need to
    // write your owen version of these to make it exception safe.


    X(X const& copy)
      // Do most of the work here in the initializer list
    { /* Do some Work Here */}

    X& operator=(X const& copy)
    {
        X tmp(copy);      // All resource all allocation happens here.
                          // If this fails the copy will throw an exception 
                          // and 'this' object is unaffected by the exception.
        swap(tmp);
        return *this;
    }
    // swap is usually trivial to implement
    // and you should easily be able to provide the no-throw guarantee.
    void swap(X& s) throws()
    {
        /* Swap all members */
    }
};

即使你从X派生了一个类D,也不会影响这种模式。
诚然,你需要重复一些工作,通过显式调用基类来完成,但这相对来说是小菜一碟。

class D: public X
{

    // Note:
    // If D contains no members and only a new version of foo()
    // Then the default version of these will work fine.

    D(D const& copy)
      :X(copy)  // Chain X's copy constructor
      // Do most of D's work here in the initializer list
    { /* More here */}



    D& operator=(D const& copy)
    {
        D tmp(copy);      // All resource all allocation happens here.
                          // If this fails the copy will throw an exception 
                          // and 'this' object is unaffected by the exception.
        swap(tmp);
        return *this;
    }
    // swap is usually trivial to implement
    // and you should easily be able to provide the no-throw guarantee.
    void swap(D& s) throws()
    {
        X::swap(s); // swap the base class members
        /* Swap all D members */
    }
};

1
您可以为用户定义的类型专门化std中的标准算法。dribeas的代码是有效的,只是大师们似乎推荐ADL解决方案。 - Steve Jessop
1
资源:指你获得的某些东西,但必须(应该)显式地归还。例如:内存/文件描述符/打开的连接/锁等。 - Martin York
1
@AbhishekMane 如果你的类包含一个资源(需要归还的东西),那么你需要有一个析构函数来归还它。如果你有一个析构函数,那么默认的复制构造函数和赋值运算符将不起作用(你需要进行深拷贝)。这被称为“三大法则”。如果你定义了其中任何一个(析构函数、复制构造函数或赋值运算符),那么你必须定义所有三个。请搜索“三大法则”。 - Martin York
1
@AbhishekMane 资源示例:动态分配内存:new int(5);类型 int 不是资源。类型 std::string 不是资源;尽管它可能在内部动态分配内存,但这是私有的(您不知道也不需要知道)。类 std::string 已经实现了适当的 CC O=O 析构函数等,因此它会自动和透明地处理所有这些。您可以像处理简单对象(如 int)一样处理它,因为它已经正确地实现了五个规则。 - Martin York
1
@AbhishekMane:https://gist.github.com/Loki-Astari/456ba81de186f923d709129f2968bb11 - Martin York
显示剩余13条评论

3
您的设计可能存在缺陷(提示:切片实体语义值语义)。通常,并不需要从多态层次结构中获取对象的完整副本/ < em>值语义。如果您想提供它以防万一将来会用到它,那么意味着您永远不会需要它。相反,请使基类不可复制(例如通过继承自boost :: noncopyable),这就是全部解决方案。

当确实出现这种需求时,唯一正确的解决方案是使用信封-信件惯用法或Sean Parent和Alexander Stepanov在《正式对象》文章中介绍的小型框架。所有其他解决方案都会在切片和/或LSP方面给您带来麻烦。

有关此主题,请参见C ++ Core Reference C.67:C.67:基类应禁止复制,如果需要“复制”,则应提供虚拟克隆


2

你需要重新定义所有非默认构造函数拷贝构造函数。你不需要重新定义拷贝构造函数和赋值运算符,因为标准提供的这些函数会调用基类的所有版本:

struct base
{
   base() { std::cout << "base()" << std::endl; }
   base( base const & ) { std::cout << "base(base const &)" << std::endl; }
   base& operator=( base const & ) { std::cout << "base::=" << std::endl; }
};
struct derived : public base
{
   // compiler will generate:
   // derived() : base() {}
   // derived( derived const & d ) : base( d ) {}
   // derived& operator=( derived const & rhs ) {
   //    base::operator=( rhs );
   //    return *this;
   // }
};
int main()
{
   derived d1;      // will printout base()
   derived d2 = d1; // will printout base(base const &)
   d2 = d1;         // will printout base::=
}

请注意,正如sbi所指出的那样,如果您定义了任何构造函数,编译器将不会为您生成默认构造函数,包括复制构造函数。

1
请注意,如果定义了任何其他构造函数(包括复制构造函数),编译器将不会提供默认构造函数。因此,如果您希望derived具有默认构造函数,则需要显式定义一个。 - sbi
正如sbi所指出的,如果您定义了任何构造函数,编译器不会是“任何构造函数”,而是“任何复制构造函数”。 - Abhishek Mane

0

原始代码有误:

class B
{
public:
    B(const B& b){(*this) = b;} // copy constructor in function of the copy assignment
    B& operator= (const B& b); // copy assignment
 private:
// private member variables and functions
};

通常情况下,你不能用复制赋值运算符来定义复制构造函数,因为复制赋值运算符必须释放资源,而复制构造函数则不需要!!!
为了理解这一点,请考虑:
class B
{
public:
    B(Other& ot) : ot_p(new Other(ot)) {}
    B(const B& b) {ot_p = new  Other(*b.ot_p);}
    B& operator= (const B& b);
private:
    Other* ot_p;
};

为避免内存泄漏,复制赋值首先必须删除 ot_p 指向的内存:
B::B& operator= (const B& b)
{
    delete(ot_p); // <-- This line is the difference between copy constructor and assignment.
    ot_p = new  Other(*b.ot_p);
}
void f(Other& ot, B& b)
{
    B b1(ot); // Here b1 is constructed requesting memory with  new
    b1 = b; // The internal memory used in b1.op_t MUST be deleted first !!!
}

因此,拷贝构造函数和拷贝赋值操作符是不同的,因为前者在初始化内存时构造了一个对象,而后者在构造新对象之前必须先释放已有的内存。

如果您按照本文最初的建议进行操作:

B(const B& b){(*this) = b;} // copy constructor

你将会删除一个不存在的内存。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接