概述
-
面向对象程序设计 的核心思想是数据抽象,继承和动态绑定。通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
-
C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区别对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明为虚函数。
class Quote { //原价出售的书籍
public:
//虚函数 基类需要其定义自己的版本;
virtual double net_price(size_t n)const { return n * price; };
}
- 派生类必须通过使用派生类列表指名它是从哪个基类继承而来,基类前面可以有访问说明符。
class Bulk_quote :public Quote {
double net_price(size_t n) const override;
}
- 通过使用动态绑定,我们能用同一段代码分别处理Quote和Bulk_quote的对象。
double Bulk_quote::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() << " total due: " << ret << endl;
return ret;
}
由于上述函数运行版本由实参决定,即在运行时选择函数版本,所以动态绑定有时又被称为运行时绑定。
定义基类与派生类
- 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;
};
- Bulk_quote类定义
//公有派生,基类的公有成员也是派生类接口的组成部分
//同时,能将共有派生类型绑定到基类的引用或指针上.
class Bulk_quote :public Quote {
public:
Bulk_quote() = default;
Bulk_quote(string &, double, size_t, double);
double net_price(size_t n) const override;
//因为参数二是基类的引用,所以可以使用子类和基类 的对象调用该函数。
double print_total(ostream &os, const Quote&item, size_t n);
private:
size_t min_qty = 0; //折扣政策所需的最小数目
double discount = 0; //折扣
};
-
派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个该派生类所继承的基类的子对象。如果有多个基类,那么这样的子对象也有多个。
同时因为派生类中包含有基类对应的部分,所以我们可以将派生类对象当成基类对象使用。即派生类可向基类转换,跟其他类型一样,编译器会隐式的执行派生类到基类的转换。
-
派生类对象初始化过程中,会先调用基类的构造函数,基类初始化完成后,按照声明的顺序依次初始化派生类成员。
-
如果基类定义了静态成员,那么无论该基类拥有多少派生类,对于每个静态成员来说只存在唯一实例。同时,静态成员遵循通用的访问控制规则,如果基类中的静态成员为private,则派生类无权访问。
-
派生类声明中不可包含派生列表。
声明语句的目的是令程序知晓某个名字的存在以及该名字表示一个什么样的实体。派生列表以及定义有关的其他细节必须与类的主体一起出现。
class Bulk_quote : public Quote; //错误
class Bulk_quote : Quote; //正确的声明方式
7.每个类都会继承直接基类的所有成员,以此类推,最终的派生类会包含其所有基类的子对象。
8.final防止继承的发生
//NoDerived 不可作为基类
class NoDerived final{
};
类型转换与继承
- 通常情况下,我们想把引用或指针绑定到一个对象上,则引用与指针的类型应与对象类型一致,或对象类型含有一个可接受的const类型转换规则。存在继承关系的类是一个例外:我们可以将基类的引用或指针绑定到派生类对象上。即,我们可以用Qoute&指向一个Bulk_quote对象,也可以把Bulk_quote的对象地址赋值给一个Qoute*。
2.使用存在继承关系的类型时,要区分它们的静态类型和动态类型。表达式的静态类型在编译时总是已知的,而动态类型要知道运行时才可知。例子可参见上文概述4。若表达式不是引用或指针,则它的动态类型与静态类型一致。
3.不存在基类向派生类的转换。因为基类可能是派生类对象的一部分,此时不可转换;也可能不是,此时可转换。所以编译器并不允许基类向派生类的转换,如果我们肯定某个转换是安全的,可以使用static_cast强制覆盖掉编译器的检查工作。
Bulk_quote bulk;
Quote *item = &bulk; // 此时item的动态类型是Bulk_quote
Bulk_quote *bulkp = item; //错误
3.派生类与基类间的类型转换只对于指针或引用类型有效。派生类类型与基类类型之间不存在这样的转换。当我们初始化或赋值一个类类型对象时,实际上是在调用某个函数。初始化时,调用构造函数,赋值时,调用赋值运算符。这些成员通常都包含一个参数,参数类型是类类型const版本的引用。所在在派生类向基类转换时允许我们向基类成员传递一个派生类的引用,此时,只属于派生类的数据会被"切掉"。
虚函数
1.对虚函数的调用可能在运行时才被解析,区别于其他函数,所以我们必须要对每个虚函数提供定义。动态绑定只有在我们通过指针或引用调用虚函数时才会发生。
//根据动态类型调用相应的虚函数
Quote base("0-201-82470-1",50);
print_total(cout,base,10); //动态类型 为Quote 调用Quote::net_price
Bulk_qoute derived("0-202-82480-1",50,5,0.8);
print_total(cout,derived,10); //动态类型为Bulk_quote 调用Bulk_quote::net_price
2.派生类的某函数若覆盖了继承而来的虚函数,则派生类的形参类型必须与被覆盖的基类函数形参类型一致,否则,编译器将会视型定义的函数与基类函数相互独立。这时,派生类函数并没有覆盖基类函数。
struct B{
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct C:B{
void f1(int) const override;//基类函数被覆盖。
void f2(int) override; //错误,未在基类找到f2(int)函数定义。
};
3.将基类函数指定为final,其派生类任何尝试覆盖该函数都将发生错误。
struct D:B{
void f1(int) const final;
}
struct E:D{
void f1(int) const; //错误,D中f1(int) 已被声明为final
};
4.虚函数可以拥有默认实参。函数使用的默认实参由调用函数的对象静态类型决定(在之前的例子中,如果net_price存在默认实参,则net_price使用基类中的默认实参,因为item静态类型为Quote)。派生类与基类定义的实参最好一致。
5.回避虚函数
//强行调用基类版本而无视baseP动态类型
double undis = baseP->Qoute::net_price(42);
抽象基类
如果我们希望书店程序支持多种折扣策略,每种折扣策略包含购买量和折扣值。我们就可以定义一个抽象基类来支持多种折扣策略。抽象基类Disc_quote负责保存购买量和折扣值,其他的特定策略的类继承该抽象基类。每个派生类通过自己的net_price函数实现自己的折扣策略。
定义的抽象基类不需要用户创建对象,因此我们可以通过将Disc_quote的net_price函数定义成纯虚函数以实现我们的意图。包含有纯虚函数(或未覆盖而直接继承)的类时抽象基类,我们不能直接创建抽象基类的对象。
//抽象基类
class Disc_quote: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;
private:
size_t quantity = 0;
double discount = 0.0;
};
访问控制与继承
1.protected:受保护的成员,声明类希望与派生类分享但不想被其他公共访问使用的成员。
派生类成员或友元只能通过**派生类对象!!!**来访问基类受保护的成员。派生类对于基类对象的受保护成员没有任何访问特权。
class Base{
protected:
int prot_mem;
};
class Ext : public Base{
//可以访问prot_mem 并且该prot_mem是Ext对象的成员!!
friend void clob(Ext&);
//无法访问prot_mem
friend void clob(Base&);
int j;
};
- 某个类对于继承而来的成员的访问限权收到两个因素印象:基类中该成员的访问说明符和派生类派生列表中的访问说明符。
class Base{
public:
void pub_mem();
protected:
int prot_mem;
private:
int priv_mem;
};
class Pub_Divert: public Base{
int f(){return prot_mem;};
//private成员对于派生类无法访问
char g(){return priv_mem;};
};
class Priv_Divert: private Base{
//private不影响派生类访问限权
int f1() const {return prot_mem;};
};
派生访问说明符对于派生类成员(包括友元,但注意不是派生类对象!!!)能否直接访问基类的成员并无影响。派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类的访问。
Pub_Divert d1;
Pub_Divert d2;
d1.pub_mem(); //pub_mem在派生类中是public的
d2.pub_mem(); //pub_mem在派生类(Pub_Divert)中是private的
//private 说明符的成员可被类的成员函数访问,但不能被使用该类的代码访问。
3.派生类向基类转换的可访问性
//
4.友元关系无法传递或继承。友元对象可访问所在的类的对象成员,即使该成员嵌套在所在类的派生类中。
class Base{
//其他声明与1同
friend class Pal;
};
class Pal{
public:
//正确Pal是Base的友元
int f(Ext s){return s.prot_mem;}
//错误 Pal不是Ext的友元
int f(Ext s){return s.j;}
};
5.using:改变个别成员的可访问性。
class Base{
public:
size_t size() const {return n;}
protected:
size_t n;
};
class Derived:private Base{
public:
using size;
protected:
using Base::n;
};
继承中的类作用域
- 类定义自己的作用域,当存在继承关系时,派生类的作用域嵌套在基类之内。
Bulk_quote bulk;
cout<<bulk.isbn();
我们通过Bulk_quote对象调用isbn()时,编译器首先在Bulk_quote中寻找isbn的定义,这一步找不到isbn,接下来再去Disc_quote,Quote中查找,最后在Quote中找到,isbn被解析为Quote中的isbn。
我们可以再看一个例子。
class Disc_quote :public Quote{
public:
pair<size_t,double> discount_policy() const {
return {quantity,discount};
}
//其他成员与之前版本一致
};
Bulk_quote b;
Bulk_quote *bp = &b;
Quote *qp = &b;
//正确,编译器向上查找,在Bulk_quote的基类Disc_quote中找到discount_policy()
bp->discount_policy();
qp->discount_policy(); //错误
- 名字冲突当派生类存在与基类同名成员时,派生类会隐藏掉外层作用域的名字。我们可以通过作用域运算符覆盖掉查找规则。
struct Base{
protected:
int mem = 666;
}
struct Divert : Base{
public:
Divert(int te):mem(te){}
int get_mem(){ return mem;}
int get_mem_per(){return Base::mem;};
private:
int mem ;
}
//用户代码
Divert d1(22);
cout<<d1.get_mem()<<endl; //返回22
cout<<d1.get_mem_per()<<endl;//返回666 直接使用Base中的mem
- 名字查找优先于类型检查,这意味着内层作用于的函数不会重载声明在外层作用域的函数。
class NO1 {
public:
void fun() { cout << "NO1" << endl; }
};
class NO2 : NO1 {
public:
void fun(int) { cout << "NO2" << endl; }
};
//用户代码
NO2 n;
n.fun(); //错误,参数太少
n.fun(1); //输出NO2;
- 虚函数与作用域,其实跟3都是一样的,注意派生类中与基类中虚函数同名的函数,若二者形参不同,则派生类并未重写基类的该虚函数(但是编译器不会报错,只是我们无法通过基类的指针或引用调用派生类虚函数,因为根本就没有),并且派生类隐藏了基类的虚函数(见3)。
5.覆盖重载的函数派生类可使用using将基类中成员函数的所有重载实例添加到自己的作用域中。
构造函数与拷贝控制
//把第十三章整理完再整理该部分