[C++学习笔记] 第 11 章 C++ 中的代码重用

第 11 章 C++ 中的代码重用


​ C++ 的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一。除了公有继承之外,本章将介绍其他方法,其中之一是使用其它类的对象。这种方法称为包含、组合或层次化。另一种方法是使用私有或保护继承。通常,包含、私有继承和保护继承用于实现 has-a 关系,即新的类将包含另一个类的对象。

​ 本章还将介绍类模板——另一种重用代码的方法。

11.1 包含对象成员的类

11.1.1 valarray 类简介

valarray 类是由头文件 valarray 支持的。顾名思义,这个类用于处理数值,它包含以下方法:

  • operator[]:允许像数组一样访问类对象
  • size():返回包含的元素数
  • sum():返回所有元素的总和
  • max():返回最大的元素
  • min():返回最小的元素

valarray 是一个模板类,因此在使用时需要在标识符后面加上一对尖括号,并在其中包含所需的数据类型。例如:

double gpa[5] = {3.1, 3.5, 3.8, 2.9, 3.3};
valarray<double> v1;
valarray<int> v2(8);		//包含8个元素的数组
valarray<int> v3(10,8);		//包含8个元素的数组,每个元素都是10
valarray<double> v4(gpa,4);	//包含gpa数组的前4个元素
valarray<int> v5 = {20, 32, 17, 9};
11.1.2 Student 类的设计

​ 学生包含名字和成绩,这里的关系是 has-a。通常,用于建立 has-a 关系的 C++ 技术是组合,即创建一个包含其它类对象的类。例如:

class Student
{
private:
	string name;
	valarray<double> scores;
    ...
};

​ 上述类将数据成员声明为私有的,这意味着 Student 类的成员函数可以使用 stringvalarray<double> 类的公有接口来访问和修改 namescores 对象,但在类的外面不能这么做,而只能通过 Student 类的公有接口访问 namescore。对于这种情况,通常描述为 Student 类获得了其成员对象的实现,但没有继承接口。

​ 接口和实现

​ 使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口是 is-a 关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是 has-a 关系的组成部分。

11.1.3 Student 类示例
#include <string>
#include <iostream>
#include <valarray>
using namespace std;
class Student
{
private:
	typedef valarray<double> ArrayDb;
	string name;
	ArrayDb scores;
	ostream& arr_out(ostream& os) const;
public:
	Student() :name("Null Student"), scores() {  }
	explicit Student(const string& s) :name(s), scores() {  }
	explicit Student(int n) :name("Nully"), scores(n) {  }
	Student(const string& s, int n) :name(s), scores(n) {  }
	Student(const string& s, const ArrayDb& a) :name(s), scores(a) {  }
	Student(const char* s, const double* pd, int n) :name(s), scores(pd, n) {  }
	~Student() {  }
	double Average() const;
	const string& Name() const;
	double& operator[](int i);
	friend istream& operator>>(istream& is, Student& stu);
	friend istream& getline(istream& is, Student& stu);
	friend ostream& operator<<(ostream& os, const Student& stu);
};

​ 上述代码中,为简化表示,Student 类包含一个 typedef。将该 typedef 放在类定义的私有部分意味着可以在 Student 类的实现中使用它,但在 Student 类外面不能使用。

​ 此外,含有一个参数的构造函数前使用 explicit 可以关闭隐式转换。

​ 下面是类的实现:

#include "Student.h"
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;
}
double Student::Average() const
{
	if (scores.size() > 0)
		return scores.sum() / scores.size();
	else
		return 0;
}
const string& Student::Name() const
{
	return name;
}
double& Student::operator[](int i)
{
	return scores[i];
}
istream& operator>>(istream& is, Student& stu)
{
	is >> stu.name;
	return is;
}
istream& getline(istream& is, Student& stu)
{
	getline(is, stu.name);
	return is;
}
ostream& operator<<(ostream& os, const Student& stu)
{
	os << "Scores for " << stu.name << ":\n";
	stu.arr_out(os);
	return os;
}

11.2 私有继承

​ C++ 还有另一种实现 has-a 关系的途径——私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。

​ 下面更深入地探讨接口问题:使用公有继承,基类的公有方法将成为派生类的公有方法;使用私有继承,基类的公有方法将成为派生类的私有方法。

​ 使用私有继承,类将继承实现。包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。

​ 因为私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现 has-a 关系。

