107.继承总结

发布时间 2023-07-18 12:01:59作者: CodeMagicianT

107.继承总结

1. 概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称子类或者派生类,被继承的类称为父类或基类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

继承是类设计层次的复用。

2.为什么要有继承

继承是一种设计模式,它允许在存在类之间关系的情况下,定义一个类可以从另一个类中获取和继承属性和方法。继承在面向对象编程中扮演着重要的角色,它可以帮助我们实现代码的重用性、灵活性和可维护性。以下是继承的几个主要优点:

  1. 代码重用:继承允许一个类从另一个类中获取属性和方法,从而避免了重复编写代码的情况。如果多个类具有相同或相似的属性或方法,我们可以通过继承这些共同的属性或方法,避免代码冗余,提高代码的复用性。
  2. 简化类定义:通过继承,我们可以从已有的类中获取属性和方法,从而减少需要在新类中定义的属性和方法数量。这样可以简化类的定义,降低代码的复杂度。
  3. 更好的代码组织:继承允许我们将相关的类分组在一起,形成一个更具结构化的代码组织方式。这有助于提高代码的可读性和可维护性。
  4. 增加代码可扩展性:通过继承,我们可以定义更一般化的类,从这些类中派生出更具体的子类。这种做法使得代码更加灵活,可以轻松地适应未来的需求变化。
  5. 多态性:继承也支持多态性,这意味着我们可以使用一个类的实例来代替其父类的实例,而不会影响程序的正确性。多态性为程序的扩展和修改提供了更大的灵活性。

总之,继承是一种强大的编程工具,它可以帮助我们更好地组织和管理代码,提高代码的复用性和可维护性,同时还能降低代码的复杂度。然而,在使用继承时,我们需要谨慎地考虑类之间的关系,避免过度使用继承导致代码的过度复杂化。

3.继承的定义

例子:

class Person
{
public:
	string _name;
	int _age;
	string _sex;
};

class Teacher :public Person
{
public:
	//可以在派生类中访问基类成员
	void print()
	{
		cout << _name << endl;
		cout << _age << endl;
		cout << _sex << endl;
		cout << _workTime << endl;
	}
	int _workTime;
};

class Student :public Person
{
public:
	//可以在派生类中访问基类成员
	void print()
	{
		cout << _name << endl;
		cout << _age << endl;
		cout << _sex << endl;
		cout << _studyTime << endl;
	}
	int _studyTime;
};

3.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;//书籍的ISBN编号
protected:
	double price = 0.0;//代表普通状态下不打折的价格
};
在c++中,基类的析构函数需要定义为虚函数,原因有以下几点:

当使用多态特性,让基类指针指向派生类对象时,如果析构函数不是虚函数,通过基类指针销毁派生类对象时,会调用静态绑定的析构函数,也就是基类的析构函数,从而只能销毁属于基类的元素,导致派生类析构不完全,程序就会出现资源泄露或未定义行为。
当派生类中不存在使用动态资源或其他自定义析构行为时,可以不写为虚析构函数,来提高程序效率。但为了程序的可扩展性和健壮性,在使用多态特性时,一般都建议将基类的析构函数定义为虚函数。

3.1.1成员函数与继承

派生类可以继承其基类的成员,然而当遇到如net_price这样与类型相关的操作时,派生类必须对其重新定义。换句话说,派生类需要对这些操作提供自己的新定义以覆盖(override)从基类继承而来的旧定义。

在C++语言中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通常将其定义为虚函数(virtual)。当我们使用指针或引用调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。

3.2定义派生类

派生类必须通过使用类派生列表(classderivation list)明确指出它是从哪个(哪些)基类继承而来的。类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有以下三种访问说明符中的一个:public、protected或者private。

派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明,因此,我们的Bulk_quote类必须包含一个net_price成员:

class Bulk_quote :public Quote//Bulk_quote继承自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;//以小数表示的折扣额
};

我们的Bulk_quote类从它的基类Quote那里继承了isbn函数和bookNo、price等数据成员。此外,它还定义了net_price的新版本,同时拥有两个新增加的数据成员min_qty和discount。这两个成员分别用于说明享受折扣所需购买的最低数量以及一旦该数量达到之后具体的折扣信息。

访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类的用户可见。

如果个派生是公有的,则基类的公有成员也是派生类接口的组成部分。此外,能将公有派生类型的对象绑定到基类的引用或指针上。因为在派生列表中使用了public,所以Bulk_quote的接口隐式地包含isbn函数,同时在任何需要Quote的引用或指针的地方我们都能使用Bulk_quote的对象。
大多数类都只继承自一个类,这种形式的继承被称作 “ 单继承 “。

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

一个派生类对象 包含多个组成部分: 一 个含有派生类自己定义的(非静态) 成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。因此,一个Bulk_quote对象将包含四个数据元素:它从Quote继承而来的bookNo和price数据成员,以及Bulk_quote自己定义的min_qty和discount成员。

C++标准并没有明确规定派生类的对象在内存中如何分布,但是我们可以认为 Bulk_quote的对象包含如图所示的两部分。

在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的。 上图只是表示类工作机理的概念模型,而非物理模型。

因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。

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

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

这种隐式特性意味着我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方:同样的,我们也可以把派生类对象的指针用在需要基类指针的地方。

NOTE:在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。

(2)派生类构造函数

尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。和其他创建了基类对象的代码一样,派生类也必须使用基类的构造函数来初始化它的基类部分。

NOTE:每个类控制它自己的成员初始化过程。

派生类对象的基类部分与派生类对象自己的数据成员都是在构造函数的初始化阶段执行初始化操作的。类似于我们初始化成员的过程,派生类构造函数同样是通过构造函数初始化列表来将实参传递给基类构造函数的。例如,接受四个参数的Bulk_quote构造函数如下所示:

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

}

该函数将它的前两个参数(分别表示ISBN和价格)传递给Quote的构造函数,由Quote的构造函数负责初始化Bulk_quote的基类部分(即bookNo成员和price成员)。当 (空的)Quote构造函数体结束后,我们构建的对象的基类部分也就完成初始化了。接下 来初始化由派生类直接定义的min_qty成员和discount成员。最后运行Bulk_quote 构造函数的(空的)函数体。

