C++primer小结15-16章

本文概述了面向对象编程中的继承、动态绑定原理,以及如何利用模板进行泛型编程。重点讲解了基类与派生类的概念、虚函数与虚析构函数,以及模板的使用和实例化策略。

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

面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定。
继承和动态绑定对程序的编写有两方面影响:一是我们可以更容易地定义与其他类相似但不完全相同的新类;二是在使用这些彼此相似的类编写程序时,我们可以在一定程度上忽略掉它们的区别。

15.1 OOP(object-oriented programming)

通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
继承
通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类称为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
在C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。
派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有访问说明符:

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

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

因为Bulk_quote在它的派生列表中使用了public关键字,因此我们完全可以把Bulk_quote的对象当成Quote的对象来使用。

派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字。
C++新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个override关键字。

动态绑定
通过使用动态绑定,我们能用同一段代码分别处理Quote和Bulk_quote的对象。
在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。

15.2 定义基类和派生类

15.2.1 定义基类

基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
派生类可以继承其基类的成员。派生类需要对这些操作提供自己的新定义以覆盖从基类继承而来的就定义。
C++中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通常将其定义为虚函数。当我们使用指针或引用调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。
基类通过在其成员函数的声明语句之前加上关键字virtual使得该函数执行动态绑定。任何构造函数之外的非静态函数都可以是虚函数。关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。在某些时候基类中还有这样一种成员,基类希望它的派生类有权访问该成员,同时禁止其他用户访问。我们用**受保护的(protected)**访问运算符说明这样的成员。

15.2.2 定义派生类

派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。
类派生的形式是:首先是一个冒号,后面紧跟以逗号分隔的积累列表,其中每个基类前面可以有三种访问说明符中的一个。
派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。
如果一个派生是公有的,则基类的公有成员也是派生类接口的组成部分。此外,我们能将公有派生类型的对象绑定到基类的引用或指针上。
大多数类都只继承自一个类,这种形式的继承被称作”单继承“。

派生类中的虚函数
派生类经常覆盖它继承的虚函数。如果派生类没有覆盖其基类某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
C++允许派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数。具体做法是在形参列表后面、或者在const成员函数的const关键字后面、或者在引用成员函数的引用限定符后面添加一个关键字override。

派生类对象及派生类向基类的类型转换
一个派生类对象包含多个组成部分:一个含有派生类自己定义的成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。
因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将积累的指针或引用绑定到派生类对象的基类部分上。
这种转换通常称为派生类到基类的类型转换。
在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。

派生类构造函数
尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。和其他创建了基类对象的代码一样,派生类也必须使用基类的构造函数来初始化它的基类部分。
每个类控制它自己的成员初始化过程。
除非我们特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化。如果想使用其他的基类构造函数,我们需要以类名加圆括号内的实参列表的形式为构造函数提供初始值。这些实参将帮助编译器决定到底应该选用哪个构造函数来初始化派生类对象的基类部分。
首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

派生类使用基类的成员
派生类可以访问积累的公有成员和受保护成员。
派生类的作用域嵌套在基类的作用域之内。因此,对于派生类的一个成员来说,它使用派生类成员的方式与使用基类成员的方式没什么不同。
关键概念:遵循基类的接口。必须明确一点,每个类负责定义各自的接口。派生类对象不能直接初始化基类的成员。

继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不管从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。
静态成员遵循通用的访问控制规则。如果基类中的成员是private的,则派生类无权访问它。假设某静态成员是可访问的,则我们既能通过基类使用它也能通过派生类使用。

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

被用作基类的类
如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。
一个类是基类,同时它也可以是一个派生类。

防止继承的发生
C++新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字final。

15.2.3 类型转换与继承

理解基类和派生类之间的类型转换是理解C++语言面向对象编程的关键所在。
我们可以将基类的指针或引用绑定到派生类对象上。
当使用基类的引用或指针时,实际上我们并不清楚该引用或指针所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。
和内置指针一样,智能指针类也支持派生类向基类的类型转换,意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内。

