【C++模板】

范式编程(C++模板)


前言

C++ 模板是支持泛型编程(Generic Programming)的核心工具,它允许我们编写类型无关的代码,从而提高代码的复用性与灵活性。在 C++11 之前,模板已经发展出函数模板、类模板、模板特化、非类型模板参数等一整套机制,使得程序员可以在编译期实现类型选择、递归计算等功能,甚至能在某种程度上模拟函数重载和分支逻辑。


一、 模板是什么?

模板(Template)是 C++ 中的一种编程机制,用于支持泛型编程(Generic Programming)。它允许你编写与类型无关的代码,编译器会在编译期根据传入的具体类型自动生成对应的代码。

二、 模板的定义:

模板是一种代码生成器,让程序员只写一次逻辑,不同类型的数据都能使用

1.引入模板(为什么要用)

代码如下(示例):
假设你要写一个函数,来交换两个值。你可能会写类似这样的代码:

void SwapInt(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

void SwapDouble(double& a, double& b) {
    double temp = a;
    a = b;
    b = temp;
}

你会发现:代码重复。这时,模板就派上用场了,允许你写一次代码适用于任何类型

template<typename T>
void Swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

2.模板的基本语法:

(1)函数模板:

template<typename T>  // 或 template<class T>
void Function(T param) {
    // 函数体
}

template:告诉编译器这是一个模板,T 是模板参数,表示类型。
T param:在函数体内,T 会被替换为实际的类型

1.1函数模板的基本使用与类型推导

template<typename T>
void Swap(T& x, T& y)
{
    T tmp = x;
    x = y;
    y = tmp;
}

测试用例:

int a = 0, b = 1;
Swap(a, b);         // 推导出 T=int

double c = 1.1, d = 2.2;
Swap(c, d);         // 推导出 T=double

Swap 是一个典型的泛型函数,模板参数 T 可被调用时的实参自动推导
编译器根据参数类型推导出 T 的具体类型,并生成对应版本的函数
模板函数在编译期实例化,即编译器根据实参生成实际代码(如 Swap < int >、Swap< double >)。
优点是避免代码重复,缺点是对编译器要求较高,错误信息往往难以读懂。


1.2多个模板参数 + 返回值为模板参数

template<typename T1, typename T2>
T1 Func(const T1& x, const T2& y)
{
    std::cout << x << " " << y << std::endl;
    return x;
}

测试用例:

Func(1, 2);       // T1=int, T2=int
Func(1, 2.2);     // T1=int, T2=double

Func 支持两个不同类型参数,返回值使用第一个类型 T1。
体现了模板的“多参数”能力。
模板支持多个类型参数,通过 <T1, T2> 传入。
函数返回值可以是某一个模板参数,增强了灵活性。


1.3模板运算函数:类型推导与隐式转换

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

测试用例:

int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;

Add(a1, a2);               // 推导为 Add<int>
Add(d1, d2);               // 推导为 Add<double>
Add(a1, (int)d1);          // 显式强转为 int
Add((double)a1, d1);       // 显式强转为 double

模板参数类型必须一致否则无法自动推导
当参数类型不一致时,可通过强制类型转换或显式指定模板参数解决。


1.4显示指定模板参数(显式实例化)

Add<int>(a1, d1);       // 强制当作 int 处理,d1 被截断
Add<double>(a1, d1);    // 强制当作 double 处理,a1 转为 10.0

当推导失败或产生歧义时,可显式指定模板类型
显式实例化可提供更清晰的意图,避免编译器推导错误


1.5普通函数、函数模板、模板特化的优先级

函数调用时存在明确的优先级规则:当多个候选函数可供选择时,编译器会按照“普通函数 > 模板特化 > 泛型模板” 的顺序进行匹配和调用。也就是说,如果一个函数调用能与普通函数完全匹配,则优先调用普通函数;如果没有普通函数匹配但存在一个针对某种类型的模板特化(即显式特化),则调用特化版本;最后,若既没有普通函数匹配,也没有特化版本,才会退而求其次调用通用的函数模板(泛型匹配)。这个优先级设计使得我们可以在使用模板时,通过添加普通函数或特化来“覆盖”默认的模板行为,以实现更精细的控制。

#include <iostream>
using namespace std;

// 普通函数
void Print(int a) {
    cout << "普通函数: int" << endl;
}

// 函数模板
template<typename T>
void Print(T a) {
    cout << "模板函数: T" << endl;
}

// 函数模板特化
template<>
void Print<double>(double a) {
    cout << "模板函数特化: double" << endl;
}

int main() {
    Print(10);      // 调用普通函数(优先级最高)
    Print(3.14);    // 调用模板特化
    Print("hello"); // 调用函数模板(泛型匹配)
    return 0;
}

在这里插入图片描述


1.6 函数重载和函数模板的优先级

函数模板和普通函数之间,也可以形成“重载”,但优先选非模板的重载版本

void Show(int a) {
    cout << "普通函数" << endl;
}

template<typename T>
void Show(T a) {
    cout << "模板函数" << endl;
}

template<typename T>
void Show(T* a) {
    cout << "模板函数(指针)" << endl;
}

int main() {
    int x = 10;
    int* p = &x;

    Show(x); // 普通函数
    Show(p); // 模板函数(指针)
    Show(3.14); // 模板函数
}

在这里插入图片描述


1.7带返回类型的函数模板:动态分配示例

=template<class T>
T* Alloc(int n)
{
    return new T[n];
}

测试用例:

double* p1 = Alloc<double>(10);

展示了模板函数返回类型为指针。
此类函数通常只能显式指定类型,编译器无法从返回值推导 T
当模板参数无法从参数中推导时(如只有返回值相关),必须显式指定
常见于分配器、工厂函数等场景。


(2)类模板

template<typename T>  // 或 template<class T>
class MyClass {
public:
    MyClass(T val) : _data(val) {}

    void Print() const {
        std::cout << _data << std::endl;
    }

private:
    T _data;
};

template这一句告诉编译器:我要定义一个类模板,这个类中的类型是泛型 T。
typename 和 class 都可以使用,含义完全相同。
T 是一个类型参数,占位用的,表示将来由用户决定的数据类型。
class MyClass
定义了一个类模板,类名是 MyClass。注意:模板类名与类型名不同具体类型要写成 MyClass< int >、MyClass< double > 等
类内部的所有 T 都是“占位类型”,在你实例化类的时候,会用实际类型替换:

MyClass<int> obj1(10);       // T 被替换为 int
MyClass<std::string> obj2("hi");  // T 被替换为 std::string

2.1函数模板与类模板协同使用

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>v1 = { 1,2,3,4 };
	Print(v1);

}

