C++学习笔记-第10单元 模板初步

news2024/10/6 20:36:45

第10单元 模板初步

文章目录

  • 第10单元 模板初步
    • 单元导读
    • 10.1 模板与泛型编程
      • 10.1.1 元编程与泛型编程
      • 10.1.2 初识模板
    • 10.2 函数模板
      • 10.2.1 函数模板
      • 10.2.2 函数模版实例化
    • 10.3 排序示例与泛型化
      • 10.3.1 例子:选择排序
      • 10.3.2 将一个函数泛型化
    • 10.4 类模板
      • 10.4.1 类模板
      • 10.4.2 类模版实例化
    • 10.5 模板参数、模板类型
      • 10.5.1 默认类型与非类型参数
      • 10.5.2 模板与继承
      • 10.5.3 何时何地使用模板

注:本部分内容主要来自中国大学MOOC北京邮电大学崔毅东的 《C++程序设计》课程。
注:94条 C++程序规范。


单元导读

本单元主要介绍 函数模板类模板 的用法。

  1. 元编程是写程序来处理程序;泛型编程是写程序来处理很多不同类型的数据。
  2. C++里面用模板来指定类型参数——也就是可以代表不同类型的一个参数。并由此定义函数模板和类模板。
  3. 模板有两种实例化方法:显式实例化、隐式实例化。
  4. 函数模板实例化之后,才是真正的函数;类模版实例化后,才是真正的类。
  5. 要编写一个函数模板或者类模板,就要首先编写一个非泛型化的函数或者类,然后改造成模板。

这一单元中,理解 模板实例化 是最重要的。在实际编程中,要将函数模板或者类模版的声明和实现都放在同一个头文件中

10.1 模板与泛型编程

10.1.1 元编程与泛型编程

  在下一小节介绍“模板”之前,本小节首先来介绍一下C++几种编程范式中的“元编程和泛型编程”。前面介绍过,C++是一种多模式的编程语言,支持结构化编程、面向对象编程、函数式编程、模板元编程/泛型编程等。下面是一些关于“编程”的概念:

  1. 编程(Programming):写一个程序去处理数据
  2. 元编程(Meta-programming):写个程序去处理程序
  3. 泛型编程(Generic Programming):写个程序去处理数据,但是只对数据结构做最小假设,以使该程序能最大化重用来处理广泛的数据类型。

上述来自于stack overflow文章:Generic programming vs. Metaprogramming

  上述“元编程”和“泛型编程”的概念上略有不同,但是在C++中这两个概念并没有区别,原因是C++使用 模板 来实现元编程,由编译器在编译期根据模板生成模板实例,也就是程序。C++编译时多态有两种:函数重载、模板多态(泛型编程)。其中C++的泛型编程是借由元编程实现的,也就是由代码模板生成代码,所以C++中“元编程”和“泛型编程”不可区分。代码模板处理的是多类型数据,可以很广泛,但是生成的每一个代码则只能处理一种数据。

10.1.2 初识模板

  本小节来介绍“模板/模版”(Template),两种中文写法老师没有查到区别,一般使用“模板”。至于为什么要引入“模板”,可以举个例子:比如现在要实现“求二整数、二双精度浮点数、二字符的最大者”,由于C语言中没有重载函数,所以只能定义三个名称不同的函数;C++中则可以使用重载函数,定义三个名称相同、类型不同的函数:

/*****************************/
//C语言定义三个函数,函数名称不同
int maxInt(int x, int y);
double maxDouble(double x, double y);
char maxChar(char x, char y);
/*****************************/
//C++使用重载函数,函数名称相同
int maxValue(const int &value1, const int &value2) {
    if (value1 > value2)    return value1;
    else                    return value2;
}
double maxValue(const double &value1, const double &value2){
    if (value1 > value2)    return value1;
    else                    return value2;
}
char maxValue(const char &value1, const char &value2) {
    if (value1 > value2)    return value1;
    else                    return value2;
}

  虽然上述C++使用重载函数已经非常出色的完成了任务要求,但是逻辑相同的函数写三遍还是很麻烦,那能不能只写一遍代码,就可以同时实现三个函数的功能呢(比如下面这段代码)?可以,这种函数就是“泛型函数”。泛型函数的优点显而易见:节省代码量、易于维护。下一节具体的介绍如何书写具体的“泛型函数”代码,下面仅仅是泛型函数的一个示例:

GenericType maxValue (const GenericType& x, const GenericType& y) {
    if (x > y)  return x;
    else        return y;
}
......
int main() {
  cout << maxValue(1, 3);
  cout << maxValue(1.0, 3.0);
  cout << maxValue('a', 'x');
  return 0;
}

10.2 函数模板

10.2.1 函数模板

