面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定。
继承和动态绑定对程序的编写有两方面的影响:
- 我们可以更容易地定义与其他类相似但不完全相同的类;
- 在使用这些彼此相似的类编写程序时,可以在在一定程度上忽略掉它们的差别。
-
数据抽象:将类的接口与实现分离
-
继承:定义相似的类型并对其相似关系建模
-
动态绑定:在一定程度上忽略相似类型的区别,而以统一的方式使用他们的对象
1. 继承
-
构成一种层次关系
-
**基类( base class)**定义层次关系中所有类共同拥有的成员,**派生类(derived class)**定义各自持有的成员
-
基类希望它的派生类各自定义适合自身的版本,这些函数声明成虚函数(virtual funciton)
类Quote
的对象表示按原价出售的书籍,类Bulk_quote
表示可以打折销售的书籍
class Quote{
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};
派生类必须通过使用**类派生列表(class derivation list)**明确指出它从哪个基类继承而来的
override
显式地注明它将使用哪个成员函数改写基类的虚函数
class Bulk_quote : public Quote{
public:
double net_price(std::size_t n) const override;
};
2. 动态绑定
-
在C++语言中,当使用基类的引用(或指针)调用一个虚函数时将发生动态绑定
-
动态绑定,运行时绑定(run-time binding):函数的运行版本由实参决定,即在运行时选择函数的版本
用同一段代码分别处理Quote
和Bulk_quote
的对象
//计算并打印销售给定数量的某种书籍所得的费用
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() //调用 Quote::isbn
<< "# sold: " << n << "total due: " << ret << endl;
return ret;
}
- 函数
print_total
的item
形参是基类Quote
的一个引用,既能使用基类 Quote 的对象调用该函数,也能使用派生类 Bulk_quote 的对象调用它 print_total
使用引用类型调用net_price
函数,实际传入 print_total 的对象类型将决定到底执行 net_price 的哪个版本
- 基类通常都定义一个虚析构函数
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;
protected:
double price = 0.0;
};
1. 成员函数与继承
在c++语言中,基类必须将它的两种成员函数区分开
- 一种是基类希望其派生类进行覆盖的函数(虚函数)
- 另一种是基类希望派生类直接继承而不要改变的函数
任何构造函数之外
的非静态函数都可以是虚函数
关键字 virtual 只能出现在类内部的声明语句之前而不能用于类外部的函数定义
2. 访问控制与继承
- 派生类能访问公有成员,而不能访问私有成员
- 基类希望它的派生类有权访问该成员,同时禁止其他用户访问,用**受保护的(protected)**访问运算符说明这样的成员
每个基类前面可以有以下三种访问说明符中的一个:public
, protected
, private
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) const override;
private:
std::size_t min_qty = 0;
double discount = 0.0;
};
如果一个派生是公有的,则基类的公有成员也是派生类接口的组成部分。
1. 派生类中的虚函数
-
派生类必须将继承而来的成员函数中需要覆盖的那些重新声明
-
派生类经常(但不总是)覆盖它继承的虚函数
2. 派生类对象及派生类向基类的类型转换
我们能把派生类的对象当成基类对象来使用,也能将基类的指针或引用绑定到派生类对象中的基类部分上
Quote item; //基类对象
Bulk_quote bulk; //派生类对象
Quote *p = &item; //p指向 Quote 对象
p = &bulk; //p 指向 bulk 的 Quote 部分
Quote &r = bulk; //r 绑定到 bulk 的 Quote 部分
-
这种转换称为**派生类到基类的(derived-to-base)**类型转换
-
在派生类对象中含有与基类对应的组成部分,这一事实是继承的关键所在
3. 派生类构造函数
-
派生类必须使用基类的构造函数来初始化它的基类部分
-
每个类控制它自己的成员初始化过程
-
首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员
Bulk_quote(const std::string& book, double p, std::size_t qty, double disc):
Quote(book,p), min_qty(qty), discount(disc){}
4. 派生类使用基类的成员
派生类可以访问基类的公有成员和受保护成员:
double Bulk_quote::net_price(size_t cnt) const
{
if(cnt >= min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
遵循基类的接口
- 每个类负责定义各自的接口,要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此
- 派生类对象不能直接初始化基类的成员
- 通过调用基类的构造函数来初始化哪些从基类中继承而来的成员
5. 继承与静态成员
- 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。
6. 派生类的声明
- 声明中包含类名但是不包含它的派生列表
//class Bulk_quote : public Quote; //错误class Bulk_quote;
7. 被用作基类的类
-
如果想将某个类用作基类,则该类必须已经定义而非仅仅声明
-
一个类不能派生它本身
class Base {/* ... */};
class D1 : public Base {/* ... */};
class D2 : public D1 {/* ... */};
Base 是 D1 的直接基类(direct base),同时是 D2 的间接基类(indirect base)
8. 防止继承的发生
c++11 新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字 final
class NoDerived final {/* */};
- 可以将基类的指针或引用绑定到派生类对象上
- 和内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内
1. 静态类型和动态类型
- 表达式的
静态类型
在编译时总是已知的,它是变量声明时的类型或表达式生成的类型 动态类型
则是变量或表达式表示的内存中的对象的类型,直到运行时才可知- 如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致
- 基类的指针或引用的静态类型可能与动态类型不一致
2. 不存在从基类向派生类的隐式类型转换
- 编译器只能通过检查指针或引用的静态类型来推断该转换是否合法
dynamic_cast
:请求一个类型转换,该转换的安全检查在运行时执行static_cast
:强制覆盖编译器的检查工作
3. 在对象之间不存在类型转换
- 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。
总结:存在继承关系的类型之间的转换规则
- 从派生类向基类的类型转换只对指针或引用类型有效
- 基类向派生类不存在隐式类型转换
- 和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行
- 必须为每一个虚函数都提供定义,而不管它是否被用到了,因为编译器也无法确定会使用哪个虚函数
1. 对虚函数的调用可能在运行时才被解析
- 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才可能与静态类型不同
2. 派生类中的虚函数
- 基类中的虚函数在派生类中隐含地也是一个虚函数
- 当派生类覆盖了某个虚函数时,该函数在基类中地形参必须与派生类中的形参严格匹配
3. final 和 override 说明符
- 在C++11 新标准中我们可以使用
override
关键字来说明派生类中的虚函数,这样做的好处是在使得程序员地意图更加清晰地同时让编译器可以为我们发现一些错误 - 把某个函数指定为
final
,则之后任何尝试覆盖该函数地操作都将引发错误
4. 虚函数与默认实参
- 如果虚函数使用默认实参,则基类和派生类中定义地默认实参最好一致
5. 回避虚函数的机制
- 希望对虚函数地调用不要进行动态绑定,而是强迫其执行虚函数地某个特定版本,使用作用域运算符可以实现这一目的
//强行调用基类中定义的函数版本而不管 baseP 的动态类型是什么
double undiscounted = baseP->Quote::net_price(42);
- 通常只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制
- 如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归
1. 纯虚函数
- 可以将 net_price 定义为纯虚(prue virtual)函数从而令程序实现我们的设计意图,这样可以清晰明了地告诉用户当前这个 net_price 函数是没有实际意义的
- 可以为纯虚函数提供定义,不过函数必须定义在类的外部
- 不能在类的内部为一个 =0 的函数提供函数体
class Disc_quote : public Quote{
public:
Disc_quote() = default;
Disc_quote(const std::string& book, double price, std::size_t qty, 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;
};
2. 含有纯虚函数地类是抽象基类
- 不能创建抽象基类的对象
- 抽象基类的派生类必须给出自己的虚函数定义,否则它们仍将是抽象基类
3. 派生类构造函数只初始化它的直接基类
class Bulk_quote : public Disc_quote{
public:
Bulk_quote() = default;
Bulk_quote(const std::string& book, double price, std::size_t qty, double disc): Disc_quote(book, price, qty, disc){}
double net_price(std::size_t) const override;
};
4. 重构
- 在 Quote 的继承体系中增加 Disc_quote 类是重构(refactoring)的一个典型示例
- 重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中
1. 受保护的成员
-
一个类使用
protected
关键字来声明哪些它希望与派生类分享但是不想被其他公共访问使用的成员 -
派生类的成员或友元只能通过派生类对象来反问基类的受保护成员
-
派生类的成员或友元只能访问派生类对象中的基类部分的受保护成员,对于普通的基类对象中的成员不具有特殊的访问权限
2. 公有、私有和受保护继承
- 对于基类的访问权限只与基类中的访问说明符有关
- 派生类说明符可以控制继承自派生类的新类的访问权限
3. 派生类向基类转换的可访问性
对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的
4. 友元与继承
不能继承友元关系,每个类负责控制各自成员的访问权限
5. 改变个别成员的可访问性
- 通过在类的内部使用
using
声明语句,可以将该类的直接或间接基类中的任何可访问成员(如,非私有成员)标记出来 - 派生类只能为那些它可以访问的名字提供
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;
};
6. 默认的继承保护级别
- 默认情况下,使用
class
关键字定义的派生类是私有继承的;而使用struct
关键字定义的派生类是公有继承的 - 一个私有派生的类最好显示的将
private
声明出来,而不要仅仅依赖于默认的设置,显式声明的好处是可以令私有继承关系清晰明了,不至于产生误会
- 派生类的作用域嵌套在其基类的作用域之内
- 如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找
1. 在编译时进行名字查找
一个对象、引用或指针的静态类型决定了该对象的那些成员是可见的
2. 名字冲突与继承
派生类的成员将隐藏同名的基类成员
struct Base{
Base() : mem(0){}
protected:
int mem;
};
struct Derived : Base{
Derived(int i) : mem(i){}
int get_mem() {return mem;}
protected:
int mem;
};
Derived d(42);
cout << d.get_mem() << endl;//42
3. 通过作用域运算符使用隐藏的成员
struct Derived : Base{
int get_mem() {return Base::mem;}
};//0
- 除覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字
4. 名字查找先于类型查找
-
如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员
-
即使派生类成员和基类成员的形参列表不一致,基类成员也会被隐藏掉
struct Base{
int memfcn();
};
struct Derived : Base{
int memfcn(int);
};
Derived d;
Base b;
b.memfcn(); // 调用Base::memfcn
d.memfcn(10); // 调用Derived::memfcn
//d.memfcn();
d.Base::memfcn();// 调用Base::memfcn
5. 虚函数与作用域
- 假如基类与派生类的虚函数接受的实参不同,则无法通过基类的引用或指针调用派生类的虚函数
6. 使用基类调用隐藏的虚函数
7. 覆盖重载的函数
- 如果派生类希望所有的重载版本对于他来说都是可见的,就需要覆盖所有的版本,或者一个也不覆盖
- 解决方案:为重载的成员提供一条 using 声明语句
- using 声明语句指定一个名字而不指定形参列表
- 一条基类成员函数的 using 声明语句就可以把该函数的所有重载实例添加到派生类作用域中
todo
todo
todo