虚函数
由于当使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。因为直到运行时才知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。
动态绑定只有当我们通过指针或引用调用虚函数才会发生。如果我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。
一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数。一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
派生类中虚函数的返回类型也必须与基类函数匹配。唯一例外是当类的虚函数返回类型是类本身的引用或指针。
struct B {
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1 : B{
void f1(int) const override; //正确
void f2(int) override; //错误:B没有形如f2(int)的函数
void f3() override; //错误:f3不是虚函数
void f4() override; //错误:B没有名为f4的函数
};
final和override说明符出现在形参列表(包括任何const或引用修饰符)以及尾置返回类型之后。
虚函数和其他函数议案呀那个,可以拥有默认实参。如果某次函数调用使用了默认实参,则此实参值由本次调用的静态类型决定。即,如果通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。
可以通过使用作用域运算符来实现强迫指向虚函数的某个特定版本。通常,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制.一般是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时需要回避虚函数的默认机制。
double undiscounted = baseP->Quote::net_price(42);
抽象基类
和普通函数不一样,纯虚函数无须定义。通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句处:
class Disc_quote : public Quote {
public:
Disc_quote() = default;
Disc_quote(const std::string& book, double price,
std::size_t qut, 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的函数提供函数体。含有(或未经覆盖直接直接继承)纯虚函数的类就是抽象基类。 抽象基类负责定义接口,而后续的其他类可以覆盖该接口。我们不能定义抽象基类的对象,但可以定义抽象基类的覆盖了纯虚函数的派生类对象。
派生类构造函数只初始化它的直接基类
class Bulk_quote : public Disc_quote{
public:
Bulk_quote() = default;
Bulk_quote(const std::string& book, double p,
std::size_t qty, double disc) :
Disc_quote(book, p, qty, disc) {}
double net_price(std::size_t) const override;
};
重构:在Quote的继承体系中增加Disc_quote类是重构(refactoring)的一个典型示例。重构负责重新设计类的体系一遍将操作和/或数据从一个类移动到另一个类中。重构后,使用Bulk_quote和Quote的代码无须进行任何改动,但类的代码需重新编译
访问控制和继承
每个类分别控制自己的成员初始化,还分别控制着其成员对于派生类是否可访问(accessible)
受保护的成员
一个类使用protected关键字来声明那些它希望和派生类分享但不想被其他公共访问使用的成员,派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员,对于普通基类对象中的成员不具备特殊的访问权限:
- 和私有成员类似,受保护的成员对于类的用户是不可访问的。
- 和公有成员类似,受保护的成员对于派生类的成员和友元是可访问的
- 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权
class Base{
protected:
int prot_mem;
};
class Sneaky : public Base{
friend void clobber(Sneaky&);
friend void clobber(Base &);
int j;
};
//正确:clobber能访问Sneaky对象的private和protected成员
void clobber(Sneaky &s) { s.j = s.port_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 d1;
Priv_Derv d2;
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中是pribate的
int use_base() { return prot_mem;}
};
派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。假定D继承自B:
- 只有当D公有继承B时,用户代码才能使用派生类向基类的转换。如果D继承B的方式是private或protected,则用户代码不能使用该转换
- 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
- 如果D继承B的方式是public或protected的,则D的派生类的成员和友元可以使用D向B的类型转换;如果D继承B的方式是private,则不能使用
基类应该把其接口成员声明成public的;同时将属于其实现的部分分为两组:一组可供派生类访问,另一组只能由基类及基类的友元访问。对于前者,应该是protected,后者应该是private。
友元和继承
友元关系不能传递也不能继承。基类的友元在访问派生类成员不具有特殊性,派生类的友元也不能随意访问基类的成员:
class Base{
friend class Pal;
};
class Pal {
int f(Base b) { return b.prot_mem; } //正确:Pal是Base的友元
int f2(Sneaky s) { return s.j; } //错误:Pal不是Seaky的友元
//对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此
int f2(Sneaky s) { return s.port_mem;} //正确:Pal是Base的友元
};
每个类负责控制自己的成员的访问权限,因此f3是正确的。Pal是Base的友元,所以Pal可以访问Base对象的成员,这种可访问包括了Base对象内嵌在其派生类对象中的情况。
当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来的那个类而言,其友元的基类和派生类不具有特殊的访问能力:
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 {
public:
using Base::size;
protected:
using Base::n;
};
由于private的继承,所以size和n在默认情况下是Derived的私有成员。然而使用using声明语句改变了这些成员的可访问性。改变后,Derived的用户(对象)可以使用size成员,而Derived的派生类将能使用n。
通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问(例如,非私有成员)成员标记出来。using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。
默认情况下,使用class关键字定义的派生类是私有继承的;使用struct关键字定义的派生类是公有继承的。
继承中的类作用域
每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外出的基类作用域中寻找该名字的定义。
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。及时静态类型与动态类型可能不一致。
class Dise_quote : public Quote {
public:
std::pair< size_t, double> discount_policy() const
{ return { quantity, discount}; }
};
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk;
Quote *itemP = &bulk;
bulkP->discount_policy(); //正确:bulkP的类型是Bulk_quote*
itemP->discount_policy(); //错误:itemP的类型Quote*
名字冲突和继承
派生类能重用定义在其直接基类或者间接基类中的名字,此时定义在内层作用域(派生类)的名字将隐藏定义在外层作用域(基类)的名字。派生类的成员将隐藏同名的基类成员:
struct Base {
Base() : mem(0) {}
protected:
int men;
};
struct Derived : Base {
Derived(int i) : men(i) {} //用i初始化Derived::mem
//Base::mem进行默认初始化
int get_mem() { return mem;} //返回Derived::mem
protected:
int men;
};
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
假定使用p->mem()或者obj.mem(),依次执行下面4个步骤:
- 首先确定p(obj)的静态类型。
- 在p(obj)的静态类型对应的类中查找men。找不到,则依次在直接基类中不断查找直至到达继承链的顶端。如果找遍也找不到,编译器将报错
- 一旦找到了men,进行常规的类型检查以确认对于当前找到的men,本次调用是否合法。
- 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码。如果是虚函数且通过引用后指针进行的调用,则编译器调用的代码将在运行时依据动态类型确认使用哪个版本。否则编译器产生一个常规函数调用。
声明在内层作用域的函数并不会重载声明在外层作用域的函数。因此派生类中定义的函数不会重载基类中的成员。因此基类和派生类中的虚函数必须有相同的形参列表,否则将出现无法通过基类的引用或指针调用派生类的虚函数的现象:
class Base {
public:
virtual int fcn();
};
class D1 : public Base {
public:
//隐藏基类的fcn,这个fcn不是虚函数。
//D1继承了Base::fcn()的定义
int fcn(int);
virtual void f2();
};
class D2 : public D1 {
public:
int fcn(int); //一个非虚函数,隐藏了D1::fcn(int)
int fcn(); //覆盖了Base::fcn()
void f2(); //覆盖了D1::f2()
};
Base bobj;
D1 d1obj;
D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); //虚调用,将在进行使调用Base::fcn()
bp2->fcn(); //虚调用,将在进行使调用Base::fcn()
bp3->fcn(); //虚调用,将在进行使调用D2::fcn()
D1 *d1p = &d1obj;
D2 *d2p = &d2obj;
dp2->f2(); //错误:Base没有f2()
d1p->f2(); //虚调用,将在进行使调用D1::f2()
d2p->f2(); //虚调用,将在进行使调用D2::f2()
Base *p1 = &d2obj;
D1 *p2 = &d2obj;
D2 *p3 = &d2obj;
p1->fcn(42); //错误:Base中没有fcn(int)
p2->fcn(42); //静态绑定:调用D1::fcn(int)
p3->fcn(42); //静态绑定:调用D2::fcn(int)
调用非虚函数,不会发生动态绑定,所以实际调用函数版本由静态类型决定。
覆盖重载函数
如果一个类仅需覆盖重载函数集合中的一些函数而非全部函数,为重载的成员提供一条using声明。using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类作用域中,此时,派生类只需要重定义其特有的函数就可以