OOP即面向对象程序设计,核心思想是数据抽象,继承和动态绑定。通过使用数据抽象,可以实现类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别。 虚函数:在C++中,基类将类型相关的函数与派生类不做改变直接基础的函数区分对待。即,对于某些函数,基类希望它的派生类能够自己定义适合自己的版本,此时,基类就将这些函数声明成虚函数(virtual)。 派生类必须通过派生列表(class derivation list)明确指出它是从哪个(哪些)基类继承而来。
类派生列表的形式是:首先是类名后接一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类面前可以有访问说明符
class Quote
{
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};
//派生出一个类
class Bulk_quote : public Quote
{
public:
double net_price(std::size_t n) const override;
//在c++新标准中允许派生类显式地标注它使用哪一个函数来改写基类地虚函数
//在该函数地形参列表之后增加关键字override
}
通过使用动态绑定,我们能用同一段代码分别处理Quote和Bulk_quote对象。在c++中,当我们使用基类地引用(或指针)调用一个虚函数时,将发生动态绑定。
//计算打印销售给定数量地某种书籍所得的费用
double print_total(ostream &os, const Quote &item, size_t n)
{
//跟据传入item形参的对象类型调用Quote::net_price
//或者Bulk_quote::net_price
double ret = item.net_price(n);
os << "ISBN: "<< item.isbn() << " # sold: " << n << " total due: " << ret << endl;
return ret;
}
因为函数的item形参是Quote的一个引用,我们既能使用基类Quote的对象调用,也能使用Bulk_quote的对象使用它
同时编译器会根据传入参数的类型决定调用哪一个net_price
Quote basic;
Bulk_quote bulk;
print_price(cout, basic, 20); //调用的是Quote的net_price
print_price(cout, bulk, 20); //调用的是Bulk_quote的net_price
定义基类。基类必须将它的两种成员函数区分开来:一种是希望派生类改写(覆盖)的函数;另一种是基类希望派生类直接继承的函数。对于前者,基类通常将其定义为虚函数(virtual),当我们使用指针或者引用来调用虚函数时,该调用将被动态绑定。
class Quote
{
public:
Quote() = default;
Quote(const std::string& book, double sales_price) : bookNo(book), price(sales_price) { }
std::string isbn() const { return bookNo; }
//返回给定数量的数据的销售总额
//派生类负责改写并使用不同的折扣计算算法
virtual double net_price(std::size_t n) const
{
return n * price;
}
virtual ~Quote() = default; //对析构函数进行动态绑定
private:
std::string bookNo; //书籍的ISBN编号
protected:
double price = 0.0; //书籍不打折的价格
};
#Note#
基类通常都该定义一个virtual的析构函数,即使该函数不执行任何操作也是如此
成员函数如果没有被声明为虚函数,则其解析发生在编译时,而虚函数解析发生在运行时。
protected(受保护的):
派生类从基类继承数据成员时,派生类可以访问公有成员(public)而不能访问私有成员(private)
但基类希望派生类有权访问一些成员,但是其他非派生类无权访问,于是定义为protected(受保护的)
该例中,派生类不能访问私有数据成员bookNo,但是可以通过成员函数isbn()来获取它,实现了封装。
关键字virtual只能出现在类内部的声明语句之前,而不能用于类外部的函数定义。如果基类把一个函数声明为虚函数,则该函数在派生类中隐式的也是虚函数 ,即派生类再派生时,该函数在派生类作基类时,同样表示虚函数。 定义派生类。一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对象的子对象,如果有多个基类,那么这样的子对象也有多个。
class Bulk_quote : public Quote
{
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, std::size_t, double);
//覆盖基类的函数,实现基于大量购买的折扣政策
double net_price(std::size_t n) const override; //override显式指出改写虚函数
private:
std::size_t min_qty = 0; //适用折扣的最小购买量
double discount = 0.0; //以小数表示折扣额
};
派生类Bulk_quote从基类Quote中继承了isbn函数和bookNo,price等数据成员。
同时改写了net_price的新版本,增加了数据成员min_qty和discount
#Note#
派生类经常(但不全完是)覆盖它继承的虚函数。如果派生类没有改写其基类中的虚函数,则该虚函数的形为类似于其他普通成员函数,派生类会直接继承他在基类中的版本
Bulk_quote对象:
{
//从Quote继承而来的成员
bookNo
price
}
{
//Bulk_price自定义的成员
min_qty
discount
}
于是,在进行如下操作时,会进行派生类对象及派生类向基类的类型转换
Quote item; //基类对象
Bulk_quote bulk; //派生类对象
Quote *p = &item; //p指向Quote对象
p = &bulk; //p指向bulk对象的Quote部分
Quote &r = bulk; //r绑定到bulk的Quote部分
这种转换通常称为==派生类到基类==的类型转换
尽管派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。派生类也必须通过使用基类的构造函数来初始化它的基类部分。即,每个类必须控制自己类的成员初始化过程。
派生类对象的基类部分与派生类对象自己数据成员都是在构造函数的初始化阶段执行初始化操作
例如:
派生类构造函数同样是通过构造函数初始化列表来将实参传递给基类构造函数
//接受四个参数的Bulk_quote构造函数
Bulk_quote::Bulk_quote(const string &book, double p, std::size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) { }
#Note#
首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员
必须明确一点:每个类负责定义各自的接口。想要与类的对象交互就必须使用该类的接口。即使这个对象是派生类的基类部分。
因此派生类对象不能直接初始化基类的成员。派生类应该遵从基类的接口,并且通过调用基类的构造函数来初始化哪些从基类中继承而来的成员。
派生类使用基类的成员
派生类可以访问基类的公有成员和受保护成员
//改写基类net_price函数,若达到优惠数量则给予折扣
double Bulk_quote::net_price(size_t n) const
{
if(n >= min_qty)
return n * (1 - discount) * price;
else
return n * price;
}
继承与静态成员。若基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。
//定义基类
class Base
{
public:
static void statmem();
};
//定义派生类继承自public的Base
class Derived : public Base
{
void f(const Derivec&);
};
void Derived::f(const Derived &d_obj)
{
Base::statmem(); //正确:在Base中定义了statmem
Derived::statemem(); //正确:Derived继承了public的静态成员函数statmem
//派生类的对象能访问基类的静态成员
d_obj.statmem(); //通过Derived对象访问
statmem(); //通过this对象访问
}
当我们使用某个类作为基类时,该类必须已经定义好了。
class Quote; //声明而未定义
class Bulk_quote : public Quote { ... }; //错误:Quote必须被定义
###
一个类是基类,同时它也可以是派生类
class Base { ... };
class D1 : public Base { ... };
class D2 : public D1 { ... };
在此例中Base作为D1的直接基类,作为D2的间接基类。D1作为Base的派生类,而作为D2的基类。
对于一个最终的派生类来说,它会继承它的直接基类和间接基类的可以继承的成员。
###
当我们不希望一个类被其他类继承时,可以使用final关键字
class NoDerived final { ... }; //NoDerived不能作为基类
类型转换与继承
通常情况下,如果我们想法引用或者指针绑定到一个对象上:
则引用或指针的类型与对象的类型一致
或者对象的类型含有一个可接受的const类型转换规则
在继承关系的类之间:
我们可以将基类的指针或者引用绑定到派生类对象上。例如,我也用Quote&指向一个Bulk_quote对象,也可以把一个Bulk_quote对象的地址赋给一个Quote*。
之所以这样使用基类的引用(或者指针),是因为实际操作中我们并不清楚传入的参数的真实类型。该对象可能是基类的对象,可能是派生类对象。
#Tip#
和内置指针一样,智能指针也支持派生类向基类的类型转换,即可以将一个派生类对象的指针存储在一个基类的智能指针内。
静态类型和动态类型。当使用存在继承关系的类型是,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开来。表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或者表达式标识的内存中的对象类型,动态类型直到运行时才可知。
例如:当使用print_total调用net_price时,
double ret = item.net_price(n);
我们定义的item的静态类型为Quote&,但是动态类型只有使用时才知道。加入传入的时一个Bulk_quote对象,则动态类型为Bulk_quote,静态类型与动态类型不一致。
!!!不存在基类向派生类的隐式类型转换,即基类不包含派生类对象的元素。
Quote base;
Bulk_quote* bulkp = &base; //错误:不能将基类转换成派生类
Bulk_quote& bulkref = base; //错误:不能将基类转换成派生类
Bulk_quote bulk;
Quote* itemp = &bulk; //正确:动态类型为Bulk_quote
Bulk_quote *bulkp = itemp; //错误:不能将基类转换成派生类
#注意#
派生类向基类转换时,只对指针或者引用有效,派生类类型和基类类型之间(即两个对象之间)不存在转换。