静态类型与动态类型
当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开来。静态类型是在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
基类的指针或引用的静态类型可能与其动态类型不一致。

不存在从基类向派生类的隐式类型转换
因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。
即使一个基类指针或引用绑定在一个派生类对象上,我们也不能执行从基类向派生类的转换。
编译器在编译时无法确定某个特定的转换在运行时是否安全,这是因为编译器之恶能通过检查指针或引用的静态类型来推断该转换是否合法。我们可以使用dynamic_cast, static_cast强制覆盖掉检查工作。

在对象之间不存在类型转换
派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。
当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。

关键概念:存在继承关系的类型之间的转换规则
要想理解在具有继承关系的类之间发生的类型转换,有三点非常重要:

  • 从派生类向基类的类型转换只对指针或引用类型有效
  • 基类向派生类不存在隐式类型转换
  • 和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行。

15.3 虚函数

对虚函数的调用可能在运行时才被解析
当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。
动态绑定只有当我们通过指针或引用调用虚函数时才会发生。
当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。

OOP的核心思想是多态性。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的”多种形式“而无须在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
如果函数是虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。
另一方面,对非虚函数的调用在编译时进行绑定。
当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该引用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

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

final和override说明符
派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的行为。但是这种声明往往意味着发生了错误,原本希望派生类能覆盖掉基类中的虚函数,但是形参列表弄错了。
C++中使用override关键字来说明派生类中的虚函数,这样做的好处是在使得程序员的意图更加清晰的同时让编译器为我们发现一些错误。
我们还能把某个函数指定为final,如果我们已经把函数定义成final了,则之后任何尝试覆盖该函数的操作都将引发错误。
final和override说明符出现在形参列表(包括任何const或引用修饰符)以及尾置返回类型之后。

虚函数与默认实参
和其他函数一样,虚函数也拥有默认实参。如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

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

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

该代码强行调用Quote的net_price函数,而不管basP实际指向的对象类型到底是什么。该调用将在编译时完成解析。
通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。
如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

15.4 抽象基类

我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句处。
我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,我们不能在类的内部为一个=0的函数提供函数体。

含有纯虚函数的类是抽象基类
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。我们不能(直接)创建一个抽象基类的对象。

派生类构造函数只初始化它的直接基类
关键概念:重构
在Quoto的继承体系中增加Disc_quoto类是重构的一个典型示例。重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中。对于面向对象的应用程序来说,重构是一种普遍现象。

15.5 访问控制与继承

每个类分别控制自己的成员初始化过程,与之类似,每个类还分别控制着其成员对于派生类来说是否可访问。
受保护的成员
派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。

公有、私有和受保护继承
派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。
对基类成员的访问权限只与基类中的访问说明符有关。
派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。
派生访问说明符还可以控制继承自派生类的新类的访问权限。

派生类向基类转换的可访问性
派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。
对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行。
关键概念:类的设计与受保护的成员
不考虑继承的话,我们可以认为一个类有两种不同的用户:普通用户和类的实现者。

友元与继承
就像友元关系不能传递一样,友元关系同样也不能继承。
当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来那个类来说,其友元的基类或者派生类不具有特殊的访问能力。
不能继承友元关系;每个类负责控制各自成员的访问权限。

改变个别成员的可访问性
有时我们需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的。
通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员标记出来。
派生类只能为那些它可以访问的名字提供using声明。

默认的继承保护级别
使用struct和class关键字定义的类之间唯一的差别就是默认成员访问说明符及默认派生访问说明符。
一个私有派生的类最好显式地将private声明出来,而不要仅仅依赖于默认的设置。

15.6 继承中的类作用域

当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致,但是我们能使用哪些成员仍然是由静态类型决定的。
和其他作用域一样,派生类也能重用定义在其基类或间接基类中的名字,此时定义在内层作用域的名字(即派生类)将隐藏定义在外层作用域(即基类)的名字。
派生类的成员将隐藏同名的基类成员。
我们可以通过作用域运算符来使用一个被隐藏的基类成员。
声明在内层作用域的函数并不会重载声明在外层作用域的函数。因此,定义派生类中的函数也不会重载其基类中的成员。如果派生类的成员和基类的某个成员同名,则派生类将在其作用域内隐藏掉该基类成员。
覆盖重载的函数
为重载的成员提供一条using声明语句,这样就无须覆盖基类中的每一个重载版本。using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类作用域中。

