我是靠谱客的博主 清新丝袜,最近开发中收集的这篇文章主要介绍C++ Primer 第五版 ——《第十五章 》面向对象程序设计 (多态与继承)学习笔记,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

目录

OPP:概述

定义基类

定义派生类

派生类中的虚函数 、override 关键字(530P)

派生类到基类的类型转换 (530P)

派生类的构造函数 (531P)

在派生类中使用继承其基类的成员 (531P)

继承与静态成员 ( 532P )

派生类的声明需要注意的地方 (532P)

被用作基类的类需要注意的地方 (533P)

通过 final 来 禁止类被继承

类型转换与继承 (534P)

静态类型 和 动态类型 (534P)

不存在从基类向派生类的隐式类型转换、dynamic_cast 、static_cast (534P)

在对象之间不存在类型转换, 切割对象 (535P)

存在继承关系的类型之间的转换规则 (536P)

虚函数 (536P)

 只有通过基类的引用或指针进行调用虚函数时,才会发生动态绑定,才能在运行时解析该调用 ( 537P)

派生类中 虚函数需要注意的是有 (537P)

将函数定义 final 和 override 说明符 (538P)

虚函数与默认实参 (539P)

使用 作用域运算符来 回避(绕过)虚函数的机制 ( 539P)

抽象基类和 纯虚函数 (540P)

访问控制与继承 (542P)

派生类公有、私有、保护成员对基类成员的可访问性 (543P)

派生类向基类转换的可访问性 (544P)

友元与继承  (545P)

在派生类中改变基类中个别成员的可访问性 (546P)

从 struct 和 class 关键字创建的类的默认的继承保护级别 (546P)

继承中的类的作用域 (547P)

派生类的名称隐藏其基类中的名称 (548P)

名字查找和继承中的 函数调用的解析过程 (重点  549P)

派生类中成员隐藏基类中成员(549P)

派生类中的成员  覆盖基类中的同名成员 (550P)

派生类函数覆盖基类中重载的虚函数 (551P)

拷贝函数与拷贝控制 (551P)

虚析构函数 (552P)

虚析构函数将阻止合成的移动操作 (552P)

合成拷贝控制与继承 (533P)

派生类中删除的拷贝控制与基类的关系 (553P)

移动操作与继承 (552)

派生类的拷贝控制成员 (554P)

在基类中的构造函数 和 析构函数中调用派生类虚函数会发生的情况 (556P)

使用 using 声明使派生类“ 继承”基类的构造函数 (557)

在容器 中 存放具有继承关系的类对象 (558P)


 

OPP:概述


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

  • 数据抽象(封装)—— 可以将类的接口与实现分离
  • 继承—— 可以定义与其他类相似但完全不相同的新类
  • 动态绑定(虚函数)—— 在使用这些彼此相似的类时,在一定程度上忽略他们的区别,统一使用它们的对象
  • 每个派生类除了定义了各自的特有的成员之外, 每个派生类还包含了基类的成员。
  • 派生类public 继承 基类, 派生类对象可以当作基类对象来使用。 但是如果是 protected 或 private 继承的话,就不可以。
  • 一个派生类必须在其类体中声明所有其需要定义的虚函数。 C++11 新标准允许派生类显式地指出哪个成员函数是重写基类中的虚函数,具体的方法是在派生类函数的形参列表之后增加一个 override 关键字

有时候 我们可以通过动态绑定,我们可以使用相同的代码来分别处理基类或派生类的对象。例如下面程序:


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
	{
		cout << "调用的是基类的net_price 函数" << endl;
		return n * price;
	}

	virtual ~Quote() = default; // dynamic binding for the destructor
private:
	std::string bookNo;
protected:
	double price = 0.0;
};

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

	double net_price(std::size_t) const override;
	
private:
	std::size_t min_qty = 0;
	double discount = 0.0;
};

double Bulk_quote::net_price(size_t cnt) const
{
	cout << "调用的是派生类的net_price 函数" << endl;
	if (cnt >= min_qty)
		return cnt * (1 - discount) * price;
	else
		return cnt * price;
}


double print_total(std::ostream &os,const Quote &item, size_t n) // 使用相同的代码来分别处理基类或派生类的对象
{
	// 根据传入 item 形参的对象类型调用 Quote::net_price 或者 net_price:: net_price
	
	double ret = item.net_price(n);
	os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: " << ret << endl;
	return ret;
}
int main()
{
	Quote basic;
	Bulk_quote bulk;
	print_total(cout, basic, 20); // calls Quote version of net_price
	print_total(cout, bulk, 20); // calls Bulk_quote version of net_price
	system("pause");
	return 0;
}

输出结果为:

调用的是基类的net_price 函数
ISBN:  # sold: 20 total due: 0

调用的是派生类的net_price 函数
ISBN:  # sold: 20 total due: 0

仔细观察输出结果。

  • 注意: 在C++语言中, 当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。根据引用或指针所绑定的对象类型不同, 该调用可能执行基类的版本, 也可能执行某个派生类的版本。

定义基类


注意: 在作为继承关系中的基类都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。

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; // dynamic binding for the destructor
private:
	std::string bookNo; 
protected:
	double price = 0.0;
};

 

在C++ 语言中的基类中有两种成员函数:

  • 一种是基类希望其派生类进行覆盖的函数( 其派生类定义为 virtual 函数)
  • 另一种是基类希望派生类直接继承而不要改变的函数
  • 任何构造函数之外的非静态函数都可以是虚函数。意思就是说一个基类中除了构造函数和静态函数不可以是虚函数,其他都可以。
  • 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数( 也可以使用 virtual 显式指出,但是该关键字只能出现在类内部的声明语句之前,而不能用于类外部的函数定义)。还可以通过override 关键字显式地指出覆盖了基类中的某个函数。
  • 只要成员函数没有被声明为虚函数,那么不管用什么样的方式调用该函数都将是静态联编 ,而不是动态联编。

练习题15.1:

  • 就是该函数 的声明处 用 virtual 关键字声明的成员函数,基类希望该成员在派生类中重定义。 还有就是任何除构造函数之外的非 static 成员函数 都可以是 虚函数。

练习题15.2:

  • 当在继承关系时, 基类的某些成员希望在派生类中也被访问,但是在类外不可访问。我们可以把这样的成员声明为 protected 成员。声明为 protected 的成员,只可以被其派生类,其自身的成员、友元访问, 类外是不可访问的。
  • 虽然派生类在什么样的继承方式下都可以继承基类中的成员, 但是派生类中的成员函数不可以访问基类中的 private 成员。声明为 private 的成员只可以让 友元 或者该类自己的成员函数访问。 类外也是不可访问的。

定义派生类


class Bulk_quote : public Quote 
{
	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;
};

 

三个访问说明符的作用:

  • 是控制派生类从基类继承而来的成员是否对派生类的用户可见。

如果一个派生类是公有继承于基类,那么该基类的公有成员也是派生类接口的组成部分。此外, 我们就可以将公有派生类型的对象绑定到基类的引用或指针上。如前所述,在任何需要 Quote 的引用或指针的地方我们都能使用 Bulk_quote的对象。

 


派生类中的虚函数 、override 关键字(530P)


  • 派生类经常(但不总是)覆盖它继承的虚函数。
  • 如果派生类没有覆盖其基类中的某个虚函数, 则该虚函数的行为与任何普通成员一样 —— 派生类会直接继承其在基类中的版本 ( 就是默认使用基类中虚函数版本)。

派生类可以在它覆盖的函数前使用virtual关键字,但不是非得这么做。

  • 因为如果基类将一个函数声明为虚函数时,当派生类重写该函数时会隐式地成为虚函数。

派生类到基类的类型转换 (530P)


一个派生类对象的成员组成部分为:

  • 派生类新增加的(非静态)成员 + 继承其基类的成员 +如果这个派生类有多个基类,成员就会从各个基类继承过来

 

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
	{
		cout << "调用的是基类的net_price 函数" << endl;
		return n * price;
	}

	virtual ~Quote() = default; // dynamic binding for the destructor
private:
	std::string bookNo; 
protected:
	double price = 0.0;
};

class Bulk_quote : public Quote 
{
public:
	Bulk_quote() = default;
	Bulk_quote(const std::string& book, double p,std::size_t qty, double disc) :
		Quote(book, p), min_qty(qty), discount(disc) { }
	
	double net_price(std::size_t) const override;
private:
	std::size_t min_qty = 0; 
	double discount = 0.0;
};

