跳转至

模版template

C++语言模版是一个重要内容,

有两种模版:

  1. 类模版
  2. 函数模版

函数模版在编译的时候,会自动的实例化,创建不同的函数。

template <class T>
class X {
  ...
};

重要的是template关键字。

类模版

数组越界问题

C++数组不检查越界。

自己写一个class,把数组封装起来,检查不允许越界。

class Array
{
    enum {size = 10};
    int A[size];
public:
    int& operator[](int index)//index 下标,要检查是否越界  内联的函数
    {
        //对下标进行检查,下标大于等于0,小于size
        return A[index];//如果没有越界,就返回数组里的数据。
    }
};
//调试
void test() {
    Array b;
    std::cout << b[8] <<std::endl;//没越界
    std::cout << b[10] <<std::endl;//越界,会检查,然后处理
}

这样封装的就比C++的好一些。

但是C++类型非常多,整型,浮点型,等等。要针对每一个类型做一个数组class。比较繁琐,写很多class。代码都是重复的。

可以使用继承,但是继承会比较复杂。简单的问题复杂化了。比较好的解决办法就是模版。

//这是一个模版,模版不是一个真正的类,使用模版进行实例化,C++会创建一个类出来。
template<class T>//T是模版的替换参数,代表一个类型
class Array
{
    enum {size = 10};//枚举常量
    T A[size];
public:
    T& operator[](int index);//重载下标运算符。index:下标。要检查是否越界
//    {
//        //对下标进行检查,下标大于等于0,小于size
//        return A[index];//如果没有越界,就返回数组里的数据。
//    }
};

//函数写到类外面,比较复杂,还要写`template<class T>` 和`Array<T>::`,写到类里面是内联函数比较简单。
template<class T>
T& Array<T>::operator[](int index)//重载下标运算符。index:下标。要检查是否越界
{
    //对下标进行检查,下标大于等于0,小于size
    return A[index];//如果没有越界,就返回数组里的数据。
}

//调试
void test() {
    Array<int> ia;//当这样写的时候,C++会帮我们从上面的模版,用int把所有的T换掉。
    Array<float> fa;
    //根据需要 C++会帮我们创建不同的类。非常灵活。

    for (int i = 0; i < 10; i++) {
        ia[i] = i * i;
        fa[i] = float(i) * 1.414;
    }
    for (int j = 0; j<10; j++) {
        std::cout << j << ":" << ia[j] << ", " << fa[j] <<std::endl;
    }
}

类模版和普通的类是不一样的,做模版的时候,代码都得放到头文件中,不能在源文件。C++编译的时候会生成类,所以必须是可见的。否则没有办法编译。

函数模版

函数模版实例化函数

做一个简单的函数

做一个交换

void swap(int &a, int &b)//传引用,就可以交换两个int型的,但是如果要交换两个double型的就不可以,只能再写一个同名函数重载

//做一个简单的函数
//做一个交换
void Swap(int &a, int &b)//传引用
{
    int temp = a;
    a = b;
    b = temp;
}
void Swap(double &a, double &b)//传引用
{
    double temp = a;
    a = b;
    b = temp;
}
调用:
void test()
{
    int x = 10, y = 20;
    double m = 1.23, n = 9.78;

    Swap(x, y);
    std::cout << x << "," << y << std::endl;
    Swap(m, n);
    std::cout << m << "," << n << std::endl;
}

C++类型特别多,int,double,还可以定义新的类型Dog,Cat等等。

这样一个函数就要针对每一种类型都要写一个同名的函数重载,工作都是重复的。(重复是万恶之源)。不应该这样做。

可以用C++的模版,做一个函数模版。C++在编译的时候根据模版动态的生成需要的函数。用到哪一个C++会自动的创建哪一个。

模版函数:
//模版
template<class T>
void Swap(T &a, T &b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;
}
调用
void test()
{
    int x = 10, y = 20;
    double m = 1.23, n = 9.78;

    Swap(x, y);
    std::cout << x << "," << y << std::endl;
    Swap(m, n);
    std::cout << m << "," << n << std::endl;
}

函数模版经常会用到,非常实用。

交换函数的这个模版C++已经做好了,有内置的。做好的是小写的swap。所以自己写的这个交换模版的函数名字首字母要大写。

做一些通用的函数,对各种类型,考虑用函数模版写。

C++容器算法模版。stl标准函数模版。

模版中的常量

模版的参数中可以使用C++内置类型(常量)。还可以设置默认值。

模版变成类的时候C++会把int size = 10变成常量,所以size是不可以修改的。