11.2.1 Student 类示例
class Student :private string, private valarray<double>
{
private:
	typedef valarray<double> ArrayDb;
	ostream& arr_out(ostream& os) const;
public:
	Student() :string("Null Student"), ArrayDb() {  }
	explicit Student(const string& s) :string(s), ArrayDb() {  }
	explicit Student(int n) :string("Nully"), ArrayDb(n) {  }
	Student(const string& s, int n) :string(s), ArrayDb(n) {  }
	Student(const string& s, const ArrayDb& a) :string(s), ArrayDb(a) {  }
	Student(const char* s, const double* pd, int n) :string(s), ArrayDb(pd, n) {  }
	~Student() {  }
	double Average() const;
	const string& Name() const;
	double& operator[](int i);
	friend istream& operator>>(istream& is, Student& stu);
	friend istream& getline(istream& is, Student& stu);
	friend ostream& operator<<(ostream& os, const Student& stu);
};

​ 要进行私有继承,需要使用关键字 private 而不是 public 来定义类。Student 类应从两个类派生而来,因此声明中列出这两个类:class Student :private string, private valarray<double>,使用多个基类的继承被称为多重继承。

​ 新的 Student 类不需要私有数据,因为两个基类已经提供了所需的所有数据成员。包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。这是这两种方法的第一个区别。

  1. 初始化基类组件

    对于继承类,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数:

    Student() :string("Null Student"), ArrayDb() {  }
    
  2. 访问基类的方法

    使用私有继承时,只能在派生类的方法中使用基类的方法,并且需要加上类名和作用域解析运算符,如:

    double Student::Average() const
    {
    	if (ArrayDb::size() > 0)
    		return ArrayDb::sum() / ArrayDb::size();
    	else
    		return 0;
    }
    
  3. 访问基类对象

    包含中返回学生姓名可以用以下代码:

    const string& Student::Name() const
    {
    	return name;
    }
    

    但是使用私有继承时,string 对象没有名称,Student 类应如何访问内部的 string 对象呢?

    答案是使用强制类型转换。由于 Student 类是从 string 类派生而来的,因此可以通过强制类型转换,将 Student 对象转换为 string 对象,结果将为继承而来的 string 对象:

    const string& Student::Name() const
    {
    	return (const string&)*this;
    }
    
  4. 访问基类的友元函数

    用类名显式地限定函数名不适合于友元函数,因为友元不属于类。如果要调用基类的友元,同样需要进行强制类型转换,如:

    ostream& operator<<(ostream& os, const Student& stu)
    {
    	os << "ArrayDb:: for " << (string&)stu << ":\n";
    	stu.arr_out(os);
    	return os;
    }
    

下面是类的具体实现:

#include "Student.h"
ostream& Student::arr_out(ostream& os) const
{
	int i;
	int lim = ArrayDb::size();
	if (lim > 0)
	{
		for (i = 0; i < lim; i++)
		{
			os << ArrayDb::operator[](i) << " ";
			if (i % 5 == 4)
				os << endl;
		}
		if (i % 5 != 0)
			os << endl;
	}
	else
		os << " empty array ";
	return os;
}
double Student::Average() const
{
	if (ArrayDb::size() > 0)
		return ArrayDb::sum() / ArrayDb::size();
	else
		return 0;
}
const string& Student::Name() const
{
	return (const string&)*this;
}
double& Student::operator[](int i)
{
	return ArrayDb::operator[](i);
}
istream& operator>>(istream& is, Student& stu)
{
	is >> (string&)stu;
	return is;
}
istream& getline(istream& is, Student& stu)
{
	getline(is, (string&)stu);
	return is;
}
ostream& operator<<(ostream& os, const Student& stu)
{
	os << "ArrayDb:: for " << (string&)stu << ":\n";
	stu.arr_out(os);
	return os;
}
11.2.2 使用包含还是私有继承

​ 由于既可以使用包含,也可以使用私有继承来建立 has-a 关系,那么应该使用哪种方式呢?大多数 C++ 程序员倾向于使用包含。因为包含。首先,包含更加易于理解,而使用继承将使关系更抽象。其次,继承会引起很多问题。例如,包含能够包括多个同类的子对象,如果某个类需要 3 3 3string 对象,可以使用包含来声明 3 3 3 个独立的 string 成员,而继承则只能使用一个这样的对象。

​ 然而,私有继承所提供的特性确实比包含多。例如,假设类包含保护成员,则这样的成员在派生类中是可用的,但使用组合时是不可用的。另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可用重新定义虚函数,而包含不能。

提示:通常,应使用包含来建立 has-a 关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。