15.7 构造函数与拷贝控制

和其他类一样,位于继承体系中的类也需要控制当其对象执行一系列操作时发生什么样的行为,这些操作包括创建、拷贝、移动、赋值和销毁。

15.7.1 虚析构函数

继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。
和其他函数一样,我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。和其他虚函数一样,析构函数的虚属性也会被继承。只要基类的析构函数是虚函数,就能确保当我们delete基类指针时将运行正确的析构函数版本。
如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
虚析构函数将阻止合成移动操作
如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。

15.7.2 合成拷贝控制与继承

基类或派生类的合成拷贝控制成员的行为与其他合成的构造函数、赋值运算符或析构函数类似:它们对类本身的成员依次进行初始化、赋值和销毁的操作。此外,这些合成的成员还负责使用直接基类中对应的操作对一个对象的直接基类部分进行初始化,赋值或销毁的操作。
派生类中删除的拷贝控制与基类的关系
就像其他任何类的情况一样,基类或派生类也能出于同样的原因将其合成的默认构造函数或者任何一个拷贝控制成员定义成被删除的函数。此外,某些定义基类的方式也可能导致有的派生类成员成为被删除的函数。
移动操作与继承
大多数基类都会定义一个虚析构函数。因此在默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中也没有合成的移动操作。

15.7.3 派生类的拷贝控制成员

派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员,还负责初始化派生类对象的基类部分。
当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。
定义派生类的拷贝或移动构造函数
当为派生类定义拷贝或移动构造函数时,我们通常使用对应的基类构造函数初始化对象的基类部分。
在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。
派生类赋值运算符
与拷贝和移动构造函数一样,派生类的赋值运算符也必须显式地为其基类部分赋值。
派生类析构函数
在析构函数体执行完毕后,对象的成员会被隐式销毁。类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源。
对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承体系的反方向直至最后。
在构造函数和析构函数中调用虚函数
如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。

15.7.4 继承的构造函数

在C++中,派生类能够重用其直接基类定义的构造函数。
通常情况下,using声明语句只是令某个名字在当前作用域内可见。而当作用域构造函数时,using声明语句将令编译器产生代码。对于积累的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数。
继承的构造函数的特点
和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。
而且,一个using声明语句不能指定explicit或constexpr。

15.8 容器与继承

当我们使用容器存放继承体系中的对象时,通常必须采取间接存储的方式。
当派生类对象被赋值给基类对象时,其中的派生类部分将被“切碎”,因此容器和存在继承关系的类型无法兼容。
在容器中放置(智能)指针而非对象
当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针(更好的选择是智能指针)。和往常一样,这些指针所指对象的动态类型可能是基类类型,也可能是派生类型。
对于C++面向对象的编程来说,一个悖论是我们无法直接使用对象进行面向对象编程。相反,我们必须使用指针和引用。

第十六章 模板与泛型编程

16.1 定义模板

定义一个通用的函数模板,一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。
模板定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用小于号(<)和大于号(>)包围起来。

template <typename T>
int compare(const T & v1, const T & v2)
{
	if(v1 < v2) return -1;
	if(v2 < v1) return 1;
	return 0;
}

在模板定义中,参数列表不能为空。
类似的,模板参数表示在类或函数定义中用到的类型或值。当使用模板时,我们指定模板实参,将其绑定到模板参数上。
实例化函数模板
当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参。即,当我们调用compare时,编译器使用实参的类型来确定绑定到模板参数T的类型。
编译器用推断出的模板参数来为我们实例化一个特定版本的函数。
模板类型参数
一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
类型参数前必须使用关键字class或typename。在模板参数列表中,这两个关键字的含义相同,可以互换使用。一个模板参数列表中可以同时使用这两个关键字。
非类型模板参数
除了定义类型参数,还可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字class或typename来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。我们不能用一个普通局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数也可以用nullptr或一个值为0的常量表达式来实例化。
非类型模板参数的模板实参必须是常量表达式。
inline和constexpr的函数模板
函数模板可以声明为inline或constrxpr的,如同非模板函数一样。inline或constexpr说明符放在模板参数列表之后,返回类型之前:

template <typpename T> inline T min(const T&, const T&); 

编写类型无关的代码
编写泛型代码的两个重要原则:

  • 模板中的函数参数是const的引用
  • 函数体中的条件判断仅使用<比较运算

通过将函数参数设定为const的引用,我们保证了函数可以用于不能拷贝的类型。
模板程序应该尽量减少对实参类型的要求。
模板编译
为了生成一个实例化模板,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。
函数模板和类模板成员函数的定义通常放在头文件中。
大多数编译错误在实例化期间报告
保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。

16.1.2 类模板

类模板是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板推断模板参数信息。为了使用类模板,我们必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。
类似函数模板,类模板以关键字template开始,后跟模板参数列表。在类模板(及其成员)的定义中,我们将模板参数当作替身,代替使用模板时用户需要提供的类型或值。
当使用一个类模板时,我们必须提供额外信息。这些额外信息是显式模板实参列表,它们被绑定到模板参数。编译器使用这些模板实参来实例化出特定的类。
一个类模板的每个实例都形成一个独立的类。
在模板作用域中引用模板类型
类模板的名字不是一个类型名。类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。
类模板的成员函数
我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。类模板的成员函数本身是一个普通函数,类模板的成员函数具有和模板相同的模板参数。定义在类模板之外的成员函数必须以关键字template开始,后接类模板参数列表。
当我们在类外定义一个成员时,必须说明成员属于哪个类。从一个模板生成的类的名字中必须包含其模板实参。当我们定义一个成员函数时,模板实参与模板形参相同。
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
当我们使用一个类模板类型时必须提供实参,但这一规则有个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。
当我们在类模板外定义其成员时,必须记住,我们并不在类的作用域中,直到遇到类名才表示进入类的作用域。
在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参。
类模板和友元
当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。
类模板与另一个(类或函数)模板间友好关系的最常见的形式是建立对应实例及其友元间的友好关系。
通用和特定的模板友好关系
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元。
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
令模板自己的类型参数成为友元
在新标准中,我们可以将模板类型参数声明为友元。
模板类型别名
类模板的一个实例定义了一个类类型,与其他任何类类型一样,我们可以定义一个typedef来引用实例化的类。
当我们定义一个模板类型别名时,可以固定一个或多个模板参数。
类模板的static成员
与任何其他类相同,类模板可以声明static成员。
与任何其他static数据成员相同,模板类的每个static数据成员必须有且仅有一个定义。但是,类模板的每个实例都有一个独有的static对象。因此,与定义模板的成员函数类似,我们将static数据成员也定义为模板。
与类模板的其他任何成员类似,定义的开始部分是模板参数列表,随后是我们定义的成员的类型和名字。

16.1.3 模板参数

模板参数与作用域
模板参数遵循普通的作用域规则。一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。
与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是,与大多数其他上下文不同,在模板内不能重用模板参数名。
由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次。
模板声明
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
使用类的类型成员
默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们使用typename关键字实现这一点。

template <typename T>
typename T::value_type top(const T& c)
{
	if(!c.empty())
		return c.back();
	else
		return typename T::value_type();
}

当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class。
默认模板实参
就像我们能为函数提供默认实参一样,我们也可以提供默认模板实参。新标准中,我们可以为函数和类模板提供默认实参。
模板默认实参与类模板
无论何时使用一个类模板,我们都必须在模板名之后接上尖括号。尖括号指出类必须从一个模板实例化而来。

16.1.4 成员模板

一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板,成员模板不能是虚函数。
普通(非模板)类的成员模板
类模板的成员模板
实例化与成员模板