除非我们特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化。如果想使用其他的基类构造函数,我们需要以类名加圆括号内的实参列表的形式为构造函数提供初始值。这些实参将帮助编译器决定到底应该选用哪个构造函数来初始化派生类对象的基类部分。

NOTE:首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

(3)派生类使用基类的成员

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

//如果达到了购买书籍的某个最低限世值,就可以享受折扣价格了
double Bulk_quote::net_price(size_t cnt) const
{
	if (cnt >= min_qty)
		return cnt * (1 - discount) * price;
	else
		return cnt * price;
}

该函数产生一个打折后的价格:如果给定的数量超过了min_qty,则将discount(一个小于1大于0的数)作用于price。

派生类的作用域嵌套在基类的作用域之内。因此,对于派生类的一个成员来说,它使用派生类成员(例如 min_qty和discount)的方式与使用基类成员(例如price)的方式没什么不同。

关键概念:遵循基类的接口

​ 必须明确一点:每个类负责定义各自的接口。要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此。
​ 因此,派生类对象不能直接初始化基类的成员。尽管从语法上来说我们可以在派生类构造函数体内给它的公有或受保护的基类成员赋值,但是最好不要这么做。和使用基类的其他场合一样,派生类应该遵循基类的接口,并且通过调用基类的构造函数来初始化那些从基类中继承而来的成员。

(4)继承与静态成员

如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。

class Base
{
public:
	static void statemem();
};
class Derived : public Base
{
	void f(const Derived&);
};

静态成员遵循通用的访问控制规则,如果基类中的成员是private的,则派生类无权访问它。假设某静态成员是可访问的,则我们既能通过基类使用它也能通过派生类使用它:

void Derived::f(const Derived& derived_obj)
{
	Base::statmem();//正确:Base定义了statemem
	Derived::statmem();//正确:Derived继承了statmem
	//正确:派生类的对象能访问基类的静态成员
	derived_obj.statmem();//通过Derived对象访问
	statmem();//通过this对象访问
}

(5)派生类的声明

派生类的声明与其他类差别不大, 声明中包含类名但是不包含它的派生列表:

class Bulk_quote : public Quote;//错误:派生列表不能出现在这里
class Bulk_quote;//正确:声明派生类的正确方式

一条声明语句的目的是令程序知晓某个名字的存在以及该名字表示一个什么样的实体,如 一个类、一个函数或一个变量等。派生列表以及与定义有关的其他细节必须与类的主体一起出现。

(6)被用作基类的类

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

class Quote;//声明但未定义
//错误:Quote必须被定义
class Bulk_quote : public Quote { ... };

这一规定的原因显而易见:派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类当然要知道它们是什么。因此该规定还有一层隐含的意思,即一个类不能派生它本身。

一个类是基类,同时它也可以是一个派生类:

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

在这个继承关系中,Base是D1的直接基类(direct base),同时是D2的间接基类(indirect base)。直接基类出现在派生列表中,而间接基类由派生类通过其直接基类继承而来。

每个类都会继承直接基类的所有成员。对于一个最终的派生类来说,它会继承其直接基类的成员;该直接基类的成员又含有其基类的成员:依此类推直至继承链的顶端。因此,最终的派生类将包含它的直接基类的子对象以及每个间接基类的子对象。

(7)防止继承的发生

有时会定义这样一种类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类。为了实现这一目的,C++11新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字final:

class NoDerived final { /* */ };//NoDerived不能作为基类
class Base { /* */ };
// Last是final的;我们不能继承Last 
class Last final : Base { /* */ };//Last不能作为基类
class Bad : NoDerived { /* */ };//错误:NoDerived是final的
class Bad2 : Last { /* */ };//错误:Last是final的

3.3类型转换与继承

在C++中,变量的类型可以分为静态类型和动态类型两种。

(1)类型

静态类型:静态类型是指在程序运行时分配的类型,它们的大小和数据结构在程序运行时就已经确定了,因此可以直接使用int、float等基本数据类型或者自定义数据类型。静态类型的变量可以通过直接赋值或者引用的方式来传递它们的值,不需要使用new/delete运算符进行动态分配和释放。

动态类型:动态类型是指在程序运行时分配的类型,它们的大小和数据结构在程序运行时可以改变,通常是通过new/delete运算符来动态分配的。动态类型的变量需要使用new/delete运算符来创建和销毁,而且它们的值传递需要使用指针或者引用的方式,而不能直接使用变量名称来访问。指针或者引用所代表的内存中的对象的类型,在运行阶段才可以确定。

GameObject* pgo = new SpaceShip // pgo 静态类型是 GameObject*, 动态类型是 SpaceShip*
Asterioid * pa = new Asterioid;  // pa 的静态类型是 Asterioid *, 动态类型也是 Asterioid *
pgo = pa;                       // pgo 静态类型总指向 GameObject *, 动态类型指向 Asterioid *
GameObject& rgo = *pa;          // rgo 的静态类型是 GameObject, 动态类型是 Asterioid
class B
{
}
class C : public B
{
}
class D : public B
{
}
D* pD = new D();//pD的静态类型是它声明的类型D*,动态类型也是D*
B* pB = pD;//pB的静态类型是它声明的类型B*,动态类型是pB所指向的对象pD的类型D*
C* pC = new C();
pB = pC;//pB的动态类型是可以更改的,现在它的动态类型是C*

(2)绑定

  • 静态绑定绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期,又称前期绑定 early binding;

  • 动态绑定绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期,又称后期绑定 late binding;

A.虚函数与作用域

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

class D1 :public Base
{
public:
	//隐藏基类的fcn,这个fcn不是虚函数
	//D1继承了Base::fcn()的定义
	int fcn(int);//形参列表于Base中的fcn不一致
	virtual void f2();//是一个新的虚函数,在Base中不存在
};

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

