当前位置: 首页 > article >正文

Day29 C++ 模板

2024.12.21 C++ 模板

问自己一以下几个问题:

1.什么是模板?

2.为什么用模板?

3.什么情况下用模板呢?

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。

模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。

每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector vector

您可以使用模板来定义函数和类,接下来让我们一起来看看如何使用。

函数模板

模板函数定义的一般形式如下所示:

template <typename type> ret-type func-name(parameter list) 
{   
    // 函数的主体 
}

在这里,type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。

下面是函数模板的实例,返回两个数中的最大值:

实例

#include <iostream>
#include <string>
 
using namespace std;
 
template <typename T>
inline T const& Max (T const& a, T const& b)  //类型为T的常量引用
{ 
    return a < b ? b:a; 
} 
int main ()
{
 
    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl; 
 
    double f1 = 13.5; 
    double f2 = 20.7; 
    cout << "Max(f1, f2): " << Max(f1, f2) << endl; 
 
    string s1 = "Hello"; 
    string s2 = "World"; 
    cout << "Max(s1, s2): " << Max(s1, s2) << endl; 
 
    return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World
template <typename T> //
inline T const& Max (T const& a, T const& b) 
{ 
    return a < b ? b:a; 
} 

我们具体分析一下这个代码:

  1. template <typename T>:

    • 这是一个模板声明,表示这个函数是一个 模板函数。模板函数使得这个函数能够接受任何类型 T,并且返回类型也是 T
    • T 是一个 占位符类型,可以在调用时由具体的类型替代。
    • 举个例子,当你调用 Max(5, 10) 时,T 会被推断为 int,而如果你调用 Max(5.5, 10.5)T 会被推断为 double
  2. inline:

    • inline 是一个建议,告诉编译器尽量将该函数的代码 内联展开,即在调用该函数的地方直接插入函数体。这样可以减少函数调用的开销,通常用于小函数。
    • 但是,是否真的内联展开由编译器决定,这只是一个提示。

    inline 的作用是告诉编译器,将函数的代码 内联展开,即直接将函数体的代码插入到调用的地方,而不是通过常规的函数调用机制来执行。它通常用于小且频繁调用的函数,能够提高程序的执行效率,减少函数调用的开销。

举个例子

假设有两个函数:

传统的函数调用:
int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 3, y = 5;
    int z = add(x, y);  // 这里会发生函数调用
    return 0;
}

在上面的代码中,调用 add(x, y) 需要:

  • 压入函数调用的参数到栈中。
  • 跳转到函数的代码执行 a + b
  • 执行完后再返回到 main 函数的位置。

这虽然开销很小,但如果 add() 这个函数被非常频繁地调用,开销会累积起来。

使用 inline 的情况:
inline int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 3, y = 5;
    int z = add(x, y);  // 编译器可能会将这个函数的代码直接插入到这里
    return 0;
}

通过在函数声明前加上 inline,编译器会尝试将 add(x, y) 函数的代码直接插入到 main 函数中。这样就避免了函数调用的所有开销:

  • 不再需要压栈,也不需要跳转到函数代码执行。
  • main 函数中直接执行 a + b

这会使得程序在性能上有所提升,尤其是在调用频繁且函数体非常简单时。