double Bulk_quote::net_price(size_t cnt) const
{
	cout << "调用的是派生类的net_price 函数" << endl;
	if (cnt >= min_qty)
		return cnt * (1 - discount) * price;
	else
		return cnt * price;
}

int main()
{
	
	Quote item; // object of base type
	Bulk_quote bulk; // object of derived type

	// 在C++语言中,当我们使用了基类的引用(或指针)调用一个虚函数时将发生动态绑定
	Quote *p = &item; // p points to a Quote object
	p->net_price(2); // 调用的是基类的该函数

	p = &bulk; // p points to the Quote part of bulk
	p->net_price(2); // 调用的是派生类类的该函数

	Quote &r = bulk; // r bound to the Quote part of bulk
	r.net_price(2); // 调用的是派生类类的该函数

	system("pause");
	return 0;
}

输出结果为:

调用的是基类的net_price 函数
调用的是派生类的net_price 函数
调用的是派生类的net_price 函数

这种转换通常称为 派生类到基类的(derived-to-base )类型转换。和其他类型转换一样,编译器会隐式地执行派生类到基类的转换。

这种隐式特性意味着我们可以把派生类对象(本人注:会调用拷贝构造函数,而且其中派生类的部分会被 “ 切掉”,但是派生类对象的引用或指针不会调用拷贝构造函数,其中派生类的部分就不会被 “ 切掉”, )或者派生类对象的引用用在需要基类引用的地方; 同样的, 我们也可以把派生类对象的指针用在需要基类指针的地方。

派生类 公有继承基类,派生类对象可以当作基类对象来使用。而且此时我们也能将基类的指针或引用绑定到派生类对象中的基类部分上( 如果该派生类是 private  和  protected  继承与基类, 那么就不可以将基类的指针或引用绑定到派生类对象中的基类部分上。看下图,派生类 protected 继承与 基类,然后编译后的截图)。虽然不可以,但是可以访问派生类自己新增加的成员函数。

 


派生类的构造函数 (531P)


派生类并不能在其构造函数中直接初始化从基类继承而来的成员,但是派生类必须使用基类的构造函数来初始化它的基类部分。所以说每个类都控制它自己的成员初始化过程。 详细请看上面的程序。

  • 如果在派生类的构造函数并没有显式调用基类中的某一个构造函数,那么派生类构造函数将隐式调用基类中默认构造函数(前提是基类中有默认构造函数,不管是显式的还是隐式的。 如果是隐式的,说明该基类中没有任何构造函数; 如果是显式的,那么还想有默认构造函数,那么必须再声明一个,可以像上面程序那样。像 这样 “ Quote() = default;  ”  写一个默认构造函数, 该形式的构造函数表明我们既需要其它形式的构造函数,也需要默认的构造函数。 该函数的作用相当于合成的默认构造函数)。那么此时,派生类对象的基类部分的数据成员将执行默认初始化。
  • 如果我们想在派生类的构造函数的初始化列表使用基类其它构造函数来显式初始化基类的成员,我们需要以类名加圆括号内的实参列表的形式为构造函数提供初始值( 在 派生类中的构造函数中)。这些实参将帮助编译器决定到底应该选用基类中哪个构造函数来初始化派生类对象的基类部分。
  • 注意: 当我们创建一个派生类对象的实例时, 首先初始化是基类的部分,当基类的构造函数的函数体完毕后。然后按照声明的数据成员的初始化顺序依次初始化派生类的成员。

在派生类中使用继承其基类的成员 (531P)


  • 不管派生类通过什么方式继承( 例如: public、protected、private)自基类, 派生类都可以访问基类中的公有成员和保护成员,但是私有成员不可以。

 


继承与静态成员 ( 532P )


下列是静态成员被继承的示例:


class Base
{
public:
	static void statmem()
	{
		cout << "调用的是基类的statmem 函数" << temp << endl;
	}
	int result()
	{
		temp = 20000000;
		return temp;
	}
protected:
	static int temp;
};

int Base::temp = 100;
class Derived : public Base
{
public:
	int result()
	{
		return temp;
	}

	void f(const Derived &derived_obj)
	{
		temp = 10;
		cout << "调用的是派生类的f 函数" << endl;
		Base::statmem();
		Derived::statmem();

		derived_obj.statmem();
		this->statmem();
	}
};

int main()
{
	Base myBase;
	Derived myDerived;
	cout << " 输出未修改的 数据成员的值:" << myBase.result() << endl;
	cout << " 输出未修改的 数据成员的值:" << myDerived.result() << endl;
	cout << endl;

	myDerived.f(myDerived);
	cout << endl;
	cout << " 输出修改后的static 数据成员的值:" << myDerived.result() << endl;
	cout << " 输出修改后的static 数据成员的值:" << myBase.result() << endl;
	cout << " 输出修改后的static 数据成员的值:" << myDerived.result() << endl;
	system("pause");
	return 0;
}

输出结果为:

 输出未修改的 数据成员的值:20000000
 输出未修改的 数据成员的值:20000000

调用的是派生类的f 函数
调用的是基类的statmem 函数10
调用的是基类的statmem 函数10
调用的是基类的statmem 函数10
调用的是基类的statmem 函数10

 输出修改后的static 数据成员的值:10
 输出修改后的static 数据成员的值:20000000
 输出修改后的static 数据成员的值:20000000
  • 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类, 对于每个静态成员来说都只存在唯一的实例。

仔细观察上面的程序可以发现当我们在派生类中的 f() 函数 修改了 temp 数据成员的值时, 不管是调用 派生类的result 函数 还是 调用 基类的 result 函数,输出的结果都是10。 说明了 如果在基类有一个静态成员,那么在整个继承体系中, 不管是派生类对象还是基类对象 都共享此成员 (  不管派生类通过什么方式继承基类)。

书上还说假设基类的静态成员是可访问的 ( 可访问的 是 public 和 protected ), 则我们既能通过基类使用它也能通过派生类使用它。

 


派生类的声明需要注意的地方 (532P)



被用作基类的类需要注意的地方 (533P)


如果我们想将某个类用作基类, 则该类必须已经定义而非仅仅声明。

class Quote; // declared but not defined

// error: Quote must be defined
class Bulk_quote : public Quote { ... };

因为派生类包含了并且可以使用从基类继承而来的成员,要使用这些成员,派生类必须知道它们是什么( 指的是基类必须被定义)。这个规则的一个含义是,即一个类不能派生自身。


通过 final 来 禁止类被继承


有时我们定义了一个不希望别人继承的类。或者我们可以定义一个我们不想考虑它是否适合作为基类的类。在新的C++11标准下,我们可以通过在类名后面加上final来防止类被用作基类:

class NoDerived final { /* */ }; // NoDerived can't be a base class
class Base { /* */ };

// Last is final; we cannot inherit from Last
class Last final : Base { /* */ }; // Last can't be a base class
class Bad : NoDerived { /* */ }; // error: NoDerived is final
class Bad2 : Last { /* */ }; // error: Last is final

类型转换与继承 (534P)


可以将基类的指针或引用绑定到派生类对象上有一层极为重要的含义:

  • 当使用基类的引用(或指针)时, 实际上我们并不清楚该引用(或指针) 绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。
  • 所以说当我们使用基类的引用(或指针)时调用一个虚函数时将发生动态绑定,调用的是可能是基类的虚函数,也可能是派生类的虚函数, 到底调用谁取决于基类的引用(或指针)绑定对象的真实类型。

Note:  和内置指针一样, 智能指针类也可以支持派生类向基类的类型转换, 这意味着我们可以将一个派生类对象的地址存储在一个基类的智能指针内。


静态类型 和 动态类型 (534P)


本节中讲的静态类型和动态类型指的是 静态联编 和 动态联编的意思。


不存在从基类向派生类的隐式类型转换、dynamic_cast 、static_cast (534P)


 

为什么 派生类可以隐式转换为基类呢?

 

  • 是不存在从基类向派生类的隐式类型的转换(意识就是说显式转换可以, 比如说用 dynamic_cast 或者 static_cast ),之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分。 而基类的引用或指针可以绑定到该基类部分上。 但是基类对象并没有派生类部分 。

 

书上有这么一句话 “ 一个基类的对象既可以以独立的形式存在, 也可以作为派生类对象的一部分存在。如果基类对象不是派生类对象的一部分, 则它只含有基类定义的成员, 而不含有派生类定义的成员。” 我是这样理解的:

 “ 一个基类的对象既可以以独立的形式存在 ”的时候:

指的是基类的成员都是private的,那么派生类不管用什么方式继承基类,派生类的成员都不可以访问基类的成员。这样基类的对象就算是独立的形式存在了。那么此时基类对象就不是派生类对象的一部分了。因为一个基类的对象可能是派生类对象的一部分, 也可能不是, 所以不存在从基类向派生类的自动类型转换。


在对象之间不存在类型转换, 切割对象 (535P)


派生类向基类的自动类型转换只对指针或引用类型有效, 在派生类类型和基类类型之间不存在这样的转换。

  • 记住,当我们初始化(或者说拷贝)或 赋值类类型的对象时,我们实际上是在调用该类的一个函数。初始化(或者说拷贝)时,我们调用构造函数; 当我们赋值时,我们调用赋值运算符。这些成员函数通常有一个参数,该参数是对类类型的const版本的引用。
  • 因为这些成员函数接受引用作为参数, 所以派生类向基类的转换允许我们可以向基类的 “ 拷贝 / 移动操作 ” 传递一个派生类的对象。注意的是这些操作不是虚函数。
  • 当我们将一个派生类对象传递(或者说拷贝)给基类构造函数时, 实际运行的构造函数(可以是隐式的,也可以是显式的; 如果是隐式的,合成版本构造函数会逐成员地执行拷贝操作)基类中定义的那个, 显然该构造函数只能处理基类自己的成员。
  • 类似的,如果我们将一个派生类对象赋值给一个基类对象, 则实际运行的赋值运算符(可以是隐式的,也可以是显式的; 如果是隐式的,合成版本赋值运算符会逐成员地执行赋值操作)也是基类中定义的那个, 该运算符同样只能处理基类自己的成员。

 Bulk_quote bulk; /派生类对象
Quote item(bulk); // 调用的是 Quote::Quote(const Quote&) 构造函数
item = bulk; // 调用的是 Quote::operator=(const Quote&)
  • 当构造item时,会调用 Quote 的拷贝构造函数,该拷贝构造函数只处理 Bulk_quote 类中基类部分的成员,同时会忽略掉 Bulk_quote类自己本身的成员。
  • 类似地,但把 bulk 赋值 item 时, 调用的是基类中的 拷贝赋值运算符, 那么只有 Bulk_quote 类基类部分的成员被赋值给了 iterm。

注意: 当我们用一个派生类对象为一个基类对象初始化(或拷贝)或赋值时, 只有该派生类对象中的基类部分会被拷贝、移动或赋值 (显式地 或 隐式地 ),它的派生类部分将被忽略(切割)掉。

练习题15.8:

静态类型指的是在该变量声明的类型,在编译时,就已经确定的了。

动态类型指的是, 要在运行时根据绑定的对象具体类型而定

练习题15.9:

当使用基类的引用或指针调用一个虚函数时,此时会发生动态绑定。该表达式的静态类型和静态类型就会不一致, 会根据动态绑定具体的类型可能调用基类中虚函数版本,也可能是 派生类的虚函数版本。

Bulk_quote bulk;
Quote *myQuote = new Bulk_quote; // 静态类型是Quote, 动态类型 是Bulk_quote
uote *mmQuo = &bulk; //  静态类型是Quote, 动态类型 是Bulk_quote
Quote &mQuo = bulk;   //  静态类型是Quote, 动态类型 是Bulk_quote

 


存在继承关系的类型之间的转换规则 (536P)


虚函数 (536P)


注意: 每个虚函数都必须有定义,不管它会不会被用到 —— 这一情况只有当使用基类的引用或指针时调用一个虚函数时才需要这样。因为我们直到运行时才能知道到底调用了哪个版本的虚函数, 可能是基类的相应版本,也可能是派生类的。 所以编译器也无法确定到底会执行哪个虚函数。 

如果说你用非基类的指针或引用的方式调用虚函数的话,  那么虚函数此时可以被定义,也可以没有定义。 编译器在编译期间就可以确定到底会执行哪个虚函数,如果说该虚函数没有被定义, 那么编译器可能可以检测到。


 只有通过基类的引用或指针进行调用虚函数时,才会发生动态绑定,才能在运行时解析该调用 ( 537P)


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
	{
		cout << "调用的是基类的net_price 函数" << endl;
		return n * price;
	}

	virtual ~Quote() = default; // dynamic binding for the destructor
private:
	std::string bookNo;  // 书籍的 ISBN 编号
protected:
	double price = 0.0;
};

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

	double net_price(std::size_t) const override;
private:
	std::size_t min_qty = 0;
	double discount = 0.0;
};

double Bulk_quote::net_price(size_t cnt) const  // 定义派生类的 net_price 函数
{
	cout << "调用的是派生类的net_price 函数" << endl;
	if (cnt >= min_qty)
		return cnt * (1 - discount) * price;
	else
		return cnt * price;
}

double print_total(std::ostream &os,const Quote &item, size_t n)
{
	// 根据传入item 形参的对象类型调用 Quote::net_price or Bulk_quote::net_price
	
	double ret = item.net_price(n);
	os << "ISBN: " << item.isbn()  << " # sold: " << n << " total due: " << ret << endl; 
	return ret;
}
int main()
{
	Quote base("0-201-82470-1", 50);
	print_total(cout, base, 10); // calls Quote::net_price

	Bulk_quote derived("0-201-82470-1", 50, 5, .19);
	print_total(cout, derived, 10); // calls Bulk_quote::net_price

	base = derived; // 把 derived 的Quote 部分拷贝给 base ,derived 部分会被切掉
	base.net_price(20); // 调用的是 Quote::net_price

	system("pause");
	return 0;
}


输出结果为:

调用的是基类的net_price 函数
ISBN: 0-201-82470-1 # sold: 10 total due: 500

调用的是派生类的net_price 函数
ISBN: 0-201-82470-1 # sold: 10 total due: 405

调用的是基类的net_price 函数

 

  • 注意: 只有通过基类的引用或指针进行调用虚函数时,才能在运行时解析该调用。只有在这种情况下,对象的动态类型才可能与其静态类型不同。
  • 如果说通过对象(或者说非引用非指针)调用函数( 不管是虚函数还是非虚函数),那么它们是在编译期间就进行绑定。 所以说对象的类型是不变的,这样的话对象的动态类型和静态类型都是一致的, 调用的是都是同一个函数。

派生类中 虚函数需要注意的是有 (537P)


如果说一个派生类的函数覆盖了某个继承而来的虚函数, 则它的形参类型必须与被它覆盖的基类虚函数完全一致。

同样的是, 派生类中的虚函数的返回类型也必须与基类的虚函数一致。 该规则存在一个例外:

  • 当类的虚函数返回类型是类本身的指针或引用时, 上述规则无效。也就是说, 如果D 由B 派生得到, 则基类的虚函数可以返回B* 而派生类的对应函数可以返回D*, 只不过这样的返回类型要求从D到B的类型转换是可访问的。

将函数定义 final 和 override 说明符 (538P)


  • 其实我们在派生类中定义的虚函数可以返回类型 和 函数标识符相同,但是形参列表可以不同。 这仍然是合法的行为。
  •   那么此时编译器会认为这个函数是新定义的,是与基类同名的函数是互相独立 的。
  • 此时, 派生类的同名函数并没有覆盖掉基类中的相应版本。
  • 那么需要注意的是, 在实际的编程中,这样的声明通常是一个错误,因为我们可能原本希望派生类能覆盖掉基类中的相应的虚函数,但是一不小心把形参列表弄错了。

那么如何解决上面可能会发生的错误呢?

  • 那么通常我们在派生类中的虚函数的形参列表之后指定override 关键字
  • 这样做的好处可以使的程序的意图更加清楚并且还能让编译器为我们发现一些错误。
  • 比如说如果我们使用了override 关键字 标记了某个函数, 但该函数并没有覆盖基类中已存在的虚函数, 此时编译器将报错。

看书中的程序 :

struct B {
	virtual void f1(int) const;
	virtual void f2();
	void f3();
};
struct D1 : B
{
	void f1(int) const override; // ok: f1 matches f1 in the base
	void f2(int) override; // error: B has no f2(int) function

	void f3() override; // error: f3 not virtual
	void f4() override; // error: B doesn't have a function named f4
};

