<C++> 模板-下

目录

前言

一、非类型模板参数

二、类模板的特化

1. 概念

2. 函数模板特化

3. 类模板特化

4. 全特化

5. 偏特化

5.1 特化部分参数

5.2 对某些类型的进一步限制

三、模板的分离编译

1. 概念

2. 分离编译

3. 解决方法

1. 显式实例化 

2.  在一个文件内写声明和定义

四、模板总结

1. 优点

2. 缺点

总结


前言

        一般情况下template<class / typename T> 中 classtypename 没有什么区别,但是有一些特殊情况,typename 与 class 是有区别的

        例如:我们泛型了Print函数,实现可以打印任意容器内容的功能

template<class Container>
void Print(const Container& v)
{
	Container::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	Print(v);

	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	Print(lt);
	return 0;
}

        但是编译不通过,编译器要求必须在 Container::const_iterator 之前加上 typename 作为前缀,即

	typename Container::const_iterator it = v.begin();

问题:为什么需要加上typename呢?

  • 这是因为编译器无法分辨 Container::const_iterator 究竟是什么,因为此时Container还没有实例化,所以 Container::const_iterator 它可能表示的是类的静态成员变量,也可能是类的内嵌类型(内部类或typedef的类型)
class A
{
public:
	int begin()
	{
		return 0;
	}
	static int const_iterator;
};

int main()
{

	A aa;
	A::const_iterator it = aa.begin();
    编译器报错,因为A::const_iteratot本身是变量,不是类型
    所以,如果没有typename,编译器是不知道它究竟是类型还是变量

	return 0;
}
  • Container可能是类型或对象,若为类型,语法可以通过;若为对象,语法错误

解决:

        在 Container::const_iterator 前加上typename,以此来告诉编译器后面的Container::const_iterator是一个类型,后面的行为是合乎语法的,等Container实例化之后,在确定后面是什么类型

总结:

  • Container是一个没有实例化的模板,那么都需要在Container::iterator 之前加上typename,即使是vector<T>类型
  • 只要我们使用没有实例化的类模板时,都需要加上typename声明,例如priority_queue模板参数less在使用未实例化的模板时就加上了typename

这里我们也可以使用auto关键字,

auto it = v.begin();

就不需要typename声明 。在Container之后 auto 自动推导类型


一、非类型模板参数

        有时候,我们可能需要非类型的模板参数,来实现所需的类

例如:当我们实现静态的栈时,需要提前知道数组需要开辟多大的空间,但是如果都统一的开同样大的空间,难免会遇到空间冗余或太小等情况。所以,当我们在实例化对象时,如果可以指定数组空间大小,那么就可以解决上面的问题,此时C++11就升级了模板,增加了非类型模板参数,以实现将一个常量作为模板参数

template<class T, size_t N>
class Stack
{

private:
	T _a[N];
	int _top;
};

int main()
{
	Stack<int, 10> st1;
	Stack<int, 100> st2;

	return 0;
}

对于非模板类型参数

        1. 只能为整型(char也可以),但例如double、string等类型都不可以

        2. 是常量        

因为非模板类型参数大部分都是用在T _a[N]情况,所以最初设计的是整型

在std中还有一个容器array,它就使用了size_t非模板类型参数

array<int, 10> a;

        它对比C语言的数组几乎没有任何优势,这就显得委员会更新的很鸡肋,array的优点几乎只有对越界情况检查,越界读写都能检查,而普通数组不能检查越界读,少部分越界写可以检查,但如果仅此优点,vector完全可以替代。

二、类模板的特化

1. 概念

        在类名或函数名后用<>指定特化的参数,特化后的类的成员变量根据自己的需求编写,因为它与原模版已经成为了不同的类,成为了分支。一般特化的类都是很小的类

        顾名思义,对模板类型进行特殊化处理,如果调用时,实参类型与特化的模板参数类型匹配,则优先调用特化的模板函数或类。

        没有特化也可以,但是有特化会更方便

        通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果。

2. 函数模板特化

template<class T>
bool Less(T left, T right)
{
	return left < right;
}

int main()
{
	cout << Less(1, 2) << endl;

	int a = 1, b = 2;
	cout << Less(&a, &b) << endl;
	return 0;
}

        当我们实参传递地址时,比较的是地址的大小,这与我们的意愿相反,这时我们可以使用特化这一功能,针对 int* 类型特化。在调用时,有现成的函数的就用现成的;没现成的用模板实例化合适的函数:

template<class T>
bool Less(T left, T right)
{
	return left < right;
}

template<>
bool Less<int*>(int* left, int* right)
{
	cout << "bool Less<int*>(int* left, int* right)" << endl;
	return *left < *right;
}

        但是,如果只是为了比较int型指针指向的值的大小,我们可以不使用模板,直接重载函数即可


bool Less(int* left, int* right)
{
	cout << "bool Less(int* left, int* right)" << endl;

	return *left < *right;
}

        如果是多类型指针,那么还是需要使用模板

template<class T>
bool Less(T* left, T* right)
{
	cout << "bool Less(T* left, T* right)" << endl;
	return *left < *right;
}

