《C++Primer》第十五章-面向对象编程-学习笔记(1)

《C++Primer》第十五章-面向对象编程-学习笔记(1)

日志:
1,2020-03-07 笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第八章-标准 IO 库-学习笔记(1)
《C++Primer》第十二章-类-学习笔记(1)

摘要

面向对象编程(object-oriented programming)基于三个基本概念:数据抽象继承动态绑定
在 C++ 中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员。
动态绑定(dynamic binding)使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。
继承和动态绑定在两个方面简化了我们的程序:

  • 能够容易地定义与其他类相似但又不相同的新类
  • 能够更容易地编写忽略这些相似类型之间区别的程序

许多应用程序的特性可以用一些相关但略有不同的概念来描述。例如,书店可以为不同的书提供不同的定价策略,有些书可以只按给定价格出售,另一些书可以根据不同的折扣策略出售。可以给购买某书一定数量的顾客打折,或者,购买一定数量以内可以打折而超过给定限制就付全价。
面向对象编程(Object-oriented programming,OOP)与这种应用非常匹配。
通过继承可以定义一些类型,以模拟不同种类的书,通过动态绑定可以编写程序,使用这些类型而又忽略与具体类型相关的差异。

面向对象编程:概述

面向对象编程的关键思想是多态性(polymorphism)。多态性派生于一个希腊单词,意思是“许多形态”。之所以称通过继承而相关联的类型为多态类型,是因为在许多情况下可以互换地使用派生类型或基类型的“许多形态”。在 C++ 中,多态性仅用于通过继承而相关联的类型的引用或指针。

继承

继承(inheritance)使我们能够定义这样的类,它们对类型之间的关系建模,共享公共的东西,仅仅特化本质上不同的东西。
派生类(derived class)能够继承基类(base class)定义的成员,派生类可以无须改变而使用那些与派生类型具体特性不相关的操作,派生类可以重定义那些与派生类型相关的成员函数,将函数特化,考虑派生类型的特性。最后,除了从基类继承的成员之外,派生类还可以定义更多的成员。
我们经常称因继承而相关联的类为构成了一个继承层次(inheritance hierarchy)。其中有一个类称为,所以其他类直接或间接继承根类
在书店例子中,我们将定义一个基类,命名为 Item_base,命名为 Bulk_item,表示带数量折扣销售的书。这些类至少定义如下操作:
• 名为 book 的操作,返回 ISBN。
• 名为 net_price 的操作,返回购买指定数量的书的价格。
Item_base 的派生类将无须改变地继承 book 函数:派生类不需要重新定义获取 ISBN 的含义。另一方面,每个派生类需要定义自己的 net_price 函数版本,以实现适当的折扣价格策略。
C++ 中基类必须指出希望派生类重写哪些函数,定义为 virtual 的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
讨论过这些之后,可以看到我们的类将定义三个(const)成员函数:
• 非虚函数 std::string book(),返回 ISBN。由 Item_base 定义,Bulk_item 继承。
• 虚函数 double net_price(size_t) 的两个版本,返回给定数目的某书的总价。Item_base 类和 Bulk_item 类将定义该函数自己的版本。

动态绑定

动态绑定(dynamic binding)使我们能够编写程序使用继承层次中任意类型的对象,无须关心对象的具体类型。使用这些类的程序无须区分函数是在基类还是在派生类中定义的。
例如,书店应用程序可以允许顾客在一次交易中选择几本书,当顾客购书时,应用程序可以计算总的应付款,指出最终账单的一个部分将是为每本书打印一行,以显示总数和售价。可以定义一个名为 print_total 的函数管理应用程序的这个部分。
给定一个项目和数量,函数应打印 ISBN 以及购买给定数量的某书的总价。这个函数的输出应该像这样:

ISBN: 0-201-54848-8 number sold: 3 total price: 98
ISBN: 0-201-82470-1 number sold: 5 total price: 202.5

可以这样编写 print_total 函数:

