c++之模板编程

模板编程,容器,多线程

一、模板编程

1.模板编程的必要性

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

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

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

2. 函数模板

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

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

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


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


int result = add( 3, 5);
cout <<"result = " << result << endl;
  • 函数模板重载

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

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 result1 = add( 1, 2 );
int result2 = add( 1, 2 ,3);

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

  • 模板可变参数

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

3. 可变参数

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

  • 省略号方式

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

//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;
}
  • initializer_list 方式

c++ 11引入的一种方式,需要导入#include <initializer_list> , 参数必须放在一组{} 里面,并且元素的类型必须是一样的。

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

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

4. 可变参数模板

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

1. 定义可变参数模板

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

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

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

2. 展开参数包

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

  • 递归调用

这是一个有缺陷的代码,因为它陷入了无限递归当中。虽然是错误的代码,但是却为解开参数包提供了一个思路。核心理念就是解开参数包,对第一项处理,再将余下的内容向下传递。这样在每一层的调用中,都能够拿到全新的第一个数字。

int add(Args...args){
   add(args...) //args1...表示参数包,就是传递过来的所有参数的包裹。
}
  • 递归调用
//这里定义一个空的函数,目的是为最后的递归终止。
int add(){return 0 ;}
template <typename T, typename ...Args>
int add(T t , Args...args){

    //int sum = 0;
    cout <<"打印的数字时:" << t << endl;
    
    //开始这里循环调用自己,直到最后那个没有参数的,才会调用外面的add()
    t += add(args...);
    return t;
}
int main(){
	cout << add(1,2,3,4,5) << endl;
}

5. 类模板编程

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

1. 定义模板类

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

  • 原始代码
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];
    };
};
  • 类模板

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

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];
    };
};

6. 练习

模拟vector实现自定义容器

  1. 底层使用数组实现
  2. 数组开辟空间,使用allocator ,否则会一块执行对象构造工作
  3. 能装任意类型
  4. 提供push_back函数,添加元素到容器末端
  5. 提供pop函数,返回容器的最前端元素
  6. 提供[]操作符,根据下标可以获取元素
  7. 提供size函数
  8. 提供*操作符 ,以便修改指定元素。如:

Stu stu = myvector[0];

*stu.name =“张三”;

发布了40 篇原创文章 · 获赞 3 · 访问量 1387

猜你喜欢

转载自blog.csdn.net/hongge_smile/article/details/104247920
今日推荐