编译器的报错:

  • 我们使用 override 关键字 标识一个函数的时候, 意思就是说希望派生类能覆盖掉基类中相应版本的虚函数。 如果说并没有做到( 比如说 形参列表不同 ), 那么编译器就会报错。
  • 因为只有虚函数才能被覆盖 , 因为B 中的f3 不是虚函数,就算是给派生类DI 中的f3 函数标识了关键字 override , 编译器也会报错,因为没有要覆盖的函数。类似的, D1 中的 f4 函数使用override 关键字也是错误的,因为B中根本没有名为f4 的函数,更谈不上该函数是虚函数,是可以被覆盖的。

总结: override 关键字标识的函数,在基类中是虚函数,并且派生类中被标识的函数原型要与基类中的版本一致。否则,发生错误。

为函数指定 final 关键字:


class Base
{
public:
	virtual void test()final // 表示该函数不能被其子类覆盖,任何覆盖该函数的操作都将引发错误
	{  // 注意的是: final 关键字只可以为虚函数使用, 不能给非虚函数使用

	}
};

class Derived :public Base
{
public:
	void test() // 错误
	{

	}

	// 正确,该函数并没有覆盖掉基类中的 test函数,因为它们形参列表不同; 该函数跟基类中 test 函数是互相独立的
	void test(int i) 
	{

	}
};

上述程序的编译截图如下:


虚函数与默认实参 (539P)


和其它函数类似, 虚函数也可以有默认实参。 当我们通过基类的引用或指针调用虚函数时,则使用的是基类中定义的默认实参, 就算实际运行的是派生类中的相应版本,也是使用的是基类中定义的默认实参。此时, 传入派生类函数的将是基类虚函数定义的默认实参。 如果派生类函数依赖不同的实参, 则程序结果将与我们的预期不符。

注意: 具有默认实参的虚函数应该在基类和派生类中使用相同的实参值。


使用 作用域运算符来 回避(绕过)虚函数的机制 ( 539P)


  • 通常我们可以使用作用域运算符来绕过虚函数的机制, 这样就可以强迫其执行某个特定版本的虚函数。 此时,是在编译时完成解析的。

练习题15.12:

  • 我觉得是根据情况而定,  而且只能在子类中 同时指定 override 和 final 关键字。  因为 override 关键字是用来标识该函数 是覆盖基类中的相应版本, 在基类中 添加 override 关键字 ,该函数覆盖谁去。 当 使用 final 关键字表示 该函数不准备被其子类覆盖,注意的是, final 关键字只能使用 虚函数身上。 如果说 当子类中 同时指定 override 和 final 关键字,表明该函数覆盖其基类的相应版本,同时该函数不能被该类的子类所覆盖。
  • 然后如果是一个普通的成员函数的话,就没必要使用 这两个关键字了, 因为它们只能被虚函数使用。

练习题15.13:

有问题的:

  • base 的 print 函数,输出的是该类自己本身的成员数据的值
  • derived 的 print函数,在其函数体中直接调用 print 函数,而没有使用作用域运算符限定想调用基类中的版本, 此时在运行时将被解析为对派生类自身的调用, 会导致不断地调用derived 类的 print 函数, 造成死循环。

练习题15.14:

  • a  , 调用的是base中 print 函数
  • b,  调用的是 Derived 中的 print 函数
  • c, 调用的是 base 中的 name 函数
  • d, 调用的是 base 中 的name 函数, 就算是基类的指针指向派生类对象, 但是调用的不是虚函数,所以不会发生动态绑定。 所以调用的是 base 中的 name 函数
  • e, 调用的是  base 中 的 print 函数,因为其 静态类型和  动态类型 都是 base
  • f ,  调用的是 Derived 中的 print 函数, 因为 静态类型是 base ,动态类型是 Derived , 但是 调用的是 print 函数是虚函数, 所以发生动态绑定, 调用的是 Derived 中 的版本。

抽象基类和 纯虚函数 (540P)


  • 当在基类中不能为虚函数给出一个有意义的实现时, 可以将其声明为纯虚函数。 纯虚函数的实现可以留给派生类来完成。 那么纯虚函数不能被调用(如果没有被定义的话。可以被定义的,但是要在类外定义), 仅仅起到提供一个与派生类一致的接口的作用 一般来说, 一个抽象类至少有一个纯虚函数。 
  • 纯虚函数声明语句时的 “ =0 ” 只能出现在类内部的虚函数声明语句处。 值得注意的是, 我们也可以为纯虚函数提供定义, 不过函数体必须定义在类的外部。也就是说, 我们不能在类的内部为一个=0的 函数提供函数体。
  • 那么包含纯虚函数的类是抽象基类。意思就是说如果在派生类中对基类的所有(记住是所有,少一个纯虚函数的定义,该派生类还是抽象类)纯虚函数进行了定义, 那么这些函数就被赋予了功能,就可以被调用。 这个派生类就不再是个抽象类,而是可以用来定义对象的具体类。   如果在派生类中没有对所有的纯虚函数进行定义,则此派生类仍然是抽象类, 不能用来定义对象。
  • 抽象类不能用作参数类型,函数的返回类型 或 显式转换的类型。

下面这个程序是本节中的示例程序:

class Quote
{
public:
	Quote() = default;
	Quote(const std::string &book, double sales_price) :bookNo(book), price(sales_price) 
	{
		cout << "调用的是基类中的构造函数!" << endl;
	}


	std::string isbn() const { return bookNo; }

	virtual double net_price(std::size_t n) const
	{
		cout << "调用的是基类的net_price 函数" << endl;
		return n * price;
	}

	virtual ~Quote() = default; // dynamic binding for the destructor
private:
	std::string bookNo;  // 书籍的 ISBN 编号
protected:
	double price = 0.0;
};


class Disc_quote : public Quote // Disc_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)
	{
		cout << "调用的是Disc_quote 类中的构造函数!" << endl;
	}

	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 price,std::size_t qty, double disc) :
		Disc_quote(book, price, qty, disc)
	
	{
		cout << "调用的是 Bulk_quote 类中的构造函数!" << endl;
	}
	// 覆盖基类中相应版本的函数, 该类不再是抽象类
	double net_price(std::size_t) const override 
	{
		cout << "调用的是Bulk_quote类中的net_price 函数!" << endl;
		return 0;
	}
};


int main()
{
	//Disc_quote discounted; // 错误,不能为抽象基类定义对象
	//Disc_quote *discounted = new Disc_quote; // 这样也是错误的,因为该类是一个带有纯虚函数的抽象类

	Bulk_quote bulk; // 正确, Bulk_quote类中没有纯虚函数, 因为该纯虚函数已经被定义了。

	/*但是可以用 基类指针 或 引用 指向 公有(不能是私有或者 保护的)派生类对象(该派生类对象,
	必须是个具体类,即派生类实现了抽象类中的同名虚函数,不再是个纯虚函数,
	否则的话不可以用基类指针 或 引用 指向),然后我们就可以通过该基类指针 或引用调用虚函数,
	就可以实现动态绑定。 就像下面这样*/

	Disc_quote *myDisc_quote1 = &bulk;
	Disc_quote *myDisc_quote2 = new Bulk_quote;
	Disc_quote &myDisc_quote3 = bulk;
	system("pause");
	return 0;
}

这个版本的Bulk_quote的直接基类是Disc_quote,间接基类是Quote。每个 Bulk _quote对象包含三个子对象: 一个(空的) Bulk _quote部分、一个Disc_quote子对象和一个Quote子对象。

 记住:派生类的构造函数只初始化它的直接基类部分, 而不初始化它的间接基类部分。 每个类各自控制其对象的初始化过程。

 


访问控制与继承 (542P)


每个类分别控制着其成员对于该类的派生类来说是否可访问的。

class Base
{
protected:
	int prot_mem = 100; // protected member
};
class Sneaky : public Base
{
public:
	friend void clobber(Sneaky&); // can access Sneaky::prot_mem
	friend void clobber(Base&); // can't access Base::prot_mem
	 void clobber()
	{ // 正确, 派生类的成员函数可以访问基类中的 protected  成员
		cout << "输出prot_mem 成员的值: " << prot_mem << endl;
	}
	 friend void clobber() // 错误,派生类的友元不能访问基类中的 protected 成员
	 { // 如果派生类的友元想访问基类中的protected 成员可以使用派生类对象来访问,就像下面的 void clobber(Sneaky &s) 函数 那样
		 cout << "输出prot_mem 成员的值: " << prot_mem << endl;
	 }
private:
	int j;
};
// 正确: clobber 能访问 Sneaky 对象的 private 和 protected 成员
void clobber(Sneaky &s)
{
	s.j = s.prot_mem = 0;
}