// calculate and print price for given number of copies, applying any discounts
void print_total(ostream &os,const Item_base &item, size_t n) //第二形参是 Item_base 的引用但可以将 Item_base对象或 Bulk_item 对象传给它。
{
	os << "ISBN: " << item.book() // calls Item_base::book
	<< "\tnumber sold: " << n << "\ttotal price: "
// virtual call: which version of net_price to call isresolved at run time
	<< item.net_price(n) << endl; //net_price 是虚函数,所以对 net_price 的调用将在运行时确定
}

该函数的工作很普通:调用其 item 形参的 book 和 net_price 函数,打印结果。关于这个函数,有两点值得注意。
第一,虽然这个函数的第二形参是 Item_base 的引用但可以将 Item_base对象或 Bulk_item 对象传给它。
第二,因为形参是引用且 net_price 是虚函数,所以对 net_price 的调用将在运行时确定。调用哪个版本的 net_price 将依赖于传给 print_total 的实参。如果传给 print_total 的实参是一个 Bulk_item 对象,将运行 Bulk_item中定义的应用折扣的 net_price;如果实参是一个 Item_base 对象,则调用由
Item_base 定义的版本。

在 C++ 中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用 的虚函数在运行时确定,被调用的函数是引用(或指针)所指 对象的实际类型所定义的。

//总结一下就是先定义了虚函数,然后派生类继承虚函数,重新定义它。然后使用的时候,通常情况下是在形参中传入一个对象,根据这个对象看使用哪个函数。

定义基类和派生类

在继承层次中定义类还需要另外一些特性。这些特性的使用对类以及使用继承类编写的程序有一些影响。

定义基类

像任意其他类一样,基类也有定义其接口和实现的数据和函数成员。在(非常简化的)书店定价应用程序的例子中,Item_base 类定义了 book 和net_price 函数并且需要存储每本书的 ISBN 和标准价格:

// Item sold at an undiscounted price
// derived classes will define various discount strategies
class Item_base {  //这个是我们用来作为例子的基类
public:
	Item_base(const std::string &book = "",double sales_price = 0.0):isbn(book), price(sales_price) { }
	std::string book() const { return isbn; }
// returns total sales price for a specified number of items
// derived classes will override and apply different discount algorithms
virtual double net_price(std::size_t n) const{ return n * price; }  //基类中的虚函数
virtual ~Item_base() { }
private:
	std::string isbn; // identifier for the item
protected:
	double price; // normal, undiscounted price继承层次的根类一般都要定义虚析构函数
};

这个类的大部分看起来像我们已见过的其他类一样。它定义了一个构造函数以及我们已描述过的函数,该构造函数使用默认实参,允许用 0个、1 个或两个实参进行调用,它用这些实参初始化数据成员。
新的部分是 protected 访问标号以及对析构函数和 net_price 函数所使用的保留字 virtual。之后解释虚析构函数,现在只需注意到继承层次的根类一般都要定义虚析构函数即可。

基类成员函数

Item_base 类定义了两个函数,其中一个前面带有保留字 virtual。保留字virtual 的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定。为了指明函数为虚函数,在其返回类型前面加上保留字 virtual。除了构造函数之外,任意非 static 成员函数都可以是虚函数保留字只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。

基类通常应将派生类需要重定义的任意函数定义为虚函数。

访问控制和继承

在基类中,public 和 private 标号具有普通含义:用户代码可以访问类的public 成员而不能访问 private 成员,private 成员只能由基类的成员和友元访问。派生类对基类的 public 和 private 成员的访问权限与程序中任意其他部分一样:它可以访问 public 成员而不能访问 private 成员。
有时作为基类的类具有一些成员,它希望允许派生类访问但仍禁止其他用户访问这些成员。对于这样的成员应使用受保护的访问标号protected 成员可以被派生类对象访问但不能被该类型的普通用户访问。
我们的 Item_base 类希望它的派生类重定义 net_price 函数,为了重定义net_price 函数,这些类将需要访问 price 成员。希望派生类用与普通用户一样通过 book 访问函数访问 isbn,因此,isbn 成员为 private,不能被Item_base 的继承类所访问。

protected 成员

可以认为 protected 访问标号是 private 和 public 的混合:

  • 像 private 成员一样,protected 成员不能被类的用户访问。
  • 像 public 成员一样,protected 成员可被该类的派生类访问。
    此外,protected 还有另一重要性质:
  • 派生类只能通过派生类对象访问其基类的 protected 成员,派生类对其基类类型对象的 protected 成员没有特殊访问权限