11.2.3 保护继承

​ 保护继承是私有继承的变体:

class Student :protected string, protected valarray<double>
{...};

​ 使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。

​ 下表总结了各种继承方式的特征:

特征公有继承保护继承私有继承
公有成员变成派生类的公有成员派生类的保护成员派生类的私有成员
保护成员变成派生类的保护成员派生类的保护成员派生类的私有成员
私有成员变成只能通过基类接口访问只能通过基类接口访问只能通过基类接口访问
能否隐式向上转换
11.2.4 使用 using 重新定义访问权限

​ 使用保护或私有继承时,基类的公有成员将变成保护成员或私有成员。假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。例如,假设希望 Student 类能够使用 valarraysum 方法,可以在类中声明一个 sum 方法:

double Student::sum() const
{
    return std::valarray<double>::sum();
}

​ 另一种方法是,将函数调用包装在另一个函数调用中,即,使用一个 using 声明来指出派生类可以使用特定的基类成员,即使采用的是私有派生。例如:

class Student :private string, private valarray<double>
{
...
public:
    using std::valarray<double>::min;
    using std::valarray<double>::max;
}

​ 上述 using 声明使得 minmax 方法可用,就像是 Student 类的公有方法一样:

cout << ada[i].max() << endl;

​ 注意,using 声明只使用成员名——没有圆括号、函数特征标和返回类型。例如,为使 Student 类可用使用 valarrayoperator[] 方法,只需要在公有部分加上 using 声明,这将使两个版本(const 和 非 const)都可用。using 声明只适用于继承,而不适用于包含。

11.3 多重继承

​ 多有继承(multiple inheritance)描述的是有多个直接基类的类。与单继承一样,公有 MI 表示的也是 is-a 关系。例如,可以从 Waiter 类和 Singer 类派生出 SingingWaier 类:

class SingingWaiter :public Waiter, public Singer {...};

​ 请注意,必须使用关键字 public 来限定每一个基类,对于没有限定的基类,编译器默认认为是私有派生。

​ MI 可能会带来很多新问题,其中两个主要的问题是:从两个不同的基类继承同名方法;从两个或更多相关基类那里继承同一个类的多个实例。

​ 下面来看一个例子。首先定义一个抽象基类 Worker,并使用它派生出 WaiterSinger 类。然后,使用 MI 从这两个类派生出 SingingWaiter 类:

#include <string>
using namespace std;
class Worker
{
private:
	string fullname;
	long id;
public:
	Worker() :fullname("no one"), id(0L) {  }
	Worker(const string& s, long n) :fullname(s), id(n) {  }
	virtual ~Worker() = 0;
	virtual void Set();
	virtual void Show() const;
};

