定义基类和派生类
【例】书店不同书的定价策略不同,有的书按原价销售,有的打折销售等等.....
编写Quote基类,表示按原价销售的书籍
派生出Bulk_quote类,表示可以打折书籍
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;
protected:
double price = 0.0;
};
成员函数和继承
- 派生类会继承基类的成员,但是像net_price这样的和类型相关的操作,派生类要定义自己的操作来覆盖基类的旧定义。
- 基类将两种函数区分开来:
- 基类希望派生类覆盖的函数,通常定义为virtual
- 另一种是基类希望派生类直接继承而不进行改变
- 使用指针或引用调用虚函数时,该调用被动态绑定。即根据引用或指针所绑定对象的类,而调用对应版本的函数
- 任何构造函数以外的非静态函数都可以是虚函数
- virtual只能出现在类内部声明而不能用于类外部的函数定义
- 如果基类把函数声明成虚函数,则该函数在派生类中隐式的也是虚函数
访问控制和继承
- 基类希望它的派生类有权访问某个成员,但是禁止其他用户访问,我们用protected说明这样的成员
- 因为Quote希望它的派生类定义各自的net_price,所以派生类会访问Quote的price,所以定义为protected的;而访问bookNo成员的方式都是只能调用isbn函数,所以bookNo被定义为private
定义派生类
class Bulk_quote : public Quote
{
public:
Bulk_quote() = default;
Bulk_quote(const string&, double, size_t, double);
double net_price(size_t) const;
private:
size_t min_qty = 0; //要想使用折扣最少得买多少
double discount = 0.0; //折扣额
};
//从Quote继承了isbn、bookNo和price
//然后定义了net_price的新版本
- 派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明
- 派生类可以不覆盖它继承的虚函数,如果没有覆盖某个虚函数,则会直接继承在基类中的版本
- 可以将public派生类型的对象绑定到基类的引用或指针上(因为有基类的所有组成部分),还能将基类的指针或引用绑定到派生类对象中的基类部分上
Quote item;
Bulk_quote bulk;
Quote *p = &item;
p = &bulk; //p指向bulk的Quote部分
Quote &r = bulk; //r只是绑定到bulk的Quote部分
- 这样就可以将派生类对象或派生类对象的引用用在需要基类引用的地方;把派生类对象的指针用在需要基类指针的地方
派生类构造函数
- 派生类构造函数通过构造函数初始化列表将实参传递给基类的构造函数
Bulk_quote(const string& book, double p, size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) {}
//Quote的构造函数负责初始化Bulk_quote的基类部分
//可以使用其他基类的构造函数
派生类使用基类成员
- 派生类可以访问基类的公有成员和受保护成员
//price为受保护成员
double Bulk_quote::net_price(size_t cnt) const
{
if (cnt >= min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
- 每个类定义各自的接口,要想和类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分。所以不能直接初始化基类的成员(虽然语法上可以在派生类构造函数体内给它的公有或受保护的基类成员赋值,但是最好还是遵循基类的接口)
继承和静态成员
- 如果基类定义了一个静态成员,则整个继承体系中只存在该成员的唯一定义
class Base
{
public:
static void statmem();
};
class Derived : public Base
{
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj)
{
Base::statmem(); //Base定义的statmem
Derived::statmem(); //Derived继承的statmem
derived_obj.statmen(); //通过Derived对象访问
statmem(); //通过this对象访问
}
派生类声明
- 派生类声明和其他普通类一样,不需要包含派生列表
class Bulk_quote; //正确
class Bulk_quote : public Quote; //错误
被用作基类的类
- 如果想将某个类用作基类,则该类必须已经定义,而不是仅仅声明
class Quote;
class Bulk_quote : public {...}; //错误!
防止继承
- 如果不希望其他类继承某个类,C++11可以在类名后增加final实现这一目的
class NoDerived final {...};
class Base {...};
class Last final : Base {...};
class Bad : NoDreived {...}; //错误
class Bad2 : Last {...}; //错误
类型转换和继承
- 之前说过可以将基类的指针或引用绑定到派生类对象上,这意味着使用基类的引用或指针时,实际上并不清楚该引用(指针)绑定的是基类对象还是派生类对象。
- 使用继承关系的类型时要将变量或表达式的静态类型和它表示对象的动态类型区分开来
- 静态类型在编译时已知,它是变量声明时的类型或表达式的类型
- 动态类型则是变量或表达式表示的内存中的对象的类型
- 如果表达式既不是指针也不是引用,则动态静态类型保持一致
double print(ostream &os, const Quote &item, size_t n)
{
//item的静态类型是Quote&
//动态类型依赖于item绑定的实参
double ret = item.net_price(n);
os << "ISBN: " << item.isbn()
<< " # sold: " << n << " total due: " << ret << endl;
return ret;
}
为什么不存在基类向派生类的转换?
派生类能向基类转换是因为派生类一定会有基类的部分,基类的指针或引用一定能绑自己的内容
而基类不一定有派生类自己定义的部分,派生类的指针会绑定不了自己的内容
因为基类可以以独立形式存在(定义自己的对象),这时候不包括派生类定义的成员
也可以作为派生类对象的一部分存在(定义派生类对象)
这样飘忽不定,所以不能让基类向派生类转换
Quote base;
Bulk_quote *bulkp = &base; //错误
Bulk_quote &bulkref = base; //错误
//指针也不行
Bulk_quote bulk;
Quote *itemP = &bulk; //正确
bulk_quote *bulkP = itemP; //错误!
对象间不存在类型转换
- 派生类向基类的自动类型转换只对指针或引用有效,普通的派生类类型和基类类型之间不存在
- 如果给基类拷贝构造或者赋值,其实都在调用基类的这两个函数,别忘了这两个函数的参数都是基类的const引用,所以实际上是可以把派生类对象传递给他的,只不过拷贝构造或赋值的都是基类那部分,派生类的部分被切掉了
Bulk_quote bulk;
Quote item(bulk); //使用Quote的构造函数
item = bulk; //使用Quote的赋值运算符
虚函数
- 当虚函数通过指针或引用调用时,编译器直到运行时才能确定调用哪个版本的函数。
Quote base("0-201-82470-1", 50);
print_total(cout, base, 10); //调用Quote::net_price
Bulk_quote derived("0-201-82470-1", 50, 5, 0.19);
print_total(cout, derived, 10); //调用Bulk_quote::net_price
- 动态绑定只有通过指针或引用调用虚函数才会发生。使用普通类型调用虚函数,编译时就能将调用版本确定下来
//普通类型
base = derived; //把derived的Quote部分拷贝给base
base.net_price(20); //调用Quote::net_price
//base = derived改变base的值但不会改变对象的类型
- 一旦某个函数被声明程虚函数,则在所有派生类中它都是虚函数。所以派生后不需要再用virtual说明。
- 派生类中虚函数的返回类型必须和基类函数匹配。有个例外是当类的虚函数返回类型是类本身的指针或引用,可以无效这个规则(比如D继承B,B的虚函数返回B*,则D的对应函数可以返回D*,但是要求从D到B的类型转换是可访问的)
override
- 派生类定义了一个和基类中虚函数名字相同但形参列表不同的函数是合法的,但是编译器会认为这个函数和基类中的函数是相互独立的,并且不会覆盖掉基类中的版本。这样其实会造成麻烦,我们可能以为我们覆盖了,实际并没有。这里就可以用override标记,表示我们打算覆盖,如果没覆盖,编译器将会报错
struct B
{
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1 : B
{
void f1(int) const override; //正确
void f2(int) override; //错误,f2没有覆盖
void f3() override; //错误,f3不是虚函数
void f4() override; //错误,没有f4
虚函数和默认实参
- 如果函数调用使用了默认实参,则该实参值由本次调用的静态类型决定(如用基类的引用\指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本)
- 根据上面的规定,如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致
回避虚函数机制
- 使用作用域运算符可以对虚函数的调用不进行动态绑定,即强迫其执行虚函数的某个特定版本
//强行调用基类的函数版本
double undiscounted = baseP->Quote::net_price(42);
- 通常只有成员函数或友元中才会使用作用域运算符来回避虚函数机制
抽象基类
【例】扩展书店程序来支持几种不同的折扣策略
购买量不超过某个限额时可以享受折扣,超过限额按原价支付
或者购买量超过一定数量后全部打折,否则全不打折
这两种策略都需要一个购买量的值和折扣值
定义Disc_quote类,保存这两个值
//Disc_quote表示的是打折书籍的通用概念,而不是具体的打折策略
//所以继承过来的net_price函数对它来说没有任何意义
//但是创建Disc_quote对象,然后传给像print_total这样的函数
//程序会调用Quote的net_price,但是这个显示毫无意义
//这里就可以将net_price定义为纯虚函数,表示没有意义
class Disc_quote : public Quote
{
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; //定义为纯虚函数
protected:
size_t quantity = 0; //购买量
double discount = 0.0; //折扣
};
- 可以为纯虚函数提供定义,但是不能定义在类内,也就是说不能在类内为一个=0的函数提供函数体
有纯虚函数的类是抽象基类
- 不能创建抽象基类的对象,但是能定义抽象基类派生类的对象,前提是这些类覆盖了抽象基类中的纯虚函数,也就是说,如果派生类没给出纯虚函数的定义,则这个派生类仍然是抽象基类
class Bulk_quote : public Disc_quote
{
public:
Bulk_quote() = default;
Bulk_quote(const string& book, double price, size_t qty, double disc) :
Disc_quote(book, price, qty, disc) {}
double net_price(size_t) const override;
};
//每个Bulk_quote包含三个子对象:一个空的Bulk_quote、一个Disc_quote、一个Quote
//注意每个类控制自己的对象初始化过程
//Bulk_quote将实参传递给Disc_quote
//Disc_quote构造函数调用Quote的构造函数
//Quote构造函数初始化bulk的bookNO和price
//结束后运行Disc_quote的构造函数,初始化quantity和discount
//最后运行Bulk_quote,但是它没实际的初始化工作
访问控制和继承
- 每个类控制自己的成员初始化过程,并且每个类分别控制其成员对于派生类来说是否可访问
- protected说明那些希望和派生类分享但是又不想被其他公共访问使用的成员
- 派生类的成员或友元只能通过派生类对象来访问派生类对象中的基类部分的protected成员。但是在派生类中,没有权限访问一个基类对象的protected成员
class Base
{
protected:
int prot_mem;
};
class Sneaky : public Base
{
friend void clobber(Sneaky&);
friend void clobber(Base&);
int j; //private
};
//正确!可以访问派生类对象的private和protected
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
//错误!不能访问Base的protected
void clobber(Base &b) { b.prot_mem = 0; }
//假设派生类能访问基类对象的protected成员,那第二个函数将是合法的
//但是第二个clobber不是Base的友元,却可以改变Base对象的内容
//这就违背了protected的原则
public、private和protected继承
- 类对继承来的成员的访问权限受限于两个因素:1、基类中该成员的访问说明符 2、派生类的派生列表的访问说明符
- 派生访问说明符对派生类的成员能否访问其直接基类没影响。对基类成员的访问权限只和基类中的访问说明符有关(不管是公有继承、私有继承还是保护继承,基类的公有、保护成员都能访问,私有成员都不能访问)
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
{
//可以访问prot_mem,也不能访问priv_mem
int f1() const { return prot_mem; }
};
- 派生访问说明符的作用是控制派生类用户(包括派生类的派生类)对于基类成员的访问权限
Pub_Derv d1;
Priv_Derv d2;
d1.pub_mem();
d2.pub_mem(); //错误!pub_mem在派生类中是private
- 派生访问说明符还可以控制继承派生类的类的访问权限
struct Derived_from_Public : public Pub_Derv
{
//正确,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 user_base() { return prot_mem; }
};
派生类向基类转换的可访问性
- 假设D继承B
- 只有当D公有继承B时,用户代码才能使用派生类向基类的转换
- 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对派生类的成员来说永远是可访问的
- 如果D是公有或者受保护的继承B,则D的派生类的成员和友元可以使用D向B的类型转换。私有继承不行
Tips
可以认为类有三种用户:普通用户、类的实现者和派生类
普通用户使用类的对象,只能访问类的公有成员
实现者编写类的成员和友元,这俩既能访问类的公有,也能访问类的私有
派生类出现后,基类将希望派生类能使用的部分声明为protected
普通用户不能访问protected,派生类不能访问私有
基类将实现部分(private)分为两组,一组能让派生类实现自己的功能时也能使用,声明为protected
另一组只能由基类和自己的友元访问,声明为priavte
友元和继承
- 友元关系不能继承。派生类的友元也不能随意访问基类的成员
class Base
{
friend class Pal;
//这个友元只对声明的Pal有效
};
/*
class Sneaky : public Base
{
friend void clobber(Sneaky&);
friend void clobber(Base&);
int j;
};
*/
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的友元,所以能访问Base的成员,哪怕是嵌在派生类中的Base成员
//基类的访问权限由基类自己控制,即使对于派生类的基类部分也是如此
};
class D2 : public Pal
{
public:
int mem(Base b) { return b.prot_mem; }
//错误,友元只对Pal有效,不能继承友元
};
改变个别成员的可访问性
- 使用using改变某个名字的访问级别。using声明语句中的访问权限由当前这个using语句所处的访问说明符决定。但是只能将可以访问的成员(非私有)提供using声明
class Base
{
public:
size_t size() const { return n; }
protected:
size_t n;
};
class Derived : private Base
//注意是private继承,Derived所有成员都是私有的
{
public:
using Base::size;
//using位于public部分,现在Dervied用户能访问size
protected:
using Base::n;
//using位于protected部分,Derived的派生类将能使用n
};
默认的继承保护级别
class Base {...};
struct D1 : Base {...}; //默认public继承
class D2 : Base {...}; //默认private继承