C++泛型编程与类模板

1.泛型编程

C语言中是针对具体的类型编程的,但是C++解决了这样的问题。最典型的就是使用交换函数Swap()的时候:

    //C code
    void SwapInt(int x, int y);
    void SwapDouble(double x, double y);
    //……

我们可以发现一个问题,只要类型不符合swap()的参数,就需要写新的交换函数,让它的参数符合交换数据的类型。尽管在C++中支持“引用”和“重载”大大提高了代码的可读性,但是依旧有一些重复性的本质问题没有解决。因此,C++又增加了一个“模板”的语法,使得不同类型的数据都可以用同一个模板生成的swap()函数,这种编程方法就是泛型编程,这种编程风格能适用大多的类型。

2.函数模板

2.1.基本格式

    template <typename T1, typename T2,, typename Tn>//其中typename可以换成class
    函数返回值 函数名()
    {...}

2.2.隐式实例化

    #include 
    using namespace std;
    template <typename T>//“虚拟类型T”或“广泛类型T”
    void Swap(T& left, T& right)
    {
        T tmp = left;
        left = right;
        right = tmp;
    }
    int main()
    {
        int i = 10;
        int j = 20;
        double x = 3.14;
        double y = 8.9;
        Swap(i, j);//1
        cout << "i,j=" << i << " " << j << endl;
        Swap(x, y);//2
        cout << "x,y=" << x << " " << y << endl;
        return 0;
    }

我们可以看到这个虚拟类型T很像是函数参数,我们姑且可以叫“类型参数”,平时我们使用的参数姑且可以叫“数据参数”,也就是说,C++不仅可以传递“数据参数”还可以传递“类型参数”。

补充:C++其实本身也有一个swap()专门用于交换,底层就是采用函数模板的。

在使用模板的时候需要注意隐式类型转化:

    #include 
    using namespace std;
    template <typename T1, typename T2>//“虚拟类型T”或“广泛类型T”
    T2 Add(const T1& x, const T2& y)
    {
        return x + y;
    }
    int main()
    {
        int a = 10;
        double b = 1.23;
        printf("%lf" ,Add(a, b));
        return 0;
    }

2.3.显式实例化

上述Add()的问题还能通过显式实例化来解决,调用得语句如下:

    #include 
    using namespace std;
    template <typename T>//“虚拟类型T”或“广泛类型T”
    T Add(const T& x, const T& y)
    {
        return x + y;
    }
    int main()
    {
        int a = 10;
        double b = 1.23;
        Add<double>(a, b);//相当于类型也被手动传递过去了
        return 0;
    }

    #include 
    using namespace std;
    template <typename T>//“虚拟类型T”或“广泛类型T”
    T* init(int n)
    {
        T* p = new T[n];
        return p;
    }
    int main()
    {
        init<int>(10);//不用显式实例化是没有办法调用这个函数的
        return 0;
    }

有的时候,隐式实例化会导致无法调用,这里举一个例子:

    #include 
    using namespace std;
    template<class T>
    T* Function(int n)
    {
        return new T[n];
    }
    int main()
    {
        Function(10);//这个函数没有办法调用
        return 0;
    }

2.4.一些新的问题

如果模板和模板的其中一个实例同时存在,则会优先使用实例,下面代码您可以尝试在编译器中调试一下,看看代码的跳转逻辑:

    #include 
    using namespace std;
    template<typename T>
    T Add(T x, T y)
    {
        return x + y;
    }
    int Add(int x, int y)
    {
        return x + y;
    }
    int main()
    {
        int i = 2, j = 3;
        double x = 5, y = 4;
        cout << Add(i, j) << endl;
        cout << Add(x, y) << endl;
        return 0;
    }

当然这种code是不建议写出来的,这里仅仅是作为演示……

2.5.模板参数推演与实例化

上述例子中12处使用的Swap()是否一样呢?不一样!模板会先对参数进行推演然后进行实例化。这两个步骤都交给了编译器来操作,理论上是会增加编译器的一点负担(编译复杂度的提高),但合理使用的话其实还好。

为什么C没有比较官方的数据结构和算法库呢?很大的原因就是因为写出来的库不具有泛化的特点,冗余度很大。

注意:auto不能使用在函数形参部分,不要和模板混淆了。