class Waiter :public Worker
{
private:
	int panache;
public:
	Waiter() :Worker(), panache(0) {  }
	Waiter(const string& s, long n, int p = 0) :Worker(s, n), panache(p) {  }
	Waiter(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 const char* pv[Vtypes];
	int voice;
public:
	Singer() :Worker(), voice(other) {  }
	Singer(const 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;
};
#include "Worker.h"
#include <iostream>

Worker::~Worker()
{

}
void Worker::Set()
{
	cout << "Enter worker's name: ";
	getline(cin, fullname);
	cout << "Enter work's ID: ";
	cin >> id;
	while (cin.get() != '\n')
		continue;
}
void Worker::Show() const
{
	cout << "Name: " << fullname << endl;
	cout << "Employee ID: " << id << endl;
}


void Waiter::Set()
{
	Worker::Set();
	cout << "Enter waiter's panache rating: ";
	cin >> panache;
	while (cin.get() != '\n')
		continue;
}
void Waiter::Show() const
{
	cout << "Category: waiter\n";
	Worker::Show();
	cout << "Panache rating: " << panache << endl;
}


const char* Singer::pv[] = { "other", "alto", "contralto", "soprano", "bass", "baritone", "tenor" };
void Singer::Set()
{
	Worker::Set();
	cout << "Enter number for singer's vocal range:\n";
	int i;
	for (i = 0; i < Vtypes; i++)
	{
		cout << i << ": " << pv[i] << " ";
		if (i % 4 == 3)
			cout << endl;
	}
	if (i % 4 != 0)
		cout << endl;
	while (cin >> voice && (voice < 0 || voice >= Vtypes))
		cout << "Please enter a value>=0 and < " << Vtypes << endl;
	while (cin.get() != '\n')
		continue;
}
void Singer::Show() const
{
	cout << "Category: singer\n";
	Worker::Show();
	cout << "Vocal range: " << pv[voice] << endl;
}

​ 类的定义和实现如上。如果添加一个从 SingerWaiter 类派生出的 SingingWaiter 类,将出现以下问题:

  • 有多少 Worker
  • 哪个方法?
11.3.1 有多少 Worker
class SingingWaiter :public Waiter, public Singer {...};

​ 因为 SingerWaiter 都继承了一个 Worker 组件,因此 SingingWaiter 将包含两个 Worker 组件。这将引起问题,例如:

SingingWaiter ed;
Worker * pw = &ed; //error! 存在二义性

​ 上述代码存在二义性。通常,这种赋值将把基类指针指向派生对象中的基类对象。但 ed 中包含两个 Worker 对象,有两个地址可供选择,所以应使用类型转换来指定对象:

Worker * pw1 = (Waiter *)&ed;
Worker * pw2 = (Singer *)&ed;

​ 除了这个问题之外,包含两个 Worker 对象还会导致其它问题。然而,真正的问题是:为什么需要 Worker 对象的两个拷贝?为解决这个问题,C++ 引入了一种新技术——虚基类,使 MI 成为可能。

  1. 虚基类

    ​ 虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如:

    class Singer : virtual public Worker {...};
    class Waiter : public virtual Worker {...};
    class SingingWaiter : public Singer, public Waiter {...};
    

    ​ 在类声明中使用关键字 virtual,可以使 Worker 被用作 SingerWaiter 的虚基类。(virtualpublic 的次序无关紧要)。现在,SingingWaiter 对象只包含 Worker 对象的一个副本。从本质上说,继承的 SingerWaiter 对象共享一个 Worker 对象,而不是各自引入自己的 Worker 对象副本。

    注意:虚基类和虚函数、抽象基类之间不存在明显的联系

    1. 新的构造函数规则

      ​ 使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数就是基类的构造函数。例如:

      class A
      {
          int a;
      public
          A(int n = 0) : a(n) { }
          ...
      };
      class B : public A
      {
          int b;
      public
          B(int m = 0, int n = 0) : A(n), b(m) { }
          ...
      };
      class C : public B
      {
          int c;
      public:
          C(int q = 0, int m = 0, int n = 0) : B(m, n), c(q) { }
          ...
      }
      

      ​ C 的构造函数只能调用 B 的构造函数,而 B 的构造函数只能调用 A 的构造函数。

      ​ 如果 Worker 是虚基类,则下面的信息传递将不起作用:

      SingingWaiter(const Worker&wk, int p=0, int v =Singer::other):Waiter(wk, p), Singer(wk, v) { }
      

      ​ 存在的问题是,自动传递信息时,将通过 2 2 2 条不同途径(WaiterSinger )将 wk 传递给 Worker 对象。为避免这种冲突,C++ 在基类是虚的时 ,禁止信息通过中间类自动传递给基类。因此,上述构造函数将初始化成员 panachevoice,但 wk 参数中的信息不会传递给子对象 Waiter,而编译器会使用 Worker 的默认构造函数。

      ​ 如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。因此,构造函数应该是这样:

      SingingWaiter(const Worker&wk, int p=0, int v =Singer::other)
          	:Worker(wk), Waiter(wk, p), Singer(wk, v) { }
      

      警告:如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。

11.3.2 哪个方法
除了修改类构造函数外, MI 通常还要求调整其他代码。假设在 `SingingWaiter` 类中包含以下代码:
SingingWaiter newhire;
newhire.Show();

​ 对于单继承,如果没有重新定义 Show()·,则将使用最近祖先中的定义。而在多重继承中,每个直接祖先都有一个 Show() 函数,这使得上述调用是二义性的。

​ 可以使用作用域解析运算符来澄清意图:

newhire.Singer::Show();

​ 然而,更好的方法是在 SingingWaier 中重新定义 Show(),并指出要使用哪个 Show()。例如,如果希望 SingingWaiter 对象使用 Singer 版本的 Show(),则可以这样做:

void SingingWaiter::Show()
{
 Singer::Show();
}
11.3.3 MI 的其他问题
  1. 混合使用虚基类和非虚基类

    ​ 当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。例如,假设类 B 被用作类 C 和 D 的虚基类,同时被用作类 X 和 Y 的非虚基类,而类 M 是从 C、D、X 和 Y 派生而来的。在这种情况下,类 M 将从虚派生祖先 C 和 D 那里共继承一个 B 类的子对象,并从每一个非虚派生祖先 X 和 Y 分别继承一个 B 类子对象。因此,M 将包含三个 B 类子对象。

    1. 虚基类和支配

      ​ 使用虚基类将改变 C++ 解析二义性的方式。使用非虚基类时,规则很简单。如果类从不同的类那里继承了两个或更多同名成员,则使用该成员名时,如果没有用类名进行限定,将导致二义性。但如果使用的是虚基类,则这样做不一定会导致二义性。在这种情况下,如果某个名称优先于其他所有名称,则使用它时,即便不使用限定符,也不会导致二义性。

      ​ 那么,一个成员名如何优先于另一个成员名呢?派生类中的名称优先于直接或间接祖先类中的相同名称。例如:

      class B
      {
      public:
          short q();
      };
      class C : virtual public B
      {
      public:
          long q();
          int omg();
      };
      class D : public C
      {
          ...
      };
      class E : virtual public B
      {
      private:
          int omg();
      };
      class F : public D, public E
      {
          ...
      };
      

      ​ 类 C 中的 q() 定义优先于类 B 中的 q() 定义,因为类 C 是从类 B 派生而来的。因此,F 中可以使用 q() 来代表 C::q()

      ​ 另一方面,任何一个 omg() 定义都不优先于其他 omg() 定义,因为 CE 都不是对方的基类。所以,在 F 中使用非限定的 omg() 将导致二义性。

      ​ 虚二义性规则与访问规则无关。也就是说,即使 E::omg() 是私有的,不能在 F 中直接访问,但使用 omg() 仍将导致二义性。同样,即使 C::q() 是私有的,它也将优先于 B::q()

11.3.4 MI 小结

​ 首先复习一下不使用虚基类的 MI。这种形式的 MI 不会引入新的规则。然而,如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。

​ 如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类的一个实例。

​ 当派生类使用关键字 virtual 来指示派生时,基类就成为了虚基类。从虚基类的一个或多个实例派生而来的类将只继承一个基类对象。为实现这种特性,必须满足其他要求:

  • 有间接虚基类的派生类的构造函数必须直接调用间接基类的构造函数
  • 通过优先规则解决名称二义性

11.4 类模板

​ C++ 的类模板为生成通用的类声明提供了一种更好的方法。模板提供参数化类型,即能够将类型名作为参数传递给接收方来建立类或函数。

11.4.1 定义类模板

​ 下面是一个类模板的示例:

template <class T>
class Stack
{
private:
	enum { MAX = 10 };
	T items[MAX];
	int top;
public:
	Stack();
	bool isempty();
	bool isfull();
	bool push(const T& item);
	bool pop(T& item);
};

template <class T>
Stack<T>::Stack()
{
	top = 0;
}

template <class T>
bool Stack<T>::isempty()
{
	return top == 0;
}

template <class T>
bool Stack<T>::isfull()
{
	return top == MAX;
}

template <class T>
bool Stack<T>::push(const T& item)
{
	if (top < MAX)
	{
		items[top++] = item;
		return true;
	}
	else
		return false;
}

template <class T>
bool Stack<T>::pop(T& item)
{
	if (top > 0)
	{
		item = items[--top];
		return true;
	}
	else
		return false;
}

​ 首先,模板类以代码 template <class Type> 开头。这和函数模板类似,这里使用 class 并不意味着 Type 必须是一个类;而只是表明 Type 是一个通用的类型说明符。在使用模板时,将使用实际的类型替换它。可以使用关键字 typename 代替 classtemplate <typename Type>

​ 另外,在定义函数时,还需将类限定符从 Stack:: 改为 Stack<Type>::。如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。

​ 此外,由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。为此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。

11.4.2 使用模板类

​ 仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名:

Stack<int> a;
Stack<string> b;

​ 注意,必须显式地提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数。

11.4.3 数组模板示例和非类型参数

​ 如何使用模板创建一个指定数组大小的简单数组模板?一种方法是在类中使用动态数组和构造函数来提供元素数目。另一种方法是使用模板参数来提供常规数组的大小,C++11 新增的模板 array 就是这做的。例如:

#include <iostream>
#include <cstdlib>
using namespace std;
template <typename 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 <typename T, int n>
ArrayTP<T,n>::ArrayTP(const T& v)
{
	for (int i = 0; i < n; i++)
		ar[i] = v;
}
template <typename T, int n>
T& ArrayTP<T, n>::operator[](int i)
{
	if (i < 0 || i >= n)
	{
		cerr << "Error in array limits: " << i << " is out of range\n";
		exit(EXIT_FAILURE);
	}
	return ar[i];
}
template <typename T, int n>
T ArrayTP<T, n>::operator[](int i) const
{
	if (i < 0 || i >= n)
	{
		cerr << "Error in array limits: " << i << " is out of range\n";
		exit(EXIT_FAILURE);
	}
	return ar[i];
}

​ 该类中,int 指出 n 的类型,这种指定特定类型的参数称为非类型或表达式参数。例如:ArrayTP<double 12> a,这将导致编译器定义名为 ArrayTP<double 12> 的类。

​ 表达式参数有一些限制:只能为整型、枚举、引用或指针。另外,模板代码不能修改参数的值,也不能使用参数的地址。所以,在 ArrayTP 中不能使用如 n++、&n 等表达式。另外,实例化模板时,用作表达式参数的值必须是常量。

​ 表达式参数方法的主要缺点是,每组数组大小都将生成自己的模板。也就是说,下面的声明将生成两个独立的类声明:

ArrayTP<double, 12> a;
ArrayTP<double, 13> b;
11.4.5 模板多功能性

​ 可以将用于常规类的技术用于模板类。模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数等。

  • 用作基类:

    template <class T>
    class GrowArray : public Array<T> {...};
    
  • 用作组件类:

    template <class T>
    class Stack
    {
        Array<T> ar;	//作为一个 component
        ...
    }
    
  • 用作其他模板的类型参数:

    Array< Stack<int> > asi;
    

    ​ 在这种用法中,C++98 要求使用至少一个空白字符将两个 > 符号分开,以免与运算符 >> 混淆。C++11 不要求这样做。

  • 递归使用模板

    ​ 另一个模板多功能性的例子是,可以递归使用模板。例如:

    ArrayTP< ArrayTP<int,5>, 10> twodee; //等价于 int arr[10][5];
    

    ​ 这使得 twodee 是一个包含 10 10 10 个元素的数组,其中每个元素都是一个包含 5 5 5int 元素的数组。

  • 使用多个类型参数

    ​ 模板可以包含多个类型参数。例如:

    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() {  }
    };
    
