C++Primer 第十五章 面向对象程序设计

第十五章 面向对象程序设计

15.1 OOP:概述

面向对象程序设计核心思想:

  • 数据抽象:将类的接口与实现分离
  • 继承:可以定义相似的类
  • 动态绑定:忽略相似类的区别,以统一的方式使用对象

继承

通过继承构建其一种层次关系。层次关系的根部是基类,其他由基类继承而来的类叫做派生类。基类定义层次关系中所有类共同拥有的成员,派生类定义各自特有的成员。
某些函数基类希望派生类定义自己的版本,基类将这些函数声明成虚函数。

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

class Bulk_quote : public Quote {
public:
  double net_price(std::size_t) const override;
};

类派生类使用派生列表指出它是从哪个基类派生出来的,类派生列表(类名:派生基类1,派生基类2,)。

动态绑定

double print_total(std::ostream &os, const Quote &item, std::size_t n) {
  double ret = item.net_price(n);
  os << "ISBN: " << item.isbn() << "# sold: " << n << "total due: " << ret << std::endl;
  return ret;
}
Quote basic;
Bulk_quote bulk;
print_total(cout, basic, 20); //调用Quote的net_price
print_total(cout, bulk, 20);  //调用Bulk_quote的net_price

根据上面的代码,传递的形参是Quote类型,但是我们既可以使用基类的对象调用net_price函数,也可以使用派生类调用该函数,根据传递的实参类型具体决定。

15.2 定义基类和派生类

15.2.1 定义基类

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;
};
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;
};

基类通常都会定义虚析构函数,无论这个虚析构函数是否执行实际操作。

访问控制与继承

派生类的成员函数能够访问基类的公有成员,但是无法访问私有成员。如果基类想让派生类访问一些成员,但禁止其他用户访问,使用受保护的(protected)运算符来说明这样的成员。

15.2.2 定义派生类

类派生列表的形式:首先是冒号,后面紧跟逗号分隔的基类列表,每个基类前面有三种访问说明符:public,protected,private,派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。

派生类对象及派生类向基类的类型转换

派生类对象的组成部分:派生类自己定义的成员的子对象以及该派生类继承的基类对应的子对象。因此Buli_quote的对象包括自己定义的:min_qtydiscount,从Quote继承而来的bookNoprice
派生类的对象中含有基类的部分,因此基类的指针和引用也能绑定到派生类对象中的基类部分。

Quote item;
Bulk_quote bulk;
Quote *p = &item; //p指向Quote对象
p = &bulk;  //p指向bulk的Quote部分
Quote &r = bulk;  //r绑定到bulk的Quote部分

派生类构造函数

每个类控制自己成员的初始化过程,派生类无法初始化从基类继承而来的成员,必须使用基类的构造函数执行初始化。

Bulk_quote(const std::string& book, double p, std::size_t qty, double disc) : 
    Quote(book, p), min_qty(qty), discount(disc) {}

派生类自己的数据成员和基类的部分都是在初始化阶段完成初始化操作,首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的部分。

继承与静态成员

基类定义了一个静态成员,在整个继承体系中只存在该成员的唯一定义,每个静态成员都只存在唯一实例。

派生类的声明

class Bulk_quote : public Quote;  //error
class Bulk_quote; //right,与一般类的声明相同

被用作基类的类

如果某个类被用作基类,该类必须被定义而非声明。一个类无法派生自己,同时继承具有传递性。

防止继承的发生

如果一个类不希望其他类继承,再类名后面加上final关键字。

class NoDerived final { /**/ };
class Base { /**/ };
class Last final : Base { /**/ }; //Last不能作为基类
class Bad : NoDerived { /**/ }; //error
class Bad2 : Last { /**/ }; //error

静态类型与动态类型

静态类型:在编译时已知,变量声明时的类型或表达式生成的类型。
动态类型:运行时才可知,变量或表达式表示的内存中的对象的类型。

Quote item;
double ret = item.net_price(n);

item的静态类型是Quote,但是动态类型依赖于绑定的实参,动态类型直到运行时调用该函数时才会知道。如果表达式既不是引用也不是指针,它的动态类型与静态类型永远一致。

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

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

