C++模板:基础解析

C++模板是C++中非常强大的特性,他允许我们编写通用的代码,从而避免重复编写相似功能的函数或类。

1 模板的基本概念

1.1 模板是什么?

用简单的程序例子来讲一下:
int add(int& a,int& b){ return a+b; }
void add(double& a,double& b){ return a+b; }

通过上面的代码,我们发现,这两个函数实现的功能都是数据交换,唯一的不同点就是数据类型不同,难道说每一次去交换不同数据类型都要重写函数?显然这样很麻烦,那么是否可以写出一个通用的代码呢?可以的,那就是模板。

官方的概念说模板是C++的一种工具,允许我们编写与数据类型无关的代码。通过模板,我们可以定义函数或类,使其能够处理多种数据类型。

既然已经知道了概念,接下来就是基本用法。

1.2 模板的分类

在讲基本用法之前,我们要知道C++模板的类别,C++模板主要分为两类:
函数模板 :用于定义通用的函数
类模板:用于定义通用的类

1.3 函数模板基本语法

cpp 复制代码
template <typename T>
T add(T& a,T& b){
return a+b;
}
  • template <typename T>:声明一个模板,T是一个占位符,表示任意数据类型
  • T add(T& a,T& b):定义一个函数,参数和返回值类型都是T
    例子:
cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    cout << add(3, 5) << endl;       // 输出 8
    cout << add(3.5, 2.5) << endl;   // 输出 6.0
    return 0;
}

1.4 函数模板的注意事项

  • 自动类型推导,必须推导出一致的数据类型T,才能使用
    简单引用例子来说一下:
cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    cout << add(3, 5) << endl;       //正确:T被推导为int
    cout << add(3.5, 2.5) << endl;   //正确:T被推导为double
    cout << add(3.5,2) <<endl;       //错误:T无法同时推导为int 和 double
    return 0;
}

我想在上面的代码中,会有疑问。就是在普通函数中,cout << add(3.5,2) <<endl;是支持隐式类型转换的,而在这里为什么不行。

在模板函数中,类型推导 的规则与普通函数不同。模板参数的类型是根据传入的参数自动推导的,且要求所有参数的类型必须一致。

模板函数的设计初衷是类型安全通用性 。模板参数的类型推导是基于传入参数的实际类型,而不是基于可能的隐式转换。这样做的好处是:

1.避免意外行为 :隐式类型转换可能导致意外的精度损失或行为变化。

2.明确类型 : 模板函数要求调用者明确传入参数的类型,避免歧义。

那么如何解决这个问题?

方法1:显示指定类型

在调用模板函数时,显示指定模板参数的类型。像这样:
add<double>(3.5,2);

方法2:使用多个模板参数

定义模板函数时,使用多个模板参数来支持不同类型的参数。例如:

cpp 复制代码
template <typename T1, typename T2>
T1 add(T1 a, T2 b) {
   return a+b;
}

int main() {
   cout << add(3.5,2) <<endl;  // T1 = double, T2 = int
    return 0;
}

其实,上述代码还是有点问题的。如果说T1是int,T2是double。这样调用时候,函数返回的类型是int,就会引起精度损失。如何解决?请看方法三。

方法3:使用std::common_type

使用std::common_type来推导出两个类型的公共类型。函数模板可以返回统一的类型。

例如:

cpp 复制代码
#include <iostream>
#include <type_traits>  // 包含 std::common_type
using namespace std;

template <typename T1, typename T2>
typename common_type<T1, T2>::type add(T1 a, T2 b) {
    return a + b;
}

int main() {
    auto result1 = add(3, 5.5);    // T1 = int, T2 = double
    auto result2 = add(3.5, 2.5);  // T1 = double, T2 = double
    auto result3 = add(3, 5);      // T1 = int, T2 = int

    cout << "Result 1: " << result1 << endl;  // 输出 8.5
    cout << "Result 2: " << result2 << endl;  // 输出 6.0
    cout << "Result 3: " << result3 << endl;  // 输出 8

    return 0;
}
  • 使用 std::common_type 可以推导出两个类型的公共类型,确保返回值不会引起精度损失。
  • 这种方法适用于任意类型的参数组合(如 int + doubledouble + double int + int 等)。
  • 通过 auto 关键字,可以方便地接收返回值,而无需手动指定类型。

1.5 函数模板案例:数组排序

案例描述:

●利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序

●排序规则从大到小,排序算法为选择排序

●分别利用char数组和int数组进行测试

cpp 复制代码
#include <iostream>
using namespace std;

// 选择排序函数模板
template <typename T>
void selectionSort(T arr[], int size) {
    for (int i = 0; i < size - 1; i++) {
        int maxIndex = i;  // 假设当前最大值的索引是 i
        for (int j = i + 1; j < size; j++) {
            if (arr[j] > arr[maxIndex]) {  // 从大到小排序
                maxIndex = j;  // 更新最大值的索引
            }
        }
        // 交换 arr[i] 和 arr[maxIndex]
        T temp = arr[i];
        arr[i] = arr[maxIndex];
        arr[maxIndex] = temp;
    }
}