    template <class T1, class T2>
    T1& Pair<T1, T2>::first()
    {
    	return a;
    }
    template <class T1, class T2>
    T2& Pair<T1, T2>::second()
    {
    	return b;
    }
    
  • 默认模板类型参数

    ​ 类模板还可以为类型参数提供默认值:

    template <class T1, class T2 = int> class Topo {...};
    

    ​ 这样,如果省略 T2 的值,编译器将使用 int:

    Topo<double, double> m1;	//T1、T2都是double
    Topo<double> m2;			//T1是double,T2是int
    

    ​ 虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值。然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。

11.4.6 模板的具体化

​ 类模板与函数模板很相似,因为可以有隐式实例化、显式实例化和显式具体化,它们统称为具体化。

  1. 隐式实例化

    ​ 到目前为止,本章所有的模板示例使用的都是隐式实例化。编译器使用通用模板生成具体的类定义,例如:

    ArrayTP<int, 100> stuff;
    

    ​ 编译器在需要对象之前,不会生成类的隐式实例化:

    ArrayTP<double, 30> * pt; 		//没有实例化
    pt = new ArrayTP<double, 30>; 	//隐式实例化
    
  2. 显式实例化

    ​ 当使用关键字 template 并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明必须位于模板定义所在的名称空间中。例如:

    template class ArrayTP<string, 100>;
    

    ​ 在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明。

  3. 显式具体化

    ​ 显式具体化是特定类型(用于替换模板中的泛型)的定义。有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以创建显式具体化。

    ​ 具体化类模板定义的格式如下:

    template <> class Classname<具体类型名> {...};
    

    ​ 例如:

    template <> class SortedArray<const char*> {...};
    

    ​ 当请求 const char* 类型的 SortedArray 模板时,编译器将使用上述专用的定义,而不是通用的模板定义。

  4. 部分具体化

    ​ C++ 还允许部分具体化,即部分限制模板的通用性。例如:

    template <class T1, class T2> class Pair {...}; //通用模板
    template <class T1> class Pair<T1, int> {...};	//部分具体化
    

    ​ 关键字 template 后面的 <> 声明的时没有被具体化的类型参数。如果指定所有类型,则 <> 内将为空,这将导致显式具体化。

    ​ 如果有多个模板可供选择,编译器将使用具体化程度最高的模板。例如:

    Pair<double, int> p1;		//使用Pair<T1, int> 模板
    Pair<double, double> p2;	//使用Pair<T1, T2> 模板
    

    ​ 也可以通过为指针提供特殊版本来部分具体化现有的模板:

    template <class T>
    class Feeb {...};
    template <class T*>
    class Feeb {...};
    

