模板和类1:模板类


在前一章中,我们介绍了函数模板,它允许我们将函数泛化,使其能够处理多种不同的数据类型。虽然这是迈向通用编程的良好开端,但它并不能解决我们所有的问题。让我们来看一个例子,看看模板还能为我们做些什么。

模板和容器类

在 “容器类”中,你学习了如何使用组合来实现包含其他类的多个实例的类。作为此类容器的一个例子,我们研究了 IntArray 类。以下是该类的一个简化示例:

IntArray.h

#ifndef INTARRAY_H
#define INTARRAY_H

#include <cassert>

class IntArray
{
private:
    int m_length{};
    int* m_data{};
public:

    IntArray(int length)
    {
        assert(length > 0);
        m_data = new int[length]{};
        m_length = length;
    }

    // We don't want to allow copies of IntArray to be created.
    IntArray(const IntArray&) = delete;
    IntArray& operator=(const IntArray&) = delete;

    ~IntArray()
    {
        delete[] m_data;
    }

    void erase()
    {
        delete[] m_data;
        // We need to make sure we set m_data to 0 here, otherwise it will
        // be left pointing at deallocated memory!
        m_data = nullptr;
        m_length = 0;
    }

    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }

    int getLength() const { return m_length; }
};

#endif

main.cpp

#include "IntArray.h"
#include <iostream>

int main()
{
    IntArray intArray{ length: 3 };
    std::cout << intArray.getLength() << '\n';

    intArray.erase();
    std::cout << intArray.getLength() << '\n';

    return 0;
}

img
虽然这个类提供了一种创建整数数组的简便方法,但如果我们想创建一个双精度浮点数数组呢?使用传统的编程方法,我们需要创建一个全新的类!这里有一个 DoubleArray 的例子,它就是一个用于存储双精度浮点数的数组类。
DoubleArray.h

#ifndef DOUBLEARRAY_H
#define DOUBLEARRAY_H

#include <cassert>

class DoubleArray
{
private:
    int m_length{};
    double* m_data{};
public:
    DoubleArray(int length)
    {
        assert(length > 0);
        m_data = new double[length]{};
        m_length = length;
    }

    DoubleArray(const DoubleArray&) = delete;
    DoubleArray& operator=(const DoubleArray&) = delete;

    ~DoubleArray()
    {
        delete[] m_data;
    }

    void erase()
    {
        delete[] m_data;
        // We need to make sure we set m_data to 0 here, otherwise it will
        // be left pointing at deallocated memory!
        m_data = nullptr;
        m_length = 0;
    }

    double& operator[](int index)
    {
        assert(index > 0 && index < m_length);
        return m_data[index];
    }

    int getLength() const { return m_length; }

};

#endif

尽管代码清单很长,但你会发现这两个类几乎完全相同!事实上,唯一的实质性区别在于它们包含的数据类型(int 与 double)。正如你可能已经猜到的,这正是模板可以发挥巨大作用的另一个领域,它使我们能够避免创建绑定到特定数据类型的类。

创建模板类与创建模板函数几乎完全相同,所以我们将通过示例来讲解。以下是我们的数组类的模板化版本:

Array.h

#ifndef ARRAY_H
#define ARRAY_H

#include <cassert>

template<typename T>
class Array
{
private:
    int m_length{};
    T* m_data{};

public:
    Array(int length)
    {
        assert(length > 0);
        m_data = new T[length]{};
        m_length = length;
    }

    Array(const Array&) = delete;
    Array& operator=(const Array&) = delete;

    ~Array()
    {
        delete[] m_data;
    }

    T& operator[](int index);

    void erase()
    {
        delete[] m_data;
        m_data = nullptr;
        m_length = 0;
    }

    int getLength() const { return m_length; }

};
template<typename T>
T& Array<T>::operator[](int index)
{
    assert(index >= 0 && index < m_length);
    return m_data[index];
}

#endif      

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

请注意,我们还在operator[]类声明之外定义了该函数。虽然这不是必需的,但新手程序员通常会因为语法问题而在第一次尝试时遇到困难,因此举例说明很有帮助。每个在类声明之外定义的模板成员函数都需要自己的模板声明。另外,请注意模板数组类的名称是 Array<T>,而不是 Array——除非在类内部使用 Array,否则 Array 指的是名为 Array 的类的非模板版本。例如,复制构造函数和复制赋值运算符使用的是 Array 而不是 Array<T>。当在类内部不使用模板参数使用类名时,参数与当前实例化的参数相同。

以下是一个使用上述模板化数组类的简短示例:

main.cpp

#include "Array.h"
#include <iostream>

int main()
{
    const int length = 12;
    Array<int> intArray { length };
    Array<double> doubleArray { length };

    for (int count{ 0 }; count < length; ++count)
    {
        intArray[count] = count;
        doubleArray[count] = count + 0.5;
    }

    for (int count{ length - 1 }; count > 0; --count)
    {
        std::cout << intArray[count] << '\t' << doubleArray[count] << '\n';
    }

    return 0;
}

此示例会输出以下内容
img
模板类的实例化方式与模板函数相同——编译器会根据需要生成一个副本,并将模板参数替换为用户所需的实际数据类型,然后编译该副本。如果您从未使用过模板类,编译器甚至不会编译它。

模板类非常适合实现容器类,因为容器需要能够处理各种数据类型,而模板类可以让你无需编写重复代码就能做到这一点。尽管模板类的语法略显繁琐,错误信息也可能晦涩难懂,但它无疑是 C++ 最优秀、最实用的特性之一。

拆分模板类

模板不是类或函数,而是用于创建类或函数的模板。因此,它的工作方式与普通的函数或类并不完全相同。大多数情况下,这不会造成什么问题。然而,有一个方面常常会给开发人员带来麻烦。

