目录
15.1 定义基类和派生类
15.1.1 定义基类
class Quote
{
public:
Quote() = default;
Quote(const string& book, double sales_price) :bookNo(book), price(sales_price) {};
string isbn() const { return bookNo; }
//派生类负责改写并使用不同的折扣计算算法
virtual double net_price(size_t n) const { return n * price; }//返回给定数量的书籍的销售总额
virtual ~Quote() = default;
private:
string bookNo; //书籍的isbn编号
protected:
double price = 0.0; //普通状态下不打折的价格
};
(1)成员函数与继承
在c++语言中,基类必须将他的两种函数区分开来:一种是基类希望其派生类进行覆盖的函数,另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通过将其定义为虚函数,任何构造函数之外的非静态构造函数都可以是虚函数。
15.1.2 定义派生类
派生类必须指明从哪个类派生而来。派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。
class Bulo_quote :public Quote
{
private:
size_t min_qty = 0; //试用折扣政策的最低购买数量
double discount = 0.0; //以小数表示的折扣额
public:
Bulo_quote() = default;
Bulo_quote(const string&, double, size_t, double);
//覆盖基类的函数版本以实现基于大量购买的折扣政策
double net_price(size_t)const override;
};
Bulo_quote 从其基类继承了isbn函数和bookNo,price等数据成员。
(1)派生类中的虚函数
派生类经常覆盖他继承的虚函数,如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他成员,派生类会直接继承其在基类中的版本。
(2)派生类对象及派生类向基类的类型转换
因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类对当成基类对象来使用,而且能将基类指针或引用绑定到派生类对象的基类部分上。
Quote item; //基类对象
Bulk_quote bulk; //派生类对象
Quote *p=&item; //p指向Quote对象
p=&bulk; //p指向bulk的Quote部分
Quote &r=bulk; //r绑定到bulk的Quote部分
(2)派生类构造函数
派生类需要有自己的构造函数
Bulk_quote(const string &book,double p,size_t qty,double disc):
Quote(book,p),min_qty(qty),discount(disc){};
函数前两个参数传递给Quote的构造函数,由Quote的构造函数负责初始化Bulk_quote的基类部分。
(3)派生类使用基类成员
派生类可以访问基类的共有成员和受保护成员
//如果达到购买数量的某个最低限量值,就可以享受则扣
double Bulk_quote::net_price(size_t cnt) const
{
if(cnt>=min_qty)
return cnt*(1-discount)*price;
else
return cnt*price;
}
(4)继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员你的唯一定义,无论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例
class Base
{
public:
static void statement();
};
class Derived:public Base
{
void f(const Derived&);
};
静态成员遵循通用的访问控制规则,既能通过基类使用它,也能通过派生类使用他。
void Derived::f(cosnt Derived &obj)
{
Base::statement();
Derived::statement();
obj.statement();
statement();
}
(5)被用作基类的类
如果先要将某个类用作基类,则该类必须已经定义而非仅仅声明
class Quote; //声明但未定义
class Bulk_quote:public Quote{.....};
(6)防止继承的发生
在某个类名后加final关键字,则该类无法被继承
class NoDerived final {.......} //该类无法被继承
15.1.3 类型转换与继承
可以将基类的指针或引用绑定到派生类对象上
(1)不存在从基类向派生类的隐式类型转换
Quote base;
Bulk_quote *bulkp=&base; //错误,不能将基类转换成派生类
Bulk_quote &bulkRef=base; //错误,不能将基类转换成派生类
15.2 虚函数
当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定,因为知道运行时才知道到底调用了哪个版本的虚函数。
(1)派生类中的虚函数
一个派生类的函数如果覆盖了某个继承而来的虚函数,则他的形参类型必须与被他覆盖的基类函数完全一致。
(2)final和override说明符
派生类如果定义了一个函数与基类中虚函数的名字相同但形参列表不同,则编译器认为新定义的函数与基类中原有的函数是相互独立的,这是,派生类函数没有覆盖掉基类中的版本。
想要检查上述情况,可以使用override关键字说明派生类中的虚函数,如果使用override标记了某个函数,但该函数没有覆盖已存在的虚函数,则编译器报错
class B {
virtual void f1(int)const;
virtual void f2();
void f3();
};
class D :B
{
void f1(int) const override; //正确,f1与基类中的f1匹配
void f2(int) override; //错误,B没有形如f2(int)的函数
void f3() override; //错误,f3不是虚函数
void f4() override; //错误,B没有名为f4的函数
};
将某个函数指定为final后,后续任何尝试覆盖该函数的操作都将引发错误。
class B {
virtual void f1(int)const;
virtual void f2();
void f3();
};
class D2 :B
{
void f1(int) const final;
};
class D3 :D2
{
void f2();
void f1(int) const; //错误
};
15.3 抽象基类
//用于保存折扣值和购买量的类,派生类使用这些数据可以实现不同的价格策略
class Disc_quote :public Quote
{
private:
size_t quantity = 0;
double discount = 0.0;
public:
Disc_quote() = default;
Disc_quote(const string& book, double price, size_t qty, double disc) :
Quote(book, price), quantity(qty), discount(disc) {};
double net_price(size_t)const = 0;
};
在函数体的位置书写=0可以将一个函数声明为纯虚函数,含有纯虚函数的类称为抽象基类
抽象基类负责定义接口,而后续的其他类可以覆盖该接口,不能直接创建一个抽象基类的对象。
重新定义Bulk_quote类
//当同一书籍的销售量超过某个值时启用折扣
//折扣值是一个小于1的正的小数值,一次来降低正常销售价格
class Bulk_quote :public Disc_quote
{
Bulk_quote() = default;
Bulk_quote(string& book, double price, size_t qty, double disc) :
Disc_quote(book, price, qty, disc) {};
//覆盖基类中的函数版本以实现一种新的折扣策略
double net_price(size_t) const;
};
15.4 访问控制与继承
(1)受保护成员
一个类使用protected关键字来声明那些他希望与派生类分享但不像被其他公共访问使用的成员
- 受保护成员对于类的用户来说是不可访问的
- 受保护成员对于派生类的成员和友元来说是可访问的
- 派生类成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。例子如下:
class Base
{
protected:
int prot_mem;
};
class Sneaky :public Base
{
friend void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }; //能访问Sneaky::prot_mem;
friend void clobber(Base& b) { b.j = b.prot_mem = 0; }; //不能访问Base::prot_mem;
int j;
};
规定:派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员,对于普通的基类对象中的成员不具有特殊的访问权限。
(2)公有、私有和受保护继承
class Base
{
public:
void pub_mem();
protected:
int prot_mem;
private:
char priv_mem;
};
class Pub_Derv :public Base
{
//正确:派生类可以访问protected成员
int f() { return prot_mem; }
//错误:private成员对于派生类来说是不可访问的
char g() { return priv_mem; }
};
class Priv_Derv :private Base
{
//private不影响派生类的访问权限
int f1() const { return prot_mem; }
};
Pub_Derv和Priv_Derv都能访问受保护的成员prot_mem,同时他们都不能访问私有成员priv_mem。
Pub_Derv d1;
priv_Derv d2;
d1.pub_mem(); //正确:pub_mem在派生类中是public的
d2.pub_mem(); //错误:pub_mem在派生类中是private的
派生类访问说明符还可以控制继承自派生类的新类的访问权限。
class Derived_from_Public :public Pub_Derv
{
//正确:Base::prot_mem在Pub_Derv中仍然是protected的
int use_base() { return prot_mem; }
};
class Derived_from_Private :public Priv_Derv
{
//错误:Base::prot_mem在Priv_Derv中仍然是private的
int use_base() { return prot_mem; }
};
Priv_Derv的派生类无法执行类的访问,对于他们来说,Priv_Derv继承自Base的所有成员都是私有的。
假设定义一个名为Prot_Derv的类,他采用受保护继承,则Base的所有公有成员在新定义的类中都是受保护的。Prot_Derv的成员和友元可以访问那些继承而来的成员
(3)改变个别成员的可访问性
通过using可改变派生类的成员访问级别
class Base
{
public:
size_t size() const { return n; }
protected:
size_t n;
};
class Derived :private Base
{
public:
using Base::size;
protected:
using Base::n;
};
Derived采用了私有继承,所以继承来的成员size和n都是私有的,但是可以采用using声明语句改变成员的可访问性。
(4)默认的继承保护级别
使用class关键字定义的派生类是私有继承,使用struct关键字定义的派生类是公有继承
class Base{};
struct D1:Base{} //默认public继承
class D2:Base{} //默认private继承
15.5 继承中的类作用域
(1)名字冲突与继承
派生类也能宠用定义在其基类中的名字,定义在内层作用域(即基类)的名字将隐藏定义在外层作用域(即基类)的名字
struct Base
{
protected:
int mem;
public:
Base() :mem(0) {};
};
struct Derived :Base
{
Derived(int i) :mem(i) {};
int get_mem() { return mem; } //返回Derived中的mem
protected:
int mem; //隐藏基类中的mem
};
可采用作用域运算符来使用隐藏的成员
struct Deirved:Base
{
int get_mem(){return Base::mem;}
}
(2)名字查找先于类型查找
声明在内层作用域的函数并不会重载声明在外层作用域的函数。定义派生类中的函数也不会重载其基类中的成员。如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏。
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(); //错误,参数列表为空的函数被隐藏了
d.Base::memfcn(); //正确
这就是为什么基类和派生类中的虚函数必须有相同的形参列表了,假如基类和派生类的虚函数接受的实参不同,则无法通过基类的引用和指针调用派生类的虚函数了。
15.6 构造函数与拷贝控制
15.6.1 虚析构函数
在delete一个动态分配的对象的指针时将执行析构函数,如果将指针指向继承体系中的某个类型,则可能出现指针的静态类型与被删除对象的动态类型不符的情况。例,如果delete一个Quote*类型的指针,则该指针有可能实际指向了一个Bulk_Quote类型的对象。则编译器必须清楚他应该执行的是Bulk_Quote的析构函数,通过将析构函数设置为虚函数以确保执行正确的版本。
和其他虚函数一样,析构函数的虚属性也会被继承
Quote *itemP=new Quote;
delete item; ///调用Quote的析构函数
itemP=new Bulk_quote;
delete itemP; //调用Bulk_quote的虚构函数
15.6.2 合成拷贝控制与继承
派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员,派生类赋值运算符也必须为其基类部分的成员赋值。
和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。
(1)定义派生类的拷贝构造函数
通常使用对应的基类构造函数初始化对象的基类部分
class Base{.......};
class D:public Base
{
public:
//默认情况下,基类的构造函数初始化基类部分,要想使用拷贝构造函数,必须显示调用
D(const &d):Base(d)......{...};
}
(2)派生类赋值运算符
派生类赋值运算符也必须显示地为其基类部分赋值。
//Base::operator=(const Base &)不会自动调用
D &D::operator=(const D &rhs)
{
Base::operator=(rhs);//为基类部分赋值
...........
return *this;
}
(3)派生类析构函数
派生类析构函数只负责销毁由派生类自己分配的资源,因此无需显示地调用基类的析构函数
15.6.3 继承的构造函数
15.7 容器与继承
采用容器存放继承体系中的对象时,通常必须采取间接存储的方式,因为不允许在容器中保存不同类型的元素,所以我们不能把具有继承关系的多种类型的对象直接存放在容器中。
vector<Quote> basket;
basket.push_back(Quote('0101',50));
basket.push_back(Bulk_quote("0102",50,10,.25)) //正确,但只能把对象的Quote部分拷贝给basket