一、模板编程#

1.模板编程的必要性#

在c++中,变量的声明必须指出它的类型,提高了编译运行效率,但是在某些场合下就有点缺陷。比如:需要定义计算两个数之和的函数,由于未来计算的数值有可能是整数、也有可能是浮点数,所以需要为这些类型准备对应的函数,但是这些函数的内部逻辑都是一样的,他们的唯一区别就是所接收的数据类型不同而已。那么有没有一种情况使得编码的时候暂时的忽略掉类型这个特性,等运行时在动态决定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

using namespace std;

int add(inta ,int b){
    return a + b;
}

float add(float x , float y ){
    return x + y;
}

int main(){

    int result = add(3, 4 ) ;
    cout << "result  = " << result << endl;

    int result2 = add(3.5 ,4.5 );
    cout << "result2 = " << result2 << endl;

    return 0 ;
}

2. 函数模板#

函数模板是通用函数的描述,使用泛型来定义函数,让编译器暂时忽略掉类型,使用参数把类型传递给模板,进而让编译器生成对应类型的函数。函数模板仅仅是表示一种模型罢了,并不是真正可以直接调用的函数,需要在使用的时候传递对应类型,生成对应类型的函数。

模板的定义以template关键字开始,后面跟随着参数列表,使用<> 包含

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

template<typename T>
T add(const T& t1 ,const T& t2){
    return t1 + t2;
}

int main(){

    int result = add<int>( 3, 5);
    cout <<"result = " << result << endl;


    int result = add( 3, 5);
    cout <<"result = " << result << endl;

    return 0 ;
}

1. 函数模板重载#

如普通函数一般,函数模板也可以重载,以便灵活应对更多的需求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

using namespace std;

template<typename T>
T add(const T& t1 ,const T& t2){
    return t1 + t2;
}

template<typename T>
T add(const T& t1 , const T& t2 , const T& t3){
    return t1 + t2 + t3;
}

int main(){

    int result1 = add( 1, 2 );
    int result2 = add( 1, 2 ,3);

    cout <<"result1 = " << result1 << endl;
    cout <<"result2 = " << result2 << endl;

    return 0 ;
}
  • 模板可变参数

如果一个函数接收的参数个数不确定,并且都是同一种类型的参数,那么可以使用可变参数的写法来简化代码。 可变参数使用 ... 来表示,通常可变参数也称之为 参数包 ,用于表示它可以对多个参数打包成一个整体。

3. 可变参数#

在C++ 中一般函数的参数个数都是固定的,但是也有一种特殊的函数,他们的参数个数是可变的。针对这种情况C++中提供了initializer_list省略号两种方法,其中initializer_list要求可变参数的类型必须都一致,而省略号方式则不具备这种限制,要更加灵活。

1. 省略号方式#

要处理省略号方式的可变参数,使用四个宏的宏va_startva_argva_endva_list 需要导入 #include , 而且这种方式没有办法计算出来可变参数的个数,需要在前面指定,并且可变参数的个数和实际传递的参数个数必须一致,否则可能结果有偏差。省略号的方式,不限定参数个数,也不限定参数类型,可以是不同的数据类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//num表示有几个参数, ... 表示右面具体的参数
// 调用:add (3, "aa","bb","cc");

int add(int count, ...) {
    //count 表示可变参数个数
    va_list vl;//声明一个va_list变量
    va_start(vl, count);//初始化,第二个参数为最后一个确定的形参

    int sum = 0;
    for (int i = 0; i < count; i++)
        sum += va_arg(vl, int); //读取可变参数,的二个参数为可变参数的类型

    va_end(vl);    //清理工作
    return sum;
}

int main(){

    int result = add(3 , 1 , 2, 3 );
    cout << "result = " << result << endl;

    return 0 ;
}

2. initializer_list 方式#

c++ 11引入的一种方式,它需要导入#include , 参数必须放在一组{} 里面,并且元素的类型必须是一样的。相比于前面省略号的方式,initializer_list 就显得简单许多。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

int add(initializer_list<int> il) {
    int sum = 0;
    for (auto ptr = il.begin(); ptr != il.end(); ptr++){ 
        sum += *ptr;
    }
    return sum;
}

int main(){
    int result = add({10,20,30}); //传递的参数必须放置在一个 {} 里面

    cout << "result = " << result << endl;

    return 0 ;
}

4. 可变参数模板#

函数模板解决了相同功能、不同数据类型造成多个方法重载的局面,而当模板的参数类型都是一样的,或者有多个一样的参数类型一样,那么使用可变参数来改进模板函数,就显得更为美观。对于参数包来说,除了获取大小之后,程序员更想关注的是,如何获取里面的数据,解构里面的元素,也有个名字:扩展

1. 定义可变参数函数模板#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

using namespace std;

