您的位置:首页 > 编程语言 > C语言/C++

《C++ Primer》读书笔记第十五章-1-OOP概述、定义基类和派生类

2017-12-24 16:59 218 查看
笔记会持续更新,有错误的地方欢迎指正,谢谢!

这是非常重要的内容,笔试面试的时候会经常考察这方面的知识~

面向对象程序设计(OOP)基于三个基本概念:数据抽象(分离接口和实现)、继承(可定义相似的类型)、动态绑定(可在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象)。

原来,我觉得有了类不就可以面向对象了吗?只要把实际中的对象抽象成类,好好设计一下,自己去定义构造函数、拷贝构造函数、重载一些运算、提供一些好的函数工具、注意类类型转换,不就好了吗?

事实上,这种认识是片面的。有了继承和动态绑定,我们在编写面向对象的时候会更加强大。

之前学的类,都是单个类,现在要学在一个已有的类的基础上快捷地编写一个相似的类并使用它。

OOP概述

继承

有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类为派生类。

基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员

继承举例:

书店中不同的书籍定价策略不同,有些书是原价出售,有些打折,还有些买够100本打折,还有些前50名购买的顾客打折。。。

我们来建模:首先我们定义一个名为Quote的类作为基类,它的对象表示按原价出售的书。Quote派生出一个Bulk_quote的类,表示可以打折的书籍。

基类和派生类都将包括下面的两个成员函数:

isbn(),返回书籍的ISBN号,因为该操作不涉及派生类的特殊性,因此定义在基类Quote中。

net_price(size_t),返回书籍的实际销售价格,前提是用户购买的数量达到一定标准。这个操作显然是类型相关的,基类和派生类应都有各自的这个函数。

基类会将这两种函数区别对待:

派生类不做改变直接继承的函数(isbn)

类型相关的函数(net_price)

如何实现类型相关的函数(net_price)?

基类把该函数声明为虚函数(virtual function)。

虚函数定义:

1. 基类希望派生类定义自己版本的函数,声明的返回类型前加virtual。

2. 虚函数在派生类中隐式为虚函数,可不加virtual;

3. 每个派生类中用于覆盖的虚函数,在声明后必须定义;

4. 声明时可在形参列表后加override编译检测。

所以,

Quote类:

class Quote
{
public:
string isbn() const;
virtual double net_price(size_t n) const;
};


类派生列表:冒号后紧跟以逗号隔开的直接基类列表,每个基类前有访问说明符控制访问。

派生类必须通过使用类派生列表(既然是列表,就表明基类可以不止一个)明确指出它是从哪个(哪些)基类继承而来:

class Bulk_quote : public Quote
{
public:
double net_price(size_t) const override;
//override表明这个函数改写自基类的虚函数
}
//当然,这里只写了函数声明,还没定义实现它们。


动态绑定

通过动态绑定,我们就可只用一段代码来分别处理Quote和Bulk_quote的对象:

//已知购买书籍和购买数量,计算并返回总费用
double print_total(osteam &os, const Quote &item, size_t n)
{
double ret = item.net_price(n); //这个item就可以作为动态绑定的依据,是不是很强
return ret;
}


在上述过程中函数的运行版本由实参(item)决定,是在运行时选择函数的版本,所以动态绑定也叫运行时绑定

注意到我们传入的item的方式是引用,因为在C++中,只有当我们使用基类的引用或指针调用一个虚函数时才会发生动态绑定。

定义基类和派生类

定义基类

基类Quote:

class Quote
{
public:
Quote() = default; //强行要求合成默认构造函数
Quote(const string &book, double sale_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类必须包含一个net_price成员:

class Bulk_quote : public Quote
{
public:
Bulk_quote() = default;

//要覆盖基类的成员变量,并可添加新的成员变量
Bulk_quote(const string&, double, size_t, double);

double net_price(size_t) const override;

private:
size_t min_qty = 0; //适用折扣政策的最低购买量
double discount = 0; //以小数表示的折扣
};


派生类中的虚函数

虽然我们经常会覆盖派生类继承的虚函数,不一定要覆盖。

派生类对象

一个派生类对象可以看成由两部分组成:

从基类继承的部分:bookNo, price

自己定义的部分:minqty, discount

派生类向基类的类型转换

简单地说:因为派生类继承了基类,所以用到基类的地方都可以用派生类代替。

派生类构造函数

记住一句话:

派生类构造函数:每个类控制自己成员的初始化,派生类必须使用基类的构造函数初始化基类部分,没有显示初始化的部分都会执行默认初始化。

派生类使用基类的成员

派生类可以访问基类的公有成员和受保护成员。

继承与静态成员

如果基类定义了一个静态成员,那么整个继承体系中只存在静态成员的唯一定义。

派生类的声明

派生类声明中不准出现派生列表:

class Bulk_quote : public Quote //错误
class Bulk_quote; //正确


为什么要这样规定呢?

声明语句的目的
是:让程序知道某个
名字的存在
以及
该名字表示一个什么样的实体
,而派生列表没有这个作用,它是定义的一部分。所以,派生列表以及与定义有关的其他细节必须与类的主体一起出现。

被用作基类的类

如果我们想将某个类作为基类,那么它应该已经定义好了,而不是就声明而已。有一层隐含意思:一个类不能派生它自己。

另外,派生是可以多层的:

class Base{}; //假装定义好了
class D1 : public Base{};
class D2 : public D1{};
//Base是D1的直接基类,是D2的间接基类


防止被继承

类名函数形参列表后加final关键字,可防止其他类再继承它。

class NoDerived final{}; //加个final就OK


类型转换与继承

理解基类和派生类之间的类型转换是理解C++语言面向对象编程的关键所在。

静态类型:

编译时可知,是变量声明时的类型或表达式生成的类型。

动态类型:

运行时才知,是变量或表达式表示的内存中的对象的类型。

例子:

double ret = item.net_price(n);


我们知道item的静态类型是Quote&,但它的动态类型依赖于item绑定的实参,这个是在运行时调用该函数才会知道,比如,传递一个Bulk_quote对象给print_total(见下方代码),则item的静态类型和动态类型将不一致,动态类型为Bulk_quote。

double print_total(osteam &os, const Quote &item, size_t n)
{
double ret = item.net_price(n);
return ret;
}


存在派生类向基类的类型转换

引用、内置指针和智能指针都支持派生类向基类的转换。

不存在从基类向派生类的隐式类型转换

之所以存在派生类向基类的类型转换,是因为每个派生类对象都包含一个基类部分。

因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。

但对于C++来说,可以显示类型转换嘛:

含有虚函数的基类可使用dynamic_cast请求一个类型转换,该转换的安全检查将在运行时执行。如果我们确定某个基类向派生类的转换是安全的,可使用static_cast强制覆盖掉编译器的检查工作。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  c++ c++primer 读书笔记
相关文章推荐