对于非模板类,通常的做法是将类定义放在头文件中,将成员函数定义放在同名的代码文件中。这样,成员函数定义就会被编译成一个单独的项目文件。但是,对于模板类,这种方法行不通。请看以下示例:

Array.h:

#ifndef ARRAY_H
#define ARRAY_H

#include <cassert>

template <typename T> // added
class Array
{
private:
    int m_length{};
    T* m_data{}; // changed type to T

public:

    Array(int length)
    {
        assert(length > 0);
        m_data = new T[length]{}; // allocated an array of objects of type T
        m_length = length;
    }

    Array(const Array&) = delete;
    Array& operator=(const Array&) = delete;

    ~Array()
    {
        delete[] m_data;
    }

    void erase()
    {
        delete[] m_data;
        // We need to make sure we set m_data to 0 here, otherwise it will
        // be left pointing at deallocated memory!
        m_data = nullptr;
        m_length = 0;
    }

    // templated operator[] function defined below
    T& operator[](int index); // now returns a T&

    int getLength() const { return m_length; }
};

// Definition of Array<T>::operator[] moved into Array.cpp below

#endif

Array.cpp:

#include "Array.h"

// member functions defined outside the class need their own template declaration
template <typename T>
T& Array<T>::operator[](int index) // now returns a T&
{
    assert(index >= 0 && index < m_length);
    return m_data[index];
}

main.cpp

#include <iostream>
#include "Array.h"

int main()
{
	const int length { 12 };
	Array<int> intArray { length };
	Array<double> doubleArray { length };

	for (int count{ 0 }; count < length; ++count)
	{
		intArray[count] = count;
		doubleArray[count] = count + 0.5;
	}

	for (int count{ length - 1 }; count >= 0; --count)
		std::cout << intArray[count] << '\t' << doubleArray[count] << '\n';

	return 0;
}

上述程序可以编译,但会导致链接器错误:
img

与函数模板类似,编译器只有在类模板在翻译单元中被使用(例如作为 intArray 等对象的类型)时才会实例化类模板。为了执行实例化,编译器必须同时看到完整的类模板定义(而不仅仅是声明)以及所需的特定模板类型。

还要记住,C++ 是逐个编译文件的。编译 main.cpp 时,Array.h 头文件的内容(包括模板类定义)会被复制到 main.cpp 中。当编译器发现我们需要两个模板实例 Array 和 Array 时,它会实例化这些实例,并将它们作为 main.cpp 翻译单元的一部分进行编译。由于operator[]成员函数有声明,编译器会接受对其的调用,并假定它在其他地方已经定义。

当单独编译 Array.cpp 时,Array.h 头文件的内容会被复制到 Array.cpp 中,但编译器在 Array.cpp 中找不到任何需要Array::operator[]实例化 Array 类模板或函数模板的代码——因此它不会实例化任何东西。

因此,当程序链接时,我们会得到一个链接器错误,因为 main.cpp 调用了某个Array::operator[]模板函数,但该模板函数从未被实例化!

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

最简单的方法是将所有模板类代码直接放在头文件中(在本例中,将 Array.cpp 的内容放在 Array.h 中,位于类定义下方)。这样,当你使用 #include 头文件时,所有模板代码都会集中在一个地方。这种方法的优点是简单易行。缺点是,如果模板类在多个文件中使用,最终会生成许多模板类的本地实例,这可能会增加编译和链接时间(链接器应该会移除重复的定义,因此不会使可执行文件体积过大)。除非编译或链接时间过长成为问题,否则我们推荐使用这种方法。

如果您觉得将 Array.cpp 代码放入 Array.h 头文件中会导致头文件过长或过于混乱,另一种方法是将 Array.cpp 的内容移动到一个名为 Array.inl 的新文件中(.inl 代表内联),然后在 Array.h 头文件的末尾(头文件保护符内)包含 Array.inl。这样做与将所有代码都放在头文件中效果相同,但有助于保持代码的整洁有序。

提示

如果您使用 .inl 方法后遇到编译器关于重复定义的错误,很可能是因为您的编译器将 .inl 文件当作代码文件进行编译。这会导致 .inl 文件的内容被编译两次:一次是编译器编译 .inl 文件本身,另一次是编译包含 .inl 文件的 .cpp 文件。如果 .inl 文件包含任何非内联函数(或变量),则会违反“只能定义一个变量”的规则。如果发生这种情况,您需要将 .inl 文件从编译过程中排除。

通常可以通过在项目视图中右键单击 .inl 文件,然后选择“属性”来将 .inl 文件从生成过程中排除。相关设置应该就在那里。在 Visual Studio 中,将“从生成中排除”设置为“是”。在 Code::Blocks 中,取消选中“编译文件”和“链接文件”。
img

其他解决方案涉及 #include .cpp 文件,但我们不建议这样做,因为 #include 的用法不符合标准。

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

templates.cpp:

// Ensure the full Array template definition can be seen
#include "Array.h"
#include "Array.cpp" // we're breaking best practices here, but only in this one place

// #include other .h and .cpp template definitions you need here

template class Array<int>; // Explicitly instantiate template Array<int>
template class Array<double>; // Explicitly instantiate template Array<double>

// instantiate other templates here

“template class”命令会使编译器显式实例化模板类。在上述示例中,编译器会在 templates.cpp 文件中生成 Array 和 Array 的定义。其他需要使用这些类型的代码文件可以包含 Array.h 头文件(以满足编译器的要求),链接器会将 template.cpp 中的这些显式类型定义链接进去。

这种方法可能更高效(取决于编译器和链接器如何处理模板和重复定义),但需要为每个程序维护 templates.cpp 文件。

在这里插入图片描述

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值