图10-1 泛型模板示意图

  上一小节简单介绍了“模板”的概念,本小节进一步介绍如何具体的书写“函数模板”代码。C++引入了带有泛型的函数模板,如上图10-1所示:

  1. 模板前缀:所有的函数模板都必须有模板前缀。其中templatetypename都是固定的关键字。尖括号里面的内容<typename T>是“泛型参数的声明”,有两种声明方式:
  • <typename T>:描述性强、老师推荐。
  • <class T>:易与类声明混淆、老师不推荐。
  1. 模板的“类型参数”类似于函数的“形式参数”。
  2. 类型参数T在函数中至少出现一次,可以作为函数返回值、函数参数、函数局部变量。

编码规范:

  • 8:用于表示模板类型的名字应该使用一个大写字母,例如template <typename T>template <class C, class D>

  当然图10-1所演示的仅仅是一个函数类型,当函数需要包含多个参数类型时,可以使用逗号分隔不同的类型参数,注意在模板前缀中每个类型前面都要写一次关键字typename/class。比如要定义加法运算的泛型函数,下面给出了具体的代码以及编译器根据模板生成代码的过程:

//模板
template <typename T, typename S>
auto add (T  x1, S x2) { //C++14
    return (x1 + x2);//编译器会自动进行隐式类型转换成double
}
//主函数
int main () {
    auto y = add (1, 2.0);//int, double
    return 0;
}
图10-2 编译器生成代码的过程

下面使用代码展示如何使用模板:

源文件GenericAdd.cpp

#include<iostream>
//任务1:编写函数模板T add(T x, T y),并调用之。
template<typename T>
T add(T x, T y) {
    return (x + y);
}
//任务2:修改上述函数模板为auto add(T x, S y),并调用之。
template<class T, typename Q>
auto add(T x, Q y) {
    return (x + y);
}
int main() {
    auto num1 = add(3, 4);
    auto num2 = add(3, 4.0);
    std::cout << typeid(num1).name() << " : " << num1 << std::endl;
    std::cout << typeid(num2).name() << " : " << num2 << std::endl;
}

运行结果:

int : 7
double : 7

10.2.2 函数模版实例化

  本节来进一步介绍上一小节中图10-2编译器根据模板生成代码的过程,也就是函数模板实例化(function template instantiation)。函数模板只是蓝图,本身不是类型或函数。编译器扫描代码,遇到模版定义时,并不立即产生代码;只有在程序调用时,确定模板实参后,编译器才会生成实际函数代码,也就是“实例化”。“模板多态”实际上是一种“编译时多态”,也叫做“静态联编”,和“函数重载”是同一个层面上的多态。

  函数模板的实例就是一段函数代码。和前几章介绍类的实例化相同,模板实例化也有两种方法:

  1. 显式实例化(Explicit instantiation):强制某些函数实例化,可出现于程序中模板定义后的任何位置。
  2. 隐式实例化(Implicit instantiation):编译器查看函数调用,推断模版实参,实现隐式实例化。

代码展示:

#include <iostream>
template <typename T>
void f(T s){
    std::cout << s << '\n';
}
/*************显示实例化*************/
template void f<double>(double);    //显式实例化1:编译器生成如下代码
//void f(double s) {
//    std::cout << s << '\n';
//}
template void f<>(char);            //显式实例化2:等价于f<char>(char),根据形参类型char推导出模板实参T
template void f(int);               //显式实例化3:等价于f<int>(int),根据形参int推导出模板实参T
/*************隐式实例化*************/
int main(){
    f<double>(1);                   //隐式实例化1:调用 f<double>(double)
    f<>('a');                       //隐式实例化2:调用 f<char>(char)
    f(7);                           //隐式实例化3:调用 f<int>(int)
    void (*ptr)(std::string) = f;   //隐式实例化4:f<string>(string)
    //注意上一行是函数指针,函数的名字就相当于一个指针
}

  最后看一个小概念“实例函数/实例类”(Instantiated function/class):由函数模板实例化得到的函数叫做“实例函数”,由类模板实例化得到的类叫做“实例类”。上述英文是C++11标准14.7节中给出的,所以注意“实例函数/实例类”的称呼不要写成 “模板函数/模板类”(template function/template class) ,这些概念不严谨,应该只存在“函数模板/类模板”,重点强调在“模板”。

下面使用代码展示“模板实例化”:
源文件Instantiate.cpp