例如,假定 Bulk_item 定义了一个成员函数,接受一个 Bulk_item 对象的引用和一个 Item_base 对象的引用,该函数可以访问自己对象的 protected 成员以及 Bulk_item 形参的 protected 成员,但是,它不能访问 Item_base 形参的 protected 成员。

void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b)
{
// attempt to use protected member
double ret = price; // ok: uses this->price
ret = d.price; // ok: uses price from a Bulk_item object
ret = b.price; // error: no access to price from an Item_base派生类对其基类类型对象的 protected 成员没有特殊访问权限
}

d.price 的使用正确,因为是通过 Bulk_item 类型对象引用 price;b.price 的使用非法,因为对 Base_item 类型的对象没有特殊访问访问权限。

派生类

为了定义派生类,使用类派生列表指定基类。类派生列表指定了一个或多个基类,具有如下形式:

class classname: access-label base-class

这里 access-label 是 public、protected 或 private,base-class 是已定义的类的名字。类派生列表可以指定多个基类。继承单个基类是为常见。现在,只需要了解访问标号决定了对继承成员的访问权限。如果想要继承基类的接口,则应该进行public 派生。
派生类继承基类的成员并且可以定义自己的附加成员。每个派生类对象包含两个部分:从基类继承的成员自己定义的成员。一般而言,派生类只(重)定义那些与基类不同或扩展基类行为的方面。

定义派生类

在书店应用程序中,将从 Item_base 类派生 Bulk_item 类,因此Bulk_item 类将继承 book、isbn 和 price 成员。Bulk_item 类必须重定义net_price 函数定义该操作所需要的数据成员:

// discount kicks in when a specified number of copies of same book are sold
// the discount is expressed as a fraction used to reduce the normal price
class Bulk_item : public Item_base {
public:
// redefines base version so as to implement bulk purchase discount policy
double net_price(std::size_t) const;
private:
std::size_t min_qty; // minimum purchase for discount to apply
double discount; // fractional discount to apply
};

每个 Bulk_item 对象包含四个数据成员:从 Item_base 继承的 isbn 和price,自己定义的 min_qty 和 discount,后两个成员指定最小数量以及购买超过该数量时给的折扣。Bulk_item 类还需要定义一个构造函数。

派生类和虚函数

尽管不是必须这样做,派生类一般会重定义所继承的虚函数。派生类没有重定义某个虚函数,则使用基类中定义的版本。
派生类型必须对想要重定义的每个继承成员进行声明。Bulk_item 类指出,它将重定义 net_price 函数但将使用 book 的继承版本。
派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
例如,Item_base 类可以定义返回 Item_base* 的虚函数,如果这样,Bulk_item 类中定义的实例可以定义为返回 Item_base* 或 Bulk_item*。
一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用 virtual 保留字,但不是必须这样做。

派生类对象包含基类对象作为子对象

派生类对象由多个部分组成:派生类本身定义的(非 static)成员加上由基类(非 static)成员组成的子对象。可以认为 Bulk_item 对象由图 1 表示的两个部分组成。
在这里插入图片描述
图1. Bulk_item 对象的概念结构
C++ 语言不要求编译器将对象的基类部分和派生部分和派生部分连续排列,因此,图 15.1 是关于类如何工作的概念表示而不是物理表示。

派生类中的函数可以使用基类的成员

像任意成员函数一样,派生类函数可以在类的内部或外部定义,正如这里的net_price 函数一样:

// if specified number of items are purchased, use discounted price
double Bulk_item::net_price(size_t cnt) const
{
if (cnt >= min_qty)
	return cnt * (1 - discount) * price;
else
	return cnt * price;  //price是基类的protected成员
}

该函数产生折扣价格:如果给定数量多于 min_qty,就对 price 应用 discount(discount 存储为分数)。因为每个派生类对象都有基类部分,类可以访问共基类的public 和 protected 成员,就好像那些成员是派生类自己的成员一样

用作基类的类必须是已定义的

已定义的类才可以用作基类。如果已经声明了 Item_base 类,但没有定义它,则不能用 Item_base 作基类:

class Item_base; // declared but not defined
// error: Item_base must be defined
class Bulk_item : public Item_base { ... };

这一限制的原因应该很容易明白:每个派生类包含并且可以访问其基类的成员,为了使用这些成员,派生类必须知道它们是什么。这一规则暗示着不可能从类自身派生出一个类。

用派生类作基类

基类本身可以是一个派生类

class Base { /* ... */ };
class D1: public Base { /* ... */ };
class D2: public D1 { /* ... */ };

每个类继承其基类的所有成员。最底层的派生类继承其基类的成员,基类又继承自己的基类的成员,如此沿着继承链依次向上。从效果来说,最底层的派生类对象包含其每个直接基类和间接基类的子对象。

派生类的声明

如果需要声明(但并不实现)一个派生类,则声明包含类名但不包含派生列表。例如,下面的前向声明会导致编译时错误:

// error: a forward declaration must not include the derivation list
class Bulk_item : public Item_base;

正确的前向声明为:

// forward declarations of both derived and nonderived class
class Bulk_item;
class Item_base;

virtual 与其他成员函数

C++ 中的函数调用默认不使用动态绑定。要触发动态绑定,满足两个条件:

  • 第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进- 行动态绑定;
  • 第二,必须通过基类类型的引用或指针进行函数调用。要理解这一要求,需要理解在使用继承层次中某一类型的对象的引用或指针时会发生什么。

从派生类型到基类的转换

因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象

// function with an Item_base reference parameter
double print_total(const Item_base&, size_t); //形参基类对象的引用
Item_base item; // 基类对象
// ok: use pointer or reference to Item_base to refer to an Item_base object
print_total(item, 10); // passes reference to an Item_base object
Item_base *p = &item; // p points to an Item_base object
Bulk_item bulk; // object of derived type 派生类对象
// ok: can bind a pointer or reference to Item_base to a Bulk_item object
print_total(bulk, 10); // passes reference to the Item_base part of bulk
p = &bulk; // p points to the Item_base part of bulk

这段代码使用同一基类类型指针指向基类类型的对象和派生类型的对象,该代码还传递基类类型和派生类型的对象来调用需要基类类型引用的函数,两种使用都是正确的,因为每个派生类对象都拥有基类部分。
因为可以使用基类类型的指针或引用来引用派生类型对象,所以,使用基类类型的引用或指针时,不知道指针或引用所绑定的对象的类型:基类类型的引用或指针可以引用基类类型对象,也可以引用派生类型对象。无论实际对象具有哪种类型,编译器都将它当作基类类型对象。将派生类对象当作基类对象是安全的,因为每个派生类对象都拥有基类子对象。而且,派生类继承基类的操作,即,
何可以在基类对象上执行的操作也可以通过派生类对象使用

基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。

可以在运行时确定 virtual 函数的调用

将基类类型的引用或指针绑定到派生类对象对基类对象没有影响,对象本身不会改变,仍为派生类对象。对象的实际类型可能不同于该对象引用或指针的静态类型,这是 C++ 中动态绑定的关键。
通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应的函数。例如,我们再来看 print_total 函数:

// calculate and print price for given number of copies, applying any discounts
void print_total(ostream &os,const Item_base &item, size_t n)
{
	os << "ISBN: " << item.book() // calls Item_base::book
	<< "\tnumber sold: " << n << "\ttotal price: "
// virtual call: which version of net_price to call is resolved at run time
	<< item.net_price(n) << endl;
}

因为 item 形参是一个引用且 net_price 是虚函数,item.net_price(n) 所调用的 net_price 版本取决于在运行时绑定到 item 形参的实参类型

Item_base base;
Bulk_item derived;
// print_total makes a virtual call to net_price
print_total(cout, base, 10); // calls Item_base::net_price
print_total(cout, derived, 10); // calls Bulk_item::net_price

在第一个调用中,item 形参在运行时绑定到 Item_base 类型的对象,因此,print_total 内部调用 Item_base 中定义的 net_price 版本。在第二个调用中,item 形参绑定到 Bulk_item 类型的对象,从 print_total 调用的是Bulk_item 类定义的 net_price 版本。