隐藏:在子类中有与父类中同名的成员函数,当父类中同名成员函数为虚函数,但形参列表不一致时(也即未发生函数重载),或者父类同名函数非虚函数,子类未重载其父类同名成员函数,父类的同名成员函数被隐藏。

覆盖(存在虚函数是前提):在子类中有与父类中同名的成员函数,当父类中同名成员函数为虚函数且形参列表一致时(也即发生函数重载),父类的同名成员函数被覆盖。

B.通过基类调用隐藏的虚函数

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();//错误:Base没有名为f2的成员
d1p->f2();//虚调用,将在运行时调用D1::f2()
d2p->f2();//虚调用,将在运行时调用D2::f2()

Base* p1 = &d2obj; D1* p2 = &d2obj; D2 *p3 = &d2obj;
p1->fcn(42);//错误:Base中没有接受一个int的fcn
p2->fcn(42);//静态绑定,调用D1::fcn(int)
p3->fcn(42);//静态绑定,调用D2::fcn(int)

//D1* d1b = &bobj;//

前三条调用语句是通过基类的指针进行的,因为fcn是虚函数,所以编译器产生的代码将在运行时确定使用虚函数的哪个版本。判断的依据是该指针所绑定对象的真实类型。

调用原则:调用函数所使用指针的类型所属类和实际指向的对象所属类共同限制了函数的检索范围,由于不存在从基类向派生类的隐式转换,所以检索范围一定是从实际指向的对象所属类往基类方向向上检索,到使用指针的类型所属类为止。

如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。例如,Quote类型的变量永远是一个Quote对象,我们无论如何都不能改变该变量对应的对象的类型。

NOTE:基类的指针或引用的静态类型可能与其动态类型不一致,读者一定要理解其中的原因。

(3)基类派生类之间的类型转换

①隐式类型转换

基类的指针(包括智能指针)或引用可以绑定到派生类对象。即存在派生类向基类的隐式转换。不存在从基类向派生类的隐式类型转换。

之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分上。一个基类的对象既可以以独立的形式存在,也可以作为派生类对象的一部分存在。如果基类对象不是派生类对象的一部分,则它只含有基类定义的成员,而不含有派生类定义的成员。
因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换:

Quote base;
Bulk_quote* bulkP = &base; //错误:不能将基类转换成派生类
Bulk_quote& bulkRef = base;//错误:不能将基类转换成派生类

如果上述赋值是合法的,则我们有可能会使用bulkP或bulkRef访问base中本不存在的成员。
除此之外还有一种情况显得有点特别,即使一个基类指针或引用绑定在一个派生类对象上,我们也不能执行从基类向派生类的转换:

Bulk_quote bulk;
Quote* itemP = &bulk;//正确:动态类型是Bulk_quote
Bulk_quote* bulkP = itemP;//错误:不能将基类转换成派生类

编译器在编译时无法确定某个特定的转换在运行时是否安全,这是因为编译器只能通过检查指针或引用的静态类型来推断该转换是否合法。如果在基类中含有一个或多个虚函数,我们可以使用dynamic_cast请求一个类型转换,该转换的安全检查将在运行时执行。同样,如果我们已知某个基类向派生类的转换是安全的,则我们可以使用static_cast来强制覆盖掉编译器的检查工作。

②强制类型转换

A.static_cast:

a.基类(父类)和派生类(子类)之间指针或引用的转换。

  • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
  • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
Bulk_quote *bulk_p = new Bulk_quote(); 						// 派生类指针
Quote *item_p = new Quote(); 								// 基类指针

Quote *item_p1 = static_cast<Quote *>(bulk_p); 				// 上行转换,安全
Bulk_quote *bulk_p1 = static_cast<Bulk_quote *>(item_p);	// 下行转换,不安全,不要使用

b.基类和派生类对象间的转换。

  • 派生类对象可以向基类对象转换
  • 基类对象不可以向派生类转换
Bulk_quote bulk; 			// 派生类对象
Quote item; 				// 基类对象

Quote item1 = static_cast<Quote>(bulk);				// 可以正常编译运行
Bulk_quote bulk1 = static_cast<Bulk_quote>(item); 	// 无法通过编译

B.dynamic_cast

dynamic_cast的转换只有下面3种:

●dynamic_cast< type* >(e)
type必须是一个类类型且必须是一个有效的指针
●dynamic_cast< type& >(e)
type必须是一个类类型且必须是一个左值
●dynamic_cast< type&& >(e)
type必须是一个类类型且必须是一个右值

dynamic_cast主要用于类层次间的上行转换和下行转换。

在进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。使用dynamic_cast要耗费重大的运行成本。

Bulk_quote *bulk_p = new Bulk_quote(); 						// 派生类指针
Quote *item_p = new Quote(); 								// 基类指针

Quote *item_p1 = dynamic_cast<Quote *>(bulk_p); 				// 上行转换,同static_cast效果一样,但是运行代价更大
Bulk_quote *bulk_p1 = dynamic_cast<Bulk_quote *>(item_p);	// 下行转换,失败,bulk_p1为空
Bulk_quote *bulk_p2 = dynamic_cast<Bulk_quote *>(item_p1);	// 下行转换,成功

C.智能指针share_ptr的类型转换
static_cast和dynamic_cast用于非智能指针的类型转换。针对智能指针进行的类型转换需要使用下面4种转换:static_pointer_cast、dynamic_pointer_cast、const_pointer_cast、reinterpret_pointer_cast。它们的功能和static_cast、dynamic_cast、const_cast、reinterpret_cast类似,只不过转换的是智能指针std::shared_ptr,返回的也是std::shared_ptr类型。只有share_ptr存在类型转换。
参考:C++ 基类与派生类间的类型转换

3.4虚函数

在C++语言中,当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。通常情况下,如果我们不使用某个函数,则无须为该函数提供定义。但是我们必须为每一个虚函数都提供定义,而不管它是否被用到了,这是因为连编译器也无法确定到底会使用哪个虚函数。

(1)虚函数的定义

在实现C++多态时会用到虚函数。虚函数使用的其核心目的是通过基类访问派生类定义的函数。所谓虚函数就是在基类定义一个未实现的函数名,为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。一般格式:

class base
{
public:
	base();
	virtual void test(); //定义的一个虚函数
private:
	char* basePStr;
};

上述代码在基类中定义了一个test的虚函数,所有可以在其子类重新定义父类的做法这种行为成为覆盖(override),或者为重写。

常见用法:声明基类指针,利用指针指向任意一个子类对象,调用相关的虚函数,动态绑定,由于编写代码时不能确定被调用的是基类函数还是那个派生类函数,所以被称为“”虚“”函数。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。

#include<iostream>  
using namespace std;

class A
{
public:
    void foo()
    {
        printf("1\n");
    }
    virtual void fun()
    {
        printf("2\n");
    }
};

class B : public A
{
public:
    void foo()  //隐藏:派生类的函数屏蔽了与其同名的基类函数
    {
        printf("3\n");
    }
    void fun()  //多态、覆盖
    {
        printf("4\n");
    }
};

int main(void)
{
    A a;
    B b;
    A* p = &a;
    p->foo();  //输出1
    p->fun();  //输出2
    p = &b;
    p->foo();  //取决于指针类型,输出1
    p->fun();  //取决于对象类型,输出4,体现了多态

    return 0;
}

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

当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。

//计算并打印销售给定数注的某种书符所得的费用
double print_total(ostream& os, const Quote& item, size_t n)
{
    //根据传入item形参的对象类型调用Quote::net _price 
    //或者Bulk_quote::net_price 
    double ret = item.net_price(n);
    os << "ISBN: " << item.isbn() //调用Quote::isbn
        << " # spld:" << n << " total due: " << ret << endl;
    return ret;
}

print_total函数通过其名为item的参数来进一步调用net_price,其中item的类型是&Quote。因为item是引用而且net_price是虚函数,所以到底调用net_price的哪个版本完全依赖于运行时绑定到item的实参的实际(动态)类型:

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

在第一条调用语句中,item绑定到Quote类型的对象上,因此当print_total调用net_price时,运行在Quote中定义的版本。在第二条调用语句中,item绑定到Bulk_quote类型的对象上,因此print_total调用Bulk_quote定义的net_price。

必须要搞清楚的一点是,动态绑定只有当我们通过指针或引用调用虚函数时才会发生。

base = derived;//把derived的Quote部分拷贝给base
base.net_price(20);//调用Quote::net_price

当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。例如,如果我们使用base调用net_price,则应该运行net_price的哪个版本是显而易见的。我们可以改变base表示的对象的值(即内容),但是不会改变该对象的类型。因此,在编译时该调用就会被解析成Quote的net_price。

(3)派生类中的虚函数

当我们在派生类中稷盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质。然而这么做并非必须,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数。
一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
同样,派生类中虚函数的返回类型也必须与基类函数匹配。该规则存在一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。也就是说,如果D由B派生得到则基类的虚函数可以返回B*而派生类的对应函数可以返回D*,只不过这样的返回类型要求从D到B的类型转换是可访问的。

注意:基类中的虚函牧在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。

(4)final 和 override 说明符

派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的行为。编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。这时,派生类的函数并没有覆盖掉基类中的版本。就实际的编程习惯而言,这种声明往往意味着发生了错误,因为我们可能原本希望派生类能覆盖掉基类中的虚函数, 但是一不小心把形参列表弄错了。

要想调试并发现这样的错误显然非常困难。在C++11新标准中我们可以使用 override关键字来说明派生类中的虚函数。这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误,后者在编程实践中显得更加重要。如果我们 使用override标记了某个函数,但该函数并没有狻盖已存在的虚函数,此时编译器将报错:

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

struct D1 : B
{
    void f1(int) const override;//正确:f1于基类的f1匹配
    void f2(int) override;//错误:B没有形如f2(int)的函数
    void f3() override;//错误:f3不是虚函数
    void f4() override;//错误:B没有名为f4的函数
};

使用override所表达的意思是我们希望能覆盖基类中的虚函数而实际上并未做到,所以编译器会报错。

因为只有虚函数才能被覆盖,所以编译器会拒绝D1的f3。该函数不是B中的能函数,因此它不能被覆盖。类似的,f4的声明也会发生错误,因为B中根本就没有名为f4的函数。

还能把某个函数指定为final,如果我们已经把函数定义成final了,则之后 任何尝试覆盖该函数的操作都将引发错误:

struct D2 : B
{
    //从B继承f2()和f3(),覆盖f1(int)
    void f1(int) const final;//不允许后续的其他类覆盖f1(int)
};

struct D3 : D2
{
    void f2();//正确:覆盖从间接基类B继承而来的f2
    void f1(int) const;//错误:D2已经将f2声明成final
};

final和override说明符出现在形参列表(包括行何const或引用修饰符)以及尾置返回类型之后。

(5)虚函数与默认实参

和其他函数一样,虚函数也可以拥有默认实参。如果某次函数调用使用了默认实参,则该实参由本次调用的静态类型决定。
换句话说,如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不符。

建议:如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

例子:

#include <iostream>
using namespace std;
class A
{
public:
    virtual void Display(int i = 0)
    {
        cout << "A:  " << i << endl;
    }
};

class B : public A
{
public:
    virtual void Display(int i = 1)
    {
        cout << "B:  " << i << endl;
    }
};

int main()
{

    B b;
    A* p = &b;

    p->Display(4);
    p->Display();  //virtual是动态绑定,而缺省参数值却是静态绑定

    return 0;
}

输出:

B:  4
B:  0

(6)回避虚函数的机制

在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。仗用作用域运算符可以实现这一目的,例如下面的代码:

//强行调用基类中定义的面数版本而不管baseP的动态类型到底是什么 
double undiscounted = baseP->Quote::net_price(42);

该代码强行调用Quote的net_price函数,而不管baseP实际指向的对象类型到底是什么。该调用将在编译时完成解析。

通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。

什么时候我们需耍回避虚函数的默认机制呢?通常是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时。在此情况下,基类的版本通常完成继承层次中所有类型都要做的共同任务,而派生类中定义的版本虚要执行一些与派生类本身密切相关的操作。