    ​ 如果提供的类型不是指针,则编译器将使用通用版本;如果提供的是指针,则编译器将使用指针具体化版本:

    Feeb<char> fb1; //使用通用版本
    Feeb<char*> fb2;//使用指针具体化版本
    

    ​ 部分具体化特性能够设置各种限制,例如:

    template <class T1, class T2, class T3> class Trio {...};
    template <class T1, class T2> class Trio<T1, T2, T2> {...};
    template <class T1> class Trio<T1, T1*, T1*> {...};
    

    ​ 给定上述声明,编译器将作出以下选择:

    Trio<int, short, char*> t1;	//使用通用模板
    Trio<int, short> t2;		//使用Trio<T1, T2, T2>
    Trio<char, char*, char*> t3;//使用Trio<T1, T1*, T1*>
    
11.4.7 成员模板

​ 模板可用作结构、类或模板类的成员,例如:

#include <iostream>
using namespace std;
template <typename T>
class beta
{
private:
 template <typename V>
 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;
 hold<int> n;
public:
 beta(T t, int i) :q(t), n(i) {  }
 template<typename U>
 U blab(U u, T t) { return (n.value() + q.value()) * u / t; }
 void Show() const { q.show(); n.show();  }
};

​ 可以在 beta 模板中声明 hold 类和 blab 方法,并在 beta 模板的外面定义它们。例如:

#include <iostream>
using namespace std;
template <typename T>
class beta
{
private:
 template <typename V>
 class hold;
 hold<T> q;
 hold<int> n;
public:
 beta(T t, int i) :q(t), n(i) {  }
 template<typename U>
 U blab(U u, T t);
 void Show() const { q.show(); n.show();  }
};

template <typename T>
template <typename V>
class beta<T>::hold
{
  private:
 	 V val;
  public:
 	 hold(V v = 0) :val(v) {  }
 	 void show() const { cout << val << endl; }
 	 V value() const { return val; }
};
template <typename T>
 template <typename U>
 U beta<T>::blab(U u, T t)
 {
 	return (n.value() + q.value()) * u / t; 
 }

​ 上述定义将 T、V、U 用作模板参数。因为模板是嵌套的,因此必须使用下面的语法:

template <typename T>
 template <typname V>

​ 定义还必须指出 holdblabbeta<T> 类的成员,这是通过作用域解析运算符来完成的。

11.4.8 将模板用作参数

​ 模板可以包含类型参数和非类型参数(如 int n),模板还可以包含本身就是模板的参数。这种参数是模板新增的特性,用于实现 STL。例如:

#include <iostream>
template <template <typename T> class Thing>
class Crab
{
private:
	Thing<int> s1;
	Thing<double> s2;
public:
	Crab() {  }
	bool push(int a, double x) { return s1.push(a) && s2.push(x); }
	bool pop(int& a, double& x) { return s1.pop(a) && s2.pop(x); }
};
int main()
{
    Crab<Stack> nebula;
    nebula.push(1,1.5);
}

​ 类定义中,模板参数是 template <typename T> class Thing,其中 template <typename T> class 是类型,Thing 是参数。此处 Crab 类的声明对 Thing 模板类做了几个假设:必须含有 push()pop() 方法,并且该模板类只含有一个模板参数。

​ 可以混合使用模板参数和常规参数,例如:

template <template <typename T> class Thing, typename U, typename V>
class Crab
{
private:
    Thing<U> s1;
    Thing<V> s2;
    ...
}
//声明一个Crab对象
Crab<Stack, int, double> nebula;
11.4.9 模板类和友元

​ 模板类声明也可以有友元。模板的友元分为 3 3 3 类:

  • 非模板友元

  • 约束模板友元,即友元的类型取决于类被实例化时的类型

  • 非约束模板友元,即友元的所有具体化都是类的每一个具体化的友元

    1. 模板类的非模板友元函数

      #include <iostream>
      using namespace std;
      template <typename T>
      class HasFriend
      {
      private:
      	T item;
      	static int ct;
      public:
      	HasFriend(const T& i) :item(i) { ct++; }
      	~HasFriend() { ct--; }
      	friend void counts();
      	friend void reports(HasFriend<T>&);
      };
      