//定义一个add函数,表示接受一种类型的可变参数。
// 上面的Args : 模板参数包,表示可以穿很多种类型的参数这里是用于描述类型
// 下面的args1 : 函数参数包 在调用函数的时候,表示可以传很多数据进来,与上面的类型匹配。

template <typename ...Args>
int add(Args...args1){
   int a =  sizeof...(args1);
   cout <<"传递进来的参数有:"<< a << endl;
}

int main(){
    //Args是: int ,int ,int,string ,string ,string
    // args1 : 2, 3, 4, a, b, c
    add(2,3,4,"a","b","c");

    return 0 ;
}

2. 展开参数包#

可变参数从设计角度是方便了,但是在获取传递过来的参数却有点棘手,因为索引下标在展开参数包场景下没有用,而且可变参数可以看成是一个包裹,里面包装了传递过来的若干个实参。通常展开参数包的做法是采用递归,不断的调用,每调用一次,减少一个实参,最终把所有实参都捕获出来。

1. 什么是递归#

递归在程序语言中简单的理解是:函数自己调用自己。但是递归需要有一个出口(也就是终止递归的条件),否则有可能陷入无限循环调用那种,进而造成栈溢出异常。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

using namespace std;

//递归实现累加效果。
int sum(int n) {
   if (n == 1) {
       return 1;
   } 
   return sum(n - 1) + n;
}

int main(){

    int count = 0 ;
    for(int i =1 ; i <= 10 ; i++){
        count += i ;
    }
    cout <<"使用for循环计算的结果是:" << count <<endl;



    int result = sum(10);
    cout << "从1 加到 10 的和是: " << result << endl;
    return 0 ;
}

2. 递归展开参数包#

递归展开参数包的核心理念就是每次调用函数本身都对第一个参数处理,再将余下的内容向下传递。这样在每一层的调用中,都能够拿到全新的第一个参数,直到最后的没有参数可取。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

using namespace std;

//这里定义一个空的函数,目的是为最后的递归终止。
int add(){return 0 ;}
template <typename T, typename ...Args>


int add(T t , Args...args){

    cout <<"打印的数字时:" << t << endl;

    //开始这里循环调用自己,知道最后那个没有参数的,才会调用外面的add()
    t += add(args...);
    return t;
}
int main(){

    cout << add(1,2,3,4,5) << endl;

    return 0 ;
}

5. 类模板编程#

有时候继承、包含并不能满足重用代码的需要,这一般在容器类里面体现的尤为突出。例如: 我们定义了一个容器类,Container, 这个Container类可以实现类似verctor一样的工作,能保存数据,能修改数据,并且数据的类型不限制,但是针对数据的操作都是一样的。那么类模板编程就成了不二之选了。

1. 类模板编程的必要性#

这里以栈作为参照对象,定义一个模板类,实现栈一样的功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Stack{
private :
    enum{MAX = 10}; //表示这个Stack容器最多只能装10个。
    int top =0 ; //表示最顶上的索引位置
    string items[MAX]; //定义一个数组,以便一会装10个元素
public:
    bool isempty(){
        return top == 0;
    }
    bool isfull(){
        return top == MAX;
    }
    //压栈
    int push(string val){
        if(isfull()){
            return -1;
        }
        //没有满就可以往里面存
        items[top++] = val;
    }
    //出栈
    string pop(){
        if (isempty()){
            return "";
        }
        //如果不是空 top 只是指向位置,而数组获取数据,索引从0开始,所以先--
        return items[--top] ;
    }

    string operator[](int index){
        if(isempty() || index > --top){
            cout <<"容器为空或者超出越界" << endl;
            return "";
        }
        return items[index];
    };
};

2. 类模板#

上面的Stack容器仅仅只能针对string这种数据类型,如果想存自定义的类型或者其他类型,那么Stack就无法满足了。 要定义类模板,需要在类的前面使用template , 然后替换里面的所有string 即可,这样Stack就能为所有的类型工作了。 如果是自定义类型,那么需要自定义类型提供无参构造函数,因为数组的定义会执行对象的构造。若想避免构造的工作发生,可以使用allocator来操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
template <typename T> class Stack{
private :
    enum{MAX = 10}; //表示这个Stack容器最多只能装10个。
    int top =0 ; //表示最顶上的索引位置
    T items[MAX]; //定义一个数组,以便一会装10个元素
public:
    bool isempty(){
        return top == 0;
    }
    bool isfull(){
        return top == MAX;
    }
    //压栈
    int push(const T& val){
        if(isfull()){
            return -1;
        }
        //没有满就可以往里面存
        items[top++] = val;
    }
    //出栈
    T pop(){
        if (isempty()){
            return "";
        }
        //如果不是空 top 只是指向位置,而数组获取数据,索引从0开始,所以先--
        return items[--top] ;
    }

    T operator[](int index){
        if(isempty() || index > --top){
            cout <<"容器为空或者超出越界" << endl;
            return "";
        }
        return items[index];
    };
};