Quote base;
Bulk_base* bulkP = &base; //error,不能将基类转换为派生类
Bulk_base& bulkRef = base;  //error

基类指针或引用绑定一个派生类,也不能执行基类向派生类的转换。

Bulk_quote = bulk;
Quote *itemp = &bulk; //right,动态类型是Bulk_quote
Bulk_quote *bulkP = itemP;  //error,基类不能转换成派生类

对象之间不存在类型转换

当我们使用基类的构造函数或拷贝赋值函数给一个派生类初始化时,实际上调用的是基类自己的构造函数。

Bulk_quote bulk;
Quote item(bulk); //调用Quote::Quote(const Quote&)构造函数,但是只能初始化二者共有的部分,bulk自己特有的成员无法初始化
item = bulk;  //调用Quote::operator=(const Quote&)

用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,派生类部分会被忽略掉

15.3虚函数

对虚函数的调用可能在运行时才被解析

动态绑定只有当我们使用指针或引用调用虚函数时才会发生。不同类型的表达式调用虚函数时,在编译阶段就会被确定下来。

当派生类覆盖了某个虚函数时,该函数在基类中的形参必须参与派生类中的形参严格匹配。

finaloverride说明符

struct B
{
  /* data */
  virtual void f1 (int) const;
  virtual void f2();
  void f3();
};

struct D1 : B
{
  /* data */
  void f1(int) const override;  //right,f1与基类中的f1完全匹配
  void f2(int) override;  //error,基类中的f2没有int成员
  void f3() override; //error,f3不是虚函数
  void f4() override; //error
};

struct D2 : B
{
  /* data */
  void f1(int) const final; //不允许后续的其他类覆盖f1(int)
};

struct D3 : D2
{
  /* data */
  void f2();  //right,覆盖从基类继承来的f2
  void f1(int) const; //error,D2将f2声明成final
};

虚函数使用默认实参,基类和派生类中定义的默认实参最好一致

回避虚函数机制

使用作用域运算符强制调用派生类中的基类版本的虚函数。

double undiscounted = baseP->Quote::net_price(42);

此时无论baseP是什么类型,都会调用Quote版本的虚函数。如果一个虚函数需要调用它的基类版本,但是没有使用作用域运算符,会导致无限递归。

15.4 抽象基类

通过在形参列表后面加上=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;
};
class Bulk_quote : public Disc_quote {
public:
  Bulk_quote() = default;
  //不允许使用间接非虚拟基类
  Bulk_quote(const std::string& book, double p, std::size_t qty, double disc) : 
    Disc_quote(book, p, qty, disc){}
  double net_price(std::size_t) const override;
private:
  std::size_t min_qty = 0;
  double discount = 0.0;
};

15.5 访问控制与继承

受保护的成员

使用protected关键字来声明与派生类共享,但不想被其他公共访问的成员。派生类的成员或友元只能通过派生类对象来访问基类受保护的成员,派生类对于基类中受保护的成员没有任何特权。


class Base {
protected:
  int prot_mem;
};

class Sneaky : public Base {
  friend void clobber(Sneaky &);
  friend void clobber(Base &);
  int j;
};

//right
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }

//error,无法访问Base中受保护的成员。
void clobber(Base &b) { b.prot_mem = 0; }

公有、私有和受保护继承

类对其继承而来的成员的访问权限受到两个因素影响:

  1. 基类中该成员的访问说明符。
  2. 派生类的派生列表中的访问说明符
class Base {
public:
  void pub_mem();
protected:
  int prot_mem;
private:
  char priv_mem;
};

struct Pub_Derv : public Base {
  int f() { return  prot_mem; }   //right,可以访问受保护成员
  //error,private成员对于派生类无法访问
  char g() { return priv_mem; }
};

//私有继承,继承而来的成员都是私有的
struct Priv_Derv : private Base {
  //right,private不影响派生类访问权限
  int f1() const { return prot_mem; }
};

struct Derived_from_Public : public Pub_Derv {
  int use_base() { return prot_mem; }
};