// 打印数组的函数模板
template <typename T>
void printArray(T arr[], int size) {
    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
}

int main() {
    // 测试 int 数组
    int intArr[] = { 5, 3, 1, 4, 2 };
    int intSize = sizeof(intArr) / sizeof(intArr[0]);

    cout << "原始int数组: ";
    printArray(intArr, intSize);
    selectionSort(intArr, intSize);
    cout << "排序后的int数组: ";
    printArray(intArr, intSize);
    // 测试 char 数组
    char charArr[] = { 'd', 'a', 'c', 'b', 'e' };
    int charSize = sizeof(charArr) / sizeof(charArr[0]);

    cout << "原始char数组: ";
    printArray(charArr, charSize);
    selectionSort(charArr, charSize);
    cout << "排序后的char数组: ";
    printArray(charArr, charSize);

    return 0;
}

输出结果是:

1.6 普通函数与函数模板的区别

1.普通函数:

(1)只能处理特定的数据类型。 如int add(int a,int b)只能处理int 类型。

(2) 可以发生自动类型转换(隐式类型转换)

2.函数模板:

(1)可以处理多种数据类型。例如,template<typename T>T add(T a,T b)可以处理int、double等类型。

(2)不显示指定类型,不会发生隐式转换。如add< double >(3.5,2);就可以发生隐式转换。

1.7 普通函数与函数模板的调用规则

1.如果函数模板和普通函数都匹配,优先调用普通函数

示例:

cpp 复制代码
#include <iostream>
using namespace std;

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

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

int main() {
    func(10); // 调用普通函数,因为完全匹配
    func(3.14); // 调用模板,因为没有匹配的普通函数
    return 0;
}

输出

2.如果普通函数和模板都能匹配,但普通函数需要类型转换,模板会被优先选择

如果普通函数匹配需要隐式类型转换,而函数模板可以直接匹配,那么编译器会优先选择函数模板。

示例

cpp 复制代码
#include <iostream>
using namespace std;

void func(double a) { // 普通函数
    cout << "普通函数调用: " << a << endl;
}

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

int main() {
    func(10); // 函数模板调用(普通函数需要 int -> double 的转换)
    return 0;
}

输出

3.使用显式模板参数(或空模板参数)时,强制调用函数模板

示例

cpp 复制代码
#include <iostream>
using namespace std;

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

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

int main() {
    func<int>(10); // 强制调用函数模板
    func<>(10);
    return 0;
}

输出

4.函数模板可以重载

如果存在多个模板匹配,编译器会选择最匹配的模板实例。

示例

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
void func(T a) {
    cout << "函数模板 1 调用: " << a << endl;
}

template <typename T, typename U>
void func(T a, U b) {
    cout << "函数模板 2 调用: " << a << " 和 " << b << endl;
}

int main() {
    func(10);      // 调用单参数模板
    func(10, 3.14); // 调用双参数模板
    return 0;
}

输出

5.函数模板可以被特化

可以专门为某个类型定义特化版本,编译器会优先选择特化版本。

示例

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
void func(T a) {
    cout << "通用模板: " << a << endl;
}

// 针对 int 类型的特化版本
template <>
void func<int>(int a) {
    cout << "int 类型特化: " << a << endl;
}

int main() {
    func(10);    // 调用 int 类型特化版本
    func(3.14);  // 调用通用模板
    return 0;
}

输出

1.8 类模板基本语法

C++的类模板允许我们编写泛型类,可以适用于多种数据类型,提供代码的复用性和灵活性。类模板的本质是:在编译时生成特定类型的类。

1.类模板的基本定义

语法格式

cpp 复制代码
template <typename T>
class 类名 {
    // 成员变量和成员函数
};

template< typename T>:声明一个模板类型参数T,T可以是任何数据类型。typename可以替换为class。

T在类中用作占位符,等真正使用类模板时,才会替换成具体的数据类型。

2.类模板示例

定义一个通用的数组类

cpp 复制代码
#include <iostream>
using namespace std;

// 定义一个类模板
template <typename T>
class MyArray {
private:
    T data[5];  // 静态数组
public:
    void set(int index, T value) {
        data[index] = value;
    }

    T get(int index) {
        return data[index];
    }
};

int main() {
    MyArray<int> intArray;   // 实例化为 int 类型
    intArray.set(0, 100);
    cout << "intArray[0]: " << intArray.get(0) << endl;

    MyArray<double> doubleArray;  // 实例化为 double 类型
    doubleArray.set(0, 3.14);
    cout << "doubleArray[0]: " << doubleArray.get(0) << endl;

    return 0;
}
 

输出

1.9 类模板的成员函数的定义