#include <iostream>
//#include <algorithm>                //包括max的标准库
#include <string>                     //为了使用重载运算符""s
using namespace std::string_literals; //为了使用重载运算符""s
//任务1:函数模板定义
template <typename T>
T max(T x, T y) {
    return (x > y ? x : y);
}
//任务2:显式实例化—整数
template <int> int max (int, int);
//任务3:在main()中演示各功能
int main() {
    //任务3.1:调用显式实例化的函数
    std::cout << "max(1,2): " << max(1, 2) << std::endl;
    //任务3.2:浮点数实例化
    std::cout << "max(2,9,1.3): " << max(2.9, 1.3) << std::endl;
    //任务3.3:字符实例化
    std::cout << "max('A','D'): " << max('A', 'D') << std::endl;
    //任务3.4:字符串字面量 const char* 实例化
    //实际上比较的是const char*指针地址的大小,下面使用生字符串引起来就不需要转义了
    std::cout << R"(max("ABC","ABD"): )" << max("ABC", "ABD") << std::endl;
    std::cout << R"(max("ABD","ABC"): )" << max("ABD", "ABC") << std::endl;
    std::cout << R"(max("123","124"): )" << max("123", "124") << std::endl;
    //任务3.5:std::string类型实例化—2023年也会引起名字空间冲突!!
    比较的是每个字符的ASCII码,""s 引起来的自动变成std::string类型
    //std::cout << R"(max("ABC"s, "ABD"s))" << max("ABC"s, "ABD"s) << std::endl;
    //std::cout << R"(max("ABD"s, "ABC"s))" << max("ABD"s, "ABC"s) << std::endl;
    //std::cout << R"(max("123"s, "124"s))" << max("123"s, "124"s) << std::endl;
    //任务3.6:名字空间冲突问题
    //引入头文件<algorithm>会编译报错。
    //不要轻易地定义可能和标准库已有函数重名的模板!!
    return 0;
}

运行结果:

max(1,2): 2
max(2,9,1.3): 2.9
max('A','D'): D
max("ABC","ABD"): ABC
max("ABD","ABC"): ABC
max("123","124"): 123//说明先写的地址大

10.3 排序示例与泛型化

10.3.1 例子:选择排序

  本10.3节以“选择排序”为例,演示如何将函数泛型化。所以本小节首先来介绍“选择排序”的非泛型函数思路。下图给出升序的“选择排序”实现思路:

  • 基本思路:每次都找到最小的元素,然后和本次循环最前面的元素进行交换。
  • 循环的关键参数:待排序列表中的最小值、待排序列表中的最小值的索引。
  • 注意:下面的代码只是一般的排序算法,还没有模板化,也就是还没有针对不同的数据类型做泛型。
图10-3 选择排序示意图

下面使用代码编写升序的“选择排序”,注意要选择C++17标准才能正常使用std::array。另外,由于C++17中引入了std::array<类型, 大小>数组,所以不仅要编写“C风格原生数组”的排序代码,还要编写“C++风格array数组”的排序代码:
头文件SelectionSort.h

#pragma once
#include<iostream>
#include<array>
//任务1:double数组升序选择排序
void selectionSort(double list[], const std::size_t size);

//任务2:std::array数组升序选择排序——注意模板的声明和实现都要放在头文件中
//这个并没有实现泛型化,只能排序double类型的std::array数组。
template<int N>//注意这个int N是非引用类型参数,不是类型,10.5节会讲
void selectionSort(std::array<double, N>& list) {
    constexpr int size = N;
    //for循环,每次从list[i]~list[size-1]中找出一个最小的数
    for (int i = 0; i < (size - 1); i++) {
        //初始化,将list[i]记为最小值,将i记为最小值的索引
        double min = list[i];
        int index = i;
        //用循环,找出list[i+1]~list[size-1]中的最小值和他的下标
        for (int j = i + 1; j < size; j++) {
            if (min > list[j]) {
                min = list[j];
                index = j;
            }
        }
        //若list[i]不是最小值,那么交换list[i]<-->list[index]
        if (index != i) {
            list[index] = list[i];
            list[i] = min;
        }
    }
}

源文件SelectionSort.cpp

#include<iostream>
#include"SelectionSort.h"
//double数组升序选择排序
void selectionSort(double list[], const std::size_t size) {
    //for循环,每次从list[i]~list[size-1]中找出一个最小的数
    for (int i = 0; i < (size - 1); i++) {
        //初始化,将list[i]记为最小值,将i记为最小值的索引
        double min = list[i];
        int index = i;
        //用循环,找出list[i+1]~list[size-1]中的最小值和它的下标
        for (int j = i + 1; j < size; j++) {
            if (min > list[j]) {
                min = list[j];
                index = j;
            }
        }
        //若list[i]不是最小值,那么交换list[i]<-->list[index]
        if (index != i) {
            list[index] = list[i];
            list[i] = min;
        }
    }
}

源文件TestSelectionSort.cpp

