C++ Pirmer第十五章①
C++ Primer
面向对象程序设计
这是时下非常流行的内容,要好好学习哦,笔试面试的时候会经常考察这方面的知识。
面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定。我们在第七章已经介绍了数据抽象的知识,就是那个类,这一章我们要学习剩下的两个:继承和动态绑定。
开始的时候,我觉得有了类不就可以面向对象了吗?只要把实际中的对象抽象成类,好好设计一下,自己去定义构造函数,拷贝构造函数,重载一些运算,提供一些好的函数工具,注意类类型转换不就好了吗?
事实证明,我还是too
naive了,有了继承和动态绑定,我们在编写面向对象的时候会更加强大。具体说呢,继承和动态绑定对程序的编写有两方面的影响:一是我们可以更容易地定义与其他类相似但不完全相同的新类;二是在使用这些彼此相似的类编写程序时,我们可以在一定程度上忽略它们的差别。
我们之前学习的类的知识基本是单个类的,现在我们要学如何在已有一个类的基础上方便地编写一个相似的类,并且我们如何使用它们。
OOP:概述
OOP(object-oriented programming):
- 通过使用数据抽象,我们可以将类的接口与实现分离
- 使用继承,可以定义相似的类型,并且对其相似关系建模
- 使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
继承
通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
我们来把之前的书店类复杂一下作为例子:现在我们书店中不同的书籍定价策略不一样了,有些书是原价出售,有些打折,还有些买够100本打折,还有些前50名购买的顾客打折
我们来建模:首先我们定义一个名为Quote的类作为基类,它的对象表示按原价出售的书。Quote派生出一个Bulk_quote的类,表示可以打折的书籍。
这些类(基类和派生类)将包括下面的两个成员函数:
- isbn(),返回书籍的ISBN号,因为该操作不涉及派生类的特殊性,因此定义在基类Quote中
- net_price(size_t),返回书籍的实际销售价格,前提是用户购买的数量达到一定标准。这个操作显然是类型相关的,基类和派生类应该都要有各自的该函数
在C++语言中,基类会将两种函数区分对待:
- 类型相关的函数(net_price)
- 派生类不做改变直接继承的函数(isbn)
对于某些函数,基类希望它的派生类各自定义自己合适的版本,此时基类就把这些函数声明为虚函数(virtual function),所以,我们可以这么写Quote类:
class Quote
{
public:
string isbn() const;
virtual double net_price(size_t n) const;
};
派生类必须通过使用类派生列表(既然是列表,就可以不止一个)明确指出它是从哪个(哪些)基类继承而来:
class Bulk_quote : public Quote
//通过public的方式继承了Quote,可以把Bulk_quote的对象当成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就可以作为动态绑定的依据,是不是很6啊
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;
}
成员函数与继承
- 任何构造函数之外的非静态函数都可以是虚函数
- 关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义
- 如果基类把一个函数声明为虚函数,那么该函数在派生类中隐式地也是虚函数(可以在派生类的派生类中继续override)
定义派生类
类派生列表前面的访问说明符有三种:public、protected和private(之后详细介绍)
派生类必须将其继承而来的成员函数中需要覆盖的,进行重新声明,因此啊,我们的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 青出于蓝而胜于蓝
因为在派生类对象中含有与基类对应的组成部分,所以啊,我们可以把派生类的对象当成基类对象使用(只使用基类部分不就好咯),我们就可以把基类的指针或引用绑定到派生类对象中的基类部分上:
//派生类到基类的类型转换(编译器会隐式执行)
Quote item;
Bulk_quote bulk;
Quote *p = &item;
p = &bulk;
Quote &r = bulk;
所以啊,我们可以把派生类或者派生类对象的引用用在需要基类引用的地方;也可以把派生类对象的指针用在需要基类指针的地方。
其实总结就是,因为派生类继承了基类,所以用到基类的地方都可以用派生类代替
派生类构造函数
每个类控制它自己的成员初始化过程,记住这句话:
尽管派生类对象中有从基类继承而来的成员,但是派生类不能直接初始化这些成员,必须调用基类的构造函数来初始化它的基类部分
派生类构造函数也可以通过构造函数初始化列表来将实参传递给基类构造函数的:
Bulk_quote(const string &book, double p, size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc){} //里面还调用了基类的构造函数哦
首先初始化基类部分,然后按照声明的顺序依次初始化派生类的成员
派生类使用基类的成员
派生类可以访问基类的公有成员和受保护成员:
double Bulk::net_price(size_t cnt) const
{
if(cnt >= min_qty)
{
return cnt * (1-discount) * price; //price是基类的protected成员,可以直接访问
}
else
{
return cnt * price;
}
}
我们要遵循基类的接口:
尽管我们可以在派生类构造函数体内给基类的public和protected赋值,但我们不建议这么做,派生类应该遵循基类的接口。
继承与静态成员
如果基类定义了一个静态成员,那么在整个继承体系中只存在该成员的唯一定义;不论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例
class Base
{
public:
static void statmem();
};
class Derived : public Base
{
void f(const Derived&);
};
void Derived::f(const Derived &obj)
{
//下面四种访问方式都对
Base::statmem();
Derived::statmem();
obj.statmem();
stamem();
}
派生类的声明
派生类声明中不准出现派生列表:
class Bulk_quote : public Quote //错误
class Bulk_quote; //正确
为什么要这样规定呢?
声明语句的目的是:让程序知道某个名字的存在以及该名字表示一个什么样的实体,而派生列表没有这个作用,它是定义的一部分,所以啊,派生列表以及与定义有关的其他细节必须与类的主体一起出现
被用作基类的类
如果我们想将某个类作为基类,那么它应该已经定义好了,而不是就声明而已,这个规定很容易理解,不过它还有一层隐含意思,一个类不能派生它本身
派生是可以多层的:
class Base{}; //假装定义好了
class D1 : public Base{};
class D2 : public D1{};
Base是D1的直接基类,是D2的间接基类
断子绝孙类:防止继承的发生
我们有时会有这样的需求,定义一个类,不希望它被继承:
class NoDerived final{}; //加个final就好了
#C++工程师#