类模板的成员函数可以在类内定义,也可以在类外定义。
在类内定义(内联)

cpp 复制代码
template <typename T>
class Box {
public:
    T value;
    void set(T val) { value = val; }
    T get() { return value; }
};

在类外定义

cpp 复制代码
template <typename T>
class Box {
private:
    T value;
public:
    void set(T val);   // 函数声明
    T get();
};

// 在类外定义成员函数(注意模板语法)
template <typename T>
void Box<T>::set(T val) {
    value = val;
}

template <typename T>
T Box<T>::get() {
    return value;
}

int main() {
    Box<int> intBox;
    intBox.set(42);
    cout << "intBox: " << intBox.get() << endl;
    return 0;
}

必须加template<typename T>
Box<T>::作用域修饰符不可省略

1.10 类模板成员函数的创建时机

在C++中,类模板的成员函数并不会立即被创建(实例化),而是在需要时才会生成。这个机制被称为延时实例化。

1.什么时候类模板成员函数会被创建?

类模板的成员函数只有在"被调用"或"被使用"时才会被编译器真正实例化。

示例

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
class MyClass {
public:
    void func1() { cout << "调用 func1()" << endl; }
    void func2();  // 只是声明,并不会立即创建
};

template <typename T>
void MyClass<T>::func2() { cout << "调用 func2()" << endl; }

int main() {
    MyClass<int> obj;  // 实例化 MyClass<int>,但不生成任何成员函数
    obj.func1();       // 只有调用 func1() 时,func1() 才会被实例化
    return 0;
}

解析
MyClass<int> obj;本身不会导致func1()func2()被实例化。
obj.func1();时,编译器才会生成func1()的代码。
func2()没有被调用,因此func2()并未实例化。

2.只有被调用的成员函数才会实例化。

举个例子,在函数实现过程中写一个错误来检测是否被实例化。

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
class Test {
public:
    void func1() { cout << "func1() 被调用" << endl; }
    void func2() { cout << 10 / 0 << endl; }  // 存在错误(除以0)
};

int main() {
    Test<int> obj;
    obj.func1();  // 只调用 func1()
    return 0;
}

输出

解析:
func2()内部存在10/0的错误,但因为func()没有被调用,所以不会报错。

如果obj.func2();被调用,编译器会实例化它,并检测到错误。

3.只有用到的成员变量才会实例化

类似于成员函数,类模板的成员变量也是按需实例化。

示例

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
class Example {
public:
    int a;       // 会实例化
    double b;    // 不会实例化(因为没有被使用)
};

int main() {
    Example<int> obj;
    obj.a = 10;
    cout << obj.a << endl;  // 只用到了 a,b 不会实例化
    return 0;
}

解析

由于b未被使用,编译器不会实例化b。

4.纯虚函数不会实例化

在类模板中,如果有纯虚函数,那么除非子类重写并调用它,否则不会实例化。

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
class Base {
public:
    virtual void show() = 0;  // 纯虚函数
};

class Derived : public Base<int> {
public:
    void show() override { cout << "派生类实现 show()" << endl; }
};

int main() {
    Derived d;  // 只有 Derived 继承并实现 show(),才会实例化 show()
    d.show();
    return 0;
}

解析

纯虚函数show()不会在Base<int>被实例化时创建。

只有Derived继承并重写show()时,该函数才会实例化。

5.显式实例化成员函数

我们可以手动强制实例化 某个类模板的特定函数。

示例

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
class MyClass {
public:
    void func1() { cout << "func1() 实例化" << endl; }
    void func2() { cout << "func2() 实例化" << endl; }
};

// 显式实例化 func2()
template void MyClass<int>::func2();

int main() {
    return 0;  // 没有调用任何函数
}

解析

由于template void MyClass<int>::func2();,即使func2()没有被调用,它仍然会被实例化。

1.11 类模板与继承

在C++中,类模板可以作为基类,也可以作为派生类。当类模板与继承结合时,需要注意一些特殊情况,例如基类依赖参数化类型,以及如何正确使用作用域解析符。

1.让普通类继承类模板

普通类可以继承类模板,并在继承时明确指定基类的模板参数

示例

cpp 复制代码
#include <iostream>
using namespace std;

// 类模板
template <typename T>
class Base {
public:
    T data;
    Base(T val) : data(val) {}
    void show() { cout << "Base data: " << data << endl; }
};

// 普通类继承类模板
class Derived : public Base<int> { // 指定 T = int
public:
    Derived(int val) : Base<int>(val) {}
    void print() { cout << "Derived data: " << data << endl; }
};

int main() {
    Derived d(42);
    d.show();  // 调用基类函数
    d.print(); // 调用派生类函数
    return 0;
}

输出

解析
Derived继承Base<int>,所以Base的T变成了int。
Derived可以直接访问Basedatashow()

2.让类模板继承类模板