#include<iostream>
#include<array>
//#include<iomanip>
#include"SelectionSort.h"
int main() {
    //double x[]{ 3.0, 4.0, 7.0, 5.0, 6.0, 1.0, 9.0, 2.0 };
    std::array x{ 3.0, 4.0, 7.0, 5.0, 6.0, 1.0, 9.0, 2.0 };
    //输出排序前数组所有元素
    for (auto i : x) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    //输出排序后数组所有元素
    //selectionSort(x, 8);
    selectionSort(x);
    for (auto i : x) {
        std::cout << i << " ";
    }
}

运行结果:

3 4 7 5 6 1 9 2
1 2 3 4 5 6 7 9

10.3.2 将一个函数泛型化

  于是在上一小节代码的基础上,本小节来将上述排序算法进行泛型化。首先来看看将函数泛型化的一般思路:

  1. 先设计/编写一个非泛型函数。
  2. 调试/测试该函数,确保可以正常运行。
  3. 将上述非泛型函数转换为泛型函数。具体就是将函数处理的数据类型转换成类型参数。注意不是将函数中所有与待处理数据的类型相同的数据类型都换成泛型,而是只转换与待处理数据有关的数据类型,比如下面的代码只需修改foo的返回值类型tx,无需修改s
int foo(int x, int s) {
    int t {0};
    for(int i=0; i<s; i++){
        t ="x;"
    }
    return t
}

下面使用代码展示如何将上一小节的“选择排序”代码泛型化:
头文件GenericSort.h

#pragma once
#include<iostream>
#include<array>
//任务1:对于原生数组的选择排序进行泛型化
template<typename T>
void selectionSort(T list[], const std::size_t size) {
    //for循环,每次从list[i]~list[size-1]中找出一个最小的数
    for (int i = 0; i < (size - 1); i++) {
        //初始化,将list[i]记为最小值,将i记为最小值的索引
        T min = list[i];
        int index = i;
        //用循环,找出list[i+1]~list[size-1]中的最小值和他的下标
        for (int j = i + 1; j < size; j++) {
            if (min > list[j]) {
                min = list[j];
                index = j;
            }
        }
        //若list[i]不是最小值,那么交换list[i]<-->list[index]
        if (index != i) {
            list[index] = list[i];
            list[i] = min;
        }
    }
}
//任务2:对于std::array数组的选择排序进行泛型化
template<typename T,int N>
void selectionSort(std::array<T, N> &list) {
    constexpr int size = N;
    //for循环,每次从list[i]~list[size-1]中找出一个最小的数
    for (int i = 0; i < (size - 1); i++) {
        //初始化,将list[i]记为最小值,将i记为最小值的索引
        T min = list[i];
        int index = i;
        //用循环,找出list[i+1]~list[size-1]中的最小值和他的下标
        for (int j = i + 1; j < size; j++) {
            if (min > list[j]) {
                min = list[j];
                index = j;
            }
        }
        //若list[i]不是最小值,那么交换list[i]<-->list[index]
        if (index != i) {
            list[index] = list[i];
            list[i] = min;
        }
    }
}

源文件GenericSort.cpp

#include<iostream>
#include"GenericSort.h"

源文件TestGenericSort.cpp

#include<iostream>
#include<iomanip>
#include"GenericSort.h"
int main() {
    //1. 测试原生数组的泛型化选择排序
    //输出排序前原生数组
    double x[]{ 3.0, 4.0, 7.0, 5.0, 6.0, 1.0, 9.0, 2.0 };
    //char x[]{ 'a', 'f', 'e', 'h', 'j', 'k', 'w', 'l' };
    //char x[]{ "djqecs;d" };
    for (auto i : x) {
        std::cout << std::fixed << std::setprecision(2) << i << " ";
    }
    std::cout << std::endl;
    //输出排序后原生数组
    selectionSort(x, 8);
    for (auto i : x) {
        std::cout <<std::fixed<<std::setprecision(2)<< i << " ";
    }
    std::cout << std::endl;

    //2. 测试原生数组的泛型化选择排序
    //输出排序前std::array数组
    std::array y{ 3.0, 4.0, 7.0, 5.0, 6.0, 1.0, 9.0, 2.0 };
    //std::array y{ "B", "D", "E", "A" };
    //std::array y{ 1, 8, 4, 0, 2, 7 };
    for (auto i : y) {
        std::cout << std::fixed<<std::setprecision(2)<<i << " ";
    }
    std::cout << std::endl;
    //输出排序后std::array数组
    selectionSort(y);
    for (auto i :y) {
        std::cout << std::fixed << std::setprecision(2) << i << " ";
    }
    return 0;
}

运行结果:

3.00 4.00 7.00 5.00 6.00 1.00 9.00 2.00
1.00 2.00 3.00 4.00 5.00 6.00 7.00 9.00
3.00 4.00 7.00 5.00 6.00 1.00 9.00 2.00
1.00 2.00 3.00 4.00 5.00 6.00 7.00 9.00