警告:如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

(7)基类和派生类的虚函数表是否为同一个

  • 派生类实现基类的虚函数时,基类中虚函数表和派生类的虚函数表地址不同,基类虚函数表中的虚函数地址和派生类虚函数表中的虚函数地址不同;
  • 派生类不实现基类的虚函数时,基类中虚函数表和派生类中虚函数表地址不同,基类虚函数表中的虚函数地址和派生类虚函数表中的虚函数地址相同。

(8)为什么基类的析构函数需要定义为虚函数?

当使用多态特性,让基类指针指向派生类对象时,如果析构函数不是虚函数,通过基类指针销毁派生类对象时,会调用静态绑定的析构函数,也就是基类的析构函数,从而只能销毁属于基类的元素,导致派生类析构不完全,程序就会出现资源泄露或未定义行为。
当派生类中不存在使用动态资源或其他自定义析构行为时,可以不写为虚析构函数,来提高程序效率。但为了程序的可扩展性和健壮性,在使用多态特性时,一般都建议将基类的析构函数定义为虚函数。

3.5抽象基类

在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。

做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class).

■纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。

■当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。

■Virtual void fun() = 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。

建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来操纵一组类,且这个公共接口不需要事先(或者不需要完全实现)。可以创建一个公共类。

假设我们希望扩展书店程序并令其支持几种不同的折扣策略。除了购买量超过一定数量享受折扣外,我们也可能提供另外一种策略,即购买品不超过某个限额时可以享受折扣, 但是一旦超过限额就要按原价支付。或者折扣策略还可能是购买品超过一定数量后购买的全部书籍都享受折扣,否则全都不打折。

上面的每个策略都要求一个购买量的值和—个折扣值。我们可以定义一个新的名为Disc_quote的类来支持不同的折扣策略,其中Disc_quote负责保存购买量的值和折扣值。其他的表示某种特定策略的类(如Bulk_quote)将分别继承自Disc_quote每个派生类通过定义自己的net_price函数来实现各自的折扣策略。

在定义Disc_quote类之前,首先要确定它的net_price函数完成什么工作。显然我们的Disc_quote类与任何特定的折扣策略都无关,因此Disc_quote类中的net_price函数是没有实际含义的。

我们可以在Disc_quote类中不定义新的net_price,此时,Disc_quote将继承Quote中的net_price函数。

然而,这样的设计可能导致用户编写出一些无意义的代码。用户可能会创建一个Disc_quote对象并为其提供购买量和折扣值,如果将该对象传给一个像print_total这样的函数,则程序将调用Quote版本的net_price。显然,最终计算出的销售没有考虑我们在创建对象时提供的折扣值,因此上述操作毫无意义。

(1)纯虚函数

可以将net_price定义成纯虚(pure virtual)函数从而令程序实现我们的设计意图,这样做可以清晰明了地告诉用户当前这个net_price函数是没有实际意义的。和普通的虚函数不一样,一个纯虚函数无须定义。我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句处:

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;//书籍的ISBN编号
protected:
	double price = 0.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;//表示折扣的小数值
};

和我们之前定义的Bulk_quote类一样,Disc_quote也分别定义了一个默认构造函数和一个接受四个参数的构造函数。尽管我们不能直接定义这个类的对象,但是Disc_quote的派生类构造函数将会使用Disc_quote的构造函数来构建各个派生类对象的Disc_quote部分。其中,接受四个参数的构造函数将前两个参数传递给Quote的构造函数,然后直接初始化自己的成员discount和quantity。默认构造函数则对这些成员进行默认初始化。

值得注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,我们不能在类的内部为一个=0的函数提供函数体。

例子2:

class Base
{
public:
	virtual void Examp() = 0;//纯虚函数
 
	~Base()
	{
		cout << "父类的析构函数" << endl;
	}
};
 
class Son:public Base
{
public:
	void Examp()
	{
		cout << "重写了父类的纯虚函数" << endl;
	}
	
	~Son()
	{
		cout << "子类的析构函数" << endl;
	}
};
 
int main()
{
	Son p1;
	p1.Examp();
	system("pause");
}

(2)含有纯虚函数的类是抽象基类

含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类(abstract base class)。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。我们不能(直接)创建个抽象基类的对象。因为Disc_quote将net_price定义成了纯虚函数,所以我们不能定义 Disc_quote的对象。我们可以定义Disc_quote的派生类的对象,前提是这些类覆盖了net_price函数:

// Disc_quote声明了纯虚函数,而Bulk_quote将覆盖该函数
Disc_quote discounted; //错误:不能定义Disc_quote的对象
Bulk_quote bulk;//正确:Bulk_quote中没有纯虚函数

Disc_quote的派生类必须给出自己的net_price定义,否则它们仍将是抽象基类。

我们不能创建抽象基类的对象。

(3)派生类构造函数只初始化它的直接基类

接下来可以重新实现Bulk_quote了,这一次我们让它继承Disc_quote而非直接继承Quote:

//当同一书籍的销售量超过某个值时启用折扣
//折扣的值是一个小于1的正的小数值,以此来降低正常销售价格
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) 
	{
	}
	//覆盖基类中的函数版本以实现一种新的折扣策略
	double net_price(std::size_t) const override;
}

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

每个类各自控制其对象的初始化过程。因此,即使Bulk_quote没有自 己的数据成员,它也仍然需要像原来一样提供一个接受四个参数的构造函数。该构造函数 将它的实参传递给Disc_quote的构造函数,随后Disc_quote的构造函数继续调用Quote的构造函数。Quote的构造函数首先初始化bulk的bookNo和price成员,当Quote的构造函数结束后,开始运行Disc_quote的构造函数并初始化quantity和中discount成员,最后运行Bulk_quote的构造函数,该函数无须执行实际的初始化或其他工作。

关键概念:重构