当类模板继承另一个类模板时,派生类可以选择:保留泛型(不指定类型参数)指定部分或全部类型参数
示例1:保留泛型

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
class Base {
public:
    T data;
    Base(T val) : data(val) {}
    void show() { cout << "Base data: " << data << endl; }
};

// 派生类也是模板类,T 由用户决定
template <typename T>
class Derived : public Base<T> {
public:
    Derived(T val) : Base<T>(val) {}
    void print() { cout << "Derived data: " << this->data << endl; }
};

int main() {
    Derived<double> d(3.14);
    d.show();
    d.print();
    return 0;
}

输出

解析
Derived<T>继承Base<T> ,派生类仍然是模板类。
this->data用于访问基类成员,避免编译器报错。

3.让类模板继承具体类型的类模板

如果派生类不想继续保留泛型,它可以直接指定基类的模板参数

示例

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
class Base {
public:
    T data;
    Base(T val) : data(val) {}
    void show() { cout << "Base data: " << data << endl; }
};

// 派生类固定 T 为 int
class Derived : public Base<int> {
public:
    Derived(int val) : Base<int>(val) {}
    void print() { cout << "Derived data: " << data << endl; }
};

int main() {
    Derived d(100);
    d.show();
    d.print();
    return 0;
}

输出

4.多重继承与类模板

类模板可以通过多重继承继承多个类模板或普通类。

示例

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
class Base1 {
public:
    T data1;
    Base1(T val) : data1(val) {}
    void show1() { cout << "Base1 data: " << data1 << endl; }
};

template <typename T>
class Base2 {
public:
    T data2;
    Base2(T val) : data2(val) {}
    void show2() { cout << "Base2 data: " << data2 << endl; }
};

// 派生类同时继承两个类模板
template <typename T>
class Derived : public Base1<T>, public Base2<T> {
public:
    Derived(T val1, T val2) : Base1<T>(val1), Base2<T>(val2) {}
    void print() {
        cout << "Derived data1: " << this->data1 << ", data2: " << this->data2 << endl;
    }
};

int main() {
    Derived<double> d(3.14, 2.71);
    d.show1();
    d.show2();
    d.print();
    return 0;
}

输出

5.注意事项

(1)访问基类成员时需要this->Base<T>::

由于C++编译器在编译模板时不会立即知道基类的成员变量,所以访问基类成员时可能会报错: 没有找到接收"overloaded-function"类型的右操作运算符(或没有可接收的转换)。

错误示例

cpp 复制代码
template <typename T>
class Base {
public:
    T data;
};

template <typename T>
class Derived : public Base<T> {
public:
    void print() {
        cout << data << endl;  // ❌ 编译错误
    }
};

正确示例

cpp 复制代码
 template <typename T>
class Derived : public Base<T> {
public:
    void print() {
        cout << this->data << endl;  // 正确
        // cout << Base<T>::data << endl;  //另一种正确方式
    }
};
(2)构造函数初始化基类

在派生类的构造函数中,基类的构造函数需要显式调用,否则编译器不知道如何初始化基类成员。

错误示例

cpp 复制代码
template <typename T>
class Base {
public:
    T data;
    Base(T val) : data(val) {}  // 需要初始化
};

template <typename T>
class Derived : public Base<T> {
public:
    // ❌ 错误,没有显式调用 Base<T> 构造函数
    Derived(T val) {}
};

正确示例

cpp 复制代码
template <typename T>
class Derived : public Base<T> {
public:
    Derived(T val) : Base<T>(val) {}  //正确
};

1.12 类模板与函数模板的区别

类模板与函数模板的区别主要体现在定义方式、使用方式、实例化时机以及适用场景等方面。

1.定义方式

定义通用函数 ,可以接受不同类型的参数,不需要重复写多个函数。

特点:编译时自动推导类型;适用于操作简单的逻辑(如加法、排序等)。
定义通用的类 ,可以存储和操作不同类型的数据。

特点:封装数据和行为(不像函数模板只是一个简单函数);实例化时需要指定类型;适用于数据结构和类的设计。

2.作用范围

对比项 函数模板 类模板
作用 定义通用函数 定义通用类
适用场景 适用于逻辑简单的算法(如加法、排序等) 适用于封装数据和操作(如容器类)
实例化时机 编译时自动推导(可以手动指定) 必须手动指定类型
代码复用性 代码复用程度较低 代码复用程度高,适合面向对象设计

3.成员函数创建时机

函数模板:只有在被调用时才会实例化。

类模板:只有当类被实例化时才会生成代码。

4.继承方式

函数模板不能继承,但类模板可以继承。

5.什么时候用函数模板?什么时候用类模板?

用函数模板:当需要单个函数的泛型处理。

用类模板:当需要封装数据和行为。

1.13 类模板的分文件编写

类模板的分文件编写(即**.h+**.cpp)。由于模板的特殊性,普通的分文件方式不适用于类模板,而是需要特殊的处理方法。