      template <typename T>
      int HasFriend<T>::ct = 0;
      
      void counts()
      {
      	cout << "int count: " << HasFriend<int>::ct << "; ";
      	cout << "double count: " << HasFriend<double>::ct << endl;
      }
      
      void reports(HasFriend<int>& hf)
      {
      	cout << "HasFriend<int>: " << hf.item << endl;
      }
      void reports(HasFriend<double>& hf)
      {
      	cout << "HasFriend<double>: " << hf.item << endl;
      }
      

      ​ 首先,先看 count() 函数。count() 函数没有参数,是模板所有实例化的友元。

      ​ 假设要为友元函数提供模板类参数,不能按照以下方式来进行友元声明:

      friend void report(HasFriend &);
      

      ​ 因为不存在 HasFriend 这样的对象,模板类只有特定的具体化,如 HasFriend<int>。要提供模板类参数,必须指明具体化:

      friend void report(HasFriend<T> &);
      

      ​ 注意,report() 并不是模板函数,而只是使用一个模板作为参数。这意味着必须为要使用的友元提供显式具体化,如:

      void report(HasFriend<int> &);
      void report(HasFriend<double> &);
      
    2. 模板类的约束模板友元函数

      ​ 可以修改前一个示例,使友元函数本身成为模板。具体地说,为约束模板友元做准备,来使类的每一个具体化都获得一个与友元匹配的具体化。这包含以下三步:

      1. 在类定义的前面声明每个模板函数。

        template <typename T> void counts();
        template <typename T> void reports(T&);
        
      2. 在类中将模板声明为友元。

        template <typename T>
        class HasFriend
        {
        private:
        	T item;
        	static int ct;
        public:
        	HasFriend(const T& i) :item(i) { ct++; }
        	~HasFriend() { ct--; }
        	friend void counts<T>();
        	friend void reports<HasFriend<T>>(HasFriend<T>&);
        };
        

        ​ 声明中的 <> 指出这是模板具体化。对于 report()<> 可以为空,因为可以从函数参数推断出模板类型参数为 HasFriend<T>

      3. 为友元提供模板定义。

        下面的程序说明了这三个方面:

        #include <iostream>
        using namespace std;
        
        template <typename T> void counts();
        template <typename T> void reports(T&);
        
        template <typename T>
        class HasFriend
        {
        private:
        	T item;
        	static int ct;
        public:
        	HasFriend(const T& i) :item(i) { ct++; }
        	~HasFriend() { ct--; }
        	friend void counts<T>();
        	friend void reports<HasFriend<T>>(HasFriend<T>&);
        };
        
        template <typename T>
        int HasFriend<T>::ct = 0;
        
        template <typename T>
        void counts()
        {
        	cout << "template size: " << sizeof(HasFriend<T>) << "; ";
        	cout << "template counts(): " << HasFriend<T>::ct << endl;
        }
        
        template <typename T>
        void reports<T>(T & hf)
        {
        	cout << hf.item << endl;
        }
        
        int main()
        {
            count<int>();
            HasFriend<double> a(0.5);
            reports(a);
        }
        

        ​ 因为 count() 函数调用没有可被编译器用来推断出所需具体化的函数参数,因此这些调用使用例如 count<int> 指明具体化。但对于 reports() 调用,编译器可以从参数类型推断出要使用的具体化。

    3. 模板类的非约束模板友元函数

      ​ 约束模板友元函数是在类外面声明模板,并使用模板具体化。通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数是不同的,例如:

      template <typename T>
      class ManyFriend
      {
      private:
          T item;
      public:
          ...
          template <typename C, typename D>
          friend void show2(C&, D&);
      }
      
      template <typename C, typename D>
      friend void show2(C& c, D& d)
      {
          cout<< c.item << ", " << d.item <<endl;
      }
      
      int main()
      {
          ManyFriend<int> a(10), b(20);
          ManyFriend<double> c(10.5);
          show2(a, b);
          show2(a, c);
      }
      
11.4.10 模板别名(C++11)

​ C++11 新增了一种功能——使用模板提供一系列别名。例如:

template <typename T>
	using arrtype = std::array<T, 12>;

arrtype<double> a;
arrtype<int> b;

​ 这将使得 arrtype<T> 表示类型 std::array<T,12>

​ C++11 允许将该语法用于非模板,这与常规 typedef 等价,如:

typedef const char * pc1;
using pc2 = const char *;

​ 这种语法的可读性比 typedef 更强,因为它让类型名和类型信息更加清晰。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值