在Quote的继承体系中增加Disc_quote类是重构(refactoring)的一个典型示例。重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中。对于 面向对象的应用程序来说,重构是一种很普遍的现象。
值得注意的是,即使我们改变了整个继承体系,那些使用了Bulk_quote或Quote的代码也无须进行任何改动。不过一旦类被重构(或以其他方式被改变),就意味着我们必须重新编译含有这些类的代码了。

3.6访问控制与继承

  • public的变量和函数在类的内部外部都可以访问
  • protected的变量和函数只能在类的内部和其派生类中访问
  • private修饰的元素只能在类内访问

派生类可以继承基类中除了构造/析构、赋值运算符重载函数之外的成员,这些成员的访问属性在派生过程中也是可以调整的,三种派生方式的访问权限如下表所示:注意外部访问并不是真正的外部访问,而是在通过派生类的对象对基类成员的访问。

派生类对基类成员的访问形象有如下两种:

●内部访问:由派生类中新增的成员函数对从基类继承来的成员的访问

外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问

3.6.1公有继承

1.父类的公有属性成员,到子类还是公有

2.父类的保护属性成员,到子类还是保护

3.父类的私有属性成员,到子类不能访问

#include<iostream>
using namespace std;

class CFather
{
public:
	int m_testA{ 0 };
protected:
	int m_testB{ 0 };
private:
	int m_testC{ 0 };
};

class CSon : public CFather
{
	void test()
	{
		m_testA = 1; // 编译正确 :public 继承后,基类的公有权限,在派生类中为公有权限,在派生类内部或者外部都可以访问基类public成员
		m_testB = 1; // 编译正确 :public 继承后,基类的保护权限,在派生类中为保护权限,在派生类在内部可以访问基类protected成员、外部不可访问
		m_testC = 1; // 编译错误 :public 继承后,基类的私有权限,在派生类中不可见,无论哪种继承,在派生类中基类私有成员都不可见,都无法访问基类private成员
	}
};

int main()
{
	CSon _test;

	_test.m_testA = 2; // 编译正确 :
	_test.m_testB = 2; // 编译错误 :
	_test.m_testC = 2; // 编译错误 :

	system("pause");
	return 0;
}

3.6.2保护继承

1.父类的公有属性成员,到子类是保护

2.父类的保护属性成员,到子类还是保护

3.父类的私有属性成员,到子类不能访问

#include<iostream>

using namespace std;

class CFather
{
public:
	int m_testA{0};
protected:
	int m_testB{0};
private:
	int m_testC{0};
};

class CSon: protected CFather
{
	void test()
	{
		m_testA = 1; // 编译正确 :protected 继承后,基类的公有权限,在派生类中为保护权限,在内部可以访问基类public成员,在外部无法访问基类public成员
		m_testB = 1; // 编译正确 :protected 继承后,基类的保护权限,在派生类中为保护权限,在内部可以访问基类protected成员,在外部无法访问基类public成员
		m_testC = 1; // 编译错误 :protected 继承后,基类的私有权限,在派生类中不可见,无论哪种继承,在派生类中基类私有成员都不可见,都无法访问基类private成员
	}
};

int main()
{
	CSon _test;
	
	_test.m_testA = 2; // 编译错误 :protected 继承后,基类的公有权限,在派生类中为保护权限,在内部可以访问基类public成员,在外部无法访问基类public成员
	_test.m_testB = 2; // 编译错误 :protected 继承后,基类的保护权限,在派生类中为保护权限,在内部可以访问基类protected成员,在外部无法访问基类public成员
	_test.m_testC = 2; // 编译错误 :protected 继承后,基类的私有权限,在派生类中不可见,无论哪种继承,在派生类中基类私有成员都不可见,都无法访问基类private成员
	
	system("pause");
	return 0;
}

3.6.3私有继承

1.父类的公有属性成员,到子类还是私有

2.父类的保护属性成员,到子类还是私有

3.父类的私有属性成员,到子类不能访问

#include<iostream>

using namespace std;

class CFather
{
public:
	int m_testA{0};
protected:
	int m_testB{0};
private:
	int m_testC{0};
};

class CSon: private CFather
{
	void test()
	{
		m_testA = 1; // 编译正确 :private 继承后,基类的公有权限,在派生类中为私有权限,在内部可以访问基类public成员,在外部无法访问基类public成员
		m_testB = 1; // 编译正确 :private 继承后,基类的保护权限,在派生类中为私有权限,在内部可以访问基类protected成员,在外部无法访问基类protected成员
		m_testC = 1; // 编译错误 :private 继承后,基类的私有权限,在派生类中不可见,无论哪种继承,在派生类中基类私有成员都不可见,都无法访问基类private成员
	}
};

int main()
{
	CSon _test;
	
	_test.m_testA = 2; // 编译错误 :private 继承后,基类的公有权限,在派生类中为私有权限,在内部可以访问基类public成员,在外部无法访问基类public成员
	_test.m_testB = 2; // 编译正确 :private 继承后,基类的保护权限,在派生类中为私有权限,在内部可以访问基类protected成员,在外部无法访问基类protected成员
	_test.m_testC = 2; // 编译错误 :private 继承后,基类的私有权限,在派生类中不可见,无论哪种继承,在派生类中基类私有成员都不可见,都无法访问基类private成员
	
	system("pause");
	return 0;
}

总结

一、访问权限

访问权限 外部 派生类 内部
public
protected
private

public、protected、private 的访问权限范围关系:

public > protected > private

二、继承权限

  1. 派生类继承自基类的成员权限有四种状态:public、protected、private、不可见
  2. 派生类对基类成员的访问权限取决于两点:一、继承方式;二、基类成员在基类中的访问权限
  3. 派生类对基类成员的访问权限是取以上两点中的更小的访问范围(除了 private 的继承方式遇到 private 成员是不可见外)。例如:
  • public 继承 + private 成员 => private
  • private 继承 + protected 成员 => private
  • private 继承 + private 成员 => 不可见

参考资料来源:

黑马程序员、阿秀

3.6.4友元