1.为什么类模板不能像普通类一样分为.h和.cpp文件?

C++编译器在编译.cpp文件时,并不会提前实例化模板,而是只有在使用类模板时才会生成代码。

如果我们把类模板的定义放在.h头文件,而实现放在.cpp文件,编译器在编译.cpp时并不会知道具体的模板参数,因此不会生成代码,导致链接错误(未定义引用)。

错误示例

cpp 复制代码
// MyClass.h
#pragma once
template <typename T>
class MyClass {
public:
    MyClass(T val);
    void show();
private:
    T data;
};

// MyClass.cpp
#include "MyClass.h"
#include <iostream>
using namespace std;

template <typename T>
MyClass<T>::MyClass(T val) : data(val) {}

template <typename T>
void MyClass<T>::show() {
    cout << "Data: " << data << endl;
}

// main.cpp
#include "MyClass.h"

int main() {
    MyClass<int> obj(10);
    obj.show();  // ❌ 这里会报链接错误 (undefined reference)
    return 0;
}

报错原因:

MyClass.h里,编译器不知道具体的T是什么,不会提前实例化MyClass<T>的代码。
MyClass.cpp里没有显式实例化MyClass<int>,导致main.cpp里无法找到show()的实现。

2.解决方案

方法1:把实现全部放到.h头文件

最常用的方法是把所有模板代码都写在头文件中。

cpp 复制代码
// MyClass.h
#pragma once
#include <iostream>
using namespace std;

template <typename T>
class MyClass {
public:
    MyClass(T val);
    void show();
private:
    T data;
};

// 直接在头文件实现函数
template <typename T>
MyClass<T>::MyClass(T val) : data(val) {}

template <typename T>
void MyClass<T>::show() {
    cout << "Data: " << data << endl;
}
cpp 复制代码
// main.cpp
#include "MyClass.h"

int main() {
    MyClass<int> obj(10);
    obj.show();  //正确输出 "Data: 10"
    return 0;
}
方法2:使用.h+.hpp方式

如果想要分离模板的声明和实现,可以创建一个.hpp文件,专门存放模板的实现代码。

cpp 复制代码
// MyClass.h (头文件: 声明)
#pragma once
template <typename T>
class MyClass {
public:
    MyClass(T val);
    void show();
private:
    T data;
};

// 让编译器包含实现文件
#include "MyClass.hpp"
cpp 复制代码
// MyClass.hpp (实现文件)
#include <iostream>
using namespace std;

template <typename T>
MyClass<T>::MyClass(T val) : data(val) {}

template <typename T>
void MyClass<T>::show() {
    cout << "Data: " << data << endl;
}
cpp 复制代码
// main.cpp
#include "MyClass.h"

int main() {
    MyClass<int> obj(10);
    obj.show();  //正确输出 "Data: 10"
    return 0;
}

优点:

分离声明和实现,提高可读性;避免.cpp文件编译错误。

方法3:显式实例化

如果一定要分为.h+.cpp文件,可以用显式实例化来强制编译器实例化特定类型的模板。

cpp 复制代码
// MyClass.h (头文件)
#pragma once

template <typename T>
class MyClass {
public:
    MyClass(T val);
    void show();
private:
    T data;
};
cpp 复制代码
// MyClass.cpp (源文件)
#include "MyClass.h"
#include <iostream>
using namespace std;

// 模板实现
template <typename T>
MyClass<T>::MyClass(T val) : data(val) {}

template <typename T>
void MyClass<T>::show() {
    cout << "Data: " << data << endl;
}

// 显式实例化
template class MyClass<int>;  // 仅实例化 MyClass<int>
template class MyClass<double>;  // 仅实例化 MyClass<double>
cpp 复制代码
// main.cpp
#include "MyClass.h"

int main() {
    MyClass<int> obj(10);  
    obj.show();  //正确输出 "Data: 10"
    return 0;
}

缺点:只能实例化特定类型,比如MyClass<float> 在这里不能使用,扩展性差。

1.14类模板与友元

在C++中,友元(friend)关键字可以用于让某个函数或类访问类的私有成员。当类涉及模板时,友元的声明和使用会有所不同。下面通过不同情况来讲解类模板与友元的使用方法。

1.友元函数(全局函数)与类模板

如何让全局函数访问类模板的私有成员?

在普通类中,我们可以直接使用friend关键字声明友元函数,但在类模板中,友元函数也需要是模板函数,或者显式声明具体类型。

解决方案

cpp 复制代码
#include <iostream>
using namespace std;

// 类模板
template <typename T>
class MyClass {
private:
    T data;
public:
    MyClass(T val) : data(val) {}

    // 声明友元函数(全局函数)
    friend void show(const MyClass<T>& obj) {
        cout << "数据: " << obj.data << endl;
    }
};

// 测试代码
int main() {
    MyClass<int> obj(42);
    show(obj);  //输出:数据: 42
    return 0;
}