// 错误: clobber 不能访问 Base 的 protected 成员
void clobber(Base &b)
{
	b.prot_mem = 0;
}

要想阻止以上的用法,我们就要做出如下规定:

  • 即派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员; 对于普通的基类对象中的成员不具有特殊的访问权限。
  • 意思是: 派生类的成员函数只能访问本派生类对象的基类部分的保护成员,但是不能访问 <<另外一个基类对象>>的保护成员; 一定要注意: C++的访问是声明在类上的,但是实际保护的是对象的非公开成员。

派生类公有、私有、保护成员对基类成员的可访问性 (543P)


派生类对其直接(或间接)基类继承而来的成员的访问权限受到两个因素影响:

  • 一是在直接(或间接)类中该成员的访问说明符,
  • 二是在派生类的类派生列表中的访问说明符。
  • 派生访问说明符对派生类的成员和friend是否可以访问其自己的直接基类的成员没有影响。派生类成员对基类成员的访问权限只与基类中的访问说明符有关。
  • 派生访问说明符的目的是控制派生类的用户(包括从派生类派生的新类)对基类成员的访问权限。
  • 派生类使用的派生访问说明符还控制从该派生类继承的类的访问权限。

派生类向基类转换的可访问性 (544P)


友元与继承  (545P)


  • 我们知道友元关系是不能传递的, 同样的, 友元关系也不能被继承。
  • 基类的友元在访问派生类成员时不具有特殊性, 类似的, 派生类的友元也不能随意访问基类的成员:

class Base
{
	// Pal 是Base的友元类,说明Pal 该类中的所有成员都可以访问Base的成员, 
	//但是Pal 不可以访问Base的派生类(如 Sneaky )中的成员, 因为友元关系不能继承
	friend class Pal;
protected:
	int prot_mem = 100; // protected member
};
class Sneaky : public Base
{

private:
	int j;
};

class Pal
{
public:
	void f(Base b) // 正确,因为 Pal 是Base的友元
	{
		cout << "输出 b.prot_mem:" << b.prot_mem << endl;
	}
	/*int f2(Sneaky s) // 错误,因为Pal 不是 Sneaky的友元,它只是 Base的友元, 友元不可以继承
	{
		return s.j;
	}*/
	void f3(const Sneaky &s) 
	{// 正确, Pal 是Base 的友元,所以Pal能够访问Base对象的成员,这种可访问性包括了Base对象内嵌在其派生类对象中的情况。
		cout << "输出 s.prot_mem :" << s.prot_mem << endl;
	}
};

 


在派生类中改变基类中个别成员的可访问性 (546P)


有时候我们需要在派生类中改变继承其基类中个别成员的可访问性,我们可以通过提供一个 using 声明来实现这一点:

class Base 
{
public:
	std::size_t size() const { return n; }
protected:
	std::size_t n = 10;
private:
	int test = 100;
};
class Derived : private Base { // note: private inheritance
public:
	// maintain access levels for members related to the size of the object
	using Base::size;
protected:
	using Base::n;
private:
	 // using Base::test;  // 错误,不可以 using 私有成员
};
class AgainDerived :private Derived // 注意这里, 这里不管用什么样的继承方式继承Derived, 它都可以访问n
{
public:
	void show()
	{
		cout << "在AgainDerived类中输出n的值:" << n << endl;
	}
};
int main()
{
	Derived myDerived;
	// 如果 没有 “ using Base::size " 声明,就不可以在派生类以外的地方用派生类对象调用基类的函数
	cout << "输出该成员的值:" << myDerived.size() << endl;
	AgainDerived myAgainDerived;
	myAgainDerived.show();
	system("pause");
	return 0;
}


输出结果为:

输出该成员的值:10
在AgainDerived类中输出n的值:10

因为 Derived 使用了私有继承, 所以继承而来的成员 size 和 n (在默认情况下) 是 Derived的私有成员。然而, 我们使用using声明语句改变了这些成员的可访问性。改变之后, Derived 的用户可以访问size成员,随后从Derived派生的类就可以访问 n ( 不管用什么样继承方式继承Derived 都可以访问n,比如说,private 继承Derived, 它可以在其类中访问n)。

 

通过在类的内部使用using声明语句, 我们可以将该类的直接或间接基类中的任何可访问成员 (非私有成员) 标记出来。using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。也就是说, 如果一条using声明语句出现在类的 private部分, 则该名字只能被类的成员和友元访问; 如果using声明语句位于public部分, 则类的所有用户都能访问它;如果using声明语句位于protected部分, 则该名字对于成员、友元和派生类是可访问的。

练习题15.18:

只有 d2 和 dd1 是正确的, 因为当派生类公有继承基类时,用户代码才能够使用 派生类向基类的转换, 如果继承的方式是 保护的和 私有的,则用户代码不能使用该转换

练习题:15.19:

Derived_from_Private 该类是错误的,  因为如果D 继承B的方式是公有的 或保护的,那么 D的派生的类的成员和友元可以使用 D 向 B 的类型转换; 反之, 如果D  继承 B 的方式是私有的,则不能使用



从 struct 和 class 关键字创建的类的默认的继承保护级别 (546P)



继承中的类的作用域 (547P)


  • 每个类都会有自己的作用域,我们可以在这个作用域内定义该类的成员。  当存在继承关系时, 派生类的作用域嵌套在其基类的作用域之内。
  • 不过也恰恰因为类作用域有这种继承嵌套的关系, 所以派生类才能像使用自己的成员一样使用基类的成员。
  • 如果一个名字在派生类的作用域内无法正确解析, 则编译器将继续在外层的基类作用域中寻找该名字的定义。

一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况 ), 但是我们能使用哪些成员仍然是由静态类型决定的。直接例举书上的例子:

class Quote
{
public:
	Quote() = default;
	Quote(const std::string &book, double sales_price) :bookNo(book), price(sales_price)
	{
		cout << "调用的是基类中的构造函数!" << endl;
	}


	std::string isbn() const { return bookNo; }

	virtual double net_price(std::size_t n) const
	{
		cout << "调用的是基类的net_price 函数" << endl;
		return n * price;
	}

	virtual ~Quote() = default; // dynamic binding for the destructor
private:
	std::string bookNo;  // 书籍的 ISBN 编号
protected:
	double price = 0.0;
};


class Disc_quote : public Quote // Disc_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)
	{
		cout << "调用的是Disc_quote 类中的构造函数!" << endl;
	}

	double net_price(std::size_t) const = 0;  // 定义纯虚函数

	std::pair<size_t, double> discount_policy() const // 新添加的 discount_policy() 成员
	{
		cout << "调用的是Disc_quote 类中的 discount_policy 函数" << endl;
		return { quantity, discount };
	}
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 price, std::size_t qty, double disc) :
		Disc_quote(book, price, qty, disc)

	{
		cout << "调用的是 Bulk_quote 类中的构造函数!" << endl;
	}
	// 覆盖基类中相应版本的函数, 该类不再是抽象类
	double net_price(std::size_t) const override
	{
		cout << "调用的是Bulk_quote类中的net_price 函数!" << endl;
		return 0;
	}
};


int main()
{
	// 下面这些都是通过 Disc_quote 派生的类来使用discount_policy
	Bulk_quote bulk;
	bulk.discount_policy(); //通过Bulk_quote对象调用

	Bulk_quote *bulkP = &bulk; // 静态类型和动态类型是一样的
	bulkP->discount_policy(); // 正确: bulkP 的类型是Bulk_quote*,通过Bulk_quote 指针调用

	Bulk_quote &bul = bulk; // 静态类型和动态类型是一样的
	bul.discount_policy(); // 正确: bulkP 的类型是Bulk_quote,通过Bulk_quote 引用调用

	//
	Quote *itemP = &bulk; //静态类型是Quote* , 动态类型是Bulk_quote, 所以静态类型和动态类型不一致
	
	// itemP->discount_policy(); // 错误: itemP 的 类型是 Quote*, 因为Quote 类中没有成员 discount_policy
	system("pause");
	return 0;
}


输出结果为:

调用的是Disc_quote 类中的 discount_policy 函数
调用的是Disc_quote 类中的 discount_policy 函数
调用的是Disc_quote 类中的 discount_policy 函数