在C++中,我们使用类对数据进行了隐藏和封装,类的数据成员一般都定义为私有成员,成员函数一般都定义为公有的,以此提供类与外界的通讯接口。但是,有时需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数。除了友元函数外,还有友元类,两者统称为友元。友元的作用是提高了程序的运行效率(即减少了类型检查和安全性检查等都需要时间开销),但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。

(1)友元函数

A.定义

友元函数(与成员函数相对)是定义在类外部,可以访问该类中的所有私有(private)成员和保护(protected)成员。指定函数为某个类的友元函数的方法是使用关键字friend。

friend <返回类型> <函数名> (<参数列表>);

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

friend void getall(const Critter& aCritter);//注意,访问对象必须是引用
friend void Display(const Person& p, const Student& s);

B.为什么需要友元函数?

a.C++中引入友元函数,是为在该类中提供一个对外(除了他自己以外)访问的窗口;
b.两个类要共享数据的时候,友元函数可以减少系统开销,调高效率;
c.运算符重载的某些场合需要使用友元函数;
d.通过友元函数,虚继承和私有构造函数可以让一个类不被继承;

C.友元函数注意事项

a.类中通过使用关键字friend 来修饰友元函数,但该函数并不是类的成员函数,其声明可以放在类的私有部分,也可放在共有部分。友元函数的定义在类体外实现,不需要加类限定。
b.一个类中的成员函数可以是另外一个类的友元函数,而且一个函数可以是多个类友元函数。
c.友元函数可以访问类中的私有成员和其他数据,但是访问不可直接使用数据成员,需要通过对对象进行引用。
d.友元函数在调用上同一般函数一样,不必通过对对象进行引用。
e.注意:使用友元函数破坏类的封装(尽量使用成员函数,除非不得已才使用友元函数)。

D.实例代码

//已知两点的坐标,求两点间距离
#include <iostream>
#include <cmath>
using namespace std;

class Point
{
public:
    Point(double xx, double yy)
    {
        x = xx;
        y = yy;
    }

    void GetXY();

    friend double Distance(Point& a, Point& b);

protected:

private:
    double x, y;
};

void Point::GetXY()
{
    cout << "(" << x << "," << y << ")" << endl;
}

double Distance(Point& a, Point& b)
{
    double length;
    length = sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));     //它可以引用类中的私有成员
    return length;
}

void main(void)
{
    Point p1(3.0, 4.0), p2(6.0, 8.0);
    p1.GetXY();    //成员函数的调用方法,通过使用对象来调用
    p2.GetXY();
    
    double d = Distance(p1, p2);     //友元函数的调用方法,同普通函数的调用一样,不要像成员函数那样调用
    cout << "distance=" << d << endl;

    return;
}
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _num; // 学号
};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._num << endl;
}

void main()
{
	Person p;
	Student s;
	Display(p, s);
}

参考:[(9条消息) C++中的友元函数_c++友元函数_鬼月行的博客-CSDN博客]

(2)友元类

A.定义

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。

class Date; // 前置声明
class Time
{
	friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
	Time(int hour = 0, int minute = 0, int second = 0): _hour(hour), _minute(minute), _second(second)
	{}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day)
	{}
	void SetTimeOfDate(int hour, int minute, int second)
	{
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}

private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

int main()
{
	return 0;
}

B.友元类特性

a.友元关系是单向的,不具有交换性。

在Time类中,声明Date 类是其友元类,可以在Date类中直接访问Time类的私有成员。但不能在Time中访问Date类中的私有成员。

b.友元关系不能传递

A是B的友元,B是C的友元,但A不是C的友元。

参考:

3.6.5内部类

A.定义
一个类定义在另一个类的内部,这个内部类就叫做内部类。

B.内部类和外部类关系:

a.内部类不属于外部类,更不能通过外部类的对象调用内部类。外部类对内部类没有任何优越的访问权限。

b.内部类是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类的所有成员,但外部类不是内部类的友元。

#include<iostream>
using namespace std;
 
class A 
{
private:
	static int k;
	int h;
public:
	class B  //B是A的内部类,B是A的友元类
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//内部类可以访问外部类的非公有成员
			cout << a.h << endl;//普通成员只能通过对象访问,不能通过类名访问
		}
 
	private:
		int _b;
	};
};
 
int A::k = 1;
 
int main()
{
	A::B b; //定义一个内部类对象
	b.foo(A());
 
	return 0;
}

C.内部类特性:

a.内部类可以定义在外部类的public、protected、private都是可以的。

b.注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。

c.sizeof(外部类)=外部类,和内部类没有任何关系

int main()
{
	A::B b;
	b.foo(A());

	//由于静态成员变量不在对象中存储,类的大小为非静态成员变量内存对齐的结果,A只有一个非静态成员变量
	cout << "sizeof(A) = " << sizeof(A) << endl;

	return 0;
}

输出:

1
0
sizeof(A) = 4;

参考:[(9条消息) 【C++】-- 友元_c++ 友元_玲娜贝儿~的博客-CSDN博客]

3.7继承中的类作用域

每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。

派生类的作用域位于基类作用域之内这一事实可能有点儿出人意料,毕竟在我们的程序文本中派生类和基类的定义是相互分离开来的。不过也恰恰因为类作用域有这种继承嵌套的关系,所以派生类才能像使用自己的成员一样使用基类的成员。例如,当我们编写下面的代码时:

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

名字isbn的解析将按照下述过程所示:

●因为我们是通过Bulk_quote的对象调用isbn的,所以首先在Bulk_quote中查找,这步没有找到名字isbn。

●因为Bulk_quote是Disc_quote的派生类,所以接下来在Disc_quote中查找,仍然找不到。

●因为Disc_quote是Quote的派生类,所以接着查找Quote;此时找到了名字 isbn,所以我们使用的isbn最终被解析为Quote中的isbn。

(1)在编译时进行名字查找

一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况),但是我们能使用哪些成员仍然是由静态类型决定的。举一个例子,我们可以给Disc_quote添加一个新成员,该成员返回一个存有最小(或最大)数量及折扣价格的pair:

class Disc_quote :public Quote
{
public:
	std::pair<size_t, double> discount_policy() const
	{
		return { quantity, discount };
	}
	//其他成员与之前的版本一致
};

