C++基础教程面向对象(学习笔记(67))

模板类

在前面,您学习函数模板如何实例化为函数模板实例,允许我们概括函数以使用许多不同的数据类型。虽然这是通用编程之路上的一个很好的开端,但它并没有解决我们所有的问题。让我们看一个这样一个问题的例子,看看哪些模板可以进一步为我们做。

模板和容器类

在容器r类的课程中,您学习了如何使用组合来实现包含其他类的多个实例的类。作为这样一个容器的一个例子,我们看了一下IntArray类。以下是该类的简化示例:

#ifndef INTARRAY_H
#define INTARRAY_H
 
#include <assert.h> // for assert()
 
class IntArray
{
private:
    int m_length;
    int *m_data;
 
public:
    IntArray()
    {
        m_length = 0;
        m_data = nullptr;
    }
 
    IntArray(int length)
    {
        assert(length > 0);
        m_data = new int[length];
        m_length = length;
    }
 
    ~IntArray()
    {
        delete[] m_data;
    }
 
    void Erase()
    {
        delete[] m_data;
        //我们需要确保在这里将m_data设置为0,否则它将会
        // 留下指向释放的内存!
        m_data = nullptr;
        m_length = 0;
    }
 
    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    int getLength() { return m_length; }
};
 
#endif

虽然这个类提供了一种创建整数数组的简单方法,但是如果我们想要创建一个双精度数组呢?使用传统的编程方法,我们必须创建一个全新的类!这是DoubleArray的一个例子,一个用于保存双精度的数组类。

#ifndef DOUBLEARRAY_H
#define DOUBLEARRAY_H
 
#include <assert.h> // assert()
 
class DoubleArray
{
private:
    int m_length;
    double *m_data;
 
public:
    DoubleArray()
    {
        m_length = 0;
        m_data = nullptr;
    }
 
    DoubleArray(int length)
    {
        assert(length > 0);
        m_data = new double[length];
        m_length = length;
    }
 
    ~DoubleArray()
    {
        delete[] m_data;
    }
 
    void Erase()
    {
        delete[] m_data;
        //我们需要确保在这里将m_data设置为0,否则它将会
        // 留下指向释放的内存!
        m_data = nullptr;
        m_length = 0;
    }
 
    double & operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    int getLength() { return m_length; }
};
 
#endif

虽然代码清单很长,但你会注意到这两个类几乎完全相同!实际上,唯一的实质区别是包含的数据类型(int vs double)。正如您可能已经猜到的那样,这是另一个可以充分利用模板的领域,使我们不必创建绑定到特定数据类型的类。

创建模板类与创建模板函数非常相似,因此我们将以示例的方式进行操作。这是我们的数组类,模板版:

Array.h:

#ifndef ARRAY_H
#define ARRAY_H
 
#include <assert.h> // assert()
 
template <class T> // 这是一个模板类,用户将为T提供数据类型
class Array
{
private:
    int m_length;
    T *m_data;
 
public:
    Array()
    {
        m_length = 0;
        m_data = nullptr;
    }
 
    Array(int length)
    {
        m_data = new T[length];
        m_length = length;
    }
 
    ~Array()
    {
        delete[] m_data;
    }
 
    void Erase()
    {
        delete[] m_data;
        //我们需要确保在这里将m_data设置为0,否则它将会
        // 留下指向释放的内存!
        m_data = nullptr;
        m_length = 0;
    }
 
 
    T& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    // 数组的长度始终是整数
    //它不依赖于数组的数据类型
    int getLength(); // 模板化的getLength()函数定义如下
};
 
template <typename T> //在类外定义的成员函数需要自己的模板声明
int Array<T>::getLength() { return m_length; } // 注意类名是Array <T>,而不是Array
 
#endif

如您所见,此版本几乎与IntArray版本相同,只是我们添加了模板声明,并将包含的数据类型从int更改为T.

请注意,我们还在类声明之外定义了getLength()函数。这不是必需的,但是由于语法原因,新程序员在第一次尝试执行此操作时通常会遇到绊倒,因此一个示例是有益的。在类声明之外声明的每个模板化成员函数都需要自己的模板声明。另请注意,模板化数组类的名称是Array ,而不是Array - Array将引用名为Array的类的非模板化版本。

这是使用上面模板化数组类的简短示例:

#include <iostream>
#include "Array.h"
 
int main()
{
	Array<int> intArray(12);
	Array<double> doubleArray(12);
 
	for (int count = 0; count < intArray.getLength(); ++count)
	{
		intArray[count] = count;
		doubleArray[count] = count + 0.5;
	}
 
	for (int count = intArray.getLength()-1; count >= 0; --count)
		std::cout << intArray[count] << "\t" << doubleArray[count] << '\n';
 
	return 0;
}

此示例打印以下内容:

11 11.5
10 10.5
9 9.5
8 8.5
7 7.5
6 6.5
5 5.5
4 4.5
3 3.5
2 2.5
1 1.5
0 0.5
模板类的实例与模板函数的实例相同 - 编译器根据需要模板化副本,模板参数替换为用户需要的实际数据类型,然后编译副本。如果您不使用模板类,编译器甚至不会编译它。