在编译时确定非 virtual 调用

不管传给 print_total 的实参的实际类型是什么,对 book 的调用在编译时确定为调用Item_base::book。
即使 Bulk_item 定义了自己的 book 函数版本,这个调用也会调用基类中的版本。
非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。
item 的类型是 const Item_base 的引用,所以,无论在运行时 item 引用的实际对象是什么类型,调用该对象的非虚函数都将会调用 Item_base 中定义的版本。

覆盖虚函数机制

在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这里可以使用作用域操作符

Item_base *baseP = &derived;
// calls version from the base class regardless of the dynamic type of baseP
double d = baseP->Item_base::net_price(42);

这段代码强制将 net_price 调用确定为 Item_base 中定义的版本,该调用将在编译时确定。
只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。
为什么会希望覆盖虚函数机制?最常见的理由是为了派生类虚函数调用基类中的版本。在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作。
例如,可以定义一个具有虚操作的 Camera 类层次。Camera 类中的 display函数可以显示所有的公共信息,派生类(如 PerspectiveCamera)可能既需要显示公共信息又需要显示自己的独特信息。可以显式调用 Camera 版本以显示公共信息,而不是在 PerspectiveCamera 的 display 实现中复制 Camera 的操作。
在这种情况下,已经确切知道调用哪个实例,因此,不需要通过虚函数机制。派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。

虚函数与默认实参

像其他任何函数一样,虚函数也可以有默认实参。通常,如果有用在给定调用中的默认实参值,该值将在编译时确定。如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义(也就是用的什么类类型就是哪个类类型的默认实参),与对象的动态类型无关。

  • 通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,
  • 如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。

在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的。

公用、私有和受保护的继承

派生类中定义的成员访问控制的处理与任意其他类中完全一样。派生类可以定义零个或多个访问标号,指定跟随其后的成员的访问级别。对类所继承的成员的访问由基类中的成员访问级别和派生类派生列表中使用的访问标号共同控制。
每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对所继承的成员的访问
基类本身指定对自身成员的最小访问控制。如果成员在基类中为 private,则只有基类和基类的友元可以访问该成员。派生类不能访问基类的 private 成员,也不能使自己的用户能够访问那些成员

如果基类成员为 public 或protected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别:

  • 如果是公用继承,基类成员保持自己的访问级别:基类的 public 成员为派生类的 public 成员,基类的 protected 成员为派生类的 protected成员。
  • 如果是受保护继承,基类的 public 和 protected 成员在派生类中为protected 成员。
  • 如果是私有继承,基类的的所有成员在派生类中为 private 成员。
  • (可以用优先级来记忆,private>protected>privated,继承的时候,优先级高的覆盖优先级低的)
    例如,考虑下面的继承层次:
class Base {
public:
	void basemem(); // public member
protected:
	int i; // protected member
// ...
};
struct Public_derived : public Base {
	int use_base() { return i; } // ok: derived classes can access i
// ...
};
struct Private_derived : private Base {
	int use_base() { return i; } // ok: derived classes can access i
};

无论派生列表中是什么访问标号,所有继承 Base 的类对 Base 中的成员具有相同的访问。派生访问标号将控制派生类的用户对从 Base 继承而来的成员的访问:

Base b;
Public_derived d1;
Private_derived d2;
b.basemem(); // ok: basemem is public
d1.basemem(); // ok: basemem is public in the derived class
d2.basemem(); // error: basemem is private in the derived class

Public_derived 和 Private_derived 都继承了 basemem 函数。当进行public 继承时,该成员保持其访问标号,所以,d1 可以调用 basemem。在Private_derived 中,Base 的成员为 private,Private_derived 的用户不能调用 basemem。
派生访问标号还控制来自非直接派生类的访问:

struct Derived_from Private : public Private_derived {
// error: Base::i is private in Private_derived  //出错
	int use_base() { return i; }
};
struct Derived_from_Public : public Public_derived {
// ok: Base::i remains protected in Public_derived
	int use_base() { return i; }
};

从 Public_derived 派生的类可以访问来自 Base 类的 i,是因为该成员在Public_derived 中仍为 protected 成员。从 Private_derived 派生的类没有这样的访问,对它们而言,Private_derived 从 Base 继承的所有成员均为private。