此时的编译是不会通过的,由于编译不确定Container::const_iterator是类型还是对象

在这里插入图片描述

使用Auto ,或者使用ypename就是明确告诉编译器这里是类型,等模板实例化再去找。

template<class Container>
void Print(const Container& v)
{
	// 编译不确定Container::const_iterator是类型还是对象
	// typename就是明确告诉编译器这里是类型,等模板实例化再去找
	// typename Container::const_iterator it = v.begin();
	auto it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

}

int main() {
	vector<int>v1 = { 1,2,3,4 };
	Print(v1);

}

2.2类模板特化(全特化)

指的是 所有模板参数都被指定类型专门为该组参数写一个新版本

template<typename T1, typename T2>
class Data {
public:
    void Print() {
        cout << "General template" << endl;
    }
};

// 全特化:只针对 <int, double>
template<>
class Data<int, double> {
public:
    void Print() {
        cout << "Specialized for <int, double>" << endl;
    }
};

调用示例:

Data<int, int> d1;       // 一般模板
Data<int, double> d2;    // 使用全特化

2.3类模板特化(偏特化)

特化部分模板参数,比如固定第二个参数为 double,第一个参数任意:

template<typename T1, typename T2>
class Data {
public:
    void Print() {
        cout << "General template" << endl;
    }
};

// 偏特化:第二个类型参数是 double
template<typename T1>
class Data<T1, double> {
public:
    void Print() {
        cout << "Partial specialization: T1, double" << endl;
    }
};

也支持对指针、引用类型进行偏特化:

// 针对两个都是指针的情况
template<typename T1, typename T2>
class Data<T1*, T2*> {
public:
    void Print() {
        cout << "Partial specialization for pointer types" << endl;
    }
};


2.4类模板调用优先级:谁会被选中?

编译器在模板匹配时会:
优先选择能完全匹配的全特化
如果没有全特化,再看有没有能部分匹配的偏特化
都没有,就使用主模板
你可以理解为“匹配越精准,优先级越高”

#include <iostream>
using namespace std;

// 1. 主模板
template<typename T1, typename T2>
class Data {
public:
    void Print() {
        cout << "Primary Template" << endl;
    }
};

// 2. 偏特化:第二个参数是 double
template<typename T1>
class Data<T1, double> {
public:
    void Print() {
        cout << "Partial Specialization <T1, double>" << endl;
    }
};

// 3. 全特化:<int, double>
template<>
class Data<int, double> {
public:
    void Print() {
        cout << "Full Specialization <int, double>" << endl;
    }
};

int main() {
    Data<char, char> d1;       // 主模板
    Data<int, double> d2;      // 全特化
    Data<float, double> d3;    // 偏特化

    d1.Print(); // Primary Template
    d2.Print(); // Full Specialization <int, double>
    d3.Print(); // Partial Specialization <T1, double>
    return 0;
}

在这里插入图片描述

2.5使用整型常量作为非类型模板参数

常量表达式:非类型模板参数的值必须在编译时已知,这意味着它们必须是常量表达式(例如 const int 或 constexpr),也就是说,传进去的值必须在编译期间就能确定。
支持类型限制:非类型模板参数的类型通常是有限制的,不能随意使用任何类型(例如,不能直接传递浮点数)。
常用于数组大小:非类型模板参数最常见的用途是用于指定数组的大小,或者作为常量用于算法的参数

#include <iostream>
using namespace std;

template<typename T, int Size> // 非类型模板参数 Size
void printArray(T(&arr)[Size]) {
    for (int i = 0; i < Size; ++i) {
        cout << arr[i] << " ";
    }
    cout << endl;
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printArray(arr); // N 被推导为 5
    return 0;
}

三、申明实现分离

在普通类中,我们可以把声明写在 .h 文件里,把实现写在 .cpp 文件中。
但是模板类不行

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

模板不是提前编译好的代码,而是在实例化点生成代码。
如果模板实现写在 .cpp 文件中其他使用它的 .cpp 文件在编译时看不到它的完整实现,就无法实例化出所需代码,最终导致链接错误。
因此,模板类和函数的定义与实现必须放在 .h 文件中,或者通过 #include 方式包含在头文件里。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值