template<class T, int size = 100>//模版常量size
class Array
{
        //enum {size = 10};//枚举常量
    T A[size];
public:
    ...
};
template<class T, int size = 10>
class Array
{
    //enum {size = 10};//枚举常量
    T array[size];
public:
    T& operator[](int index);//重载下标运算符。index:下标。要检查是否越界
    int length() const { return size; }//成员函数,返回长度。
};

template<class T, int size>//这里不用写size = 1
T& Array<T, size>::operator[](int index)//重载下标运算符。index:下标。要检查是否越界
{
    //对下标进行检查,下标大于等于0,小于size
    return array[index];//如果没有越界,就返回数组里的数据。
}

//再做一个Number类 封装float
class Number
{
    float f;
public:
    //类型转换,float转为Number
    Number(float ff = 0.0f) : f(ff) {}//构造函数,做类型转换用的
    Number& operator=(const Number& x)//赋值运算符重载
    {
        f = x.f;
        return *this;
    }
    //Number转float
    operator float() const {return f;}
    //友元函数 用来输出,也是用运算符重载 流输出运算符重载
    friend std::ostream& operator<<(std::ostream& os, const Number& x)
    {
        return os << x.f;
    }
};

//测试
void test() {
    Array<int, 100> ia; //第二个参数,如果不写size就是默认的10,写了就是写的值100,可以重新定义.
    Array<Number, 50> a;
    Number n = 1.23;
    std::cout << n <<std::endl;//输出运算符 使用运算符重载

    float f = n;//Number变成float
    std::cout << f <<std::endl;//输出运算符 使用运算符重载

}

例二:

再做一个模版

//创建一个容器Holder类
template<class T, int size = 20>
class Holder
{
    Array<T,size>* np;//数组 指针指向数组
public:
//    Holder()
//    {
//        np = new Array<T, size>;//在构造函数中初始化数组,这是其中的一种方法
//    }
    Holder():np(0){}
    T& operator[](int i)//重载下标运算符。index:下标。
    {
        //第一次使用的时候,初始化数组,使用这种方法比较好
        if (!np)  new Array<T, size>;
        return np->operator[](i); //调用数组ARRAY的运算符重载
    }
    int length() const { return size; }//成员函数,返回长度。
    ~Holder() {delete np;}
};

//测试
void test() {
    Holder<Number> h;//调用构造函数创建对象,这时候数组还没有创建。
    for (int i = 0; i<20; i++) {
        h[i] = i * 10;
    }
    //数组中的元素 显示出来
    for (int j = 0; j < 20; j++) {
        std::cout << h[j] <<std::endl;
    }
}

模版化的指针Stash

模版要求所有的代码都放在头文件里,不能在源文件里写。

头文件代码:

namespace ThinkingInCppDemoLib {
    template<class T, int incr = 10>//stash存储空间不够的时候,每次增大incr10,定义一个常量,默认10.
    class PStash
    {
    private:
        int quantity;   // 数量。一共可以保存多少个。
        int next;           // 已经保存了多少个。
        T** storage;    //通过指针 在堆上动态的创建对象,分配内存。可以是整型数据,字符串数据等各种类型。普通的stach有size大小,指针的stach没有size,因为是T* 大小是一个指针的大小。
        void inFlate(int increase = incr);//增大默认的大小
    public:
        PStash() : quantity(0),next(0),storage(0){}

        int add(T* element);

        T* operator[](int index) const;//下标操作

        T* remove(int index);//使用remove移除。因为里面有T*,delete的时候 可能不走析构函数

        int count()
        {
            return next;
        }

        ~PStash();
    };
    template<class T,int incr>
    int PStash<T,incr>::add(T* element)
    {
          //add先判断大小。不够了需要重新分配内存。增大内存。
        if (next >= quantity) {
            inFlate(incr);
        }
        storage[next++] = element;
        return (next - 1);
    }

    template<class T,int incr>
    PStash<T,incr>::~PStash()
    {
        for (int i = 0; i < next; i++) {
            delete storage[i];//delete由客户端去delete,谁使用谁delete。因为析构函数中delete T*有问题。
            storage[i] = 0;//指针变成空指针
        }
        delete [] storage;
    }

    //客户端可以使用remove,通过operator拿到指定下标的指针然后进行delete。
    template<class T,int incr>
    T* PStash<T,incr>::remove(int index)
    {
        T* v = operator[](index);
          //把指针返回并且清零。客户端拿到指针,进行delete。
        if (v != 0) {
            storage[index] = 0;
        }
        return v;
    }