16.1.5 控制实例化

当模板被使用时才会进行实例化这一特性意味着,相同的实例可能出现在多个对象文件中。
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。新标准中,通过显式实例化来避免这种开销。
一个显式实例化有如下形式:

extern template declaration;   //实例化声明
template declaration;   //实例化定义

对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。
在一个类模板的实例化定义中,所用模板必须能用于模板的所有成员函数。
实例化定义会实例化所有成员
在一个类模板的定义中,所用类型必须能用于模板的所有成员函数。

16.1.6 效率与灵活性

16.2 模板实参判断

从函数实参来确定模板实参的过程被称为模板实参判断。在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最为匹配。

16.2.1 类型转换与模板类型参数

与非模板函数一样,我们在一次调用中传递给函数模板的实参被用来初始化函数的形参。
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换。
使用相同模板参数类型的函数形参
一个模板类型参数可以用作多个函数形参的类型。由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。
如果希望允许对函数实参进行正常的类型转换,我们可以将函数模板定义为两个类型参数。
正常类型转换应用于普通函数实参
函数模板可以有用普通类型定义的参数,即,不涉及模板类型参数的类型。
如果函数参数类型不是模板参数,则对实参进行正常的类型转换。

16.2.2 函数模板显式实参

在某些情况下,编译器无法推断出模板实参的类型。其他一些情况下,我们希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。
指定显式模板实参
提供显式模板实参的方式与定义类模板实例的方式相同。显式模板实参在尖括号中给出,位于函数名之后,实参列表之前:

auto val3 = sum<long long>(i, lng);

此调用显式指定T1的类型。而T2和T3的类型则由编译器从i和lng的类型推断出来。
显式模板实参按由左至右的顺序与对应的模板参数匹配。
正常类型转换应用于显式指定的实参
对于用普通类型定义的函数参数,允许进行正常的类型转换,出于同样的原因,对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换。

16.2.3 尾置返回类型与类型转换

尾置返回出现在参数列表之后,它可以使用函数的参数。

//尾置返回允许我们在参数列表之后声明返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
	//	处理序列
	return *beg;  //返回序列中一个元素的引用
}

进行类型转换的标准库模版类
为了获得元素类型,我们可以使用标准库的类型转换模板。这些模板定义在头文件type_traits中。

16.2.4 函数指针和实参推断

当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。

16.2.5 模板实参推断和引用

16.3 重载与模板

函数模板可以被另一个模板或一个普通非模板函数重载。与往常一样,名字相同的函数必须具有不同数量或类型的参数。
正确定义一组重载的函数模板需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解。
当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
重载模板和类型转换
在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。

16.4 可变参数模板

一个可变参数模板就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数参数。
我们用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class…或typename…指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。
当我们需要知道包中有多少元素时,可以使用sizeof…运算符。类似sizeof,sizeof…也返回一个常量表达式,而且不会对其实参求值。

16.4.1 编写可变参数模板

我们可以使用一个initializer_list来定义一个可接受可变数目实参的函数。
但是,所有实参必须具有相同的类型。当我们及不知道想要处理的实参的数目也不知道它们的类型时,可变参数函数是很有用的。
可变参数函数通常是递归的。
当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中。否则,可变参数版本会无限递归。

16.4.2 包扩展

对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展。当扩展一个包时,我们还要提供用于每个扩展元素的模式。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(…)来触发扩展操作。
扩展中的模式会独立地应用于包中的每个元素。

16.4.3 转发参数包

在新标准下,我们可以组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他函数。

16.5 模板特例化

一个特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型。
当我们特例化一个函数模板时,必须为原模版中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应使用关键字template后跟一个空尖括号对(< >)。空尖括号对指出我们将为原模版的所有模板参数提供实参。
特例化的本质是实例化的一个模板,而非重载它。因此,特例化不影响函数匹配。
关键概念:普通作用域规则应用于特例化。
类模板特例化
类模板部分特例化
与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。
我们只能部分特例化类模板,而不能部分特例化函数模板。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值