我们只能通过Disc_quote及其派生类的对象、引用或指针使用discount_policy:

Bulk_quote bulk;
Bulk_quote* bulkP = &bulk; //静态类型与动态类型一致
Quote* itemP = &bulk; //静态类型与动态类型不一致
bulkP->discount_policy();//正确:bulkP的类型是Bulk_quote*
itemP->discount_policy();//错误:itemP的类型是Quote*

尽管许bulk中确实含有一个名为discount_policy的成员,但是该成员对于itemP却是不可见的。itemP的类型是Quote的指针,意味着对discount_policy的搜索将从Quote开始。显然Quote不包含名为discount_policy的成员,所以我们无法通过Quote 的对象、引用或指针调用discount_policy。

(2)名字冲突与继承

和其他作用域一样,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字:

struct Base
{
	Base() :mem(0)
	{ }
protected:
	int mem;
};

struct Derived : Base
{
	Derived(int i):mem(i) {}//用i初始化 Derived::mem
	                        //Base::mem 进行默认初始化
	int get_mem()
	{
		return mem;//返回Derived::mem 
	}
protected:
	int mem;//隐藏基类中的mem
};

get_mem中mem引用的解析结果是定义在Derived中 的名字,下面的代码

Derived d(42);
cout << d.get_mem() << endl;//打印42

的输出结果将是42。

NOTE:派生类的成员将隐藏同名的基类成员。

(3)通过作用域运算符来使用隐藏的成员

可以通过作用域运算符来使用一个被隐藏的基类成员:

structDerived:Base
{
	int get_base_mem() { return Base: : mem; }
        //...
};

作用域运算符将覆盖掉原有的查找规则,并指示编译器从Base类的作用域开始查找mem。如果使用最新的Derived版本运行上面的代码,则d.get_mem()的输出结果将是0。

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

关键概念:名字查找与继承

理解函数调用的解析过程对于理解C++的继承至关重要,假定我们调用p->mem() (或者obj.mem()),则依次执行以下4个步骤:

●首先确定p(或obj)的静态类型。因为我们调用的是一个成员,所以该类型必然是类类型。

●在p(或obj)的静态类型对应的类中查找mem。如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器将报错。

●一旦找到了mem,就进行常规的类型检查以确认对于当前找到的mem,本次调用是否合法。

●假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:一如果mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据是对象的动态类型。
一反之,如果mem不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规函数调用。

(4)一如往常,名字查找先于类型检查

如前所述,声明在内层作用域的函数并不会重载声明在外层作用域的函数。因此,定义派生类中的函数也不会重载其基类中的成员。和其他作用域 一样,如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,甚类成员也仍然会被隐藏掉:

Derived d; 
Base b; 
b.memfcn();//调用Base::memfcn
d.memfcn10);// 调用Derived::memfcn
d.memfcn();//错误:参数列表为空的memfcn被隐藏了
ct.Base::memfcn();//正确:调用Base::memfcn 

Derived中的memfcn声明隐藏了Base中的memfcn声明。在上面的代码中前两条调用语句容易理解,第一个通过Base对象b进行的调用执行基类的版本;类似的,第二个通过d进行的调用执行Derived的版本;第三条调用语句有点特殊,d.memfcn()是非法的。

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

(5)虚函数与作用域

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

class D1 :public Base
{
public:
	//隐藏基类的fcn,这个fcn不是虚函数
	//D1继承了Base::fcn()的定义
	int fcn(int);//形参列表于Base中的fcn不一致
	virtual void f2();//是一个新的虚函数,在Base中不存在
};

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

隐藏:在子类中有与父类中同名的成员函数,当父类中同名成员函数为虚函数,但形参列表不一致时(也即未发生函数重载),或者父类同名函数非虚函数,子类未重载其父类同名成员函数,父类的同名成员函数被隐藏。

覆盖(存在虚函数是前提):在子类中有与父类中同名的成员函数,当父类中同名成员函数为虚函数且形参列表一致时(也即发生函数重载),父类的同名成员函数被覆盖。

(6)通过基类调用隐藏的虚函数

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();//错误:Base没有名为f2的成员
d1p->f2();//虚调用,将在运行时调用D1::f2()
d2p->f2();//虚调用,将在运行时调用D2::f2()

Base* p1 = &d2obj; D1* p2 = &d2obj; D2 *p3 = &d2obj;
p1->fcn(42);//错误:Base中没有接受一个int的fcn
p2->fcn(42);//静态绑定,调用D1::fcn(int)
p3->fcn(42);//静态绑定,调用D2::fcn(int)

//D1* d1b = &bobj;//

前三条调用语句是通过基类的指针进行的,因为fcn是虚函数,所以编译器产生的代码将在运行时确定使用虚函数的哪个版本。判断的依据是该指针所绑定对象的真实类型。

调用原则:调用函数所使用指针的类型所属类和实际指向的对象所属类共同限制了函数的检索范围,由于不存在从基类向派生类的隐式转换,所以检索范围一定是从实际指向的对象所属类往基类方向向上检索,到使用指针的类型所属类为止。

如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。例如,Quote类型的变量永远是一个Quote对象,我们无论如何都不能改变该变量对应的对象的类型。

NOTE:基类的指针或引用的静态类型可能与其动态类型不一致,读者一定要理解其中的原因。

(7)覆盖重载的函数

和其他函数一样,成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的0个或多个实例。如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。

有时一个类仅需覆盖重载集合中的一些而非全部函数,此时,如果我们不得不覆盖基类中的每一个版本的话,显然操作将极其烦琐。

一 种好的解决方案是为重载的成员提供一条using声明语句,这样我们就无须覆盖基类中的每个个重载版本了。using声明语句指定名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类作用域中。此时,派生类只需要定义其特有的函数就可以了,而无须为继承而来的其他函数重新定义。

类内using声明的一般规则同样适用于重载函数的名字;基类函数的每个实例在派生类中都必须是可访问的。对派生类没有重新定义的重载版本的访问实际上是对using声明点的访问。