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

How to use base class's constructors and assignment operator in C++?

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

这里是:

1
2
3
4
5
6
7
8
9
10
11
12
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相同的一组构造函数,包括复制构造函数和赋值运算符:

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

我是否必须在D中重写所有这些代码,或者是否有方法使用B的构造函数和运算符?我特别想避免重写赋值运算符,因为它必须访问B的所有私有成员变量。


可以显式调用构造函数和赋值运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
    }
};

有趣的是,即使您没有显式地定义这些函数(然后它使用编译器生成的函数),它也能工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
    }
};


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

长回答:

如果派生类"d"不包含新的成员变量,则默认版本(由编译器生成)可以正常工作。默认复制构造函数将调用父复制构造函数,默认分配运算符将调用父分配运算符。

但如果类"d"包含资源,则需要做一些工作。

我觉得你的复制构造函数有点奇怪:

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

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

通常复制构造函数链,以便从基部向上复制构造它们。这里,因为您要调用赋值运算符,所以复制构造函数必须调用默认的构造函数来默认地首先从下向上初始化对象。然后使用赋值运算符再次向下。这似乎相当低效。

现在,如果你做了一个任务,你是自下而上(或自上而下)复制,但似乎很难做到这一点,并提供强有力的例外保证。如果在任何时候资源复制失败,并且抛出异常,则对象将处于不确定状态(这是一件坏事)。

通常情况下,我看到的情况是相反的。赋值运算符是根据复制构造函数和交换来定义的。这是因为它更容易提供强有力的例外保证。我认为你这样做不能提供有力的保证(我可能是错的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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,这也不会影响这个模式。诚然,您需要通过对基类进行显式调用来重复一些工作,但这相对来说是微不足道的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 */
    }
};


您的设计很可能有缺陷(提示:切片、实体语义和值语义)。对于来自多态层次结构的对象具有完整的复制/值语义通常根本不需要。如果你想提供它,以防以后有人需要它,这意味着你永远不需要它。将基类改为不可复制(例如,通过继承boost::non copyable),仅此而已。

当这种需求真正出现时,唯一正确的解决方案是信封字母习语,或者SeanParent和AlexanderStepanovIIRC关于常规对象的文章中的小框架。所有其他解决方案都会给切片和/或LSP带来麻烦。

关于这个主题,也请参阅C++ CaleRelay.C.67:C.67:一个基类应该抑制复制,并提供一个虚拟克隆来代替"复制"。


原始代码错误:

1
2
3
4
5
6
7
8
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
};

一般来说,不能根据复制分配定义复制构造函数,因为复制分配必须释放资源,而复制构造函数不能!!!!

要理解这一点,请考虑:

1
2
3
4
5
6
7
8
9
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指向的内存:

1
2
3
4
5
6
7
8
9
10
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 !!!
}

因此,复制构造函数和复制分配是不同的,因为前者的构造和对象进入了一个初始化的内存,后者必须在构建新对象之前先释放现有的内存。

如果您执行本文最初建议的操作:

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

您将删除不存在的内存。


必须重新定义所有不是默认构造函数或复制构造函数的构造函数。您不需要重新定义复制构造函数或赋值运算符,因为编译器提供的那些(根据标准)将调用所有基本版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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所指出的,如果您定义了任何构造函数,编译器将不会为您生成默认的构造函数,其中包括复制构造函数。