3. 类模板特化

        上面是针对函数模板举例,对于函数模板的特化,可以使用重载替代,但是对于类模板的特化就不同了

template<class T1, class T2>
class Data
{
public:
	Data()
	{
		cout << "Data<T1, T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};

template<>
class Data<int, double>
{
public:
	Data()
	{
		cout << "Data<int, double>" << endl;
	}
};

        又例如priority_queue中Less仿函数的特化,因为在Less仿函数编写时,已经讲解了如果实参是Date*类型,那么每次比较的是指针的大小,这是错误的,所以我们可以特化类型Date*

template<class T>
class Less
{
public:
    bool operator()(const T& x, const T& y)
    {
        return x < y;
    }
};
template<>
class Less<Date*>
{
public:
    这里去掉&符号,因为const修饰不到&,会优先修饰*
    bool operator()(const Date* x, const Date* y)
    {
        return *x < *y;
    }
};

        特化后,就可以在实例化时只传一个参数,编译器用第一个模板参数去匹配,若与特化版本类型相同,则直接使用特化版本

priority_queue<Date*> pq;

        还可以改为模板T*,适配所有指针类型

template<class T1, class T2>
class Less<T1*, T2*>
{
public:
    bool operator()(const T* x, const T* y)
    {
        return *x < *y;
    }
};

4. 全特化

如果将全部的模板类型都特化即为全特化,全特化后template<>内将没有内容

template<>
class Data<int, double>
{
public:
	Data()
	{
		cout << "Data<int, double>" << endl;
	}
private:
    //T1 _d1;
    //T2 _d2;
};

        这样的特化,会在实参类型为int、double时被调用

5. 偏特化

5.1 特化部分参数

        只将一部分模板类型特化即为偏特化

        适配此类型(T, double)时,优先调用

template<class T1>
class Data<T1, double>
{
public:
	Data()
	{
		cout << "Data<T1, double>" << endl;
	}
private:
	//T1 _d1;
};

若第二个不是double,则会调用T1, T2模板

5.2 对某些类型的进一步限制

        例如:限制参数类型全部都是指针类型时

template<class T1, class T2>
class Data<T1*, T2*>
{
public:
	Data()
	{
		cout << "Data<T1*, T2*>" << endl;
	}
private:
};
template<>
class Data<int, double>
{
public:
	Data()
	{
		cout << "Data<int, double>" << endl;
	}
private:
    //T1 _d1;
    //T2 _d2;
};

        也可以特化引用

template<class T1, class T2>
class Data<T1&, T2&>
{
public:
	Data()
	{
		cout << "Data<T1&, T2&>" << endl;
	}
private:
};

三、模板的分离编译

1. 概念

        一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式
        模板不支持直接的分离编译

2. 分离编译

// a.h
template<class T>
T Add(const T& left, const T& right);

// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
     return left + right;
}

// main.cpp
#include"a.h"
int main()
{
     Add(1, 2);
     Add(1.0, 2.0);
 
     return 0;
}

C/C++程序运行要经历四个阶段

        预处理——>编译——>汇编——>链接

预处理 :替换头文件,将.h文件在cpp文件展开,生成 .i 文件

编译:对程序按照语言特性进行词法、语法、语义分析是否错误,并确认已定义的函数的地址,只有声明的函数没有地址,检查无误后生成 .s 汇编文件。因为有声明,这是一种承诺,函数的定义部分编译器会在链接步骤拿着修饰后的函数名去其他文件符号表寻找,所以编译检查可以通过。

汇编:生成 .o 文件,即二进制文件

链接:将文件链接起来,生成 a.out文件

        程序链接错误,Add找不到函数地址,这就是模板分离编译的坏处 

3. 解决方法

1. 显式实例化 
template<class T>
T Add(const T& left, const T& right)
{
     return left + right;
}

template
class Add<int>;

template
class Add<double>;
  •  这样就失去了模板的意义,对于每一种类型都需要手动显式实例化,所以不推荐这种解决方法
2.  在一个文件内写声明和定义
  • 类内部一般将代码量较短的直接写在类内部,成为内联函数,对于代码量较长的模板函数,可以在类内部声明,在类外部但在同一文件内定义

四、模板总结

1. 优点

  • 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库STL因此而产生
  • 增强了代码的灵活性,如适配器、仿函数

2. 缺点

  • 模板会导致代码膨胀问题,导致编译时间变长
  • 出现模板编译错误时,错误信息很乱,不容易定位错误

总结

        模板还是有很多细节需要掌握的,了解模板知识后再练习大多数问题就可以解决了