itemP 的类型是 Quote 的指针, 意味着对discount_policy 的搜索将从Quote开始。显然 Quote 类中不包含名为 discount_policy 的成员, 所以我们无法通过 Quote 的对象、引用或指针调用 discount_policy 。

 


派生类的名称隐藏其基类中的名称 (548P)



struct Base
{
	Base() : mem(0)
	{ 
		cout << "调用的是基类中的构造函数!" << endl;
	}
	int get_mem() { return mem; }
protected:
	int mem;
};
struct Derived : Base
{
	Derived(int i) : mem(i)   // i初始化Derived 中的 mem 成员
	{
		cout << "调用的是派生类中的构造函数!" << endl;
	} 
	// Base::mem is default initialized
	int get_mem() { return mem; } // returns Derived::mem
protected:
	int mem; // hides mem in the base
};

int main()
{
	Derived d(42);
	cout << "输出派生类的mem的值:" << d.get_mem() << endl;
	cout << "输出基类的mem的值:" << d.Base::get_mem() << endl;//可以显式使用作用域运算符来访问Base 中的 get_mem 成员函数

	Derived *myDerived = &d;
	cout << "n输出派生类的mem的值:" << myDerived->get_mem() << endl;
	cout << "输出基类的mem的值:" << myDerived->Base::get_mem() << endl;

	Base *myBase = &d;
	// 下面这两个调用的都是基类中的函数版本,为什么呢? 因为调用的该函数不是虚函数,所以调用是在编译期就进行绑定
	cout << "n输出基类的mem的值:" << myBase->get_mem() << endl;
	cout << myBase->myDerived::get_mem() << endl; // 这样写是错误的,因为派生类的成员在其基类中可能有也可能没有
	system("pause");
	return 0;
}
输出结果为:

调用的是基类中的构造函数!
调用的是派生类中的构造函数!
输出派生类的mem的值:42
输出基类的mem的值:0

输出派生类的mem的值:42
输出基类的mem的值:0

输出基类的mem的值:0
  • 记住:派生类的成员将隐藏(只要该同名成员不是虚函数,就是隐藏;否则,就是覆盖)同名的基类成员, 如果想要访问基类中的同名成员,可以使用作用于运算符显式访问基类中被隐藏的同名成员(前提是可以访问)。
  • 还有就是 派生类的同名成员只能隐藏 其基类中 protected 和 public的成员,并不会隐藏其基类中的 private 成员。 即使 派生类中的该成员名字和 基类中的名字一样。

建议: 除了覆盖继承而来的虚函数的名称之外, 派生类最好不要重用其他定义在基类中的名字。


名字查找和继承中的 函数调用的解析过程 (重点  549P)


派生类中成员隐藏基类中成员(549P)


  • 如前所述, 声明在内层作用域的函数并不会重载( 注:会隐藏)声明在外层作用域的函数。
  • 因此,  定义派生类中的函数也不会重载(注: 可能会隐藏 或 覆盖基类中同名成员)其基类中的成员。
  • 和其他作用域一样, 如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名, 则派生类将在其作用域内隐藏(注: 如果是虚函数,将覆盖基类中的同名成员)该基类成员。
  • 就算是派生类成员和基类成员的形参列表(还有返回类型,在进行名字查找时,只根据名字,跟形参列表和返回类型无关)不一致, 基类成员也仍然会被隐藏(如果是覆盖的话,还是有区别的)掉

下面这个程序演示这一点 —— 派生类中的成员隐藏基类中的成员 :

struct Base 
{
	void memfcn() { cout << "Base::memfcn" << endl; } // 该函数的参数列表跟派生类中的同名函数不一样
	int memfcn1()  该函数跟派生类中的同名函数返回类型不一样
	{
		cout << "Base::memfcn1" << endl;
		return 0;
	}
};
struct Derived : Base 
{
	void memfcn(int i) { { cout << "Derived::memfcn" << endl; } } // 隐藏基类中的memfcn
	void memfcn1()
	{
		cout << "Derived::memfcn1" << endl;
		
	}
};

int main()
{
	Derived d; Base b;
	b.memfcn(); // calls Base::memfcn
	d.memfcn(10); // calls Derived::memfcn
	// d.memfcn(); // 错误, 参数列表为空的 memfcn 被隐藏了
	d.Base::memfcn(); // ok: calls Base::memfcn

	cout << endl;
	

	b.memfcn1();  // calls Base::memfcn1
	d.memfcn1(); // calls Base::memfcn1
	d.Base::memfcn1(); // ok: calls Base::memfcn1
	cout << endl;
	system("pause");
	return 0;
}

输出结果为:

Base::memfcn
Derived::memfcn
Base::memfcn

Base::memfcn1
Derived::memfcn1
Base::memfcn1

“ d.memfcn() ”  该调用是错误的,为什么?

  • 为了解析这条调用语句 , 编译器首先在 Derived 中查找名字 memfcn ; 因为Derived确实定义了一个名为memfcn的成员,所以查找过程终止。一旦名字找到, 编译器就不再继续查找了。Derived中 的 memfcn 版本需要一个int实参,  而当前的调用语句无法提供任何实参,  所以该调用语句是错误的。

派生类中的成员  覆盖基类中的同名成员 (550P)


下面这个程序演示的是基类中的虚函数和派生类中的同名函数参数列表不一致的情况:

struct Base 
{
	virtual void memfcn() { cout << "Base::memfcn" << endl; } // 该函数的参数列表跟派生类中的同名函数不一样
	
};
struct Derived : Base 
{
	
	void memfcn(int i) // 这里没有写 override 关键字显式指出该函数必须被覆盖
	{
		cout << "Derived::memfcn" << endl;
		
	}
};

int main()
{
	Derived d; Base b;
	b.memfcn(); // calls Base::memfcn
	d.memfcn(10); // calls Base::memfcn
	// d.memfcn(); // 错误, 参数列表为空的 memfcn 被隐藏了
	d.Base::memfcn(); // ok: calls Base::memfcn

	Base *myBase = new Derived;
	myBase->memfcn(10); // 错误,编译器给出的错误提示是: Base::memfcn 不接受参数
	myBase->memfcn(); // 正确

	Base *myBase1 = &d;
	myBase1->memfcn(10); // 错误,编译器给出的错误提示是: Base::memfcn 不接受参数
	myBase1->memfcn(); // 正确

	Base &myBase2 = d;
	myBase2.memfcn(10); // 错误,编译器给出的错误提示是: Base::memfcn 不接受参数
	myBase2.memfcn(); // 正确
	system("pause");
	return 0;
}

Derived 的 memfcn 函数并没有覆盖Base的虚函数 memfcn , 原因在于它们的形参列表不同。实际上, Derived 的memfcn 函数 将隐藏Base的memfcn 。此时 Derived 拥有了两个名为memfcn 的函数: 一个是Derived 从Base继承而来的虚函数memfcn ; 另一个是Derived 自己定义的接受一个 int 参数的非虚函数memfcn 。

  • 我们现在可以理解为什么基类与派生类中的虚函数必须有相同的形参列表了。假如基类与派生类的虚函数接受的实参不同, 则我们就无法过基类的引用或指针调用派生类的虚函数了。所以说,我们必须在派生类中要覆盖基类中函数时,添加override 关键字, 当我们不小心把派生类中的形参列表弄错了, 这样编译器就可以替我们发现错误。

下面在演示一下,这个是书上比较复杂的示例:

class Base
{
public:
	virtual int fcn()
	{
		cout << "调用的是Base类中的fcn" << endl;
		return 0;
	}
};
class D1 : public Base
{
public:
	//隐藏基类的fcn, 这个fcn不是虚函数 
		//D1继承了Base::fcn()的定义
	int fcn(int i)// 与基类中的 fcn的形参列表不一致
	{
		cout << "调用的是D1类中的fcn( int i)" << endl;
		return 0;
	}
	virtual void f2() // 是一个新的虚函数,在 Base 中不存在这个函数
	{
		cout << "调用的是D1类中的f2" << endl;
	}
};
class D2 : public D1
{
public:
	int fcn(int i,int j) // 是一个非虚函数,隐藏了D1::fcn (int) , 也隐藏了base::fcn ()
	{
		cout << "调用的是D2类中的fcn( int i, int j)" << endl;
		return 0;
	}
	int fcn() override // 覆盖了Base的虚函数fcn
	{
		cout << "调用的是D2类中的fcn()" << endl;
		return 0;
	}
	void f2()override // 覆盖了D1的虚函f2
	{
		cout << "调用的是D2类中的f2" << endl;
	}
};