输出

解释

这里friend void show(const MyClass<T>& obj);show()成为所有MyClass<T>实例友元。这样show()可以直接访问MyClass的私有成员datashow()仍然是一个普通的非模板函数,只是可以访问MyClass<T>的私有数据。

2.友元模板函数

如果希望所有类型的MyClass<T> 都使用相同的友元函数,可以将友元函数声明为模板函数。

cpp 复制代码
#include <iostream>
using namespace std;

// 类模板
template <typename T>
class MyClass {
private:
    T data;
public:
    MyClass(T val) : data(val) {}

    // 友元模板函数
    template <typename U>
    friend void showData(const MyClass<U>& obj);
};

// 友元模板函数的实现
template <typename U>
void showData(const MyClass<U>& obj) {
    cout << "数据: " << obj.data << endl;
}

// 测试代码
int main() {
    MyClass<int> obj1(100);
    MyClass<double> obj2(3.14);

    showData(obj1);  //  输出:数据: 100
    showData(obj2);  //  输出:数据: 3.14
    return 0;
}

输出

解释
showData<U>是一个友元模板函数,可以访问所有类型MyClass<T>的私有成员。 这样MyClass<int>MyClass<double>都可以共享showData()

3.友元类(让另一个类访问类模板的私有成员)

如何让一个类访问MyClass<T>的私有成员?

如果我们想要另一个类访问MyClass<T>的私有成员,我们可以把这个类声明为友元类。

解决方案

cpp 复制代码
#include <iostream>
using namespace std;

// 预先声明 FriendClass
template <typename T>
class FriendClass;

// 类模板
template <typename T>
class MyClass {
private:
    T data;
public:
    MyClass(T val) : data(val) {}

    // 友元类声明
    friend class FriendClass<T>;
};

// 友元类
template <typename T>
class FriendClass {
public:
    void show(const MyClass<T>& obj) {
        cout << "FriendClass 访问私有数据: " << obj.data << endl;
    }
};

// 测试代码
int main() {
    MyClass<int> obj(77);
    FriendClass<int> friendObj;
    friendObj.show(obj);  //输出: FriendClass 访问私有数据: 77
    return 0;
}

输出

解释
friend class FriendClass<T>;使得FriendClass<T>可以访问MyClass<T>的私有成员;FriendClass<T>只能访问相同类型的MyClass<T>(比如FriendClass<int>只能访问MyClass<int>);模板类FriendClass需要提前声明,否则编译器无法识别。

4.友元类模板

如果我们希望所有MyClass<T>都可以被FriendClass<X>访问,不管T和X是否相同,那么FriendClass也应该是模板类。

cpp 复制代码
 #include <iostream>
using namespace std;

// 预先声明 FriendClass
template <typename T, typename U>
class FriendClass;

// 类模板
template <typename T>
class MyClass {
private:
    T data;
public:
    MyClass(T val) : data(val) {}

    // 让所有 FriendClass<U, V> 都是友元
    template <typename U, typename V>
    friend class FriendClass;
};

// 友元模板类
template <typename U, typename V>
class FriendClass {
public:
    void show(const MyClass<U>& obj) {
        cout << "FriendClass<U> 访问私有数据: " << obj.data << endl;
    }
    void show(const MyClass<V>& obj) {
        cout << "FriendClass<V> 访问私有数据: " << obj.data << endl;
    }
};

// 测试代码
int main() {
    MyClass<int> obj1(88);
    FriendClass<int, double> friendObj1;  // X = int, Y = double
    friendObj1.show(obj1);  // 输出: FriendClass 访问私有数据: 88
   
    MyClass<double> obj2(99);
    FriendClass<double, int> friendObj2;  // X = double, Y = int
    friendObj2.show(obj2);  // 输出: FriendClass 访问私有数据: 99
    
    FriendClass<char, int> friendObj3; 
    friendObj3.show(obj1);  // FriendClass<int, double> 是 MyClass<double> 的友元
    return 0;
}

输出

解释
friend class FriendClass<U,V>;允许所有FriendClass<U,V>访问MyClass<T> 的私有成员;这样FriendClass<int,double>FriendClass<double,int>也可以访问MyClass<int>

1.15模板的局限性

C++模板虽然强大,但也存在一些局限性。这些局限性可能会影响代码的可读性、编译时间、调试难度以及跨平台兼容性。

1.编译时间增加

模板代码在编译时会实例化为具体的类型,如果模板被广泛使用或嵌套层次较深,编译时间会显著增加。

cpp 复制代码
template <typename T>
void foo(T value) {
    // 复杂的模板逻辑
}

foo(10);  // 实例化为 foo<int>
foo(3.14); // 实例化为 foo<double>

解决方法:

  • 尽量减少模板的嵌套层次。
  • 使用显式实例化来减少重复实例化。

2.代码膨胀