10.4 类模板

10.4.1 类模板

  既然10.3节介绍了如何将“函数”泛型化并调用,那么本10.4节就来介绍如何将“类”泛型化并实例化。和前面“函数泛型化”的过程类似,“类模板”(Class Template)就是将类中某些类型变为泛型,从而定义一个模板。可以泛型化的类成员包括:

  1. 数据域成员。
  2. 函数成员:返回值类型、参数类型、局部变量类型都可以成为泛型。
图10-4 类的泛型化

  上图是将一个int型的栈StackOfIntegers泛型化成Stack。可以看到,elements类型以及所有要存取数据的函数类型都进行了泛化;当然size类型和getSize()类型也可以泛化成模板类型,但是没有必要。下面的伪代码则针对上图所示,演示了类模板的语法。注意在类外定义成员函数时,类名后面应该加上<类型参数>

/**************类的声明*************/
template<typename T>
class Stack {
public:
    Stack();
    bool empty();   //演示1
    T peek();       //演示2
    T push(T value);
    T pop();
    int getSize();
    private:
    T elements[100];
    int size;
};
/***********类成员函数的定义*********/
//返回值不是模板类型
template<typename T>
bool Stack<T>::empty() {
    return (size == 0);
}
//返回值是模板类型
template<typename T>
T Stack<T>::peek() {
    return elements[size - 1];
}

下面使用代码来展示一下“类模板”,任务如下:

任务1:基于Unit05的StackOfIntegers,将之改造为泛型。
任务2:创建一个字符栈,将一个字符串的内容反转输出。

头文件 Stack.h

#pragma once
//类声明
template<typename T>
class Stack {
private:
    T elements[100];//栈最大容量为100
    int size{ 0 };
public:
    Stack();        //构造函数
    bool empty();   //判断栈是否为空
    T peek();       //读取栈顶元素
    T push(T value);//压入元素
    T pop();        //弹出栈顶元素
    int getSize();  //获取当前栈大小
};
//类外定义成员函数,注意类模板的声明和定义要放在一起
template<typename T>
Stack<T>::Stack() {
    size = 0;
    for (auto&i : elements) {
        i = 0;
    }
}
template<typename T>
bool Stack<T>::empty() {
    return (size == 0 ? true : false);
}
template<typename T>
int Stack<T>::getSize() {
    return size;
}
template<typename T>
T Stack<T>::peek() {
    return elements[size - 1];
}
template<typename T>
T Stack<T>::pop() {
    T temp = elements[size - 1];
    elements[size - 1] = 0;
    size--;
    return temp;
}
template<typename T>
T Stack<T>::push(T value) {
    elements[size] = value;
    size++;
    return value;
}

源文件 GenericStack.cpp

#include<iostream>
#include<string>
#include"Stack.h"
int main() {
    Stack<char> c;
    std::string s{ "Hello,World!" };
    //将字符串压栈
    std::cout << "反转前:";
    for (auto i : s) {
        c.push(i);
        std::cout << i;
    }
    std::cout << std::endl;
    //将字符串依次弹出
    std::cout << "反转后:";
    for (; c.empty() != true;) {//注意这个for循环的控制很奇妙
        std::cout << c.pop();
    }
}

运行结果:

反转前:Hello,World!
反转后:!dlroW,olleH

编程感想:

  1. 关于改名。上面的代码有个操作是将StackOfIntegers全部改名成Stack,一个一个改很麻烦。若为函数,可以直接在函数名上右键“快速操作与重构”;但本例中是类名,所以只能在类名右键“重命名”。

10.4.2 类模版实例化

  上一小节最后的演示代码中,main()函数中实例化了类模板Stack<char> c,本小节就展开讲讲“类模板实例化”,也就是编译器是如何根据模板生成具体的类代码的过程。显然,只要是对类模版进行实例化,编译器就会生成一个类。类的实例化包括也分为“显示实例化”、“隐式实例化”:

  1. 显式实例化:比如要将上一小节的Stack类模板实例化,可以直接定义语句template class Stack<int>;,就可以将类模板显式实例化为一个处理int类型的Stack类。这个新生成的类实例的名称由编译器按规则生成,实际上非常复杂,但是为了叙述方便我们可以假设为IntStack
  2. 隐式实例化:并不专门使用templateclass关键字进行实例化,而是在定义一个类的对象的时候进行隐式实例化。注意下面的Stack<char>会生成一个不知道名字的类(假设是CharStack),而charStack就是这个类的实例对象:
//1. 实例化成char型
Stack<char>  charStack;
// 先实例化一个CharStack类(名字由编译器按规则生成):class CharStack { … char elements[100]; … };
// 然后用 CharStack charStack; 创建一个对象