int main()
{
	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
	cout << endl;

	D1 *d1p = &d1obj;    D1 *d2p = &d2obj;
	// bp2->f2(); // 错误: Base没有名为f2的成员 
	d1p->f2(); //  虚调用,将在运行时调用D1 :: f2 ()
	d2p->f2(); // 虚调用,将在运行时调用D2::f2 ()
	cout << endl;


	Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj;

	// p1->fcn(42); // 错误: Base中没有接受一个int的fcn 静态绑定,调用D2:: fcn (int)
	p2->fcn(42); //  静态绑定,调用D1::fcn (int)
	p3->fcn(42,55); // 静态绑定,调用D2:: fcn (int,int)
	system("pause");
	return 0;
}


输出结果为:

调用的是Base类中的fcn
调用的是Base类中的fcn
调用的是D2类中的fcn()

调用的是D1类中的f2
调用的是D2类中的f2

调用的是D1类中的fcn( int i)
调用的是D2类中的fcn( int i, int j)

 


派生类函数覆盖基类中重载的虚函数 (551P)


拷贝函数与拷贝控制 (551P)


位于继承体系中的类(基类或派生类)没有定义拷贝控制操作(比如: 创建、拷贝、移动、复制和销毁操作), 则编译器将在必要的时候为它合成一个版本。 当然,这些合成的版本中的任何一个都可以被定义为删除的函数。


虚析构函数 (552P)


只要基类中的析构函数显式定义成虚函数 , 那么该析构函数的虚属性就会被继承。 因此, 无论派生类使用的是合成的析构函数 还是自定义的析构函数,都将是虚析构函数。 只要基类的析构函数是虚函数 ( 不管合成的还是自定义的),就能确保当我们delete基类指针时将运行正确的析构函数版本。

注意:如果基类的析构函数不是虚函数, 则 delete 一个指向派生类对象的基类指针,将产生未定义的行为。

之前我们曾介绍过一条经验准则,:

  • 即如果一个类需要析构函数, 那么它也同样需要拷贝和赋值操作。基类的析构函数并不遵循上述准则, 它是一个重要的例外。一个基类总是需要析构函数, 而且它能将析构函数设定为虚函数。此时该析构函数为了成为虚函数而令内容为空, 我们显然无法由此推断该基类还需要赋值运算符或拷贝构造函数。

虚析构函数将阻止合成的移动操作 (552P)


基类需要虚拟析构函数这一事实对基类和派生类的定义有着重要的间接影响:

  • 如果一个类定义了析构函数,不管它是自定义的,还是使用= default来使用合成版本,编译器也不会合成该类的移动操作。因此当我们移动该类对象时实际使用的是合成的拷贝操作。记住: 该类没有移动操作意味着它的派生类也没有。
  • 如果说基类定义了一个虚析构函数。因此,默认情况下,基类通常不会有合成移动操作。并且没有移动操作的基类,其派生类也不会得到合成移动操作。
  • 因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作, 所以当我们在派生类中确实需要执行移动操作时应该首先在基类中进行定义。

练习题15.24:

  • 只要是基类都需要有虚析构函数,这样就能够确保delete 一个动态分配的基类指针时,能够根据指针所指向的动态类型,运行适当的析构函数。

合成拷贝控制与继承 (533P)


基类或派生类中的合成拷贝控制成员像任何其他合成构造函数、赋值运算符或析构函数类似,它们对类本身的成员依次进行初始化、赋值或销毁的操作。此外,这些合成的成员通过使用派生类直接基类中的相应操作来初始化、分配或销毁一个派生类对象的直接基类部分。

值得注意的是,基类成员本身是合成的,还是用户自定义的,其实并不重要。重要的是对应的成员是可访问的,并且它不是一个已删除的函数。

  •  合成的析构函数的函数体(通常)是空的,其隐式的析构部分负责销毁类的成员。对于派生类的析构函数来说, 它除了销毁派生类自己的成员之外, 还负责销毁派生类的直接基类部分; 该直接基类又销毁它自己的直接基类部分, 以此类推直至继承链的顶端。

派生类中删除的拷贝控制与基类的关系 (553P)


基类 或 派生类 都可以把其合成的默认构造函数 或者 任何一个拷贝控制成员定义成被删除的函数(注意: 析构函数不能定义为已删除的,否则的话就不能销毁此类型的对象了。(450页有介绍))。此外,这些成员在基类中的定义方式可能会导致派生类相应成员被定义为已删除的:

  • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符 或 析构函数 是被删除的函数或不可访问 (指的是该成员被在基类中被定义为 private 中的成员),则派生类中的相应成员被定义为已删除,因为编译器不能使用基类成员来构造、赋值或销毁、派生类对象的基类部分。
  • 如果在基类中有一个不可访问或删除掉的析构函数, 则派生类中合成的默认和拷贝构造函数将是被删除的, 因为编译器无法销毁派生类对象的基类部分。
  • 和过去一样, 编译器将不会合成一个删除掉的移动操作。当我们使用 =default 在派生类中请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的, 那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的。
class B {
public:
	B();
	B(const B&) = delete;
	// other members, not including a move constructor
};
class D : public B
{
	// no constructors
};

int main()
{
	D d; //D 的合成默认构造函数 使用了其基类的默认构造函数
	D d2(d);  // 错误,D 的合成的拷贝构造函数想使用其基类中的拷贝构造函数,但是基类中的 已删除的,所以错误
	D d3(std::move(d)); // 错误,隐式地使用D 的被删除的拷贝构造函数
	system("pause");
	return 0;
}

在上述程序中,我们的 基类B 中定义了一个 已删除的 拷贝构造函数, 那么此时既不能移动也不可以拷贝B 的对象。

所以说,为一个基类中定义一个拷贝构造函数,即使被定义成 delete 已删除的,编译器也不会合成一个移动构造函数。


移动操作与继承 (552)


  • 如前所述,大多数基类都定义了一个虚析构函数。因此,默认情况下,基类通常不会得到合成移动操作。从没有移动操作的基类,其派生类也不会得到合成移动操作。
  • 因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作, 所以当我们在派生类中确实需要执行移动操作时应该首先在基类中进行定义。
  • 而且一旦基类中定义了自定义的移动操作,那么也必须同时显式定义其拷贝操作。

练习题 15.24:

  • 因为 Disc_quote 类中 定义了一个接受四个参数的构造函数, 表明 Disc_quote 类编译器不会为其合成的默认构造函数。 但我们创建 " Bulk_quote myBulk_quote " 这样的对象时, Bulk_quote 类的 默认构造函数 会调用 Disc_quote 类中的默认构造函数, 如果该类中没有, 因为Disc_quote 类中有其他的构造函数,表示不会再生产合成版本, 说明合成的默认构造函数是 不可访问或已删除的,那么 Bulk_quote 类就不能通过其基类 初始化 Bulk_quote 类的基类部分。
  • 如果 Disc_quote 类没有默认构造函数, 也就无法调用其基类中 默认构造函数来初始化 Disc_quote 类的基类部分。

派生类的拷贝控制成员 (554P)


当派生类定义了拷贝或移动操作时, 相应的操作负责拷贝或移动包括基类部分成员在内的整个对象。类似的,派生类的赋值运算符也必须为其基类部分的成员赋值。 但是, 派生类的析构函数只负责销毁自己分配的资源, 记住: 对象的成员是隐式销毁的; 类似的, 派生类的基类部分也是自动销毁的 。

在默认情况下, 基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动) 派生类对象的基类部分, 则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。

class Base { /* ... */ };
class D : public Base 
{
public:

	D(const D& d) : Base(d) // copy the base members
		/* initializers for members of D */ 
	{ /* ... */
	}
	D(D&& d) : Base(std::move(d)) // move the base members
		/* initializers for members of D */ 
	{ /* ... */
	}

	D &operator=(const D &rhs)
	{
		Base::operator=(rhs); // assigns the base part
		return *this;
	}
};

值得注意的是:无论基类的构造函数或赋值运算符自定义的版本还是合成的版本, 派生类的对应操作都能使用它们 ( 前提是它们是可访问的,并且不是已删除的)。例如, 上述程序对于Base : :operator= 的调用语句将执行Base的拷贝赋值运算符, 至于该运算符是由Base显式定义的还是由编译器合成的无关紧要。


在基类中的构造函数 和 析构函数中调用派生类虚函数会发生的情况 (556P)


