第十五章 面向对象程序设计
在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键
派生类构造函数
尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。和其他创建了基类对象的代码一样,派生类也必须使用基类的构造函数来初始化它的基类部分。
每个类控制它的成员初始化过程
派生类对象的基类部分与派生类对象自己的数据成员都是在构造函数的初始化阶段执行初始化操作的。类似于我们的初始化成员的过程,派生类构造函数同样是通过构造函数初始化列表来讲实参传递给基类构造函数。例如,接收四个参数的 Bulk_quote
构造函数如下所示:
Bulk_quote(const std::string& book, double p, std::size_t qty, double disc):
Quote(book, p), min_qty(qty), discount(disc) { }
// ...
该函数将它的前两个参数传递给 Quote
的构造函数,由 Quote
的构造函数负责初始化 Bulk_quote
的基类部分。当 Quote
构造函数体结束后,我们构建的对象的基类部分也就完成初始化了。接下来初始化由派生类直接定义的 min_qty
成员和 discount
成员。最后运行 Bulk_quote
的构造函数体
也许这就是为什么不能在构造函数中调用虚函数的原因?会执行两次?
关键概念:遵循基类的接口
必须明确的是:每个类负责定义各自的接口。要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此
因此,派生类对象不能直接初始化基类的成员。尽管从语法上来说可以在派生类构造函数体内给它的共有或受保护的基类成员赋值,但是最好不要这样做。和使用基类的其他场合一样,派生类应该遵循基类的接口,并且通过调用基类的构造函数来初始化那些从基类中继承而来的成员
继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。
class Base{
public:
static void statemem();
};
class Drived : public Base{
void f(const Derived&);
};
静态成员遵循通用的访问控制规则,如果基类中的成员是 private
的,则派生类无权访问。假设某静态成员是可访问的,则我们既能通过基类使用它也能通过派生类使用它
void Derived::f(const Derived& derived_obj){
Base::statmem(); // 正确:Base定义了 statmem
Derived::statmem(); // 正确:Derived 继承了 statmem
// 正确:派生类对象能访问基类的静态成员
derived_obj.statmem(); // 通过 Derived 对象访问
statmem(); // 通过 this 对象访问
}
防止继承的发生
C++11
新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字 final
class NoDerived final { /* */ }; // NoDerived 不能作为基类
class Base{ /* */ };
// Last 是 final 的; 我们不能继承 Last
class Last final : Base { /* */ }; // Last 不能作为基类
class Bad : NoDerived { /* */ }; // 错误:NoDerived 是 final 的
class Bad2: Last { /* */ }; // 错误:Last 是 final 的
虚函数
动态绑定只有当我们通过指针或引用调用虚函数时才会发生
base = derived; // 把 derived 的 Quote 部分拷贝给 base
base.net_price(20); // 仍调用 Quote::net_price
final 和 override 说明符
派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然时合法的行为。编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。这时,派生类的函数并没有覆盖掉基类中的版本。就实际的编程习惯而言,这种声明往往意味着发生了错误,因为可能原本希望派生类覆盖掉基类中的虚函数,但是一不小心把形参列表弄错了
我们可以通过 override
标注某个函数,让程序员意图更清晰的同时编译器也可以为我们发现一些错误。如果标记了函数,但该函数没有覆盖已经存在的虚函数,编译器报错
struct B{
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1 : B{
void f1(int) const override; // 正确:f1 与基类中 f1 匹配
void f2(int) override; // 错误:B 没有形如 f2(int) 的函数
void f3() override; // 错误:f3 不是虚函数
void f4() override; // 错误:B 没有名为 f4 的函数
};
除了把类声明为 class NoDerived final { /* */ };
的方式阻止继承外,还可以把某个函数指定为 final
,则之后任何尝试覆盖该函数的操作都将引发错误
struct D2 : B{
// 从 B 继承 f2() 和 f3(), 覆盖 f1(int)
void f1(int) const final; // 不允许后继的其他类覆盖 f1(int)
};
struct D3 : D2{
void f2(); // 正确:覆盖从简介基类 B 继承而来的 f2
void f1(int) const; // 错误:D2 已经将 f2 声明成 final
};
final
和override
说明符出现在形参列表( 包括任何const
或引用修饰符)以及尾置返回类型之后
虚函数和默认实参
虚函数也可以拥有默认实参,如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
也就是说,如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是金磊函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不符
如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致
抽象基类
纯虚函数
// 用于保存折扣值和购买量的类,派生类使用这些数据可以实现不同的价格策略
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; // 折扣使用的购买量
double discount = 0.0; // 表示折扣的小数值
};
值得注意的是,可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,不能在类的内部为一个 =0 的函数提供函数体
访问控制与继承
每个类分别控制自己的成员初始化过程,与之类似,每个类害分别控制着其成员对于派生类来说是否 可访问(accessible)
受保护的成员
如前所述,一个类使用 protected
关键字来声明那些它希望与派生类分享但是不想被其他公共访问使用的成员。protected
说明符可以看作是 public
和 private
中和的产物
- 和私有成员类似,受保护的成员对于类的用户来说是不可访问的
- 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的
此外,protected
还有另外一条重要的性质
- 派生类的成员和友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权
class Base{
protected:
int prot_mem;
};
class Sneaky : public Base{
friend void clobber(Sneaky&); // 能访问 Sneaky::prot_mem
friend void clobber(Base&); // 不能访问 Base::prot_mem
int j; // j 默认为 private
};
// 正确:clobber 能访问 Sneaky 对象的 private 和 protected 成员
void clobber(Sneaky& s){ s.j = s.prot_mem = 0; }
// 错误:clobber 不能访问 Base 的 protected 成员
void clobber(Base& b){ b.prot_mem = 0; }
派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员;对于普通的基类对象中的成员不具有特殊的访问权限
公有、私有和受保护继承
class Base{
public:
void pub_mem():
protected:
int prot_mem;
private:
char priv_mem;
};
struct Pub_Derv : public Base{
// 正确:派生类能访问 protected 成员
int f(){ return prot_mem; }
// 错误:private 成员对于派生类来说是不可访问的
char g() { return priv_mem; }
};
struct Priv_Derv : private Base{
// private 不影响派生类的访问权限
int f1() const { return prot_mem; }
};
派生类访问说明符对于派生类的成员( 及友元 )能否访问其直接基类的成员没有什么影响。对基类成员的访问权限只与基类中的访问说明符有关。Pub_Derv
和 Priv_Derv
都能访问受保护的成员 prot_mem
,同时都不能访问私有成员 priv_mem
派生访问说明符的目的是控制派生类用户( 包括派生类的派生类在内 )对于基类成员的访问权限
Pub_Derv d1; // 继承自 Base 的成员是 public 的
Priv_Derv d2; // 继承自 Base 的成员是 private 的
d1.pub_mem(); // 正确:pub_mem 在派生类中是 public 的
d2.pub_mem(); // 错误:pub_mem 在派生类中是 private 的
派生访问说明符还可以控制继承自派生类的新类的访问权限:
struct Derived_from_Public : public Pub_Derv{
// 正确:Base::prot_mem 在 Pub_Derv 中仍然是 protected
int use_base() { return prot_mem; }
};
struct Derived_from_Private : public Priv_Derv{
// 错误:Base::prot_mem 在 Priv_Derv中是 private 的
int use_base() { return prot_mem; }
};
友元和继承
class Base{
friend class Pal;
protected:
int prot_mem;
};
class Sneaky : public Base{
friend void clobber(Sneaky&); // 能访问 Sneaky::prot_mem
friend void clobber(Base&); // 不能访问 Base::prot_mem
int j; // j 默认为 private
};
class Pal{
public:
int f(Base b){ return b.prot_mem; } // 正确:Pal 是 Base 的友元
int f2(Sneaky s) { return s.j; } // 错误:Pal 不是 Sneaky 的友元
// 对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此
int f3(Sneaky s) { return s.prot_mem; } // 正确:Pal 是 Base 的友元
};
如前,每个类负责控制自己成员的访问权限,因此尽管看起来有点怪,但是 f3
确实时正确的。Pal
是 Base
的友元,所以 Pal
能访问 Base
对象的成员,这种可访问性包括了 Base
对象内嵌在其派生类对象中的情况
当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来的类来说,其友元的基类或派生类不具有特殊的访问能力
// D2 对 Base 的 protected 和 private 成员不具有特殊访问能力
class D2 : public Pal{
public:
int mem(Base b)
{ return b.prot_mem; } // 错误:友元关系不能继承
};
不能继承友元关系;每个类负责控制各成员的访问权限
改变个别成员的可访问性
有时会需要改变派生类继承的某个名字的访问级别,通过使用 using
声明可以达到这一目的
class Base{
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived : private Base{ // 注意:private 继承
public:
// 保持对象尺寸相关的成员的访问级别
using Base::size;
protected:
using Base::n;
};
因为 Derived
使用了私有继承,所以继承而来的成员 size
和 n
是 Derived
的私有成员。然后,我们使用 using
声明语句改变这些成员的可访问性。改变之后,Derived
的用户将可以使用 size
成员,而 Derived
的派生类将能使用 n
通过在类的内部使用 using
声明语句,我们可以将该类的直接或简介基类中的任何可访问成员标记出来。using
声明语句中名字的访问权限由该 using
声明语句之前的访问说明负来决定。也就是所,如果一条 using
声明语句出现在类的 private
部分,则改名字只能被类的成员和友元访问;如果出现在 public
部分,则类的所有用户都能访问它;如果位于 protected
部分,则该名字对于成员、友元和派生类是可访问的
默认继承保护级别
class Base { /* ... */ };
struct D1 : Base { /* ... */ }; // 默认 public 继承
class D2 : Base { /* ... */ }; // 默认 private 继承
名字查找先于类型检查
声明在内层作用域的函数并不会重载声明在外层作用域的函数。因此,定义派生类中的而函数也不会重载其基类中的成员。和其他作用域一样,如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉
struct Base{
int memfcn();
};
struct Derived : Base{
int memfcn(int); // 隐藏基类的 memfcn
};
Derived d;
Base b;
b.memfcn(); // 调用 Base::memfcn
d.memfcn(10); // 调用 Derived::memfcn
d.memfcn(); // 错误:参数列表为空的 memfcn 被隐藏了
d.Base::memfcn(); // 正确:调用 Base::memfcn