模板会为每种类型生成一份代码,导致二进制文件体积增大。

cpp 复制代码
template <typename T>
class MyClass {
public:
    void doSomething(T value) {
        // 复杂的逻辑
    }
};

MyClass<int> obj1;
MyClass<double> obj2;

编译器会生成MyClass<int>MyClass<double>两份代码。

解决方法:

  • 使用类型查出技术(如std::functionstd::any)。
  • 将通用逻辑提取到非模板代码中。

3.调试难度增加

模板错误信息通常非常冗长且难以理解,尤其是在模板嵌套较深的情况下。

cpp 复制代码
template <typename T>
void foo(T value) {
    value.non_existent_method(); // 编译错误
}

foo(10); // 错误信息可能非常复杂

解决方法:

  • 使用静态断言(static_assert)提前检测类型约束。
  • 使用概念(concepts)来约束模板参数。

4.跨平台兼容性问题

不同编译器对模板的支持可能不一致,尤其是在模板的高级特性(如 SFINAE、可变参数模板)上。

cpp 复制代码
template <typename T>
void foo(T value) {
    // 使用 SFINAE 或可变参数模板
}

某些编译器可能不支持这些特性,或者支持不完整。

解决方法:

  • 使用跨平台兼容的模板特性。
  • 在代码中增加编译器特定的条件编译

5.类型推导的局限性

模板类型推导在某些情况下可能无法正常工作,尤其是涉及隐式类型转换或重载时。

cpp 复制代码
template <typename T>
void foo(T value) {}

foo(10); // T 推导为 int
foo(3.14); // T 推导为 double
foo("hello"); // T 推导为 const char*

如果希望 T 推导为 std::string,需要显式指定类型:
foo<std::string>("hello");

解决方法:

  • 使用显式类型指定。
  • 使用 std::decaystd::remove_reference 等类型转换工具。

6.模板特化的复杂性

模板特化(全特化或偏特化)可能导致代码复杂性增加,尤其是在处理多种类型组合时。

cpp 复制代码
template <typename T>
class MyClass {
public:
    void doSomething() {
        cout << "通用实现" << endl;
    }
};

template <>
class MyClass<int> {
public:
    void doSomething() {
        cout << "int 特化实现" << endl;
    }
};

如果需要对多种类型进行特化,代码会变得冗长。

解决方法:

  • 尽量减少模板特化的使用。
  • 使用 if constexpr(C++17)或概念(C++20)来替代部分特化。

7.模板的隐式实例化

模板会在使用时隐式实例化,可能导致未使用的代码也被实例化,增加编译时间和二进制体积。

cpp 复制代码
template <typename T>
class MyClass {
public:
    void doSomething() {
        // 复杂的逻辑
    }
};

MyClass<int> obj; // 即使未调用 doSomething,代码也会被实例化  

解决方法:

  • 使用显式实例化(template class MyClass<int>;)。
  • 将模板代码分离到独立的源文件中。

8.模板的调试信息不友好

模板实例化后的调试信息通常非常冗长,难以阅读。

cpp 复制代码
template <typename T>
void foo(T value) {
    // 复杂的逻辑
}

foo(10); // 调试信息可能包含大量模板实例化细节

解决方法:

  • 使用类型别名(using)简化模板类型。
  • 使用 static_assert 或概念(C++20)提前检查类型约束。

9.模板元编程的复杂性

模板元编程(TMP)可以实现强大的编译时计算,但代码通常难以理解和维护。

cpp 复制代码
template <int N>
struct Factorial {
    static const int value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
    static const int value = 1;
};

cout << Factorial<5>::value << endl; // 输出 120

这种代码虽然强大,但可读性差。

解决方法:

  • 使用 constexpr 函数替代模板元编程。
  • 使用 C++20 的概念和约束来简化模板逻辑。

10.动态多态与静态多态的冲突

模板是基于静态多态的,而虚函数是基于动态多态的。两者结合时可能导致设计复杂。

cpp 复制代码
template <typename T>
class MyClass {
public:
    void doSomething(T value) {
        value.doSomething();
    }
};

class Base {
public:
    virtual void doSomething() = 0;
};

class Derived : public Base {
public:
    void doSomething() override {
        cout << "Derived::doSomething" << endl;
    }
};

MyClass<Base> obj; // 错误:Base 不是具体类型  

解决方法:

  • 使用类型擦除技术(如 std::functionstd::any)。

  • 避免将模板与虚函数混合使用。

1.16 类模板案例

案例描述:实现一个通用的数组类,要求如下:

●可以对内置数据类型以及自定义数据类型的数据进行存储

●将数组中的数据存储到堆区

●构造函数中可以传入数组的容量

●提供对应的拷贝构造函数以及operator=防止浅拷贝问题

●提供尾插法和尾删法对数组中的数据进行增加和删除

●可以通过下标的方式访问数组中的元素

