- 构造函数和复制控制
每个派生类对象由派生类中定义的(非 static)成员加上一个或多个基类子对象构成,当我们构造、复制、赋值和撤销一个派生类对象时,也会构造、复制、赋值和撤销这些基类子对象。
构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。//而不是靠继承
- 基类构造函数和复制控制
继承对基类构造函数的唯一影响是,在确定提供哪些构造函数时,必须考虑一类新用户。像任意其他成员一样,构造函数可以为 protected 或 private,某些类需要只希望派生类使用的特殊构造函数,这样的构造函数应定义为 protected。
- 派生类的构造函数
派生类的构造函数受继承关系的影响,每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。
派生类的合成默认构造函数与非派生的构造函数只有一点不同:除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。
对于 Bulk_item 类,合成的默认构造函数会这样执行:调用 Item_base 的默认构造函数,将 isbn 成员初始化空串,将 price 成员初始化为 0;用常规变量初始化规则初始化 Bulk_item 的成员,也就是说,qty 和 discount 成员会是未初始化的。
定义默认构造函数:因为 Bulk_item 具有内置类型成员,所以应定义自己的默认构造函数:
class Bulk_item : public Item_base { public: Bulk_item(): min_qty(0), discount(0.0) { } // as before };
这个构造函数使用构造函数初始化列表初始化 min_qty 和 discount 成员,该构造函数还隐式调用 Item_base 的默认构造函数初始化对象的基类部分。运行这个构造函数的效果是,首先使用 Item_base 的默认构造函数初始化 Item_base 部分,那个构造函数将 isbn 置为空串并将 price 置为 0。Item_base 的构造函数执行完毕后,再初始化 Bulk_item 部分的成员并执行构造函数的函数体(函数体为空)。
- 向基类构造函数传递实参
除了默认构造函数之外,Item_base 类还使用户能够初始化 isbn 和 price 成员,我们希望支持同样 Bulk_item 对象的初始化,事实上,我们希望用户能够指定整个 Bulk_item 的值,包括折扣率和数量。//既初始化派生类对象的基类部分又初始化派生类对象的派生部分
派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。相反派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员。
class Bulk_item : public Item_base { public: Bulk_item(const std::string& book, double sales_price, std::size_t qty = 0, double disc_rate = 0.0): Item_base(book, sales_price), min_qty(qty), discount(disc_rate) { } // as before };
这个构造函数使用有两个形参的 Item_base 构造函数初始化基类子对象,它将自己的 book 和 sales_price 实参传递给该构造函数。这个构造函数可以这样使用:
// arguments are the isbn, price, minimum quantity, and discount Bulk_item bulk("0-201-82470-1", 50, 5, .19);
要建立 bulk,首先运行 Item_base 构造函数,该构造函数使用从 Bulk_item 构造函数初始化列表传来的实参初始化 isbn 和 price。Item_base 构造函数执行完毕之后,再初始化 Bulk_item 的成员。最后,运行 Bulk_item 构造函数的(空)函数体。构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。
举例:
#include <iostream> #include <string> // Item sold at an undiscounted price // derived classes will define various discount strategies class Item_base { public: Item_base(const std::string &book = "", double sales_price = 0.0) : isbn(book), price(sales_price) { std::cout << price << std::endl; std::cout << "constructor of Item_base" << std::endl; } std::string book() const { return isbn; } // returns total sales price for a specified number of items // derived classes will override and apply different discount algorithms virtual double net_price(std::size_t n) const { return n * price; } virtual ~Item_base() { } private: std::string isbn; // identifier for the item protected: double price; // normal, undiscounted price }; class Bulk_item : public Item_base { public: Bulk_item() :Item_base("isbn-1", 49.9), min_qty(10), discount(0.8){ std::cout << price << std::endl; std::cout << "default constructor of Bulk_item" << std::endl; } // redefines base version so as to implement bulk purchase discount policy double net_price(std::size_t) const; private: std::size_t min_qty; // minimum purchase for discount to apply double discount; // fractional discount to apply }; double Bulk_item::net_price(size_t cnt) const { if (cnt >= min_qty) return cnt * (1 - discount) * price; else return cnt * price;//price是public } int main() { Bulk_item bi; system("pause"); return 0; }
输出:
因为这里派生类给基类传递了初始化值,所以覆盖了基类中的默认初始化值。
- 在派生类构造函数中使用默认实参
当然,也可以将这两个 Bulk_item 构造函数编写为一个接受默认实参的构造函数:
class Bulk_item : public Item_base { public: Bulk_item(const std::string& book, double sales_price, std::size_t qty = 0, double disc_rate = 0.0): Item_base(book, sales_price), min_qty(qty), discount(disc_rate) { } // as before };
这里为每个形参提供了默认值,因此,可以用 0 至 4 个实参使用该构造函数。
- 只能初始化直接基类
一个类只能初始化自己的直接基类。直接就是在派生列表中指定的类。如果类 C 从类 B 派生,类 B 从类 A 派生,则 B 是 C 的直接基类。虽然每个 C 类对象包含一个 A 类部分,但 C 的构造函数不能直接初始化 A 部分。相反,需要类 C 初始化类 B,而类 B 的构造函数再初始化类 A。这一限制的原因是,类 B 的作者已经指定了怎样构造和初始化 B 类型的对象//即指定了如何初始化类型A。像类 B 的任何用户一样,类 C 的作者无权改变这个规约。
作为更具体的例子,书店可以有几种折扣策略。除了批量折扣外,还可以为购买某个数量打折,此后按全价销售,或者,购买量超过一定限度的可以打折,在该限度之内不打折。这些折扣策略都需要一个数量和一个折扣量,可以定义名为 Disc_item 的新类存储数量和折扣量,以支持这些不同的折扣策略。Disc_item 类可以不定义 net_price 函数,但可以作为定义不同折扣策略的其他类(如 Bulk_item 类)的基类。
- 关键概念:重构
将 Disc_item 加到 Item_base 层次是重构(refactoring)的一个例子。重构包括重新定义类层次,将操作和/或数据从一个类移到另一个类。为了适应应用程序的需要而重新设计类以便增加新函数或处理其他改变时,最有可能需要进行重构。重构常见在面向对象应用程序中非常常见。值得注意的是,虽然改变了继承层次,使用 Bulk_item 类或 Item_base 类的代码不需要改变。然而,对类进行重构,或以任意其他方式改变类,使用这些类的任意代码都必须重新编译。
要实现这个设计,首先需要定义 Disc_item 类:
// class to hold discount rate and quantity // derived classes will implement pricing strategies using these data class Disc_item : public Item_base { public: Disc_item(const std::string& book = "", double sales_price = 0.0, std::size_t qty = 0, double disc_rate = 0.0): Item_base(book, sales_price), quantity(qty), discount(disc_rate) { } protected: std::size_t quantity; // purchase size for discount to apply double discount; // fractional discount to apply };
这个类继承 Item_base 类并定义了自己的 discount 和 quantity 成员。它唯一的成员函数是构造函数,用以初始化基类和 Disc_item 定义的成员。其次,可以重新实现 Bulk_item 以继承 Disc_item,而不再直接继承 Item_base:
// discount kicks in when a specified number of copies of same book are sold // the discount is expressed as a fraction to use to reduce the normal price class Bulk_item : public Disc_item { public: Bulk_item(const std::string& book = "", double sales_price = 0.0, std::size_t qty = 0, double disc_rate = 0.0): Disc_item(book, sales_price, qty, disc_rate) { } // redefines base version so as to implement bulk purchase discount policy double net_price(std::size_t) const; };
Bulk_item 类现在有一个直接基类 Disc_item,还有一个间接基类 Item_base。每个 Bulk_item 对象有三个子对象:一个(空的)Bulk_item 部分和一个 Disc_item 子对象,Disc_item 子对象又有一个 Item_base 基类子对象。虽然 Bulk_item 没有自己的数据成员,但为获取值用来初始化其继承成员,它定义了一个构造函数。
- 关键概念:尊重基类接口
构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口。定义 Disc_item 时,通过定义它的构造函数指定了怎样初始化 Disc_item 对象。一旦类定义了自己的接口,与该类对象的所有交互都应该通过该接口,即使对象是派生类对象的一部分也不例外//成员对象或者基类。同样,派生类构造函数不能初始化基类的成员且不应该对基类成员赋值。如果那些成员为 public 或 protected,派生构造函数可以在构造函数函数体中给基类成员赋值,但是,这样做会违反基类的接口。派生类应通过使用基类构造函数尊重基类的初始化意图,而不是在派生类构造函数函数体中对这些成员赋值。//在派生类初始化列表中调用一个基类的构造函数来为基类初始化,这样基类就会按照自己的原意去初始化自身了;如果派生类构造函数在函数体中逐一初始化基类的成员,那么也许会有没有做到的部分。
- 复制控制和继承
合成操作对对象的基类部分连同派生部分的成员一起进行复制、赋值或撤销,使用基类的复制构造函数、赋值操作符或析构函数对基类部分进行复制、赋值或撤销。类是否需要定义复制控制成员完全取决于类自身的直接成员。基类可以定义自己的复制控制而派生类使用合成版本,反之亦然。只包含类类型或内置类型数据成员、不含指针的类一般可以使用合成操作,复制、赋值或撤销这样的成员不需要特殊控制。具有指针成员的类一般需要定义自己的复制控制来管理这些成员。Item_base 类及其派生类可以使用复制控制操作的合成版本。复制 Bulk_item 对象时,调用(合成的)Item_base 复制构造函数复制 isbn 和 price 成员。使用 string 复制构造函数复制 isbn,直接复制 price 成员。一旦复制了基类部分,就复制派生部分。Bulk_item 的两个成员都是 double 型,直接复制这些成员。赋值操作符和析构函数类似处理。
- 定义派生类复制构造函数
如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值。如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分:
class Base { /* ... */ }; class Derived: public Base { public: // Base::Base(const Base&) not invoked automatically Derived(const Derived& d): Base(d) /* other member initialization */ { /*... */ } };
初始化函数 Base(d) 将派生类对象 d 转换为它的基类部分的引用,并调用基类复制构造函数。如果省略基类初始化函数,如下代码:
// probably incorrect definition of the Derived copy constructor Derived(const Derived& d) /* derived member initizations */ {/* ... */ }
效果是运行 Base 的默认构造函数初始化对象的基类部分。假定 Derived 成员的初始化从 d 复制对应成员,则新构造的对象将具有奇怪的配置:它的 Base 部分将保存默认值,而它的 Derived 成员是另一对象的副本。
举例:
class Base { public: Base(int n = 5,std::string s="default str"):base_num(n),base_str(s){ std::cout << "Constructor of Base" << std::endl; } Base(const Base &b){ std::cout << "Copy constructor of Base" << std::endl; } private: int base_num; std::string base_str; }; class Derived : public Base { public: // Base::Base(const Base&) not invoked automatically Derived(int d = 0) :derived_num(d){ std::cout << "Constructor of Derived" << std::endl; } Derived(const Derived& d) : Base(d) /* other member initialization */ { /*... */ std::cout << "Copy constructor of Drived" << std::endl; } private: int derived_num; }; int main() { //Bulk_item bi; Derived d(3); Derived d2 = d; system("pause"); return 0; }
输出:
如果这里改成:
输出结果为:
//没有显示使用基类的复制构造函数,调用的就是基类的构造函数对基类对象部分进行初始化
- 派生类赋值操作符
赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。
// Base::operator=(const Base&) not invoked automatically Derived &Derived::operator=(const Derived &rhs) { if (this != &rhs) { Base::operator=(rhs); // assigns the base part // do whatever needed to clean up the old value in the derived part // assign the members from the derived } return *this; }
赋值操作符必须防止自身赋值。假定左右操作数不同,则调用 Base 类的赋值操作符给基类部分赋值。该操作符可以由类定义,也可以是合成赋值操作符,这没什么关系——我们可以直接调用它。基类操作符将释放左操作数中基类部分的值,并赋以来自 rhs 的新值。该操作符执行完毕后,接着要做的是为派生类中的成员赋值。
现在来看这个例子:
class Base { public: Base(int n = 5,std::string s="default str"):base_num(n),base_str(s){ std::cout << "Constructor of Base" << std::endl; } Base(const Base &b){ std::cout << "Copy constructor of Base" << std::endl; } void print_base(){ std::cout << "base_str:" << base_str << " and " << "base_num:" << base_num << std::endl; } private: int base_num; std::string base_str; }; class Derived : public Base { public: // Base::Base(const Base&) not invoked automatically Derived(int d = 0,int n=0,std::string s="") :Base(n,s),derived_num(d){ std::cout << "Constructor of Derived" << std::endl; } Derived(const Derived& d) /*Base(d) *//* other member initialization */ { /*... */ std::cout << "Copy constructor of Drived" << std::endl; } Derived &operator=(const Derived &rhs); private: int derived_num; }; Derived &Derived::operator=(const Derived &rhs) { std::cout << "Operator of Derived" << std::endl; if (this != &rhs) { //Base::operator=(rhs); // assigns the base part // do whatever needed to clean up the old value in the derived part // assign the members from the derived } return *this; }
如果没有显示的赋值,则输出结果为:
相反,改为显示赋值:
则输出为:
- 派生类的析构函数
析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员:
class Derived: public Base { public: // Base::~Base invoked automatically ~Derived() { /* do what it takes to clean up derived members */ } };
对象的撤销顺序与构造顺序相反:首先运行派生析构函数,然后按继承层次依次向上调用各基类析构函数。
- 虚析构函数
自动调用基类部分的析构函数对基类的设计有重要影响。删除指向动态分配对象的指针时,需要运行析构函数在释放对象的内存之前清除对象。处理继承层次中的对象时,指针的静态类型(指针的类型可能是基类) 可能与被删除对象的动态类型(基类指针可能指向派生类)不同,可能会删除实际指向派生类对象的基类类型指针。
如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为:
比如基类中有析构函数:
运行:
输出:
正常析构。
如果改为:
输出结果表明没有调用派生类对象的析构函数:
如果添加virtual:
则会先调用派生类对象的析构函数,再调用基类对象的析构函数:
这说明,如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同:
Item_base *itemP = new Item_base; // same static and dynamic type delete itemP; // ok: destructor for Item_base called itemP = new Bulk_item; // ok: static and dynamic types differ delete itemP; // ok: destructor for Bulk_item called
像其他虚函数一样,析构函数的虚函数性质都将继承。因此,如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。
基类析构函数是三法则的一个重要例外。三法则指出,如果类需要析构函数,则类几乎也确实需要其他复制控制成员。基类几乎总是需要构造函数,从而可以将析构函数设为虚函数。如果基类为了将析构函数设为虚函数则具有空析构函数,那么,类具有析构函数并不表示也需要赋值操作符或复制构造函数。
即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
- 构造函数和赋值操作符不是虚函数
在复制控制成员中,只有析构函数应定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。//构造函数在对象完全构造前运行,而动态绑定是在运行时实现
虽然可以在基类中将成员函数 operator= 定义为虚函数,但这样做并不影响派生类中使用的赋值操作符。每个类有自己的赋值操作符,派生类中的赋值操作符有一个与类本身类型相同的形参,该类型必须不同于继承层次中任意其他类的赋值操作符的形参类型。
将赋值操作符设为虚函数可能会令人混淆,因为虚函数必须在基类和派生类中具有同样的形参。基类赋值操作符有一个形参是自身类类型的引用,如果该操作符为虚函数,则每个类都将得到一个虚函数成员,该成员定义了参数为一个基类对象的 operator=。//这句话想要说的是,虚函数在派生类和基类中的形参是一样的,而实际上赋值操作符的形参又是自身类型的引用,如果操作符是虚函数,那么派生类中的赋值操作符的参数就是基类对象的引用了 但是,对派生类而言,这个操作符与赋值操作符是不同的。//派生类赋值操作符的参数不能是基类类型的引用
总之,将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。
- 构造函数和析构函数中的虚函数
构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个派生类对象。撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。在这两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。为了适应这种不完整,编译器将对象的类型视为在构造或析构期间发生了变化。在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。
构造或析构期间的对象类型对虚函数的绑定有影响。如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。//而不是其派生类中的对应的函数 无论由构造函数(或析构函数)直接调用虚函数,或者从构造函数(或析构函数)所调用的函数间接调用虚函数,都应用这种绑定。
要理解这种行为,考虑如果从基类构造函数(或析构函数)调用虚函数的派生类版本会怎么样。虚函数的派生类版本很可能会访问派生类对象的成员,毕竟,如果派生类版本不需要使用派生类对象的成员,派生类多半能够使用基类中的定义。但是,对象的派生部分的成员不会在基类构造函数运行期间初始化,实际上,如果允许这样的访问,程序很可能会崩溃。