接口继承与实现继承

public 派生类继承基类的接口,它具有与基类相同的接口。设计良好的类层次中,public 派生类的对象可以用在任何需要基类对象的地方。
使用 private 或 protected 派生的类不继承基类的接口,相反,这些派生通常被称为实现继承。派生类在实现中使用被继承但继承基类的部分并未成为其接口的一部分。类是使用接口继承还是实现继承对派生类的用户具有重要含义。迄今为止,最常见的继承形式是 public。

去除个别成员

如果进行 private 或 protected 继承,则基类成员的访问级别在派生类中比在基类中更受限:

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

派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。
在这一继承层次中,size 在 Base 中为 public,但在 Derived 中为private。为了使 size 在 Derived 中成为 public,可以在 Derived 的 public部分增加一个 using 声明。如下这样改变 Derived 的定义,可以使 size 成员能够被用户访问,并使 n 能够被从 Derived 派生的类访问:

class Derived : private Base {
public:
// maintain access levels for members related to the size of the object
	using Base::size;
protected:
	using Base::n;
// ...
};

正如可以使用 using 声明从命名空间使用名字,也可以使用using 声明访问基类中的名字,除了在作用域操作符左边用类名字代替命名空间名字之外,使用形式是相同的。

默认继承保护级别

用 struct 和 class 保留字定义的类具有不同的默认访问级别,同样,默认继承访问级别根据使用哪个保留字定义派生类也不相同。
使用 class 保留字定义的派生默认具有 private 继承,而用 struct 保留字定义的类默认具有 public 继承

class Base { /* ... */ };
struct D1 : Base { /* ... */ }; // public inheritance by default
class D2 : Base { /* ... */ }; // private inheritance by default

有一种常见的误解认为用 struct 保留字定义的类与用 class 定义的类有更大的区别。唯一的不同只是默认的成员保护级别和默认的派生保护级别,没有其他区别:

class D3 : public Base {
public:
/* ... */
};
// equivalent definition of D3
struct D3 : Base { // inheritance public by default
/* ... */ // initial member access public by default
};
struct D4 : private Base {
private:
/* ... */
};
// equivalent definition of D4
class D4 : Base { // inheritance private by default
/* ... */ // initial member access private by default
};

尽管私有继承在使用 class 保留字时是默认情况,但这在实践中相对罕见。因为私有继承是如此罕见,通显式指定 private 是比依赖于默认更好的办法。显式指定可清楚指出想要私有继承而不是一时疏忽。

友元关系与继承

像其他类一样,基类或派生类可以使其他类或函数成为友元友元可以访问类的 private 和 protected 数据。
友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
每个类控制对自己的成员的友元关系:

class Base {
	friend class Frnd;
protected:
	int i;
};
// Frnd has no access to members in D1
class D1 : public Base {
protected:
	int j;
};
class Frnd {
public:
	int mem(Base b) { return b.i; } // ok: Frnd is friend to Base
	int mem(D1 d) { return d.i; } // error: friendship doesn't inherit
};
// D2 has no access to members in Base
class D2 : public Frnd {
public:
	int mem(Base b) { return b.i; } // error: friendship doesn't inherit
};

如果派生类想要将自己成员的访问权授予其基类的友元,派生类必须显式地这样做(因为基类的友元对从该基类派生的类型没有特殊访问权限)。同样,如果基类和派生类都需要访问另一个类,那个类必须特地将访问权限授予基类的和每一个派生类。

继承与静态成员

如果基类定义 static 成员,则整个继承层次中只有一个样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。
static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类不能访问它。我们假定可以访问成员,则既可以通过基类访问 static 成员,也可以通过派生类访问 static 成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。