如果说在基类的构造函数中调用虚函数的派生类版本时会发生什么情况:

  • 如果我们允许这样的访问,则程序很可能会崩溃。

为什么呢?

我们都知道在创建派生类对象时其基类部分会首先被构造。所以当执行基类的构造函数时,该对象的派生类部分是未初始化的状态。 

类似的,当销毁派生类对象的次序正好相反, 因此当执行基类的析构函数时, 派生类部分已经被销毁了。 因此可知, 当我们执行基类的构造和析构函数时, 其派生类对象的状态是不完整的。所以说 “ 在基类的构造函数中调用虚函数的派生类版本时 ,则程序很可能会崩溃 ”。

  • 注意:如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。

使用 using 声明使派生类“ 继承”基类的构造函数 (557)


我们知道一个派生类只初始化它的直接基类部分,在C++11 新标准中,派生类只能够重用(或者说 “ 继承 ”)直接基类定义的构造函数。 但是呢,派生类不能从直接基类那 “ 继承 ” 默认、拷贝和移动构造函数,如果说派生类没有直接定义这些构造函数, 则编译器将为派生类合成它们。

派生类重用(或者说 “ 继承 ”)基类构造函数的方式是:

  • 在类定义中提供其(直接)基类的 using 声明来继承其基类构造函数。

例如:看下列程序:


class Quote
{
public:
	Quote() = default;
	Quote(const std::string &book, double sales_price) :bookNo(book), price(sales_price)
	{
		cout << "调用的是基类中的构造函数!" << endl;
	}


	std::string isbn() const { return bookNo; }

	virtual double net_price(std::size_t n) const
	{
		cout << "调用的是基类的net_price 函数" << endl;
		return n * price;
	}

	virtual ~Quote() = default; // dynamic binding for the destructor
private:
	std::string bookNo;  // 书籍的 ISBN 编号
protected:
	double price = 0.0;
};


class Disc_quote : public Quote // Disc_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)
	{
		cout << "调用的是Disc_quote 类中的构造函数!" << endl;
	}

	double net_price(std::size_t) const = 0;  // 定义纯虚函数

	std::pair<size_t, double> discount_policy() const // 新添加的 discount_policy() 成员
	{
		cout << "调用的是Disc_quote 类中的 discount_policy 函数" << endl;
		return { quantity, discount };
	}
protected:
	std::size_t quantity = 0; // 折扣适用的购买量
	double discount = 0.0; // 表示折扣的小数值
};


class Bulk_quote : public Disc_quote
{
public:
	using Disc_quote::Disc_quote; // 继承 Disc_quote 的构造函数
	
	// 覆盖基类中相应版本的函数, 该类不再是抽象类
	double net_price(std::size_t) const override
	{
		cout << "调用的是Bulk_quote类中的net_price 函数!" << endl;
		return 0;
	}
};

上面在  Bulk_quote  类  中使用 using 声明继承其直接基类Disc_quote  中的所有构造函数( 这些继承到派生类中的每一个构造函数跟基类中对应的构造函数的形参列表完全相同),那么 这些继承到派生类中的构造函数会初始化 派生类对象的基类部分,其派生类自己的部分如果没有其它的构造函数作初始化工作,那么这些成员将被默认初始化。

因为在 Disc_quote 类中有两个构造函数,分别为:

Disc_quote() = default;
Disc_quote(const std::string &book, double price, std::size_t qty, double disc);

那么在其 派生类 Bulk_quote 中就会有两个与其基类中形参列表中完全相同的构造函数, 在我们的 Bulk_quote 中,其构造函数也有两个:

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

 

使用 using 时, 还是有两个例外  派生类不会继承其基类中的所有构造函数:

第一个例外是:

  • 派生类可以继承基类的一部分构造函数, 而另一部分可以定义自己的版本; 但是要注意的是:自定义版本的构造函数参数列表要跟基类中对应的构造函数的参数列表相同。这样基类中的相应版本就不会被继承。定义在派生类中的构造函数将替换基类继承而来的构造函数。

第二个例外是:

  • 基类中的 默认、拷贝和移动构造函数不会其被派生类继承。如果需要的话, 派生类会合成它们。 
  • 注意的是: 从基类继承过来的构造函数是不会被作为用户定义的构造函数来使用 ( 意识就是说 如果正在创建一个派生类对象, 它是不会调用从其基类中继承过来的构造函数的)。 所以说,如果一个类只含有从基类继承来的构造函数,则它也将拥有一个合成的默认构造函数。

继承的构造函数的特点:

  • 与对普通成员使用 using 声明不同,在派生类中使用 using 声明不会更改继承来的构造函数的访问级别。 例如, 不管 using 声明出现在哪儿, 基类的私有构造函数在派生类中还是一个私有构造函数;  受保护的构造函数和公有构造函数也是同样的规则。
  • 而且, 一个 using 声明语句是不能被指定 explicit 或 constexpr 关键字。如果基类的构造函数是 explicit 或者constexpr ,则继承的构造函数也拥有相同的属性。
  • 注意: 当基类中的构造函数含有默认实参,但使用using 时,默认实参是不会被其派生类所继承。
  • 相反,派生类将获得多个继承的构造函数,其中每个构造函数会分别省略掉一个含有默认实参的形参。例如, 如果基类有一个接受两个形参的构造函数, 其中第二个形参含有默认实参, 则派生类将获得两个构造函数: 一个构造函数接受两个形参(没有默认实参),  另一个构造函数只接受一个形参,它对应于基类中最左侧的没有默认值的那个形参。

在容器 中 存放具有继承关系的类对象 (558P)


当我们使用容器来存储继承层次结构中的对象时,通常必须间接存储这些对象。 我们不能把具有继承关系的多种类型的对象直接存放在容器当中,因为在容器中不允许保存不同类型的元素。


class Quote
{
public:
	Quote() = default;
	Quote(const std::string &book, double sales_price) :bookNo(book), price(sales_price)
	{
		cout << "调用的是基类中的构造函数!" << endl;
	}


	std::string isbn() const { return bookNo; }

	virtual double net_price(std::size_t n) const
	{
		cout << "调用的是基类的net_price 函数" << endl;
		return n * price;
	}

	virtual ~Quote() = default; // dynamic binding for the destructor
private:
	std::string bookNo;  // 书籍的 ISBN 编号
protected:
	double price = 0.0;
};


class Disc_quote : public Quote // Disc_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)
	{
		cout << "调用的是Disc_quote 类中的构造函数!" << endl;
	}

	double net_price(std::size_t) const = 0;  // 定义纯虚函数

	std::pair<size_t, double> discount_policy() const // 新添加的 discount_policy() 成员
	{
		cout << "调用的是Disc_quote 类中的 discount_policy 函数" << endl;
		return { quantity, discount };
	}
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 price, std::size_t qty, double disc) :
		Disc_quote(book, price, qty, disc)

	{
		cout << "调用的是 Bulk_quote 类中的构造函数!" << endl;
	}
	// 覆盖基类中相应版本的函数, 该类不再是抽象类
	double net_price(std::size_t) const override
	{
		cout << "调用的是Bulk_quote类中的net_price 函数!" << endl;
		return 0;
	}
};


int main()
{
	vector<Quote> basket;
	basket.push_back(Quote("0-201-82470-1", 50));

	// 但是只能把 Bulk_quote 对象的 基类部分拷贝给 basket
	basket.push_back(Bulk_quote("0-201-54848-8", 50, 10, .25));


	// 首先获取到 容器中最后一个元素的引用,然后调用 该函数
	cout << basket.back().net_price(15) << endl;

	system("pause");
	return 0;
}

如果 容器保存的类型是基类对象时,此时我们向该容器中添加一个其派生类对象时,那么该派生类对象的其基类部分会被 “ 切割 ”掉, 所以说容器中不能把具有继承关系的多种类型的对象直接存放在容器中。

 

当我们希望在容器中存放具有继承关系的对象时, 实际上存放的通常是基类的指针 (更好的选择是智能指针 )。和往常一样, 这些指针所指对象的动态类型可能是基类类型,也可能是派生类类型。


暂时完结.....

最后

以上就是清新丝袜为你收集整理的C++ Primer 第五版 ——《第十五章 》面向对象程序设计 (多态与继承)学习笔记的全部内容,希望文章能够帮你解决C++ Primer 第五版 ——《第十五章 》面向对象程序设计 (多态与继承)学习笔记所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(66)

评论列表共有 0 条评论

立即
投稿
返回
顶部