struct Derived_from_Private : public Priv_Derv
{
  /* error, Base::prot_mem在Priv_Derv中是private的 */
  int use_base() { return prot_mem; }
};

Pub_Derv d1;
Priv_Derv d2;
d1.pub_mem(); //right,pub_mem在派生类中是public
d2.pub_mem(); //error,pub_mem在派生类中时Private 
return 0;

派生类向基类转换的可访问性

  • D公有的继承B时,用户代码才能使用派生类向基类的转换,protectedprivate不行
  • 不论D以何种方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说都是可访问的。
  • 如果D继承B的方式是protectedpublic的,D的派生类的成员和友元可以使用D向B的类型转换。

友元与继承

友元关系不能传递也不能继承,每个类负责控制自己的成员的访问权限。

改变个别成员的可访问性

使用using声明来改变派生类继承的某个名字的访问级别。

class Base {
public:
  std::size_t size() const { return n; }
protected:
  std::size_t n;
};

class Derived : private Base {
public:
  using Base::size;
protected:
  using Base::n;
};

Derived是私有继承,继承而来的成员都是私有成员。使用using语句来改变成员的可访问性,声明之后的成员的访问性有前面的访问说明符来决定。

默认的继承保护级别

如果没有类型说明符,class默认为私有继承,struct默认为公有继承。

class Base { /**/};
struct D1 : Base { }; //默认public继承
class D2 : Base { };  //默认private继承

15.6 继承中的类作用域

类的作用域是按照继承的嵌套关系进行。

Bulk_quote bulk;
cout << bulk.isbn();

现在Bulk_quote中查找isbn函数,找不到再从Disk_quote(Bulk_quote的直接基类),再找不到在Quote中查找。按照继承关系回溯查找。

在编译时进行名字查找

class Quote {
public:
  int price;
};

class Disc_quote : public Quote {
public:
  void discount_policy() const {
    std::cout << "yes" << std::endl;
  }
};

class Bulk_quote : public Disc_quote {
};

Bulk_quote bulk;
Bulk_quote *bulkP = &bulk;  //静态类型与动态类型一样
Quote *itemP = &bulk;       //静态类型与动态类型不一样
bulkP->discount_policy();
itemP->discount_policy(); //error

使用哪些成员由静态成员来决定,itemPQuote类型,在编译时先在Quote中查找有没有discount_policy()成员函数。

名字冲突与继承

内层作用域(派生类)会隐藏外层作用域(基类)的名字。

struct D {
  Base() : mem(0) {}
protected:
  int mem;
};
struct B : D {
  B(int i) : mem(i) {}  //i初始化的是B中的mem
protected:
  int mem;  //隐藏基类的meme
}

可以使用作用域运算符来使用隐藏的成员。


class Base {
public:
  virtual int fcn();
};

class D1 : public Base {
public:
  //隐藏基类的fcn,且这个fcn不是虚函数,形参列表与Base的不同,但是D1继承了Base::fcn的定义
  int fcn(int);
  virtual void f2();
};

class D2 : public D1 {
public:
  int fcn(int); //非虚函数,隐藏了D1的fcn
  int fcn();  //覆盖了Base的虚函数fcn
  void f2();  //覆盖了D1的虚函数f2
};

Base bobj;
D1 d1obj;
D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); //虚调用,调用Base::fcn
bp2->fcn(); //虚调用,调用Base::fcn
bp3->fcn(); //调用D2::fcn
D1 *d1p = &d1obj; D2 *d2p = &d2obj;
bp2->f2();  //error,bp2为Base,Base没有f2成员
d1p->f2();  //虚调用,调用D1::f2()
d2p->f2();  //调用D2::f2()
Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj;
p1->fcn(42);  //error,base没有接受int的fcn
p2->fcn(42);  //静态绑定,调用的都是非虚函数,不会产生动态绑定
p3->fcn(42);  //静态绑定,调用D2::fcn(int)

15.7 构造函数与拷贝控制

15.7.1 虚析构函数

当静态类型和动态类型不同时,比如指针是Quote*类型但是实际上指向的是Bulk_quote类型,如果使用delete删除指针时,就需要让编译器知道执行正确的析构函数。在基类中将析构函数定义成虚函数确保执行正确的析构函数版本。


