Ch15 OOP
基类和派生类的定义
定义基类
考虑书上的例子, 定义一个 Quote 基类:
class Quote {
public:
Quote() = default; //= default see § 7.1.4 (p. 264), 默认构造函数
Quote(const std::string &book, double sales_price):
bookNo(book), price(sales_price) { }
std::string isbn() const { return bookNo; }
//returns the total sales price for the 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 ~Quote() = default; //dynamic binding for the destructor, 虚析构函数
private:
std::string bookNo; //ISBN number of this item
protected:
double price = 0.0; //normal, undiscounted price
};
一般建议每个基类都应定义一个虚析构函数.
C++ 要求区分在派生类中实现不变的函数和要改变的函数, 这种函数在基类声明中应加 virtual 关键字, 称为虚函数. 在派生类中要对虚函数加 override 关键字, 此时函数的参数将根据不同的类型动态绑定. 所有非 static 成员函数, 除了构造函数, 都可以成为虚函数.
派生类不能使用基类中的 private 成员, 可以使用 public 和 protected 成员, 而 protected 成员和 private 成员一样不能被类外部访问.
定义派生类
派生类在名字后带上类继承列表, 指明继承的类. 基类名字前可以带上 public, private 或 protected, 用来决定用户是否允许知道此类是从基类继承的. 如果没有则默认为 private 继承.
class Bulk_quote : public Quote { //Bulk_quote inherits from Quote
Bulk_quote() = default;
Bulk_quote(const std::string&, double, std::size_t, double);
//overrides the base version in order to implement the bulk purchase discount policy
double net_price(std::size_t) const override;
private:
std::size_t min_qty = 0; //minimum purchase for the discount to apply
double discount = 0.0; //fractional discount to apply
};
在此处使用 public, 意味着公有继承. 特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
派生类中含有基类中的成员作为其子对象. 对基类的指针和引用可以用派生类来绑定, 此时将执行隐式的派生类到基类转换.
Quote item; //object of base type
Bulk_quote bulk; //object of derived type
Quote *p = &item; //p points to a Quote object
p = &bulk; //p points to the Quote part of bulk
Quote &r = bulk; //r bound to the Quote part of bulk
"RESPECTING THE BASE-CLASS INTERFACE" 意味着即使我们可以在派生类的构造函数中直接初始化基类成员, 但我们要尊重基类的接口. 派生类成员的初始化仍应使用基类的构造函数:
Bulk_quote(const std::string& book, double p,
std::size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) { }
执行顺序是先执行基类的构造函数, 之后按照声明顺序逐一初始化剩下的成员. (注意构造函数先执行初始化列表再执行大括号里的代码)
派生类可以直接使用从基类中继承的成员, 使用方式与派生类自己定义的成员毫无差别.
如果基类中有静态成员, 则派生类中的不会创造新的静态实例, 静态实例自始至终只有一个.
class Base {
public:
static void statmem();
};
class Derived : public Base {
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj)
{
Base::statmem();//ok: Base defines statmem
Derived::statmem(); //ok: Derived inherits statmem
//ok: derived objects can be used to access static from base
derived_obj.statmem();//accessed through a Derived object
statmem();//accessed through this object
}
在一个继承链中, 最底层的类会将其所有的直接派生类和间接派生类作为子对象.
class Base { /*. . .*/ };
class D1: public Base { /*. . .*/ };
class D2: public D1 { /*. . .*/ };
用 final 关键字阻止一个类被其他类继承:
class NoDerived final { /**/ }; //NoDerived can’t be a base class
class Base { /**/ };//Last is final; we cannot inherit from Last
class Last final : Base { /**/ }; //Last can’t be a base class
class Bad : NoDerived { /**/ };//error: NoDerived is final
class Bad2 : Last { /**/ };//error: Last is final
转换和继承
对于一个类的指针或引用, 它们的静态类型和动态类型通常不同. 静态类型在编译期间即可判断, 而动态类型只能在执行过程中判断.
double ret = item.net_price(n);
//item 静态类型为 Quote&, 动态类型根据执行时绑定对象的类型可能为 Bulk_Quote& 或 Quote&
基类不能转换为派生类, 即使是动态类型为派生类的基类指针也不能绑定到派生类上:
Quote base;
Bulk_quote* bulkP = &base;//error: can’t convert base to derived
Bulk_quote& bulkRef = base; //error: can’t convert base to derived
Bulk_quote bulk;
Quote *itemP = &bulk;//ok: dynamic type is Bulk_quote
Bulk_quote *bulkP = itemP;//error: can’t convert base to derived
可以将派生类传递给基类的复制/移动操作 (即构造函数或 operator= 函数), 此时不存在基类中的成员将会被切除. (这个过程称为向下转型)
Bulk_quote bulk; //object of derived type
Quote item(bulk); //uses the Quote::Quote(const Quote&) constructor
item = bulk; //calls Quote::operator=(const Quote&)
虚函数
虚函数必须被定义, 以提示编译器.
派生类 override 的虚函数需要与基类有相同的形参和返回值. 一个例外是返回值也可以在派生类到基类转换可用的前提下返回派生类 (的基类子对象) 的指针或引用.
也可用 final 关键字阻止函数被派生类 override:
struct D2 : B {
//inherits f2() and f3() from B and overrides f1(int)
void f1(int) const final; //subsequent classes can’t override f1(int)
};
struct D3 : D2 {
void f2(); //ok: overrides f2 inherited from the indirect base, B
void f1(int) const; //error: D2 declared f2 as final
};
如果虚函数中有默认参数, 则应保证基类和派生类用相同的参数值.
使用类作用域操作符来绕过虚函数机制, 在派生类中调用基类的虚函数:
//calls the version from the base class regardless of the dynamic type of baseP
double undiscounted = baseP->Quote::net_price(42);
抽象基类
纯虚函数的意义在于让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,"你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它"。
通过在函数声明结束后加上 =0 来表明它是一个纯虚函数.
//class to hold the discount rate and quantity
//derived classes will implement pricing strategies using these data
class Disc_quote : public Quote {
public:
Disc_quote() = default;
Disc_quote(const std::string& book, double price,
std::size_t qty, double disc):
Quote(book, price),
quantity(qty), discount(disc) { }
double net_price(std::size_t) const = 0;
protected:
std::size_t quantity = 0; //purchase size for the discount to apply
double discount = 0.0; //fractional discount to apply
};
纯虚函数可能在类外部定义, 但不能在类内部定义.
有纯虚函数的类称为抽象基类, 从抽象基类中继承的类却没有 override 纯虚函数也是抽象基类.
抽象类的派生类的构造函数需要用到其直接继承类的构造函数, 并通过继承链到最基类的构造函数:
//the discount kicks in when a specified number of copies of the same book are sold
//the discount is expressed as a fraction to use to reduce the normal price
class Bulk_quote : public Disc_quote {
public:
Bulk_quote() = default;
Bulk_quote(const std::string& book, double price,
std::size_t qty, double disc):
Disc_quote(book, price, qty, disc) { }
//overrides the base version to implement the bulk purchase discount policy
double net_price(std::size_t) const override;
};
访问控制与继承
派生类成员或友元只能通过派生对象访问基类的受保护成员。派生类对基类对象的受保护成员没有特殊访问权:
class Base {
protected:
int prot_mem;//protected member
};
class Sneaky : public Base {
friend void clobber(Sneaky&);//can access Sneaky::prot_mem
friend void clobber(Base&);//can’t access Base::prot_mem
int j;//j is private by default
};
//ok: clobber can access the private and protected members in Sneaky objects
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
//error: clobber can’t access the protected members in Base
void clobber(Base &b) { b.prot_mem = 0; }
public, private 和 protected 继承的区别: https://blog.51cto.com/u_15290941/3048818.
如果基类的公共成员可以访问, 那么派生类到基类的转换也可以访问, 反之不可访问.
友元没有继承性, 即基类的友元不能访问派生类的成员, 派生类的成员也不能访问基类的友元:
class Base {
//added friend declaration; other members as before
friend class Pal; //Pal has no access to classes derived from Base
};
class Pal {
public:
int f(Base b) { return b.prot_mem; } //ok: Pal is a friend of Base
int f2(Sneaky s) { return s.j; } //error: Pal not friend of Sneaky
//access to a base class is controlled by the base class, even inside a derived object
int f3(Sneaky s) { return s.prot_mem; } //ok: Pal is a friend
};
f3 是合法的是因为 Pal 是 Base 的友元, 意味着它可以访问 Base 的成员以及嵌入 Base 的派生类之中的子对象.
下述例子是为了说明友元没有继承性:
//D2 has no access to protected or private members in Base
class D2 : public Pal {
public:
int mem(Base b)
{ return b.prot_mem; } //error: friendship doesn’t inherit
};
用 using 改变派生类的成员访问权限等级:
class Base {
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived : private Base { //note: private inheritance
public:
//maintain access levels for members related to the size of the object
using Base::size;
protected:
using Base::n;
};
using 改变后的权限等级与 using 之前的访问标识符相关, 如果写在 public 的对应区域内, 则改变为 public, 依此类比 protected 与 private.
继承下的类作用域
派生类的作用域嵌套在基类的作用域中, 名称查找在编译期间就已完成, 因此名称查找只与成员的静态类型有关:
class Disc_quote : public Quote {
public:
std::pair<size_t, double> discount_policy() const
{ return {quantity, discount}; }
//other members as before
};
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk; //static and dynamic types are the same
Quote *itemP = &bulk; //static and dynamic types differ
bulkP->discount_policy(); //ok: bulkP has type Bulk_quote*
itemP->discount_policy(); //error: itemP has type Quote*
名称查找发生在类型检查之前, 因此派生类的函数会将基类的同名函数隐藏, 即使两个函数的形参不同:
struct Base {
int memfcn();
};
struct Derived : Base {
int memfcn(int); //hides memfcn in the base
};
Derived d; Base b;
b.memfcn();//calls Base::memfcn
d.memfcn(10); //calls Derived::memfcn
d.memfcn(); //error: memfcn with no arguments is hidden
d.Base::memfcn(); //ok: calls Base::memfcn
这也是为什么虚函数在基类和派生类中必须得有相同的形参列表.
构造函数与拷贝控制 (坑)
虚析构函数
析构函数一般定义为虚函数以便在 delete 指向派生类的基类指针时可以顺利完成.
容器与继承
坑. 这东西有用吗?
多态与虚继承
多态继承
多态即从多个基类继承, 在派生列表中填入多个基类的名字:
class Bear : public ZooAnimal { /*. . .*/ };
class Panda : public Bear, public Endangered { /*. . .*/ };
派生类的构造函数需要分别调用其直接派生的类的构造函数:
//explicitly initialize both base classes
Panda::Panda(std::string name, bool onExhibit)
: Bear(name, onExhibit, "Panda"),
Endangered(Endangered::critical) { }
//implicitly uses the Bear default constructor to initialize the Bear subobject
Panda::Panda(): Endangered(Endangered::critical) { }
构造函数的执行顺序为派生列表的执行顺序, 与构造函数初始化列表顺序无关.
派生类可以继承多个类的构造函数, 但不能从多个类中继承相同的构造函数 (i.e. 形参列表相同的函数):
struct Base1 {
Base1() = default;
Base1(const std::string&);
Base1(std::shared_ptr<int>);
};
struct Base2 {
Base2() = default;
Base2(const std::string&);
Base2(int);
};
//error: D1 attempts to inherit D1::D1(const string&) from both base classes
struct D1: public Base1, public Base2 {
using Base1::Base1; //inherit constructors from Base1
using Base2::Base2; //inherit constructors from Base2
};
此时必须定义自己版本的同名构造函数:
struct D2: public Base1, public Base2 {
using Base1::Base1; //inherit constructors from Base1
using Base2::Base2; //inherit constructors from Base2
//D2 must define its own constructor that takes a string
D2(const string &s): Base1(s), Base2(s) { }
D2() = default; //needed once D2 defines its own constructor
};
析构函数按照但继承相同的方法写出, 其执行顺序恰好与构造函数相反.
多态派生类的拷贝和移动操作会分别把派生类中作为子对象的基类的拷贝和移动操作激活:
Panda ying_yang("ying_yang");
Panda ling_ling = ying_yang; //uses the copy constructor
转换与多态基类
多继承的转换与单继承一致:
//operations that take references to base classes of type Panda
void print(const Bear&);
void highlight(const Endangered&);
ostream& operator<<(ostream&, const ZooAnimal&);
Panda ying_yang("ying_yang");
print(ying_yang); //passes Panda to a reference to Bear
highlight(ying_yang); //passes Panda to a reference to Endangered
cout << ying_yang << endl; //passes Panda to a reference to ZooAnimal
编译器无法区分从不同基类重载的函数, 此时将派生类做参数传入会引发错误:
void print(const Bear&);
void print(const Endangered&);
Panda ying_yang("ying_yang");
print(ying_yang); //error: ambiguous
剩下部分见书.
多态继承下的类作用域
和单继承一样, 名称查找会同时在多个继承子树结构中进行, 但如果在不同子树中都找到了相应的函数, 则调用函数时会出错. 这种错误甚至在形参列表不同, 或一个函数在 public 中, 另一个在 private 或 protected 中仍会出现. 避免方法有: 1. 永远不在派生类中调用; 2. 加上类作用域符来明确调用的是哪个; 3. 在派生类中重载相应的函数.
虚继承
考虑如果继承关系形成了环, 此时派生类将会有多个同一个基类的子对象. 如果想避免这种情况, 就必须使用虚继承.
这么写:
//the order of the keywords public and virtual is not significant
class Raccoon : public virtual ZooAnimal { /* . . . */ };
class Bear : virtual public ZooAnimal { /* . . . */ };
class Panda : public Bear, public Raccoon, public Endangered { };
转换仍然可以执行:
void dance(const Bear&);
void rummage(const Raccoon&);
ostream& operator<<(ostream&, const ZooAnimal&);
Panda ying_yang;
dance(ying_yang); //ok: passes Panda object as a Bear
rummage(ying_yang); //ok: passes Panda object as a Raccoon
cout << ying_yang; //ok: passes Panda object as a ZooAnimal
如果 B 中有个成员 x, 同时 D1 和 D2 分别继承了 B, D 继承了 D1 与 D2. 那么如果访问 D.x 有三种情况:
D1与D2没有定义x, 则为B::x.D1与D2有且仅有一个定义了x, 则派生类优先虚基类;D1与D2都定义了x, 则产生歧义.
构造函数与虚继承
派生类可以直接控制虚基类的构造函数:
Panda::Panda(std::string name, bool onExhibit)
: ZooAnimal(name, onExhibit, "Panda"),
Bear(name, onExhibit),
Raccoon(name, onExhibit),
Endangered(Endangered::critical),
sleeping_flag(false) { }
ZooAnimal 先调用构造函数, 之后 Endangered, Bear 和 Raccoon 按继承列表顺序先后调用.
如果有多个虚基类, 则构造函数按继承列表从左到右的顺序先后调用 (先检查直接基类中有没有任何虚基类):
class Character { /*. . .*/ };
class BookCharacter : public Character { /*. . .*/ };
class ToyAnimal { /*. . .*/ };
class TeddyBear : public BookCharacter,
public Bear, public virtual ToyAnimal { /*. . .*/ };
按一下顺序执行构造函数:
ZooAnimal(); // Bear’s virtual base class
ToyAnimal(); // direct virtual base class
Character(); // indirect base class of first nonvirtual base class
BookCharacter(); // first direct nonvirtual base class
Bear(); // second direct nonvirtual base class
TeddyBear(); // most derived class
浙公网安备 33010602011771号