struct Base {
static void statmem(); // public by default
};
struct Derived : Base {
	void f(const Derived&);
};
void Derived::f(const Derived &derived_obj)
{
	Base::statmem(); // ok: Base defines statmem
	Derived::statmem(); // ok: Derived in herits statmem
// ok: derived objects can be used to access static from base
	derived_obj.statmem(); // accessed through Derived object
	statmem(); // accessed through this class

转换与继承

理解基类类型和派生类型之间的转换,对于理解面向对象编程在 C++ 中如何工作非常关键。

我们已经看到,每个派生类对象包含一个基类部分,这意味着可以像使用基类对象一样在派生类对象上执行操作。因为派生类对象也是基类对象,所以存在从派生类型引用到基类类型引用的自动转换,即,可以将派生类对象的引用转换为基类子对象的引用,对指针也类似。
基类类型对象既可以作为独立对象存在,也可以作为派生类对象的一部分而存在,因此,一个基类对象可能是也可能不是一个派生类对象的部分,结果,没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自动)转换。
相对于引用或指针而言,对象转换的情况更为复杂。虽然一般可以使用派生类型的对象对基类类型的对象进行初始化或赋值,但,没有从派生类型对象到基类类型对象的直接转换。

派生类到基类的转换

如果有一个派生类型的对象,则可以使用它的地址对基类类型的指针进行赋值或初始化。同样,可以使用派生类型的引用或对象初始化基类类型的引用。严格说来,对对象没有类似转换。编译器不会自动将派生类型对象转换为基类类型对象。
但是,一般可以使用派生类型对象对基类对象进行赋值或初始化。对对象进行初始化和赋值以及可以自动转换引用或指针,这之间的区别是微妙的,必须好好理解。

引用转换不同于转换对象

我们已经看到,可以将派生类型的对象传给希望接受基类引用的函数。也许会因此认为对象进行转换,但是,事实并非如此。将对象传给希望接受引用的函数时,引用直接绑定到该对象,虽然看起来在传递对象,实际上实参是该对象的引用,对象本身未被复制并且,转换不会在任何方面改变派生类型对象,该对象仍是派生类型对象。
将派生类对象传给希望接受基类类型对象(而不是引用)的函数时,情况完全不同。在这种情况下,形参的类型是固定的——在编译时和运行时形参都是基类类型对象。如果用派生类型对象调用这样的函数,则该派生类对象的基类部分被复制到形参。
一个是派生类对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值,理解它们之间的区别很重要。

用派生类对象对基类对象进行初始化或赋值

对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用构造函数,赋值时调用赋值操作符。
用派生类对象对基类对象进行初始化或赋值时,有两种可能性。第一种(虽然不太可能的)可能性是,基类可能显式定义了将派生类型对象复制或赋值给基类对象的含义,这可以通过定义适当的构造函数或赋值操作符实现:

class Derived;
class Base {
public:
	Base(const Derived&); // create a new Base from a Derived
	Base &operator=(const Derived&); // assign from a Derived
// ...
};

在这种情况下,这些成员的定义将控制用 Derived 对象对 Base 对象进行初始化或赋值时会发生什么。
然而,类显式定义怎样用派生类型对象对基类类型进行初始化或赋值并不常见,相反,基类一般(显式或隐式地)定义自己的复制构造函数和赋值操作符,这些成员接受一个形参,该形参是基类类型的(const)引用。因为存在从派生类引用到基类引用的转换,这些复制控制成员可用于从派生类对象对
基类对象进行初始化或赋值:

Item_base item; // object of base type
Bulk_item bulk; // object of derived type
// ok: uses Item_base::Item_base(const Item_base&) constructor
Item_base item(bulk); // bulk is "sliced down" to its Item_base portion
// ok: calls Item_base::operator=(const Item_base&)
item = bulk; // bulk is "sliced down" to its Item_base portion

用 Bulk_item 类型的对象调用 Item_base 类的复制构造函数或赋值操作符时,将发生下列步骤:

  • 将 Bulk_item 对象转换为 Item_base 引用,这仅仅意味着将一个Item_base 引用绑定到 Bulk_item 对象。
  • 将该引用作为实参传给复制构造函数或赋值操作符。
  • 那些操作符使用 Bulk_item 的 Item_base 部分分别对调用构造函数或赋值的 Item_base 对象的成员进行初始化或赋值。
  • 一旦操作符执行完毕,对象即为 Item_base。它包含 Bulk_item 的Item_base 部分的副本,但实参的 Bulk_item 部分被忽略。