//2. 实例化成int型
Stack<int>  intStack;
// 先实例化一个IntStack类:class IntStack { … int elements[100]; … };
// 然后用 IntStack intStack; 创建一个对象

//3. 初始化语句自动推导成int型
Stack intStack{1, 2, 3};//实例化为 Stack<int>
// C++17,模板类型参数根据初始化语句自动推导(std::vector就是类模板)

10.5 模板参数、模板类型

10.5.1 默认类型与非类型参数

  还记得在10.3节“选择排序”非泛型化时,用了一个template <int N>来声明double原生数组的大小,当时说这不是模板类型,这其实是“非类型参数”,本节就来介绍一下这部分内容。非类型参数(Non-type Parameters)在模板前缀中使用,并且在实例化模板时需要指定一个实际的对象作为非类型参数实参。下面的伪代码演示非类型参数:

//示例1:值作为非类型参数
template<typename T, int capacity>
class Stack{
    //...
private:
    T elements[capacity];
    int size;
};
Stack<char, 100> charStack; //定义的时候要写清楚非类型参数的实参

//示例2:类对象作为非类型参数
template<typename T, Color c>
class Label{
    //...
};
Color color(0, 0, 255);     //先定义好作为非类型参数实参的类对象
Label<char, color> label;   //隐式实例化类模板

注意C++中的std::array就是一个模板类型,在定义时需要写std::array<类型,数组大小>。比如定义一个大小为10的整型数组,起名叫liststd::array<int, 8> list

  除了上一段介绍的“非类型参数”外,还有一个概念是“默认类型”(Default type),也就是类模板中的“类型参数”也可以指定一个默认类型,但注意 只能在“类模板”中使用默认类型,而不能在“函数模板”中使用默认类型! 比如指定泛型类Stack的默认类型为int,这样做的好处就是可以很方便的直接隐式生成该类型的类对象

//指定泛型类的默认类型为int
template<typename T = int>
class Stack{
    //...
};
//用默认类型定义一个对象
Stack<> stack;//等价于 Stack<int> stack;

下面基于上一节的stack类模板代码,展示模板中的“默认类型参数”和“非类型参数”:

任务1:基于stack类模板,展示默认类型参数。
任务2:基于stack类模板,展示非类型参数,以及非类型参数的默认值。

头文件 Stack.h

#pragma once
//类声明
template<typename T=char, int N=100>
class Stack {
private:
    T elements[N];//栈默认最大容量为100
    int size{ 0 };
public:
    Stack();        //构造函数
    bool empty();   //判断栈是否为空
    T peek();       //读取栈顶元素
    T push(T value);//压入元素
    T pop();        //弹出栈顶元素
    int getSize();  //获取当前栈大小
};
//类外定义成员函数,注意类模板的声明和定义要放在一起
template<typename T,int N>
Stack<T,N>::Stack() {
    size = 0;
    for (auto&i : elements) {
        i = 0;
    }
}
template<typename T, int N>
bool Stack<T, N>::empty() {
    return (size == 0 ? true : false);
}
template<typename T, int N>
int Stack<T, N>::getSize() {
    return size;
}
template<typename T, int N>
T Stack<T, N>::peek() {
    return elements[size - 1];
}
template<typename T, int N>
T Stack<T, N>::pop() {
    T temp = elements[size - 1];
    elements[size - 1] = 0;
    size--;
    return temp;
}
template<typename T, int N>
T Stack<T, N>::push(T value) {
    elements[size] = value;
    size++;
    return value;
}

源文件 GenericStack.cpp

#include<iostream>
#include<string>
#include"Stack.h"
//创建一个字符栈,将一个字符串的内容反转输出
int main() {
    Stack<> c;
    std::string s{ "Hello,World!" };
    //将字符串压栈
    std::cout << "反转前:";
    for (auto i : s) {
        c.push(i);
        std::cout << i;
    }
    std::cout << std::endl;
    //将字符串依次弹出
    std::cout << "反转后:";
    for (; c.empty() != true;) {//注意这个for循环的控制很奇妙
        std::cout << c.pop();
    }
}

运行结果:

反转前:Hello,World!
反转后:!dlroW,olleH

10.5.2 模板与继承

  既然有“类模板”的存在,那就很自然的想到“类模板”的继承是什么样子的呢?本小节就来介绍“类模板”的继承原则。注意到模板并不是一个类,于是“模板和继承”就会有一些特殊的原则:

  1. “普通类”可从“类模板实例”继承,而不能直接从“类模板”继承,如下图左一。
  2. “模板”可从“普通类”继承,如下图左二。
  3. “类模板”可从“类模板”继承,如下图右一。
图10-5 模板的继承原则
上图中T1、T2为类模板,C为普通类

