分类: C/C++
2008-11-28 14:57:19
第6章讲到,我们可以定义函数模板,以自动生成在实参或返回值类型方面不同的函数。C++具有适用于类的类似机制。类模板本身不是类,而只是某种编译器用来生成类代码的类的“配方”。从图8-5可以看出,类模板如同函数模板一样,我们也是通过指定模板中尖括号之间的形参类型(本例中是T),从而确定希望生成的类。以这种方式生成的类被称作类模板的实例,根据模板创建类的过程被称为实例化模板。
图 8-5
当我们以特定的类型实例化模板类的某个对象时,编译器将生成适当的类定义,因此一个类模板可以衍生出任意数量、各不相同的类。通过看一个示例,我们将能够充分理解类模板的实际工作过程。
我们将选择一个简单的示例来说明如何定义和使用类模板,并且不过多考虑因误用而可能出现的错误——那样将使问题复杂化。假设我们希望定义几个可以存储大量某种数据样本的类,每个类都应当提供一个求出所存储样本中最大值的Max()函数。该函数类似于我们在第6章讨论类模板时介绍的那个Max()函数。我们可以定义一个类模板,用来生成可以存储任何类型样本的CSamples类。
template
class CSamples
{
public:
// Constructor definition to accept an array of samples
CSamples(const T values[], int count)
{
m_Free = count < 100? count:100; // Don't exceed the array
for(int i = 0; i < m_Free; i++)
m_Values[i] = values[i]; // Store count number of samples
}
// Constructor to accept a single sample
CSamples(const T& value)
{
m_Values[0] = value; // Store the sample
m_Free = 1; // Next is free
}
// Default constructor
CSamples(){ m_Free = 0 } // Nothing stored, so first is free
// Function to add a sample
bool Add(const T& value)
{
bool OK = m_Free < 100; // Indicates there is a free place
if(OK)
m_Values[m_Free++] = value; // OK true, so store the value
return OK;
}
// Function to obtain maximum sample
T Max() const
{
// Set first sample or 0 as maximum
T theMax = m_Free ? m_Values[0] : 0;
for(int i = 1; i < m_Free; i++) // Check all the samples
if(m_Values[i] > theMax)
theMax = m_Values[i]; // Store any larger sample
return theMax;
}
private:
T m_Values[100]; // Array to store samples
int m_Free; // Index of free location in m_Values
};
为了指出正在定义的是模板而非简单的类,我们在关键字class和类名CSamples之前,插入关键字template和尖括号包围的类型形参T。该语法实质上与第6章定义函数模板的语法相同。形参T是类型变量,它将在我们声明类对象时被具体类型代替。类定义中出现形参T的任何位置,都将被对象声明中指定的类型代替,这将创建一个对应于指定类型的类定义。我们可以指定任何类型(基本数据类型或类类型),但指定的类型必须在类模板的上下文中有意义。任何用来根据模板实例化某个类的类类型,都必须已经定义过模板的成员函数处理本类对象时要用到的所有运算符。例如,如果我们的类没有实现operator>(),则不能使用CSamples类模板。一般来说,如果需要的话,我们可以在类模板中指定多个形参,本章稍后再来讨论这种可能性。
回到本示例上来,存储样本的数组的类型被指定为T。因此,该数组将成为我们声明CSamples对象时为T指定的那种类型的数组。可以看出,我们不仅在Add()和Max()函数中,而且还在类的两个构造函数中也使用了类型T。当我们使用该模板实例化类对象时,构造函数中出现的T同样将被替换掉。
构造函数支持创建空对象、只有一个样本的对象以及用样本数组进行初始化的对象。Add()函数允许一次一个地将样本添加到对象中。我们也可以重载这个函数,以允许样本数组相加。类模板提供了基本的措施来防止在Add()函数和接受样本数组的构造函数中m_Values数组的最大容量被超过。
前面曾经说过,理论上我们可以创建可处理任何数据类型的CSamples类的对象:int类型、double类型或任何已经定义过的类类型。在实践中,这种可能性并不意味着所创建的对象必定能够编译,且像我们预期的那样工作。实际情况完全取决于模板定义所做的事情,通常一个模板仅仅适用于特定的类型范围。例如,Max()函数隐含地认为>运算符可以用于被处理的任何类型。如果实际情况不然,则程序将不能编译。无疑,通常我们定义的模板只是为了处理某些类型而非此外的其他类型,但无法限制施加到模板上的类型。
我们或许希望将类模板成员函数的定义放在模板定义的外部。实现该功能的语句不是特别明显,因此我们来看一下应该如何做。我们应当以正常的方式将函数声明放在类模板定义的内部。例如:
template
class CSamples
{
// Rest of the template definition...
T Max() const; // Function to obtain maximum sample
// Rest of the template definition...
}
此处的代码将Max()函数声明为类模板的成员,但没有定义该函数。现在,我们需要为这个成员函数的定义创建单独的函数模板,创建时必须使用模板类的名称加上尖括号内的形参,以标识函数模板所属的类模板:
template
T CSamples
{
T theMax = m_Values[0]; // Set first sample as maximum
for(int i = 1; i < m_Free; i++) // Check all the samples
if(m_Values[i] > theMax)
theMax = m_Values[i]; // Store any larger sample
return theMax;
}
我们在第6章学过函数模板的语法。因为该函数模板是形参为T的类模板的成员,所以这里的函数模板定义应该有与类模板定义相同的形参。本例中只有一个形参T,但通常可能有好几个。如果类模板有两个或更多形参,则每个定义成员函数的模板也应该有同样的形参。
注意,作用域解析运算符之前只能使用附带形参名称T的类模板名。这是必需的——形参对于识别出根据该模板生成的函数属于哪个类非常重要。类模板的类型是CSamples
在类模板定义外部定义构造函数或析构函数与此类似。我们可以将接受样本数组的那个构造函数的定义写成下面的形式:
template
CSamples
{
m_Free = count < 100? count:100; // Don't exceed the array
for(int i = 0; i < m_Free; i++)
m_Values[i] = values[i]; // Store count number of samples
}
我们以定义普通成员函数时使用的相同方式,在模板中指定构造函数属于哪个类。注意,构造函数名不要求指定返回类型——它只能是CSamples,但需要用类模板类型CSamples
当我们使用函数模板定义的函数时,编译器能够根据使用的实参类型生成函数。函数模板的类型形参是通过使用特定的函数隐式确定的。类模板有些不同。为了以类模板为基础创建对象,我们必须总是在声明中指定类名后面的类型形参。
例如,为了声明一个CSamples<>对象来处理double类型的样本,需要将声明写成下面这样:
CSamples
该语句定义了一个CSamples
试一试:类模板
我们可以根据CSamples<>模板,创建一个存储CBox对象的对象。这是没有问题的,因为CBox类实现了重载大于运算符的operator>()函数。利用下面清单中给出的main()函数,我们可以练习一下类模板的用法:
// Ex8_07.cpp
// Using a class template
#include
using std::cout;
using std::endl;
// Put the CBox class definition from Ex8_06.cpp here...
// CSamples class template definition
template
{
public:
// Constructors
CSamples(const T values[], int count);
CSamples(const T& value);
CSamples(){ m_Free = 0; }
bool Add(const T& value); // Insert a value
T Max() const; // Calculate maximum
private:
T m_Values[100]; // Array to store samples
int m_Free; // Index of free location in m_Values
};
// Constructor template definition to accept an array of samples
template
{
m_Free = count < 100? count:100; // Don't exceed the array
for(int i = 0; i < m_Free; i++)
m_Values[i] = values[i]; // Store count number of samples
}
// Constructor to accept a single sample
template
{
m_Values[0] = value; // Store the sample
m_Free = 1; // Next is free
}
// Function to add a sample
template
{
bool OK = m_Free < 100; // Indicates there is a free place
if(OK)
m_Values[m_Free++] = value; // OK true, so store the value
return OK;
}
// Function to obtain maximum sample
template
{
T theMax = m_Free ? m_Values[0] : 0; // Set first sample or 0 as maximum
for(int i = 1; i < m_Free; i++) // Check all the samples
if(m_Values[i] > theMax)
theMax = m_Values[i]; // Store any larger sample
return theMax;
}
int main()
{
CBox boxes[] = { // Create an array of boxes
CBox(8.0, 5.0, 2.0), // Initialize the boxes...
CBox(5.0, 4.0, 6.0),
CBox(4.0, 3.0, 3.0)
};
// Create the CSamples object to hold CBox objects
CSamples
CBox maxBox = myBoxes.Max(); // Get the biggest box
cout << endl // and output its volume
<< "The biggest box has a volume of "
<< maxBox.Volume()
<< endl;
return 0;
}
我们应该用本章前面Ex8_06.cpp文件中CBox类的定义代替该程序开始部分相应的那行注释。不必担心支持CBox对象与double类型数值进行比较的那个operator>()函数,因为本示例不需要那个函数。除默认构造函数以外,该模板的所有成员函数都是通过单独的函数模板定义的,本例只是为了给出一个说明类模板用法的完整示例。
在main()函数中,我们创建了一个包含3个CBox对象的数组,然后使用该数组初始化一个可以存储CBox对象的CSamples对象。CSamples对象的声明基本上与普通类对象的声明相同,但在模板类名称后面增加了以尖括号包围的类型形参。
该程序产生下面的输出:
The biggest box has a volume of 120
注意,当我们创建类模板的实例时,不能理解成用于创建函数成员的那些函数模板的实例也将被创建。编译器只创建程序中实际调用的那些成员函数的模板实例。事实上,我们的函数模板甚至可以包含编码错误,而且只要不调用该模板生成的成员函数,编译器就不会有什么抱怨。我们可以利用该示例证实这一点。试着给Add()成员的模板引入几处错误。该程序仍然能够编译和运行,因为它没有调用Add()函数。
我们可以尝试修改上面的示例,看看用不同类型的模板实例化类时,可能会发生什么事情。
注意:
如果给类的构造函数添加一些输出语句,我们很可能对所发生的事情感到惊讶。CBox的构造函数被调用了103次!看一看在main()函数中究竟发生了什么。我们首先创建了一个包含3个CBox对象的数组,因此发生了3次调用。然后创建了一个容纳这些对象的CSamples对象,但CSamples对象包含的数组有100个CBox类型的变量,因此需要再调用默认构造函数100次,每个数组元素需要一次。当然,maxBox对象将由编译器提供的默认复制构造函数创建。
要在类模板中使用多个类型形参,只需对我们刚才看到的使用单个形参的示例作简单的扩充即可。我们可以在模板定义中的任何位置使用各个类型形参。例如,我们可以定义下面这样一个使用两个类型形参的类模板:
template
class CExampleClass
{
// Class data members
private:
T1 m_Value1;
T2 m_Value2;
// Rest of the template definition...
};
上面那两个类数据成员的类型取决于我们初始化对象时为形参提供的类型。
类模板中的形参不仅仅限于数据类型。我们还可以在类定义中使用一些需要以常量或常量表达式进行替换的形参。在前面的CSamples模板中,我们任意地将数组m_Values定义成包含100个元素。然而,我们还可以让该模板的用户在实例化对象时选择数组的大小,方法是将该模板定义成如下形式:
template
{
private:
T m_Values[Size]; // Array to store samples
int m_Free; // Index of free location in m_Values
public:
// Constructor definition to accept an array of samples
CSamples(const T values[], int count)
{
m_Free = count < Size? count:Size; // Don't exceed the array
for(int i = 0; i < m_Free; i++)
m_Values[i] = values[i]; // Store count number of samples
}
// Constructor to accept a single sample
CSamples(const T& value)
{
m_Values[0] = value; // Store the sample
m_Free = 1; // Next is free
}
// Default constructor
CSamples()
{
m_Free = 0; // Nothing stored, so first is free
}
// Function to add a sample
int Add(const T& value)
{
int OK = m_Free < Size; // Indicates there is a free place
if(OK)
m_Values[m_Free++] = value; // OK true, so store the value
return OK;
}
// Function to obtain maximum sample
T Max() const
{
// Set first sample or 0 as maximum
T theMax = m_Free ? m_Values[0] : 0;
for(int i = 1; i < m_Free; i++) // Check all the samples
if(m_Values[i] > theMax)
theMax = m_Values[i]; // Store any larger sample
return theMax;
}
};
创建对象时给Size提供的数值将代替整个模板定义中该形参的所有实例。现在,我们可以像下面这样声明前面那个示例中的CSamples对象:
CSamples
因为我们可以为Size形参提供任何常量表达式,所以还可以这样写:
CSamples
MyBoxes(boxes, sizeof boxes/sizeof CBox);
不过,该示例是很少见的模板用法,原来的版本更为合适。使Size成为模板形参的结果是,那些存储相同类型的对象但Size形参值不同的模板实例完全是不同的类,而且不可能被混淆。例如,CSamples
当实例化模板时,我们需要小心处理那些包含比较运算符的表达式。看看下面这条语句:
CSamples
该语句不能正确编译,因为表达式中y前面的>被解释为右尖括号。我们应该将这条语句写成:
CSamples
括弧确保第二个模板实参的表达式不会与尖括号混淆