新手入门 C++ 泛型编程:函数模板与类模板初识

一、泛型编程

泛型编程是一种编程范式,它允许我们编写不依赖于特定类型(而是适用于多种数据类型)的代码。这种技术在 C++ 中主要通过模板(Templates)实现,使得算法和数据结构可以应用于不同类型的变量,提高了代码的复用性和灵活性。

// 泛型编程示例:一个简单的通用交换函数
template<typename T>
void swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    swap(x, y); // 这里T被实例化为int类型
    std::cout << "x: " << x << ", y: " << y << std::endl; // 输出"y: 10, x: 20"

    double a = 3.14, b = 2.71;
    swap(a, b); // 这里T被实例化为double类型
    std::cout << "a: " << a << ", b: " << b << std::endl; // 输出"b: 3.14, a: 2.71"
    return 0;
}

二、函数模板

1. 函数模板概念

函数模版是泛型编程中的核心组件之一。它允许我们声明一个函数,该函数能够处理不同类型的数据,而无需重复编写针对每种类型的独立函数。

// 函数模版示例定义
template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

上述 max 函数模版可以接受任何类型 T 的两个参数,并返回它们中的较大者。

2. 函数模板格式

函数模版的格式通常包含关键字 template 后面跟一对尖括号,里面是模板参数列表,然后是正常的函数声明。

template <typename T>
return_type function_name(T param1, T param2, ...);

3. 函数模板原理 

函数模板是一个蓝图,本身并非函数,而是编译器按使用方式生成特定具体类型函数的模具。

编译器在遇到函数模版调用时,会根据传入的实际参数类型自动创建相应的函数实例,这个过程称为“模板实例化”。

4. 函数模板实例化

当函数模版被实际类型参数调用时,编译器会生成相应的特定版本。

模板参数实例化分为隐式实例化显式实例化

(1)隐式实例化:让编译器根据实参推演模板参数的实际类型

隐式实例化是指编译器在遇到使用模板的地方,并且可以根据上下文确定模板参数的具体类型时,自动地为该类型生成模板实例的过程。例如:

// 定义一个函数模板
template<typename T>
T Add(T a, T b) {
    return a + b;
}

int main() {
    int sum = Add(5, 10); // 当调用Add函数并传递int类型参数时
                          // 编译器隐式地实例化出 Add<int>(int, int)
    double product = Add(3.14, 2.71); // 同样,这里实例化出 Add<double>(double, double)
    return 0;
}

 在这个例子中,当我们在 main 函数中调用 max 函数时,编译器能够从传入的参数类型推断出模板参数 T 的类型,因此会隐式地生成对应的函数实例。

(2)显式实例化:在函数名后的 < > 中指定模板参数的实际类型

 显示实例化则要求程序员明确指出要为哪些特定类型生成模板实例。这通常在头文件中声明模板而在源文件中显示实例化,避免在每个使用模板的文件中都实例化同一模板的所有可能类型,减少编译时间和链接时间。

int main(){
    int a = 5;
    double b = 5.0;
    // 显式实例化
    Add<int>(a,b);
    return 0;
}

5. 模板参数的匹配原则

1. 非模板函数优先匹配 在函数调用时,如果存在与实参类型完全一致的非模板函数,编译器将优先选择匹配非模板函数,而非考虑函数模板。这是因为非模板函数在类型匹配上更为明确和直接。

2. 函数模板参数类型匹配 若不存在匹配的非模板函数,编译器将开始尝试匹配函数模板。此时,编译器基于实参类型来推断模板参数,寻找能成功实例化的函数模板版本。如果存在多个函数模板都能够通过类型转换与实参相匹配,那么:

(1)最佳匹配原则

编译器会选择转换级别最低(即最自然或无转换)的函数模板进行实例化。

(2)引用折叠规则

对于引用类型的模板参数,编译器会正确处理引用传递的问题,例如传入左值引用时维持引用性质,右值引用可能会折叠。

3. 类型转换后的匹配 在缺乏完美匹配的情况下,如果某个函数模板经过一次合理的类型转换后可以与实参类型匹配,编译器也可能选择这样的模板进行实例化。但是要注意,编译器倾向于选择类型转换最少或代价最小的模板实例。

三、类模板

1. 类模板格式

类模板用于定义可适应多种数据类型的类。其基本格式类似于函数模版,但作用于整个类。

template <typename T>
class ClassName {
public:
    T data;
    ClassName(T value) : data(value) {}
    // ...
};

2. 类模板实例化

创建类模板的实例需要明确指定模板参数。

int main() {
    // 类模板实例化
    MyClass<int> myIntObj(10);
    MyClass<double> myDoubleObj(3.14);

    // 现在myIntObj.data是一个int,myDoubleObj.data是一个double
}

四、不建议将模板的声明和定义分离到头文件和源文件中 

  1. 模板实例化时机: 模板并非具体的函数或类,而是一套产生具体函数或类的蓝图。只有在模板被实例化时(即模板参数被具体类型替换时),编译器才会生成实际的函数或类代码。这意味着模板的实例化工作是在调用模板的地方完成的,而不是在模板本身被定义的地方。

  2. 编译单位独立性失效: 若模板的声明和定义分开在不同的文件中,当编译单元(通常是 .cpp 文件)包含了模板的声明但不包含其定义时,编译器虽然可以解析模板的声明,但并不会生成任何实例代码。当另一个编译单元尝试使用这个模板时,编译器将依据实参类型生成对应的实例,但由于模板定义不在该编译单元内,编译器就无法找到模板实现,从而无法生成所需的函数或类实例。

  3. 链接阶段问题: 即使编译器能在使用模板的文件中发现模板声明,但在链接阶段,由于模板定义并未被编译成目标代码,链接器将找不到相应模板实例的实现,导致链接错误(通常表现为“未定义的引用”错误)。

  4. 模板实例唯一性: C++ 标准规定了一个模板在同一个翻译单元内(即一个源文件加上所有包含的头文件形成的预处理文本)只能实例化一次。即便多个源文件包含了相同的模板定义,只要它们都在各自的翻译单元内,就不会违反此规则。但如果将模板定义移到单独的 .cpp 文件中,将会导致模板在每一个包含模板使用(从而触发实例化)的源文件的翻译单元内实例化,造成重复实例化的问题。

为了避免这些问题,通常推荐的做法是将模板的声明和定义都放在头文件中,确保编译器在需要实例化模板时可以直接看到完整的模板定义,从而顺利地生成和链接所需要的函数或类实例。这种方式也被称为“即时实例化”(instantiate-on-demand)。尽管这可能导致头文件包含的内容增多,但却是确保模板正常工作的有效途径。 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值