10.5.3 何时何地使用模板

  回顾本章,较为详细介绍了“模板”,那么在实际开发时,应该在何时使用模板编程呢?下面列出两种不得不使用模板的情况:

  1. 使用别人写好的模板库时,如STL(Standard Template Library)中的std::arraystd::stringstd::vector等;或者第三方库Boost里有很多实验性的代码来测试很多新的机制,该库将模板的技巧用到了极致。
  2. 对不同类型的数据做类似处理(算法、容器、遍历等)。

但需要注意的是,一般只有那些开发标准库的作者才会希望获得“模板大师”的称号,我们在进行一般的应用开发时,还是不要过量使用模板,除非你想:

  1. 编写出团队中其他人都无法理解的代码。
  2. 编写出7天后无法理解的代码。
  3. 代码性能比可维护性更重要。
  4. 把“模板元编程”列为简历上的一项技能。
  5. 编写不太可能在许多实际编译器上运行的代码。

  虽然上面说在应用开发过程中,不推荐过度使用模板,但是C++并不是一个完全面向对象的编程语言。之前提到,“泛型编程”在C++中广泛使用,经常可以取代“面向对象编程”。几乎整个C++标准库都依赖于泛型编程,比如C++标准库中的sort函数使用了泛型编程;在C++标准库较少使用继承和运行时多态,异常、字符串和IO流中使用了较多的继承。于是相比于C++,Java则大量使用了继承,所以Java是一个面向对象的编程语言

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/720668.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于matlab使用深度学习从分割图生成图像(附源码)

一、前言 此示例演示如何使用 pix2pixHD 条件生成对抗网络 &#xff08;CGAN&#xff09; 从语义分割映射生成场景的合成图像。 Pix2pixHD [1] 由两个同时训练的网络组成&#xff0c;以最大限度地提高两者的性能。 生成器是一种编码器-解码器风格的神经网络&#xff0c;可从语…

设置云服务器和配置docker

一、设置云服务器 刚租完服务器&#xff0c;直接利用公网ip登录此时进入到的是root目录下 ssh root公网ip 但是root的权限太大&#xff0c;一般做项目不会在root路径下直接操作&#xff0c;会创建一个子用户&#xff0c;一台服务器可以创建多个子用户&#xff0c;就像一个大…

通用二进制方式安装Mysql

一、去官网下载MySQL glibc版本 示例环境为CentOS 7.9版本&#xff0c;要安装的Mysql版本为5.7 1.选择版本下载到自己本地 下载地址&#xff1a;https://dev.mysql.com/downloads/mysql/ 2.将下载的tar包上传到自己的CentOS虚拟主机上 传输完成后&#xff0c;查看确认一下 …

Tomcat相关

1. 运行项目 将java项目打包为war或者war所对应的文件夹&#xff0c;放置于tomcat的webapps目录下。其实tomcat运行时会解压war到项目中并运行class文件&#xff0c;延伸开来&#xff0c;为啥不能用jar包&#xff0c;因为jar可能可以表示项目但也能表示依赖&#xff0c;tomcat…

Linux 内核和驱动开发工程师的发展前景怎么样?

或许这样的标题&#xff0c;应该是由像Linus或Greg KH这样的大师级的高手才有资格写的吧。但是作为我来说&#xff0c;也许我更想把这个标题作为一个疑问句来使用&#xff0c;整理一下自己的认识&#xff0c;用来勉励自己&#xff0c;和大家一起努力实现这个目标。认识肤浅的地…

4.51ue4:savegame

1.创建savegame&#xff0c;蓝图搜索savegame创建 2.ue4是类似于计算机磁盘读取和内存响应的方式进行保存数据&#xff0c;又称序列化。 详解&#xff1a; 序列化时类似于&#xff1a;从内存保存数据到磁盘的过程&#xff0c;是将数据进行序列化存入磁盘。 读取数据就是反序…

社区说|浅谈 WorkManager 的设计与实现:系统概述

什么是 社区说 ? 反思 系列博客是一种看似 “内卷” &#xff0c;但却 效果显著 的学习方式&#xff0c;该系列起源和目录请参考 这里 。 困境 作为一名 Android 开发者&#xff0c;即使你没有用过&#xff0c;也一定对 WorkManager 耳熟能详。 自2018年发布以来&#xff0c…

设计模式第22讲——访问者模式(Visitor)

目录 一、什么是访问者模式 二、角色组成 三、优缺点 四、 应用场景 4.1 生活场景 4.2 Java场景 五、代码实现 5.0 UML类图 5.1 抽象访问者——Visitor 5.2 具体访问者——Tourist 5.3 抽象元素——Spot 5.4 具体元素——View、Relic 5.5 对象结构——SpotCollecti…

大厂面试打起12万分小心?3轮技术面过,你也可能挂在HR手上!