模板类是实现容器类的理想选择,因为非常希望容器能够处理各种各样的数据类型,并且模板允许您在不重复代码的情况下执行此操作。虽然语法很难看,并且错误消息可能很神秘,但模板类确实是C ++最好和最有用的功能之一。

标准库中的模板类

现在我们已经介绍了模板类,你应该理解std :: vector 现在意味着什么 - std :: vector实际上是一个模板类,而int是模板的类型参数!标准库中包含可供您使用的预定义模板类。我们将在后面的章节中介绍这些内容。

拆分模板类

模板不是类或函数 - 它是用于创建类或函数的模板。因此,它与普通函数或类完全不同。在大多数情况下,这不是一个问题。但是,有一个领域通常会给开发人员带来问题。

对于非模板类,常见的过程是将类定义放在头文件中,将成员函数定义放在类似命名的代码文件中。通过这种方式,类的源被编译为单独的项目文件。但是,使用模板,这不起作用。考虑以下:

Array.h:

#ifndef ARRAY_H
#define ARRAY_H
 
#include <assert.h> // assert()
 
template <class T>
class Array
{
private:
    int m_length;
    T *m_data;
 
public:
    Array()
    {
        m_length = 0;
        m_data = nullptr;
    }
 
    Array(int length)
    {
        m_data = new T[length];
        m_length = length;
    }
 
    ~Array()
    {
        delete[] m_data;
    }
 
    void Erase()
    {
        delete[] m_data;
        //我们需要确保在这里将m_data设置为0,否则它将会
        // 留下指向释放的内存!
        m_data = nullptr;
        m_length = 0;
    }
 
 
    T& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    //数组的长度始终是整数
    // I它不依赖于数组的数据类型
    int getLength();
};
 
#endif

Array.cpp:

#include "Array.h"
 
template <typename T>
int Array<T>::getLength() { return m_length; }

main.cpp中:

#include "Array.h"
 
int main()
{
	Array<int> intArray(12);
	Array<double> doubleArray(12);
 
	for (int count = 0; count < intArray.getLength(); ++count)
	{
		intArray[count] = count;
		doubleArray[count] = count + 0.5;
	}
 
	for (int count = intArray.getLength()-1; count >= 0; --count)
		std::cout << intArray[count] << "\t" << doubleArray[count] << '\n';
 
	return 0;
}

上面的程序将编译,但会导致链接器错误:

未解析的外部符号“public:int __thiscall Array:: getLength(void)“(?GetLength @?$ Array @ H @@ QAEHXZ)
为了使编译器使用模板,它必须同时看到模板定义(不仅仅是声明)和用于实例化模板的模板类型。还要记住C ++单独编译文件。当Array.h头在main中是#included时,模板类定义被复制到main.cpp中。当编译器发现我们需要两个模板实例,Array 和Array 时,它将实例化这些实例,并将它们编译为main.cpp的一部分。但是,当它分别编译Array.cpp时,它将忘记我们需要一个Array 和Array ,因此模板函数永远不会被实例化。因此,我们得到链接器错误,因为编译器找不到Array :: getLength()或Array :: getLength()的定义。

有很多方法可以解决这个问题。

最简单的方法是简单地将所有模板类代码放在头文件中(在这种情况下,将Array.cpp的内容放入Array.h中,在类下面)。这样,当你#include标题时,所有的模板代码都会在一个地方。这个解决方案的优点是它很简单。这里的缺点是如果模板类在很多地方使用,你最终会得到模板类的许多本地副本,这会增加你的编译和链接时间(你的链接器应该删除重复的定义,所以它不应该膨胀你的可执行文件)。除非编译或链接时间开始成为问题,否则这是我们的首选解决方案。

如果您认为将Array.cpp代码放入Array.h标头会使标题太长/太乱,另一种方法是将Array.cpp重命名为Array.inl(.inl代表内联),然后包含Array.inl从Array.h标题的底部。这会产生与将所有代码放在标题中相同的结果,但有助于使事情变得更清晰。

其他解决方案涉及#including .cpp文件,但由于#include的非标准用法,我们不建议使用这些文件。

另一种方法是使用三文件方法。模板类定义位于标题中。模板类成员函数位于代码文件中。然后添加第三个文件,其中包含您需要的所有实例化类:

templates.cpp:

// 确保可以看到完整的Array模板定义
#include "Array.h"
#include "Array.cpp" // 我们在这里打破了最佳实践,但仅限于这一个地方
 
// #include此处需要的其他.h和.cpp模板定义
 
template class Array<int>; // 显式实例化模板Array <int>
template class Array<double>; // 显式实例化模板Array <double>
 
//在这里初始化其他的模板

“template class”命令使编译器显式实例化模板类。在上面的例子中,编译器将模板化templates.cpp中的Array 和Array 。因为templates.cpp在我们的项目中,所以这将被编译。然后可以从其他地方链接这些功能。

此方法更有效,但需要维护每个程序的templates.cpp文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值