●可以获取数组中当前元素个数和数组的容量

示例:

cpp 复制代码
#include <iostream>
#include<string>
using namespace std;

template<typename T>
class MyArray {
private:
    T* data; //指向堆区数据
    int capacity; //数组容量
    int size; //当前元素个数
public:
    //构造函数(初始化容量)
    MyArray(int cap) {
        this->capacity = cap;
        this->size = 0;
        this->data = new T[cap]; //在堆区分配内存
    }
    //拷贝构造函数(深拷贝)
    MyArray(const MyArray& other) {
        this->capacity = other.capacity;
        this->size = other.size;
        this->data = new T[this->capacity];
        for (int  i = 0; i < this->size; i++){
            this->data[i] = other.data[i]; //拷贝数据
        }
    }
    //赋值运算符重载(防止浅拷贝)
    MyArray& operator=(const MyArray& other) {
        if (this == &other)
            return *this; //防止自赋值
        //释放旧内存
        delete[] this->data;
        //深拷贝
        this->capacity = other.capacity;
        this->size = other.size;
        this->data = new T[this->capacity];
        for (int i = 0; i < this->size; i++) {
            this->data[i] = other.data[i];
        }
        return *this;
    }
    //尾插法(增加元素)
    void push_back(const T& value) {
        if (size >= capacity) {
            cout << "数组已满,无法插入新的元素!" << endl;
            return;
        }
        data[size++] = value;
    }
    //尾删法(删除最后一个元素)
    void pop_back() {
        if (size > 0) {
            size--;
        }
        else {
            cout << "数组为空,无法删除元素!" << endl;
        }
    }
    //下标运算符重载
    T& operator[](int index) {
        if (index < 0 || index >= size) {
            cout << "索引超出范围" << endl;
            return data[0]; //直接返回第一个元素,放在系统崩溃
        }
        return data[index];
    }
    //获取当前数组中的元素个数
    int getSize() const {
        return size;
    }
    //获取数组容量
    int getCapacity() const {
        return capacity;
    }
    //析构函数(释放堆区内存)
    ~MyArray(){
        delete[] data;
    }
};
//测试
class Person {
public:
    string m_Name;
    int m_Age;
    //默认构造
    Person() :m_Name(""), m_Age(0) {}
    //带参构造
    Person(string name, int age) :m_Name(name), m_Age(age) {}
    void show() const {
        cout << "姓名:" << m_Name << ",年龄:" << m_Age << endl;
    }
};

int main()
{
    //存储int类型
    MyArray<int> arr(5);
    //存放三个元素
    arr.push_back(10);
    arr.push_back(20);
    arr.push_back(30); 
    cout << "数组大小:" << arr.getSize() << ",容量:" << arr.getCapacity() << endl;
    cout << "第2个元素:" << arr[1] << endl;
    //删除最后一个元素
    arr.pop_back();
    cout << "删除之后,元素的个数:" << arr.getSize() << endl;
    cout << "=====================================" << endl; //分隔符

    //存储自定义类型Person类
    MyArray<Person> persons(3);
    //存储2个人
    persons.push_back(Person("张伟", 25));
    persons.push_back(Person("李明", 26));

    cout << "==存储Person情况==" << endl;
    for (int  i = 0; i < persons.getSize(); i++){
        persons[i].show();
    }
    cout << "=====================================" << endl;
    //测试重载=符号
    MyArray<int> arr2(4);
    //存放三个元素
    arr2.push_back(40);
    arr2.push_back(50);
    arr2.push_back(60);
    arr2 = arr;
    cout << "arr2 复制后大小: " << arr2.getSize() << ", 容量: " << arr2.getCapacity() << endl;
    cout << "arr2 的元素: ";
    for (int i = 0; i < arr2.getSize(); i++){
        cout << arr2[i] << " ";
    }
    cout << endl;
    return 0; 
}
相关推荐
小李苦学C++9 分钟前
C++模板特化与偏特化
开发语言·c++
小王努力学编程18 分钟前
元音辅音字符串计数leetcode3305,3306
开发语言·c++·学习·算法·leetcode
佚明zj19 分钟前
【C++】如何高效掌握UDP数据包解析
开发语言·c++·udp
Maple_land38 分钟前
C++初阶——类和对象(二)
c++
讨厌下雨的天空1 小时前
C++之多态
开发语言·c++
进击的jerk2 小时前
力扣 11.盛水最多的容器(双指针)
c++·算法·leetcode
啊吧怪不啊吧3 小时前
C++相关基础概念之入门讲解(上)
c语言·开发语言·c++
azaz_plus3 小时前
C++ priority_queue 堆
开发语言·c++·stl··priority_queue
TANGLONG2224 小时前
【C++】STL全面简介与string类的使用(万字解析)
java·c语言·开发语言·c++·python·面试·蓝桥杯
xcyxiner5 小时前
snmp v1 get请求优化(上)
c++