很多朋友在面试大厂时存在一个误区&#xff0c;认为面试你的那个是最初给你打电话的HR&#xff0c;其实不然&#xff0c;更大可能是业务部门相关的 HRBP导致你面试失败。 1、什么是HRBP? 为了解释清楚这个问题&#xff0c;先说 HRBP 是什么。HRBP全称为 Human Resource Busin…

java对象clone

Object提供了colne方法给我们定义的类&#xff0c;用来进行对象克隆&#xff0c;但是这个clone方法是protected的&#xff0c;所以需要在我们需要使用clone的类中重写Object的clone方法&#xff0c;并且需要实现Cloneable接口&#xff0c;Cloneable接口是一个标记接口&#xff…

「JVS低代码开发平台2.1.8版本」-首页功能介绍

JVS是面向软件开发团队可以快速实现应用的基础开发脚手架&#xff0c;主要定位于企业信息化通用底座&#xff0c;采用微服务分布式框架&#xff0c;提供丰富的基础功能&#xff0c;集成众多业务引擎&#xff0c;它灵活性强&#xff0c;界面化配置对开发者友好&#xff0c;底层容…

【Java】Netty中ByteBuf学习笔记

文章目录 1) ByteBuf创建与自动扩容2&#xff09;直接内存 vs 堆内存3&#xff09;池化 vs 非池化4&#xff09;组成5&#xff09;写入6&#xff09;扩容7&#xff09;读取8&#xff09;retain & release9&#xff09;slice10&#xff09;duplicate11&#xff09;copy12&am…

给LLM装上知识:从LangChain+LLM的本地知识库问答到LLM与知识图谱的结合

第一部分 基于LangChain ChatGLM-6B的本地知识库问答的应用实现 1.1 什么是LangChain&#xff1a;连接本地知识库与LLM的桥梁 作为一个 LLM 应用框架&#xff0c;LangChain 支持调用多种不同模型&#xff0c;提供相对统一、便捷的操作接口&#xff0c;让模型即插即用&#x…

java并发编程 6:java内存模型与volatile(重点)

目录 硬件内存模型Java 内存模型主内存工作内存内存交互的八个原子操作JMM作用 可见性退不出的循环volatile解决同步模式之 Balking 有序性指令重排解决指令重排 volatile 原理volatile如何保证可见性volatile如何保证有序性volatile 不能解决指令交错double-checked locking 问…

2023/7/5总结

JS BOM 是浏览器对象模型 window对象是一个全局对象&#xff0c;也是JavaScript的顶级对象 所以通过var定义在全局作用域中的变量、函数都会变成window对象的属性和方法 定时器-延时函数 setTimeout(回调函数&#xff0c;等待的毫秒数) 延时函数只会执行一次 清除延时函…

Java基础---String的长度限制

目录 典型回答 常量池限制 运行期限制 典型回答 String有长度限制&#xff0c;编译期和运行期不一样编译期需要用 CONSTANT_Utf8_info 结构用于表示字符串常量的值&#xff0c;而这个结构是有长度限制&#xff0c;他的限制是65535运行期&#xff0c;String的length参数是Int…

Jenkins 配置用户角色和权限

1、配置条件&#xff1a; 1&#xff09;已安装Jenkins&#xff0c;当前案例使用版本&#xff1a;2.319.3 2&#xff09;已成功进入Jenkins&#xff0c;并新建用户&#xff1a;dev_java 2、安装插件【系统管理-插件管理-搜索-可选插件】&#xff1a;Role-based Authoriz…

JavaScript 进阶 - 第4天

JavaScript 进阶 - 第4天笔记 文章目录 JavaScript 进阶 - 第4天笔记1 深浅拷贝1.1 浅拷贝1.2 深拷贝&#xff08;面试&#xff09;1.2.1 递归实现深拷贝1.2.2 js库lodash里面cloneDeep内部实现了深拷贝1.2.3 JSON序列化 2 异常处理2.1 throw 抛异常2.2 try ... catch 捕获异常…

Linux之Kafka保姆式详细安装教程

下载Kafka 《Kafka官网下载》 注意&#xff1a;下载的是二进制文件&#xff0c;不要下载源码&#xff01;这里可以采用第三方下载工具加速下载&#xff0c;如&#xff1a;迅雷等 上传到Linux服务器的/data/目录下进行解压 tar -zxvf是解压文件命令&#xff0c;-C表示把解压…

【C++学习笔记】1.6 引用

目录 &#x1f36f;1.6 引用 &#x1f95d;1. 引用的概念 &#x1f95d;2. 引用的特性 1、引用在定义时必须初始化 2、一个变量可以有多个引用 3、引用一旦引用一个实体&#xff0c;再不能引用其他实体 &#x1f95d;3. 常引用 1、取别名的原则&#xff1a;对原引用的…