​ 3.T const&`:

  • T const& 是返回类型,表示该函数返回的是类型为 T常量引用
  • 之所以使用 const& 是为了避免不必要的拷贝操作,同时确保返回的值不能被修改。
  • const 保证了你不能通过这个引用修改返回值,而 & 是引用,避免了拷贝,提升效率。

类模板

正如我们定义函数模板一样,我们也可以定义类模板。泛型类声明的一般形式如下所示:

template <class type> class class-name {
.
.
.
}

在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。

下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:

实例

#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
 
using namespace std;
 
// template <class T> 中的 class 其实是可以用 typename 来代替的。
// class 和 typename 在模板声明中是等价的。
template <class T> 
class Stack { 
  private: 
    vector<T> elems;     // 元素 
 
  public: 
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
    bool empty() const{       // 如果为空则返回真。
        return elems.empty(); 
    } 
}; 
 
template <class T>
void Stack<T>::push (T const& elem) 
{ 
    // 追加传入元素的副本
    elems.push_back(elem);    
} 
 
template <class T>
void Stack<T>::pop () 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::pop(): empty stack"); 
    }
    // 删除最后一个元素
    elems.pop_back();         
} 
 
template <class T>
T Stack<T>::top () const 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::top(): empty stack"); 
    }
    // 返回最后一个元素的副本 
    return elems.back();      
} 
 
int main() 
{ 
    try { 
        Stack<int>         intStack;  // int 类型的栈 
        Stack<string> stringStack;    // string 类型的栈 
 
        // 操作 int 类型的栈 
        intStack.push(7); 
        cout << intStack.top() <<endl; 
 
        // 操作 string 类型的栈 
        stringStack.push("hello"); 
        cout << stringStack.top() << std::endl; 
        stringStack.pop(); 
        stringStack.pop(); 
    } 
    catch (exception const& ex) { 
        cerr << "Exception: " << ex.what() <<endl; 
        return -1;
    } 
}

当上面的代码被编译和执行时,它会产生下列结果:

7
hello
Exception: Stack<>::pop(): empty stack

我们分析一下这个代码:

// T 是一个占位符,表示栈的元素类型可以是任意的(如 int、double、std::string 等)
template <class T> 
// 这里定义了一个 Stack 类,表示栈数据结构。类中的成员函数和数据成员定义了栈的操作和存储方式。
class Stack { 
  // 数据成员
  private: 
    //std::vector 是 C++ 标准库中的容器,可以根据需要扩展大小,方便我们动态添加或移除元素。
    vector<T> elems;     // 元素  elems 是一个 std::vector<T> 类型的成员变量,存储栈中的元素。
 
  public:
    // 常量引用 参数 T const&,意味着你传入的参数不会被修改,而且避免了不必要的拷贝。
    void push(T const&);      // 入栈 
    //在实现中,pop() 函数通常会调用 elems.pop_back() 来删除 vector 中的最后一个元素。
    void pop();               // 出栈
    // top() 是一个 const 函数,表示这个函数不会修改栈的状态,它只会读取栈顶元素
    // 返回类型是 T,即栈中元素的类型,可能会返回 elems.back(),即 vector 中的最后一个元素。
    T top() const;            // 返回栈顶元素
    // empty 用于检查栈是否为空。如果栈中没有元素,则返回 true,否则返回 false。
    bool empty() const{       // 如果为空则返回真。
        return elems.empty(); 
    } 
}; 
const放在前后的区别:
举个例子:

假设你有一个类 Stack,并且它有一个 pop() 函数和一个 empty() 函数:

class Stack {
private:
    std::vector<int> elems;

public:
    // 不加 const 的成员函数,可能会修改类的成员
    void pop() {
        elems.pop_back();  // 这会改变对象的状态
    }

    // 加上 const 的成员函数,不会修改类的成员
    bool empty() const {
        return elems.empty();  // 这只是读取状态,不修改任何成员
    }
};
关键点:
  • pop() 函数不加 const,因为它会改变对象的状态(移除栈顶元素)。
  • empty() 函数加上 const,因为它 不会 修改对象的状态,只是检查栈是否为空。
如果没有 const 会发生什么?

如果 empty() 没有 const,就表示它可能修改类的成员变量,那么你就不能在一个 const 对象上调用 empty() 函数了。例如:

const Stack s;    // 创建一个常量 Stack 对象
s.empty();        // 错误!不能调用非-const 成员函数

上面的代码会出错,因为你不能对 const 对象调用可能修改它的成员变量的函数。如果 empty()const,则允许调用。

为什么是 class 而不是 type 或其他?

class 作为模板类型参数的关键字其实是历史遗留下来的设计。最初,C++模板机制只允许类作为模板参数,因此使用了 class 关键字来表示类型参数。后来,C++ 增强了模板机制,允许不仅仅是类,还有其他类型(如基本数据类型、指针、函数等)作为模板的参数,但为了保持兼容性,class 依然保留了下来。

总结

  • empty() const 中的 const 放在函数声明的末尾,是用于修饰成员函数,表示这个成员函数不会修改对象的状态(即不会修改类的成员变量)。
  • 如果没有 const,那么在 const 对象上不能调用这个函数,因为它承诺可能会修改对象的状态。

这是 C++ 中的一个设计约定,使得程序员能够明确知道哪些成员函数不会修改对象的状态,从而允许在常量对象上调用这些函数。

好了我们现在再来回答最开始的三个问题:

1.什么是模板?

在 C++ 中,模板(Template)是一种 泛型编程 的技术,它允许我们编写可以处理多种数据类型的代码,而不必重复编写每种数据类型的实现。模板使得程序可以更加灵活和通用。通过模板,程序员可以定义 类模板函数模板,使得相同的代码可以处理不同的数据类型。

模板的基本形式:

  • 函数模板:使得一个函数可以操作不同的数据类型。
  • 类模板:使得一个类可以操作不同的数据类型。
示例:函数模板
template <typename T>
T add(T a, T b) {
    return a + b;
}
示例:类模板
template <typename T>
class Box {
private:
    T value;
public:
    Box(T v) : value(v) {}
    T getValue() const { return value; }
};

这里,Box 是一个模板类,T 是类型参数,Box 可以存储任意类型的数据。

2.为什么用模板?

模板的使用可以带来以下好处:

  1. 代码重用

模板使得相同的代码可以在不同的数据类型上复用。你不需要为每个类型写多个版本的函数或类。通过模板,程序可以一次性处理多种类型,避免了代码的重复。

  1. 提高灵活性和通用性

使用模板可以编写通用的代码,使得同一段代码可以适应不同类型的数据。这使得代码更加灵活,适应性强。

  1. 类型安全

模板是类型安全的,因为在编译时,编译器会根据模板实例化时传入的类型进行检查,确保类型的正确性。这样,你可以在编译时发现潜在的类型错误。

  1. 性能优化

使用模板可以让编译器在编译时生成类型特定的代码,这种方式比运行时进行动态类型检查更加高效。许多模板函数和类会通过 内联类型特定优化 提高执行效率。

  1. 减少代码重复

模板使得开发者不需要为不同的类型编写相似的代码。一次性编写模板函数或类,可以避免重复代码的维护和管理。

3.什么情况下用模板呢?

模板通常用于以下几种情况:

  1. 需要泛型函数或类的情况

当你希望编写一个函数或类,可以处理多种数据类型时,模板非常有用。例如,你可以写一个排序函数,它可以处理整数、浮点数、字符串等不同的数据类型。

示例:
// typename 是必须的,它告诉编译器 T 是一个类型,不能直接省略。
template <typename T>
void printArray(T arr[], int size) {
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

在这个例子中,printArray 函数是一个模板函数,它可以接受任何类型的数组并打印出来。

  1. 实现容器类(例如:栈、队列、链表等)

如果你需要定义一个通用的容器类(如栈、队列、链表等),并且希望它能存储不同类型的数据,模板是一个非常好的选择。C++ 标准库中的 std::vectorstd::liststd::map 等容器类都是模板类。

示例:
template <typename T>
class Stack {
private:
    std::vector<T> elems;  // 存储元素的容器
public:
    void push(const T& elem) { elems.push_back(elem); }
    void pop() { elems.pop_back(); }
    T top() const { return elems.back(); }
    bool empty() const { return elems.empty(); }
};

在上面的代码中,Stack 是一个模板类,可以存储任何类型的元素。

  1. 类型不确定时

如果你在编写的函数或类中无法事先确定数据类型,或者需要处理多种类型的对象,可以使用模板。模板允许在编译时指定类型,使得函数或类可以在不同场景下使用。

  1. 编写库或工具类

当你开发一个通用库或工具类(例如数学库、数据结构库等)时,模板可以使你编写一个只需实现一次的代码,而不是为每个特定类型重写代码。

示例:
template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

在这个例子中,max 函数模板可以用来比较任意类型的数据,如整数、浮点数等。

  1. 性能敏感的应用

模板可以在编译时生成特定类型的代码,避免了运行时的性能开销。在需要高度优化性能的场景中,模板可以让你在编译期进行更多的优化,例如 内联常量折叠 等。

  1. 类型特征(Type Traits)

在一些高级编程场景中,你可能需要根据类型的不同特性(比如是否为某种特定类型、是否有某种成员函数等)来调整代码的行为。模板和类型特征(如 std::is_integral)可以帮助你根据类型做出决策。

总结

1. 什么是模板?

模板是 C++ 中的一种机制,允许你定义类或函数的通用形式,使用类型参数来处理不同的数据类型。

2. 为什么用模板?
  • 提高代码复用性。
  • 提供更高的灵活性和通用性。
  • 在编译时进行类型检查,保证类型安全。
  • 通过编译期生成特定类型的代码,优化性能。
  • 减少代码重复。
3. 什么时候用模板?
  • 当需要编写泛型函数或类时。
  • 实现容器类或数据结构时。
  • 在类型不确定的情况下处理不同类型的数据。
  • 编写通用库或工具类时。
  • 在性能敏感的场景中进行编译期优化。
  • 使用类型特征来做类型特定的选择。

http://www.kler.cn/a/446240.html

相关文章:

  • 【k8s集群应用】K8S二进制安装大致步骤(简略版)
  • FPGA:FPGA器件选型
  • springboot 与 oauth2 版本对应关系
  • 【C语言】特殊指针汇总
  • 基底展开(Expansion in a Basis):概念、推导与应用 (中英双语)
  • SparkSQL运行架构及原理
  • day-95 定长子串中元音的最大数目
  • 计算机视觉:原理、分类与应用
  • 头歌实训数据结构与算法-图的最短路径(第2关:多源最短路径)
  • 在 C# 中加载图像而不锁定文件
  • Xcode 文件缺失:Missing submodule xxx
  • 基于Spring Boot的大学就业信息管理系统
  • MPLS小实验:静态建立LSP
  • 【Spring】Spring的模块架构与生态圈—Spring MVC与Spring WebFlux
  • thinkphp框架diygw-ui-php进销存出库记录操作
  • 基于Spring Boot的高校素拓分管理系统
  • ImageGlass:基于C#开发的轻量级、多功能的图像查看器
  • 仿途唬养车系统汽修服务小程序修车店小程序源码
  • 数据库 MYSQL的概念
  • 怎么样保持mysql和redis数据一致性
  • CLION中运行远程的GUI程序
  • Nuc9 Truenas 和 Macmini4组雷电网桥 上传速度异常 1Mbp/s 解决
  • datasets 笔记:加载数据集(基本操作)
  • 【Qt编程入门】
  • 了解过.css 的优化吗?
  • 【计算机网络】lab2 Ethernet(链路层Ethernet frame结构细节)