class Quote {
public:
  virtual ~Quote() = default;
};

Quote *itemP = new Quote; //静态类型与动态类型一致
delete itemP; //调用Quote的析构函数
itemP = new Bulk_quote; //静态类型与动态类型不一致
delete itemP; //调用Bulk_quote的析构函数

虚析构函数将阻止合成移动操作。

15.7.2 合成拷贝控制与继承

派生类中删除的拷贝控制与基类的关系

定义基类的方式导致派生类成员成为被删除的函数:

  • 基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的,因为编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
  • 基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,应为编译器无法销毁派生类对象的基类部分。
  • 编译器不会合成一个删除掉的移动操作。
class B {
public:
  B();
  B(const B&) = delete;
};
class D : public B {
  //没有声明任何构造函数
};
D d;  //right,D的默认构造函数使用B的默认构造函数
D d2(d);  //error:D的合成拷贝构造函数是被删除的
D d3(std::move(d)); //error:隐式的使用D的被删除的拷贝构造函数

B显式的定义了构造函数,编译器不会为B合成移动构造函数。如果派生类D想自己的对象能被移动和拷贝,需要定义自己的,同时还需要考虑从B继承来的成员。如果基类中没有默认、拷贝或移动构造函数,一般情况下派生类不会定义相应的操作。

移动操作与继承

基类缺少移动操作会阻止派生类拥有自己的合成移动操作,如果派生类确实需要移动操作,首先在基类中定义。Quote可以使用合成的版本,不过前提是Quote必须显式地定义这些成员。一旦Quote定义了自己的移动操作,必须同时显式地定义拷贝操作。

class Quote {
public:
  Quote() = default;  //对成员进行默认初始化
  Quote(const Quote&) = default;  //对成员依次拷贝
  Quote(Quote &&) = default;  //对成员依次拷贝
  Quote& operator=(const Quote&) = default; //拷贝赋值
  Quote& operator=(Quote &&) = default; //移动赋值
  virtual ~Quote() = default;
};

15.7.3 派生类的拷贝控制成员

派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象

定义派生类的拷贝或移动构造函数

当为派生类定义拷贝或移动构造函数时,使用对应的基类构造函数初始化对象的基类部分。

class Base { /**/ };
class D : public Base {
public:
  D(const D& d) : Base(d) {}
  D(D&& d) : Base(std::move(d)) {}
};

Base(d)D类型的对象d将被绑定到该构造函数的Base&形参,Base的拷贝构造函数负责将D的基类部分拷贝给要创建的对象。

派生类赋值运算符

派生类赋值运算符必须显式地为基类部分赋值。

D &D::operator=(const D &rhs) {
  Base::operator=(rhs);
  return *this;
}

派生类析构函数

派生类析构函数只负责销毁派生类自己分配的资源。

class D : public Base {
public:
  //Base::~Base被自动调用执行
  ~D();
};

对象销毁的顺序和创建的顺序正好相反,先执行派生类的析构函数,在执行基类的析构函数,沿着继承体系反方向执行。

15.7.4 继承的构造函数

派生类使用using语句继承基类的构造函数。


class Bulk_quote : public Disc_quote {
public:
  using Disc_quote::Disc_quote;
  double net_price(std::size_t) const;
};

继承而来的构造函数等价于

Bulk_quote(const std::string& book, double price, std::size_t qty, double disc) : 
          Disc_quote(book, price, qty, disc) {}

15.8 容器与继承

派生类向基类自动转换只对指针和引用类型有效,派生类和基类之间无法自动转化,因此一个容器无法直接存取具有继承体系的对象。容器中存取的是智能指针,而非对象。


std::vector<std::shared_ptr<Quote>> basket;
basket.push_back(std::make_shared<Quote>("0-201-82470-1", 50));
basket.push_back(std::make_shared<Bulk_quote>("0-201-54848-8", 50, 10, 25));
std::cout << basket.back()->net_price(15) << std::endl;

上一篇:FLOPS


下一篇:C++格式标识和操纵器