3.类模板

类模板的使用频率要比函数模板要高,类模板实际上是为了解决typedef的问题的。

    #include 
    using namespace std;
    typedef int STDataType;
    //这里的typedef有点泛型编的意思
    //但不是真正的泛型编程
    //没有办法交给编译器自动替换类型
    //只能自己手动更改类型
    class Stack
    {
    public:
        Push(STDataType x)
        {}
    private:
        STDataType* _data;
        int _top;
        int _capacity;
    };
    int main()
    {
        Stack s1;//存储int的栈
        s1.Push(10);//这个倒是没有什么问题
        Stack s2;//存储float的栈
        s2.Push(10.2);//这里就出现问题了,没有办法存储多种类型
        return 0;
    }

3.1.基本格式

因此我们需要使用类模板,类模板的基本格式如下:

    template<class T1, class T2, ..., class Tn>//也可以写成typename
    class Stack
    {...}
    //需要注意的是,类模板必须使用显式实例化,不能隐式推导

3.2.使用例子

需要我们注意的是,在代码中使用类模板,一定要显式使用:

    #include 
    using namespace std;
    template<class T>//也可以写成typename
    class Stack
    {
    public:
        Stack(size_t capacity = 4)
            :_data(nullptr), _top(0), _capacity(0)//最后一个是为了防止Stack传0的情况    
        {
            if (capacity > 0)
            {
                _data = new T[capacity];
                _capacity = capacity;
                _top = 0;
            }
        }
    private:
        T* _data;
        size_t _top;
        size_t _capacity;
    };
    int main()
    {
        Stack<int> a(10);//1,显式使用
        Stack<char> b(5);//2,显式使用
        Stack<double> c(8);//3,显式使用
        return 0;
    }

使用类模板的时候也需要注意,上述例子中1、2、3使用的也不是同一个类型。接下来让我们把上面得Stack类写得更加完整一些:

    #include 
    #include 
    using namespace std;
    template<typename T>//也可以改typename成class
    class Stack
    {
    public:
        //2.构造函数
        Stack(size_t capacity = 4)//默认在创建对象的时候开辟4个空间
            :_data(nullptr), _top(0), _capacity(0)//初始化列表初始化
        {
            cout << "Stack(size_t capacity = 4)" << endl;//打印,表示调用了构造函数
            if (capacity > 0)//小于0就无法创建一个栈
            {
                _data = new T[capacity];//创建了容量为capacity的数组
                _capacity = capacity;
                _top = 0;
            }
            else
            {
                cout << "栈容量不合法" << endl;
            }
        }
        //3.析构函数
        ~Stack()
        {
            cout << "~Stack()" << endl;
            delete[] _data;//释放资源
            _capacity = _top = 0;
        }
        //4.入栈
        void Push(const T& x)
        {
            if (_top == _capacity)//扩容机制
            {
                size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
                T* tmp = new T[newCapacity];
                if (_data)//如果数组之前申请成功
                {
                    memcpy(tmp, _data, sizeof(T) * _top);//复制一份
                    delete[] _data;
                }
                _data = tmp;
                _capacity = newCapacity;
            }
            _data[_top] = x;
            ++_top;
        }
        //5.出栈
        void Pop()
        {
            assert(_top > 0);
            _top--;
        }
        //6.判栈
        bool Empty()
        {
            return _top == 0;
        }
        //7.取栈顶
        T& Top()//这里最好加上引用,这里还可以替代修改栈顶数据的功能,不过如果在最前面加上const就不行了,这具体看需求
        {
            assert(_top > 0);
            return _data[_top - 1];
        }
    private:
        //1.定义栈类的相关成员变量
        T* _data;//存储栈数据的数组
        size_t _top;//栈顶
        size_t _capacity;//栈容量
    };
    int main()
    {
        //有关try和catch配合new的使用以后我们会提及
        try//这里是申请空间成功的做法
        {
            Stack<int> a(10);
            a.Push(1);
            a.Push(2);
            a.Push(3);
            a.Push(4);
            a.Push(5);
            a.Push(6);
            a.Top()++;
            while (!a.Empty())
            {
                cout << a.Top() << " ";
                a.Pop();
            }
            cout << endl;
            Stack<float> a(10);
            a.Push(1.1);
            a.Push(2.2);
            a.Push(3.3);
            a.Push(4.4);
            a.Push(5.5);
            a.Push(6.6);
            a.Top()++;
            while (!a.Empty())
            {
                cout << a.Top() << " ";
                a.Pop();
            }
            cout << endl;
        }
        catch (exception& e)//这里是申请空间失败的做法
        {
            cout << e.what() << endl;
        }
        return 0;
    }

