导图社区 C++ 第十四章:C++中的代码重用
《C++Primer Plus》第十四章同步思维导图,介绍了C++的核心内容之代码重用的技巧。包括对上章继承的一些补充,和C++的突出特性:类模板的使用。不看书只看思维导图就能掌握。
编辑于2021-01-02 19:45:31第14章 C++中的代码重用
14.1 包含对象成员的类
14.1.1 valarray类简介
1. 介绍
· valarray类是由头文件valarray支持的。这个类用于处理数值(或具有类似特性的类),它支持诸如将数组中所有元素的值相加以及在数组中找出最大和最小的值等操作
· valarray被定义为一个模板类,以便能够处理不同的数据类型
2. 使用
使用valarray类来声明一个对象时,需要在标识符valarray后面加上一对尖 括号,并在其中包含所需的数据类型: valarray<int> q_values; // an array of int valaaray<double> weights; // an array of double
3. 举例
double gpa[5] = {3.1, 3.5, 3.8, 2.9, 3.3}; valarray<double> v1; // an array of double, size 0 valarray<int> v2(8); // an array of 8 int elements valarray<int> v3(10, 8); // an array of 8 int elements // each set to 10 valarray<double> v4(gpa, 4); // an array of 4 elements // initialized to the first 4 elements of gpa
4. 一些类方法
· operator:让您能够访问各个元素
· size():返回包含的元素数
· sum():返回所有元素的总和
· max():返回最大的元素
· min():返回最小的元素
14.1.2 Student类的设计
1. 设计方案
· 使用一个string对象来表示姓名 · 使用一个valarray<double>来表示考试分数
2. 设计思路
您可能想以公有的方式从这两个类派生出Student类,这将是多重公有继承,C++允许这样做,但在这里并不合适,因为学生与这些类之间的关系不是is-a模型。学生不是姓名,也不是一组考试成绩。这里的关系是has-a,学生有姓名,也有一组考试分数。通常,用于建立has-a关系的C++技术是组合(包含),即创建一个包含其他类对象的类
3. Student类声明
class Student { private: string name; // use a string object for name valarray<double> scores; // use a valarray<double> object for scores ... };
4. 说明
· 上述类将数据成员声明为私有的。这意味着Student类的成员 函数可以使用string和valarray<double>类的公有接口来访问和修改name和scores对象,但在类的外面不能这样做,而只能通过Student类的公有接口访问name和score
· 对于这种情况,通常被描述为Student类获得了其成员对象的实现,但没有继承接口
5. 接口和实现
使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口是is-a关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分
14.1.3 Student类示例
概述
代码示例
#include <iostream> #include <string> #include <valarray> class Student { private: typedef std::valarray<double> ArrayDb; std::string name; // contained object ArrayDb scores; // contained object // private method for scores output std::ostream & arr_out(std::ostream & os) const; public: Student() : name("Null Student"), scores() {} explicit Student(const std::string & s) : name(s), scores() {} explicit Student(int n) : name("Nully"), scores(n) {} Student(const std::string & s, int n) : name(s), scores(n) {} Student(const std::string & s, const ArrayDb & a) : name(s), scores(a) {} Student(const char * str, const double * pd, int n) : name(str), scores(pd, n) {} ~Student() {} double Average() const; const std::string & Name() const; double & operator[](int i); double operator[](int i) const; // friends // input friend std::istream & operator>>(std::istream & is, Student & stu);// 1 word friend std::istream & getline(std::istream & is, Student & stu); // 1 line // output friend std::ostream & operator<<(std::ostream & os, const Student & stu); };
简化表示
typedef std::valarray<double> ArrayDb; 这样,在以后的代码中便可以使用表示ArrayDb,而不是std::valarray<double>,因此类方法和友元函数可以使用ArrayDb类型
关键字explicit用法
explicit Student(const std::string & s) : name(s), scores() {} explicit Student(int n) : name("Nully"), scores(n) {} 可以用一个参数调用的构造函数将用作从参数类型到类类型的隐式转换函数;但这通常不是好主意。在上述第二个构造函数中,第一个参数表示数组的元素个数,而不是数组中的值,因此将一个构造函数用作int到Student的转换函数是没有意义的,所以使用explicit关闭隐式转换
C++和约束
C++包含让程序员能够限制程序结构的特性——使用explicit防止单参数构造函数的隐式转换,使用const限制方法修改数据,等等。这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。
1. 初始化被包含的对象
(1)复习前面几个例子
在前面的一些例子中,构造函数用这种语法来初始化内置类型的成员: Queue::Queue(int qs) : qsize(qs) { ... } 上述代码在成员初始化列表中使用的是数据成员的名称(qsize)
另外,前面介绍的示例中的构造函数还使用成员初始化列表初始化派生对象的基类部分: hasDMA::hasDMA(const hasDMA& hs) : baseDMA(hs) { ... } 对于继承的对象,构造函数在成员初始化列表中使用类名来调用特定的基类构造函数
(2)Student类
对于成员对象,构造函数则使用成员名: Student(const char * str, const double * pd, int n) : name(str), scores(pd, n) {} 因为该构造函数初始化的是成员对象,而不是继承的对象,所以在初始化列表中使用的是成员名,而不是类名。初始化列表中的每一项都调用与之匹配的构造函数,即name(str)调用构造函数string(const char*),scores(pd, n)调用构造函数ArrayDb(const double*, int)
如果不使用初始化列表语法,情况将如何呢?C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象。因此,如果省略初始化列表,C++将使用成员对象所属类的默认构造函数
(3)初始化顺序
· 当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序
· 如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就显得十分重要
2. 使用被包含对象的接口
(1)被包含对象的接口不是公有的,但可以在类方法中使用它
(2)辅助函数
Student类定义了一个使用string版本的<<运算符的友元函数: // use string version of operator<<() ostream & operator<<(ostream & os, const Student & stu) { os << "Scores for " << stu.name << ":\n"; stu.arr_out(os); // use private method for scores return os; } // private method ostream & Student::arr_out(ostream & os) const { int i; int lim = scores.size(); if (lim > 0) { for (i = 0; i < lim; i++) { os << scores[i] << " "; if (i % 5 == 4) os << endl; } if (i % 5 != 0) os << endl; } else os << " empty array "; return os; } 通过使用这样的辅助方法,可以将零乱的细节放在一个地方,使得友元函数的编码更为整洁
3. 使用新的Student类(*)
14.2 私有继承
概述
C++还有另一种实现has-a关系的途径——私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们
使用私有继承,类将继承实现。例如,如果从String类派生出 Student类,后者将有一个String类组件,可用于保存字符串。另外,Student方法可以使用String方法来访问String组件
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。我们将使用术语子对象(subobject)来表示通过继承或包含添加的对象
因此私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现has-a关系
使用多个基类的继承被称为多重继承(multiple inheritance,MI)。通常,MI尤其是公有MI将导致一些问题,必须使用额外的语法规则(虚基类)来解决它们
14.2.1 Student类示例(新版本)
1. 初始化基类组件
(1)包含版本提供了两个被显式命名的对象成员;而私有继承提供了两个无名称的子对象成员: class Student : private std::string, private std::valarray<double> { public: ... };
(2)对于构造函数,包含将使这样的构造函数: Student(const char * str, const double * pd, int n) : name(str), scores(pd, n) {} 对于继承类,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数: Student(const char * str, const double * pd, int n) : std::string(str), ArrayDb(pd, n) {}
包含和私有继承的两个区别
2. 访问基类的方法
私有继承使得能够使用类名和作用域解析运算符来调用基类的方法: double Student::Average() const { if (ArrayDb::size() > 0) return ArrayDb::sum()/ArrayDb::size(); else return 0; }
3. 访问基类对象
由于Student类是从string类派生而来的,因此可以通过强制类型转换,将Student对象转换为string对象;结果为继承而来的string对象: const string & Student::Name() const { return (const string &) *this; } 上述方法返回一个引用,该引用指向用于调用该方法的Student对象中的继承而来的string对象
4. 访问基类的友元函数
(1)可以通过显式地转换为基类来调用正确的函数。例如,对于下面的友元函数定义: ostream & operator<<(ostream & os, const Student & stu) { os << "Scores for " << (const string &) stu << ":\n"; stu.arr_out(os); // use private method for scores return os; }
(2)必须进行显示转换的原因
引用stu不会自动转换为string引用。在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针
(3)即使是公有继承,也需要显示类型转换: · 如果不使用类型转换,下述代码将与友元函数原型匹配,从而导致递归调用: os << stu; · 由于这个类使用的是多重继承,编译器将无法确定应转换成哪个基类,如果两个基类都提供了函数operator<<( )
5. 使用修改后的Student类(*)
14.2.2 使用包含还是私有继承
1. 包含的优点
· 易于理解
· 继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题
· 包含能够包括多个同类的子对象
2. 私有继承的优点
· 具有派生的性质,可继续派生
· 若需要重新定义虚函数,则需要使用私有继承
3. 选择
通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。一般情况下,大多数C++程序员倾向于使用包含
14.2.3 保护继承
使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们
14.2.4 使用using重新定义访问权限
1. 需要解决的问题:假设要让基类的方法在派生类外面可用
2. 方法
(1)定义一个使用该基类方法的派生类方法: double Student::sum() const { return std::valarray<double>::sum(); }
(2)将函数调用包装在另一个函数调用中,即,使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生: calss Student : private std::string, private std::valarray<double> { ... public: using std::valarray<double>::min; using std::valarray<double>::max; ... }; 上述using声明使得valarray<double>::min()和valarray<double>::max()可用,就像它们是Student的公有方法一样
3. 注意
using声明只使用成员名——没有圆括号、函数特征标和返回类型
using声明只适用于继承,而不适用于包含
14.3 多重继承
概述
问题概述
我们将定义一个抽象基类Worker,并使用它派生出Waiter类和Singer类。然后,便可以使用MI从Waiter类和Singer类派生出SingingWaiter类
相关声明
class Worker // an abstract base class { private: std::string fullname; long id; public: Worker() : fullname("no one"), id(0L) {} Worker(const std::string & s, long n) : fullname(s), id(n) {} virtual ~Worker() = 0; // pure virtual destructor virtual void Set(); virtual void Show() const; };
class Waiter : public Worker { private: int panache; public: Waiter() : Worker(), panache(0) {} Waiter(const std::string & s, long n, int p = 0) : Worker(s, n), panache(p) {} Waiter(const Worker & wk, int p = 0) : Worker(wk), panache(p) {} void Set(); void Show() const; };
class Singer : public Worker { protected: enum {other, alto, contralto, soprano, bass, baritone, tenor}; enum {Vtypes = 7}; private: static char *pv[Vtypes]; // string equivs of voice types int voice; public: Singer() : Worker(), voice(other) {} Singer(const std::string & s, long n, int v = other) : Worker(s, n), voice(v) {} Singer(const Worker & wk, int v = other) : Worker(wk), voice(v) {} void Set(); void Show() const; };
14.3.1 有多少Worker
1. 虚基类
通过在类声明中使用关键字virtual,可以使Worker被用作Singer和Waiter的虚基类(virtual和public的次序无关紧要): class Singer : virtual public Worker { ... }; class Singer : public virtual Worker { ... }; 然后,可以将SingingWaiter类定义为: class SingingWaiter : public Singer, public Waiter { ... }; 现在,SingingWaiter对象将只包含Worker对象的一个副本。从本质上说,继承的Singer和Waiter对象共享一个Worker对象,而不是各自引入自己的Worker对象副本
2. 新的构造函数规则
SingingWaiter(const Worker& wk, int p = 0, int v = Singer::other) : Waiter(wk, p), Singer(wk, v) {} // flawed 存在的问题是,自动传递信息时,将通过2条不同的途径(Waiter和Singer)将wk传递给Worker对象。为避免这种冲突,C++在基类是虚的时,禁止信息通过中间类自动传递给基类。因此,上述构造函数将初始化成员panache和voice,但wk参数中的信息将不会传递给子对象Waiter。然而,编译器必须在构造派生对象之前构造基类对象组件;在上述情况下,编译器将使用Worker的默认构造函数
如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所 需的基类构造函数。因此,构造函数应该是这样: SingingWaiter(const Worker& wk, int p = 0, int v = Singer::other) : Worker(wk), Waiter(wk, p), Singer(wk, v) {} 上述代码将显式地调用构造函数worker(const Worker &)。请注意,这种用法是合法的,对于虚基类,必须这样做;但对于非虚基类,则是非法的
14.3.2 哪个方法
1. 函数调用的二义性
假设没有在SingingWaiter类中重新定义Show()方法,并试图使用SingingWaiter对象调用继承的Show()方法。对于单继承,如果没有重新定义Show(),则将使用最近祖先中的定义。而在多重继承中,每个直接祖先都有一个Show()函数,这使得上述调用是二义性的: SingingWaiter newhire("Elise Hawks", 2005, 6, soprano); newhire.Show(); // ambiguous 可以使用作用域解析运算符来澄清编程者的意图: SingingWaiter newhire("Elise Hawks", 2005, 6, soprano); newhire.Singer::Show();
然而,更好的方法是在SingingWaiter中重新定义Show(),并指出要使用哪个Show()
2. 多重继承的重复调用
我们希望对SingingWaiter对象能有自己的Show()函数来显示所有信息: void SingingWaiter::Show() { Singer::Show(); Waiter::Show(); } 然而,这将显示姓名和ID两次,因为Singer::Show()和Waiter::Show()都调用了Worker::Show()
解决方案一:模块化方式 提供一个只显示Worker组件的方法和一个只显示Waiter组件或Singer组件(而不是Waiter和Worker组件)的方法。然后,在SingingWaiter::Show()方法中将组件组合起来: void Worker::Data() const { cout << "Name: " << fullname << endl; cout << "Employee ID: " << id << endl; } void Waiter::Data() const { cout << "Panache rating: " << panache << endl; } void Singer::Data() const { cout << "Vocal range: " << pv[voice] << endl; } void SingingWaiter::Data() const { Singer::Data(); Waiter::Data(); } void SingingWaiter::Show() const { cout << "Category: singing waiter\n"; Worker::Data(); Data(); } 采用这种方式,对象仍可使用Show()方法。而Data()方法只在类内部可用,作为协助公有接口的辅助方法。然而,使Data()方法成为私有的将阻止Waiter中的代码使用Worker::Data(),这正是保护访问类的用武之地
解决方案二:将所有的数据组件都设置为保护的,而不是私有的 使用保护方法(而不是保护数据)将可以更严格地控制对数据的访问。
3. 一种新方法
使用C-风格字符串库函数strchr(): while (strchr("wstq", choice) == NULL) 该函数返回参数choice指定的字符在字符串"wstq"中第一次出现的地址,如果没有这样的字符,则返回NULL指针。使用这种检测比使用if语句将choice指定的字符同每个字符进行比较简单
14.3.3 MI小结(*)
14.4 类模板
14.4.1 定义类模板
概述
以第10章的Stack类为例建立模板: class Stack { private: enum {MAX = 10}; // constant specific to class Item items[MAX]; // holds stack items int top; // index for top stack item public: Stack(); bool isempty() const; bool isfull() const; // push() returns false if stack already is full, true otherwise bool push(const Item & item); // add item to stack // pop() returns false if stack already is empty, true otherwise bool pop(Item & item); // pop top into item };
1. 类模板建立方法
(1)模板类以下面这样的代码开头: template <class Type> 关键字template告诉编译器,将要定义一个模板。尖括号中的内容相当于函数的参数列表。可以把关键字class看作是变量的类型名,该变量接受类型作为其值,把Type看作是该变量的名称。较新的C++实现允许在这种情况下使用不太容易混淆的关键字typename代替class: template <typename Type> // newer choice
(2)将声明中所有的typedef标识符Item替换为Type: Item items[MAX]; // holds stack items 应该为: Type items[MAX]; // holds stack items
(3)每个函数头都 将以相同的模板声明打头: template<class Type> 同样应使用泛型名Type替换typedef标识符Item
(4)需将类限 定符从Stack::改为Stack<Type>::: bool Stack::push(const Item& item) { ... } 应该为: template<class Type> bool Stack<Type>::push(const Type& item) { ... } 如果在类声明中定义了方法(内联定义),则可以省略模板前缀和 类限定符
2. 模板建立规则
不能将 模板成员函数放在独立的实现文件中(以前,C++标准确实提供了关键字export,让您能够将模板成员函数放在独立的实现文件中,但支持该关键字的编译器不多;C++11不再这样使用关键字export,而将其保留用于其他用途)。由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。为此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件
14.4.2 使用类模板
1. 使用方法
仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名: Stack<int> kernels; // create a stack of ints Stack<string> colonels; // create a stack of string objects 看到上述声明后,编译器将按Stack<Type>模板来生成两个独立的类声明和两组独立的类方法。类声明Stack<int>将使用int替换模板中所有的Type,而类声明Stack<string>将用string替换Type。
2. 注意的问题
(1)使用的算法必须与类型一致。例如,Stack类假设可以将一个项目赋给另一个项目。这种假设对于基本类型、结构和类来说是成立的(除非将赋值运算符设置为私有的),但对于数组则不成立
(2)必须显式地提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数
3. 使用代码
int main() { Stack<std::string> st; // create an empty stack char ch; std::string po; cout << "Please enter A to add a purchase order,\n" << "P to process a PO, or Q to quit.\n"; while (cin >> ch && std::toupper(ch) != 'Q') { while (cin.get() != '\n') continue; if (!std::isalpha(ch)) { cout << '\a'; continue; } switch(ch) { case 'A': case 'a': cout << "Enter a PO number to add: "; cin >> po; if (st.isfull()) cout << "stack already full\n"; else st.push(po); break; case 'P': case 'p': if (st.isempty()) cout << "stack already empty\n"; else { st.pop(po); cout << "PO #" << po << " popped\n"; break; } } cout << "Please enter A to add a purchase order,\n" << "P to process a PO, or Q to quit.\n"; } cout << "Bye\n"; // cin.get(); // cin.get(); return 0; }
14.4.3 深入探讨类模板
1. 不正确地使用指针栈
概述
以下三个示例都以完全正确的Stack<Type>为基础: Stack<char*> st; // create a stack for pointers-to-char
(1)版本一
std::string po; 替换为 char* po; 这旨在用char指针而不是string对象来接收键盘输入。这种方法很快就失败了,因为仅仅创建指针,没有创建用于保存输入字符串的空间(程序将通过编译,但在cin试图将输入保存在某些不合适的内存单元中时崩溃)
(2)版本二
std::string po; 替换为 char po[40]; 这为输入的字符串分配了空间。另外,po的类型为char*,因此可以被放在栈中。但数组完全与pop()方法的假设相冲突: template<class Type> bool Stack<Type>::pop(Type& item) { if(top > 0) { item = items[--top]; return true; } else return false; } 首先,引用变量item必须引用某种类型的左值,而不是数组名。其次,代码假设可以给item赋值。即使item能够引用数组,也不能为数组名赋值。因此这种方法失败了
(3)版本三
std::string po; 替换为 char* po = new char[40]; 这里将会遇到最基本的问题:只有一个po变量,该变量总是指向相同的内存单元。确实,在每当读取新字符串时,内存的内容都将发生改变,但每次执行压入操作时,加入到栈中的的地址都相同。因此,对栈执行弹出操作时,得到的地址总是相同的,它总是指向读入的最后一个字符串。具体地说,栈并没有保存每一个新字符串,因此没有任何用途
2. 正确使用指针栈
使用指针栈的方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。把这些指针放在栈中是有意义的,因为每个指针都将指向不同的字符串
注意,创建不同指针是调用程序的职责,而不是栈的职责。栈的任务是管理指针,而不是创建指针
14.4.4 数组模板示例和非类型参数
概述
首先介绍一个允许指定数组大小的简单数组模板。一种方法是在类中使用动态数组和构造函数参数来提供元素数目,最后一个版本的Stack模板采用的就是这种方法。另一种方法是使用模板参数来提供常规数组的大小,C++11新增的模板array就是这样做的
1. 非类型参数(表达式参数)
关键字class(或在这种上下文中等价的关键字typename)指出T为类型参数,int指出n的类型为int。这种参数(指定特殊的类型而不是用作泛型名)称为非类型(non-type)或表达式(expression)参数
2. 有关表达式参数的说明
(1)表达式参数可以是整型、枚举、引用或指针。因此,double m是不合法的,但double * rm和double * pm是合法的
(2)模板代码不能修改参数的值,也不能使用参数的地址。所以,在ArrayTP模板中不能使用诸如n++和&n等表达式
(3)实例化模板时,用作表达式参数的值必须是常量表达式
3. 与Stack动态数组比较的优缺点
优点
执行速度将更快,尤其是在使用了很多小型数组时
缺点
每种数组大小都将生成自己的模板。也就是说,下面的声明将生成两个独立的类声明: ArrayTP<double, 12> eggweights; ArrayTP<double, 13> donuts; 但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数: Stack<int> eggs(12); Stack<int> dunkerss(13);
4. 说明
构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中的。这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类
template <class T, int n> class ArrayTP { private: T ar[n]; public: ArrayTP() {}; explicit ArrayTP(const T & v); virtual T & operator[](int i); virtual T operator[](int i) const; }; template <class T, int n> ArrayTP<T,n>::ArrayTP(const T & v) { for (int i = 0; i < n; i++) ar[i] = v; } template <class T, int n> T & ArrayTP<T,n>::operator[](int i) { if (i < 0 || i >= n) { std::cerr << "Error in array limits: " << i << " is out of range\n"; std::exit(EXIT_FAILURE); } return ar[i]; } template <class T, int n> T ArrayTP<T,n>::operator[](int i) const { if (i < 0 || i >= n) { std::cerr << "Error in array limits: " << i << " is out of range\n"; std::exit(EXIT_FAILURE); } return ar[i]; }
14.4.5 模板多功能性
1. 递归使用模板
对于前面的数组模板定义,可以这样使用它: ArrayTP<ArrayTP<int, 5>, 10> twodee; 这使得twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组。与之等价的常规数组声明如下: int twodee[10][5]; 请注意,在模板语法中,维的顺序与等价的二维数组相反
2. 使用多个类型参数
template <class T1, class T2> class Pair { private: T1 a; T2 b; public: T1 & first(); T2 & second(); T1 first() const { return a; } T2 second() const { return b; } Pair(const T1 & aval, const T2 & bval) : a(aval), b(bval) { } Pair() {} };
3. 默认类型模板参数
类模板的另一项新特性是,可以为类型参数提供默认值: template <class T1, class T2 = int> class Topo { ... }; 这样,如果省略T2的值,编译器将使用int: Topo<double, double> m1; // T1 is double, T2 is double Topo<double> m2; // T1 is double, T2 is int 虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值。然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的
14.4.6 模板的具体化
1. 隐式实例化
到目前为止,本章所有的模板示例使用的都是隐式实例化(implicit instantiation),即它们声明一个或多个对象,指出所需的类型,而编译 器使用通用模板提供的处方生成具体的类定义: ArrayTP<int, 100> stuff; // implicit instatiation 编译器在需要对象之前,不会生成类的隐式实例化: ArrayTP<double, 30> *pt; // a pointer, no object needed yet pt = new ArrayTP<double, 30>; // now an object is needed 第二条语句导致编译器生成类定义,并根据该定义创建一个对象
2. 显示实例化
当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化(explicit instantiation)。声明必须位于模板定义所在的名称空间中。例如,下面的声明将ArrayTP<string, 100>声明为一个类: template class ArrayTP<string, 100>; // generate ArrayTP<string, 100> class 在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化
3. 显示具体化
具体化类模板定义的格式如下: template <> class Classname<specialized-type-name> { ... }; 要使用新的表示法提供一个专供const char *类型使用的SortedArray 模板,可以使用类似于下面的代码: template <> class SortedArray<const char*> { ... // details omitted };
4. 部分具体化
(1)C++还允许部分具体化(partial specialization),即部分限制模板的通用性。例如,部分具体化可以给类型参数之一指定具体的类型: // general template template <class T1, class T2> class Pair { ... }; // specialization with T2 set to int template <class T1> class Pair<T1, int> { ... }; 关键字template后面的<>声明的是没有被具体化的类型参数。因此,上述第二个声明将T2具体化为int,但T1保持不变。
(2)注意,如果指定所有的类型,则<>内将为空,这将导致显式具体化: // spacialization with T1 and T2 set to int template <> class Pair<int, int> { ... }; 如果有多个模板可供选择,编译器将使用具体化程度最高的模板
(3)也可以通过为指针提供特殊版本来部分具体化现有的模板: template <class T> // general version class Feeb { ... }; template <class T*> // pointer partial specialization class Feeb { ... }; // modified code 如果提供的类型不是指针,则编译器将使用通用版本;如果提供的是指针,则编译器将使用指针具体化版本: Feeb<char> fb1; // use general Feeb template, T is char Feeb<char*> fb2; // use Feeb T* specialization, T is char 如果没有进行部分具体化,则第二个声明将使用通用模板,将T转换为char*类型。如果进行了部分具体化,则第二个声明将使用具体化模板,将T转换为char
(4)部分具体化特性使得能够设置各种限制。例如,可以这样做: // general template template <class T1, class T2, class T3> class Trio { ... }; // specialization with T3 set to T2 template <class T1, class T2> class Trio<T1, T2, T2> { ... }; // specialization with T3 and T2 set to T1 template <class T1> class Trio<T1, T1*, T1*> { ... }; 给定上述声明,编译器将作出如下选择: Trio<int, short, char*> t1; // use general template Trio<int , short> t2; // use Trio<T1, T2, T2> Trio<char, char*, char*> t3; // use Trio<T1, T1*, T1*>
14.4.7 成员模板
概述
模板可用作结构、类或模板类的成员。要完全实现STL的设计,必须使用这项特性。tempmemb.cpp是一个简短的模板类示例,该模板类将另一个模板类和模板函数作为其成员
1. 程序说明
下述方法调用的第一个参数将U的类型设置为int(参数10对应的类型): cout << guy.blab(10, 2.5) << endl; 因此,虽然混合类型引起的自动类型转换导致blab()中的计算以double类型进行,但返回值的类型为U(即int),因此它被截断为28,如下面的程序输出所示: T was set to double 3.5 3 V was set to T, which is double, then V was set to int 28 U was set to int 28..2609 U was set to double Done 注意到调用guy.blab()时,使用10.0代替了10,因此U被设置为double,这使得返回类型为double,因此输出为28.2608
2. 外部定义方法
可以在beta模板中声明hold类和blah方法,并在beta模板的外面定义它们。然而,很老的编译器根本不接受模板成员,而另一些编译器接受模板成员,但不接受类外面的定义。然而,如果所用的编译器接受类外面的定义,则在beta模板之外定义模板方法的代码如下: // member definition template <typename T> template <typename V> class beta<T>::hold { private: V val; public: hold(V v = 0) : val(v) {} void show() const { std::cout << val << std::endl; } V Value() const { return val; } }; // member definition template <typename T> template <typename U> class bete<T>::blab(U u, T t) { return (n.Value() + q.Value()) * u / t; } 上述定义将T、V和U用作模板参数。因为模板是嵌套的,因此必须使用下面的语法: template <typename T> template <typename V> 而不能使用下面的语法: template <typename T, typename V> 定义还必须指出hold和blab是beta<T>类的成员,这是通过使用作用域解析运算符来完成的
// tempmemb.cpp -- template members #include <iostream> using std::cout; using std::endl; template <typename T> class beta { private: template <typename V> // nested template class member class hold { private: V val; public: hold(V v = 0) : val(v) {} void show() const { cout << val << endl; } V Value() const { return val; } }; hold<T> q; // template object hold<int> n; // template object public: beta(T t, int i) : q(t), n(i) {} template<typename U> // template method U blab(U u, T t) { return (n.Value() + q.Value()) * u / t; } void Show() const { q.show(); n.show(); } }; int main() { beta<double> guy(3.5, 3); cout << "T was set to double\n"; guy.Show(); cout << "V was set to T, which is double, then V was set to int\n"; cout << guy.blab(10, 2.3) << endl; cout << "U was set to int\n"; cout << guy.blab(10.0, 2.3) << endl; cout << "U was set to double\n"; cout << "Done\n"; // std::cin.get(); return 0; }
14.4.8 将模板用作参数
概述
模板可以包含类型参数(如typename T)和非类型参数(如int n)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现STL
1. 介绍
开头的代码如下: template <template <typename T> class Thing> class Crab 模板参数是template <typename T>class Thing,其中template <typename T>class是类型,Thing是参数。 这意味着什么呢?假设有下面的声明: Crab<King> legs; 为使上述声明被接受,模板参数King必须是一个模板类,其声明与模板参数Thing的声明匹配: template <typename T> class King { ... };
2. 程序说明
在tempparm.cpp中,Crab的声明声明了两个对象: Thing<int> s1; Thing<double> s2; 前面的legs声明将用King<int>替换Thing<int>,用King<double>替换Thing<double>。然而,tempparm.cpp包含下面的声明: Crab<Stack> nebula; 因此,Thing<int>将被实例化为Stack<int>,而Thing<double>将被实例化为Stack<double>。总之,模板参数Thing将被替换为声明Crab对象时被用作模板参数的模板类型
3. 混合使用模板参数和常规参数
可以混合使用模板参数和常规参数,例如,Crab类的声明可以像下面这样打头: template <template <typename T> class Thing, typename U, typpename V> class Crab { private: Thing<U> s1; Thing<V> s2; ... }; 现在,成员s1和s2可存储的数据类型为泛型,而不是用硬编码指定的类型。这要求将程序中nebula的声明修改成下面这样: Crab<Stack, int, double> nebula; // T = Stack, U = int, V = double
14.4.9 模板类和友元
1. 模板类的非模板友元函数
1. 友元示例
在模板类中将一个常规函数声明为友元: template <class T> class HasFriend { public: friend void counts(); // friend to all HasFriend instantiations ... }; 上述声明使counts()函数成为模板所有实例化的友元。例如,它将是类hasFriend<int>和HasFriend<string>的友元
2. 定义友元函数
要提供模板类参数,必须指明具体化。例如,可以这样做: template <class T> class HasFriend { friend void report(HasFriend<T>&); // bound template frined ... }; 为理解上述代码的功能,想想声明一个特定类型的对象时,将生成的具体化: HasFriend<int> hf; 编译器将用int替代模板参数T,因此友元声明的格式如下: class HasFriend<int> { friend void report(HasFriend<int>&); // bound template frined ... }; 也就是说,带HasFriend<int>参数的report()将成为HasFriend<int>类的友元。同样,带HasFriend<double>参数的report()将是report()的一个重载版本——它是Hasfriend<double>类的友元
3. 注意事项
report()本身并不是模板函数,而只是使用一个模板作参数。这意味着必须为要使用的友元定义显式具体化: void report(HasFriend<short>&) { ... }; // explicit specialization for short void report(HasFriend<int>&) { ... }; // explicit specialization for int
2. 模板类的约束模板友元函数
1. 首先,在类定义的前面声明每个模板函数: template <typename T> void counts(); template <typename T> coid report(T&);
2. 然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化: template <typename TT> class HasFriendT { ... friend void counts<TT>(); friend void report<>(HasFriendT<TT>&); }; 声明中的<>指出这是模板具体化。对于report(),<>可以为空,因为可以从函数参数推断出如下模板类型参数: HasFriendT<TT> 然而,也可以使用: report<HasFriendT<TT>>(HasFriendT<TT>&) 但counts()函数没有参数,因此必须使用模板参数语法(<TT>)来指明其具体化。还需要注意的是,TT是HasFriendT类的参数类型
3. 为友元提供模板定义
3. 模板类的非约束模板友元函数
通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数是不同的: template <typename T> class ManyFriend { ... template <typename C, typename D> friend void show2(C&, D&); };
14.4.10 模板别名(C++11)
1. C++11的模板别名
如果能为类型指定别名,将很方便,在模板设计中尤其如此: template <typename T> using arrtype = std::array<T, 12>; // template to create multiple aliases 这将arrtype定义为一个模板别名,可使用它来指定类型,如下所示: arrtype<double> gallons; // gallons is type std::array<double, 12> arrtype<int> days; // days is type std::array<int, 12> arrtype<std::string> months; // months is type std::array<std::string, 12>
2. using=语法
C++11允许将语法using =用于非模板。用于非模板时,这种语法与常规typedef等价: typedef const char* pc1; // typedef syntax using pc2 = const char*; // using = syntax typedef const int*(*pa1)[10]; // typedef syntax using pa2 = const int*(*)[10]; // using = syntax 习惯这种语法后,您可能发现其可读性更强,因为它让类型名和类型信息更清晰