        最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

<think>首先,用户的问题是关于C++模板类的。具体问题是:“我想理解C++模板类的定义和为何需要多处使用template<T>声明。请问C++模板类中为什么需要在每个成员函数前重复使用template<T>?” 用户引用了几个参考内容,这些是关于模板类的定义和使用。关键点是: - 引用[1]:展示了模板类可以有成员模板函数,并解释了如何定义和调用它们。 - 引用[2]:说明了在类外定义成员函数时,需要添加模板参数列表。 - 引用[3]:解释了类模板可以有多个类型参数,并且每个类型前必须加class。 现在,用户的核心疑问是:为什么在模板类中,每个成员函数在类外定义时需要重复使用template<T>。 我的回答需要: 1. 使用中文回答。 2. 结构清晰,帮助用户逐步解决问题。 3. 保证回答真实可靠,基于C++标准。 4. 在行内数学表达式中使用$...$格式,但用户的问题是关于C++代码的,所以可能没有数学表达式。不过,如果有变量或类似的东西,我会使用它。 5. 独立公式使用$$...$$,但这里可能不适用。 6. 生成相关问题。 7. 在回答中引用的段落末尾添加引用标识。 用户的问题是关于C++模板的,所以我不需要数学表达式,但如果有代码或变量,我会处理。 关键点:为什么在模板类中,成员函数在类外定义时需要重复template<T>。 原因: -C++中,模板类(template class)是一个蓝图,用于生成特定类型的类。当你在类外定义成员函数时,编译器需要知道这个函数是模板的一部分,所以你必须指定模板参数。 - 语法是:template<typename T> 或 template<class T>,然后定义函数,如 void MyClass<T>::myFunction() { ... }。 - 这是因为成员函数本身依赖于模板参数T,所以它必须在模板上下文中定义。 - 在类内部定义成员函数时,不需要重复template<T>,因为整个类已经在模板作用域内。但在类外部,你需要重新进入模板作用域。 回答结构: 1. 解释模板类的定义:简要介绍模板类是什么。 2. 解释为什么需要重复template<T>:因为成员函数在类外定义时,编译器需要知道它属于模板实例。 3. 使用示例:提供一个简单的代码示例来展示。 4. 引用参考内容:用户提供了引用,所以我应该在相关部分添加引用标识。 5. 生成相关问题:在最后添加相关问题。 确保回答真实可靠:基于C++标准,模板定义规则。 最终,添加相关问题。</think>### C++模板类中成员函数定义为何需要重复使用template<T> 在C++中,模板类(template class)是一种泛型编程机制,允许你定义一个通用的类蓝图,其中类型(如`T`)可以在实例化时指定。例如,定义一个模板类`MyClass`: ```cpp template <typename T> class MyClass { public: void display(); // 成员函数声明 private: T data; }; ``` 当你在类内部定义成员函数时(如内联定义),不需要重复`template<typename T>`,因为整个类已在模板作用域内。但如果在类**外部**定义成员函数,就必须在每个函数前添加`template<typename T>`(或`template<class T>`)。原因如下: #### 为什么需要重复使用`template<T>`? 1. **编译器需要模板上下文**: 当成员函数在类外定义时,编译器不知道这个函数属于哪个模板实例。添加`template<typename T>`表示该函数是模板的一部分,依赖于模板参数`T`。否则,编译器会将其视为普通函数,导致编译错误(如“未定义的引用”或“模板参数缺失”)。 例如,在定义`display`函数时: ```cpp // 正确:添加template<typename T>以进入模板作用域 template <typename T> void MyClass<T>::display() { std::cout << data << std::endl; } ``` 如果不加`template<typename T>`,编译器无法解析`MyClass<T>::`中的`T`,因为`T`是模板参数,不属于全局作用域[^2]。 2. **成员函数本身是模板化的**: 每个模板类的成员函数都隐式地依赖于模板参数。重复`template<typename T>`显式声明了这种依赖关系,确保函数能正确访问类型`T`。例如: ```cpp template <typename T> void MyClass<T>::display() { // 这里可以使用T类型的数据成员 } ``` 如果省略`template<typename T>`,函数定义中的`T`会变成未定义的标识符,导致编译失败[^1]。 3. **避免二义性**: C++允许模板类有普通成员函数和模板成员函数(如引用[1]所示)。重复`template<typename T>`帮助编译器区分: - **普通成员函数**:定义时不加`template`,如`void f(int)`。 - **模板成员函数**:定义时加`template`,如`template<typename T2> void f(T2)`。 这确保了重载解析的正确性[^1]。 #### 完整示例 以下代码演示了模板类的定义和成员函数的类外实现: ```cpp #include <iostream> template <typename T> class Score { public: void Print(); // 声明成员函数 private: T _Math; T _Chinese; }; // 类外定义Print():必须添加template<typename T> template <typename T> void Score<T>::Print() { std::cout << "Math: " << _Math << std::endl; std::cout << "Chinese: " << _Chinese << std::endl; } int main() { Score<int> scoreInt; // 实例化为int类型 scoreInt.Print(); // 输出整数成绩 } ``` - 如果省略`template<typename T>`,编译器会报错:`error: 'T' was not declared in this scope`。 #### 总结 - **类内定义**:不需要重复`template<typename T>`(整个类已在模板作用域)。 - **类外定义**:必须添加`template<typename T>`,以提供模板上下文并解析类型`T`[^2][^3]。 - 根本原因:C++模板机制要求显式声明依赖关系,确保类型安全性和编译正确性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值