    //下标运算符重载
    template<class T,int incr>
    T* PStash<T,incr>::operator[](int index) const
    {
        //先检查下标index>=0,不能小于0
        if (index >= next) {
            return 0;
        }
        //return必须是有效的指针,不能是0
        return storage[index];
    }
    //追加内存的方法
    template<class T,int incr>
    void PStash<T,incr>::inFlate(int increase)
    {
        assert(increase >= 0);
        const int psz = sizeof(T*);
        T** st = new T*[quantity + increase];
        memset(st,0,(quantity + increase)*psz);
        //把原来的数据copy到新的内存里。
        memcpy(st,storage,quantity*psz);
        quantity += increase;
        //原来的内存空间删除。
        delete [] storage;
        storage = st;
    }
}

//调试:
void test() {
    ThinkingInCppDemoLib::PStash<int> intStash;
    for (int i = 0; i < 25; i++) {
        intStash.add(new int(i));
    }
    for (int i = 0; i < intStash.count(); i++) {
        std::cout << *(int*)intStash[i] << std::endl;
    }
    for (int k = 0; k < intStash.count(); k++) {
        delete (int*)intStash.remove(k);
    }
}

Stack堆栈模板

Stack先进后出

简单的Stack模板

//做的这个stack是固定大小的数组,比较简单。数组大小用模版常量定义。
template<class T, int size = 100>
class StackTemplate
{
    T stack[size];
public:
    void push(const T& i){...}
    T pop(){...}
    ...

};

直接在头文件中写。

不是模版的例子:

//整型堆栈,保存整型
class IntStack
{
    enum {size = 100};
    int stack[size];
    int top;//栈顶
public:
    IntStack() : top(0) {}//构造函数 栈顶初始化为0
    void push(int i) //压入堆栈
    {
        //堆栈是否已经满了
        stack[top++] = i; //top++
    }
    //从堆栈取数据
    int pop()
    {
        //取数据的时候,堆栈不能是空的,堆栈必须有数据
        return stack[--top];
    }
};

然后改成模版:

//堆栈模版,在堆栈里可以保存各种类型的数据。
template<class T, int ssize = 100>
class StackTemplate
{
    T stack[ssize];
    int top;//栈顶
public:
    StackTemplate() : top(0) {}//构造函数 栈顶初始化为0
    void push(const T& i) //压入堆栈
    {
        //堆栈是否已经满了
        stack[top++] = i; //top++
    }
    //从堆栈取数据
    T pop()
    {
        //取数据的时候,堆栈不能是空的,堆栈必须有数据
        return stack[--top];
    }
    //返回堆栈的大小 ,堆栈里有多少数据
    int size() {return top;}
};

测试:

void test() {
    StackTemplate<int,20> is;
    for (int i = 0; i < 20; i++) {
        is.push(i);
    }
    for (int k = 0; k < 20; k++) {
        std::cout << is.pop() <<std::endl;
    }


    //读取文件
    StackTemplate<string> strings;
    ifstream in("main.cpp");
    string line;
    while(getline(in, line))
        strings.push(line);
    while (strings.size() > 0) {
        std::cout << strings.pop() <<std::endl;
    }
}

链表的Stack模版

堆栈里面不是数组,而是链表。

链表长度是动态的。

//堆栈模版,在堆栈里可以保存各种类型的数据。
template<class T>
class Stack
{
    class Link
    {
    public:
        T* data;
        Link* next;
        Link(T* dat, Link* nxt) : data(dat),next(nxt){}
        ~Link(){}
    }* head;
public:
    Stack() : head(0){}
    ~Stack()
    {
        while (head) {
            delete pop();
        }
    }
    void push(T* dat)
    {
        head = new Link(dat,head);
    }
    T* pop()
    {
        if (head == 0) {
            return 0;
        }
        T* result = head->data;
        Link* oldHead = head;
        head = head->next;
        delete oldHead;
        return result;
    }
    T* peek()
    {
        return head ? head->data : 0;
    }
};

堆栈模版可以保存各种类型,包括自定义的class类型。

如果想要放入class Q的子类行,那么Q必须有虚的析构函数(防止销毁的时候,没有销毁,有内存泄漏)。

只要有继承,基类必须有虚的析构函数。

class Q {
public:
    virtual ~Q(){}
};
class R : public Q
{
};

测试:

//测试
void test() {
    ifstream in("main.cpp");
    Stack<string> textlines;
    string line;
    while (getline(in, line)) {
        textlines.push(new string(line));//堆栈里保存的是指针,new得到的是指针
    }
    //pop得到的是指针
    string* s;
    for (int i = 0; i < 10; i++) {
        if ((s = (string*)textlines.pop()) == 0) break;
        std::cout << *s <<std::endl;
        delete s;
    }


    //存自定义class
        Stack<Q> qq;
    for (int j = 0; j < 10; j++) {
        qq.push(new R);//存子类
    }
}