在这种情况下,我们说 bulk 的 Bulk_item 部分在对 item 进行初始化或赋值时被“切掉”了。Item_base 对象只包含基类中定义的成员,不包含由任意派生类型定义的成员,Item_base 对象中没有派生类成员的存储空间。

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

像继承的成员函数一样,从派生类到基类的转换可能是也可能不是可访问的。转换是否访问取决于在派生类的派生列表中指定的访问标号

要确定到基类的转换是否可访问,可以考虑基类的 public成员是否访问,如果可以,转换是可访问的,否则,转换是不可访问的。

  • 如果是 public 继承,则用户代码和后代类都可以使用派生类到基类的转换。
  • 如果类是使用 private 或 protected 继承派生的,则用户代码不能将派生类型对象转换为基类对象。
  • 如果是 private 继承,则从 private 继承类派生的类不能转换为基类。
  • 如果是 protected 继承,则后续派生类的成员可以转换为基类类型。
    无论是什么派生访问标号,派生类本身都可以访问基类的 public 成员,因此,派生类本身的成员和友元总是可以访问派生类到基类的转换。

基类到派生类的转换

从基类到派生类的自动转换是不存在的。需要派生类对象时不能使用基类对象:

Item_base base;
Bulk_item* bulkP = &base; // error: can't convert base to derived
Bulk_item& bulkRef = base; // error: can't convert base to derived
Bulk_item bulk = base; // error: can't convert base to derived

没有从基类类型到派生类型的(自动)转换,原因在于基类对象只能是基类对象,它不能包含派生类型成员。如果允许用基类对象给派生类型对象赋值,那么就可以试图使用该派生类对象访问不存在的成员。有时更令人惊讶的是,甚至当基类指针或引用实际绑定到绑定到派生类对象时,从基类到派生类的转换也存在限制:

Bulk_item bulk;
Item_base *itemP = &bulk; // ok: dynamic type is Bulk_item
Bulk_item *bulkP = itemP; // error: can't convert base to derived

编译器在编译时无法知道特定转换在运行时实际上是安全的编译器确定转换是否合法,只看指针或引用的静态类型。
在这些情况下,如果知道从基类到派生类的转换是安全的,就可以使用static_cast强制编译器进行转换。或者,可以用 dynamic_cast申请在运行时进行检查。

类设计与受保护成员

如果没有继承,类只有两种用户:类本身的成员(private|public)和该类的用户(public)。将类划分为 private 和 public 访问级别反映了用户种类的这一分隔:用户只能访问 public 接口,类成员和友元既可以访问 public 成员也可以访问 private 成员。
有了继承,就有了类的第三种用户:从类派生定义新类的程序员。派生类的提供者通常(但并不总是)需要访问(一般为 private 的)基类实现,为了允许这种访问而仍然禁止对实现的一般访问,提供了附加的protected 访问标号。类的 protected 部分仍然不能被一般程序访问,但可以被派生类访问。只有类本身和友元可以访问基类的 private 部分,派生类不能访问基类的 private 成员。
定义类充当基类时,将成员设计为 public 的标准并没有改变:仍然是接口函数应该为 public 而数据一般不应为 public。被继承的类必须决定实现的哪些部分声明为 protected 而哪些部分声明为 private。希望禁止派生类访问的成员应该设为 private,提供派生类实现所需操作或数据的成员应设为protected。换句话说,提供给派生类型的接口是protected 成员和 public 成员的组合。

C++ 中的多态性

引用和指针的静态类型与动态类型可以不同,这是 C++ 用以支持多态性的基石。
通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生类型的。
如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本
从编写代码的角度看我们无需担心。只要正确地设计和实现了类,不管实际对象是基类类型或派生类型,操作都将完成正确的工作。
另一方面,对象是非多态的——对象类型已知且不变。**对象的动态类型总是与静态类型相同,这一点与引用或指针相反。**运行的函数(虚函数或非虚函数)是由对象的类型定义的。
只有通过引用或指针调用,虚函数才在运行时确定。只有在这些情况下,直到运行时才知道对象的动态类型。

参考资料

【1】C++ Primer 中文版(第四版·特别版)

注解

发布了52 篇原创文章 · 获赞 72 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/engineerxin/article/details/104654226