4.新的问题

模板是不支持分离文件编译(这是指声明放在.h,定义放在.cpp,而不是指在一个文件内函数的声明和定义分离),如果您这样做了,编译会报错,最好是单独放在一个文件里,原因我们以后再提及。

不过可以先看看下面的code,下面代码我们将类模板的定义和声明放在了一个文件中:

    #include 
    #include 
    using namespace std;
    template<class T>//也可以写成typename
    class Stack
    {
    public:
        Stack(size_t capacity = 4)
            :_data(nullptr), _top(0), _capacity(0)
        {
            cout << "Stack(size_t capacity = 4)" << endl;
            if (capacity > 0)
            {
                _data = new T[capacity];
                _capacity = capacity;
                _top = 0;
            }
        }
        ~Stack()
        {
            cout << "~Stack()" << endl;
            delete[] _data;
            _capacity = _top = 0;
        }
        void Push(const T& x);
        void Pop()
        {
            assert(_top > 0);
            _top--;
        }
        bool Empty()
        {
            return _top == 0;
        }
        T& Top()//这里最好加上引用,这里还可以替代修改栈顶数据的功能,不过如果在最前面加上const就不行了,这具体看需求
        {
             assert(_top > 0);
             return _data[_top - 1];
        }
    private:
        T* _data;
        size_t _top;
        size_t _capacity;
    };
    template<class T>//这里是声明一下模板参数
    void Stack<T>::Push(const T& x)//类模板声明与定义分离(同一个文件下)
    {
        if (_top == _capacity)
        {
            size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
            T* tmp = new T[newCapacity];
            if (_data)//如果栈不为空
            {
                memcpy(tmp, _data, sizeof(T) * _top);
                delete[] _data;
            }
            _data = tmp;
            _capacity = newCapacity;
        }
        _data[_top] = x;
        ++_top;
    }
    int main()
    {
        //有关try和catch配合new的使用以后我们会提及
        try//这里是申请空间成功的做法
        {
            Stack<int> a(10);
            a.Push(1);
            a.Push(2);
            a.Push(3);
            a.Push(4);
            a.Push(5);
            a.Push(6);
            a.Top()++;
            while (!a.Empty())
            {
                cout << a.Top() << " ";
                a.Pop();
            }
            cout << endl;
        }
        catch (exception& e)//这里是申请空间失败的做法
        {
            cout << e.what() << endl;
        }
        return 0;
    }

类模板只需要放在一个单独的文件就可以(一般也是这么做的),由于这个文件比较特殊,既有声明又有定义。因此我们可以叫这个新的文件为.hpp,代表“既有定义又有声明的意思”。

实际上,对于类模板来说,放在.h文件和.hpp文件都是可以的(推荐使用后者)。

补充:对于类模板来说,要分清楚两个概念:“类的类名、类的类型”。在上述代码中,Stack是类的类名,而Stack是类的类型。

在没学类模板之前,类的函数声明和定义分离时,函数定义需要使用“类的类名”和“作用域访问限定符”来确定是类内部的函数:

    class Data
    {
    public:
        void Print();
    };
    void Data::Print()
    {
        //函数的具体定义
    }

而学习和使用了类模板后,就会发现这里使用的不是“类的类名”而是“类的类型”:

    template<typename T>
    class Data
    {
    public:
        void Print();
    };
    template<class T>//这里是单独声明一下模板参数
    void Data<T>::Print()
    {
        //函数的具体定义
    }

5.模板缺省值

既然函数形参有缺省值,模板里的类型是否也有缺省值呢?答案是有的:

    template <typename T = 缺省类型>
     //后续要使用缺省类型可以如此调用:Stack<> sta;

还有一些关于模板的更加复杂的知识我们以后再来做详细解答……

你可能感兴趣的:(C++学习笔记,c++,算法,开发语言)