C++ Primer第十六章

C++ Primer

模板与泛型编程

这一章会介绍一个神器-模板,我们可以用它实现泛型编程,我们先来说点大的官方的话:

  • 面向对象编程能处理类型未知的情况,因为它都封装为对象了
  • 泛型编程能处理在编译之前类型不知道的情况

说得这么玄乎,其实我们早就在用泛型编程了,比如我们学过的容器、迭代器和算法都是泛型编程,下面我们就来学习它,看看它到底方便在哪

模板是C++中泛型编程的基础,记住这句话:一个模板就是一个创建类或函数的蓝图

定义模板

我们现在要实现一个功能,很简单,比较两个值的大小,在实际中,我们可能需要写很多歌,因为这两个值可能是int,, double, string等等,如果我们写出这些函数就会发现,它们除了参数类型不同,其他都一样,相当于是重载函数,其实这样很烦,这样我们就引出了神器-用模板实现泛型编程(泛型,意思就是类型很泛,不再针对某一种类型):

//这是一个函数模板
template <typename T> //以关键字template开始,
//后面跟的是模板参数列表(不能为空),用逗号分割的一个或多个模板参数
int compare(const T &v1, const T &v2)
{
    if(v1<v2){return -1;}
    if(v2<v1){return 1;}
    return 0;
}

这里我们写了一个函数蓝图,当我们实际去调用它时,编译器会根据实参来生成相应的函数:

compare(1, 0); //实例化出int compare(const int&, const int&)
compare(2.5, 3.8); //实例化出int compare(const double&, const double&)
compare("abc", "def"); //实例化出int compare(const string&, const string&)

怎么样,是不是很吊?

下面我们要看很多细节的东西,但泛型编程最大的好处我们已经了解了

模板类型参数

这个指的就是我们的T,这个T在后面调用编译的时候就被我们的实参类型替代了,它在模板中可以被用在任何地方:返回类型、函数参数类型、声明变量等,就跟变量类型一样用:

//都可以用
template <typename T>
T foo(T* p)
{
    T tmp = *p;
    return temp;
}
非类型模板参数

模板参数列表不一定非要放类型参数,还可以放非类型参数,只不过这个非类型参数是用来表示具体的一个值而不是一个类型

//处理字符串字面常量,我们要比较不同长度的,所以需要两个非类型参数
//当非类型参数在编译被替代时,一定是常量表达式,毕竟它是个值,不是类型
template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}

再强调一下:在模板定义内,模板非类型参数是一个常量,所以呢:

  • 如果它是整型参数,必须是常量表达式
  • 如果是指针或者引用,它所绑定的对象必须有静态生存期(谁也不知道你什么时候会调用编译生成,所以你得保证你绑定的东西不会被销毁)
inline和constexpr的函数模板

它俩还是可以修饰函数模板的,注意放置的位置,别忘了constexpr是编译时检查它修饰的是不是常量(我也是翻书看的。。。):

template <typename T>
inline T min(const T&, const T&);
模板编译

当编译器遇到一个模板定义时,它并不生成代码,只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。

当我们在调用一个函数时,编译器只需要知道函数的声明就好了;当我们使用类类型对象时,也只需要类定义就好了(成员函数的定义不必已经出现),所以啊,我们可以把函数声明和类定义放在头文件中,把普通函数定义和类的成员函数的定义放在源文件

模板跟它们不一样的,为了生成一个实例化的版本,编译器需要掌握函数模板

类模板

前面我们用来介绍的模板泛型编程都是函数模板,看着还是挺简单的,接下来就来介绍一下更复杂的类模板。顾名思义,类模板是用来生成类的蓝图的,与函数模板不同,编译器不会为类模板推断模板参数类型,我们必须在模板名后的尖括号内提供额外信息,说了那么多,其实就是vector<int>

定义类模板

作为例子,我们将实现Blob,它不再只针对string,而是作为一个类模板,可以用于更多类型的元素:

//有些成员函数只写了声明,后面会再去实现定义的
template <typename T> 
class Blob
{
public:
    typedef T value_type;
    typedef typename vector<T>::size_type size_type;

    //构造函数
    Blob();
    Blob(initializer_list<T> il);

    //Blob中的元素数目
    size_type size() const {return data->size();}
    bool empty() const {return data->empty();}

    //添加删除元素
    void push_back(const T &t) {data->push_back(t);}
    void push_back(T &&t) {data->push_back(std::move(t));} //移动版本
    void pop_back();

    //元素访问
    T& back();
    T& operator[](size_type i);

private:
    shared_ptr<vector<T>> data;
    //检查错误,若i违法,抛出错误msg
    void check(size_type i, const string &msg) const
};

除了类型变为T外,其他基本没改,是不是超级简单

实例化类模板
Blob<int> ia; //空Blob<int>

根据这个实例化的类,编译器会实例化出一个类,类中的T由int代替,总之你给什么类型,编译器就会实例化出对应的类,其实上面这段简单的代码包含了两个实例化过程:

  1. 类模板实例化为类
  2. 类实例化为对象
在模板作用域中引用模板类型

这句话读着是不是有些拗口啊,其实它的意思就是我们在Blob定义中用了vector(它本身也是模板),我觉得理解上不会有什么问题,没感觉到有啥奇怪,就不去解释了

类模板成员函数的定义
  1. 定义在类模板内部:内联函数
  2. 定义在外部:要写成函数模板的形式,其他规定与类成员函数一样
check和元素访问成员

我们首先来定义check成员:

template <tyoename T>
void Blob<T>::check(size_type i, const string &msg) const
{
    if(i >= data->size())
    {
        throw std::out_of_range(msg);
    }
}

接下来我们就可以实现元素访问成员函数了:

template <typename T>
T& Blob<T>::back()
{
    check(0, "空了你还back");
    return data->back();
}

template <typename T>
T& Blob<T>::operator[](size_type i)
{
    check(i, "这个索引不合法");
    return (*data)[i];
}

template <typename T>
void Blob<T>::pop_back()
{
    check(0, "空了你还弹出");
    data->pop_back();
}
Blob构造函数

直接写了,没啥好说的,跟类模板成员函数定义差不多的:

//默认构造函数
template <typename T>
Bolb<T>::Blob() : data(make_shared<vector<T>>()){}

//接受参数的转换构造函数
template <typename T>
Blob<T>::Blob(initializer_list<T> il) : data(make_shared<vector<T>>(il)) {}
类模板成员函数的实例化

默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化

在类代码内简化模板类名的使用

我觉得没啥卵用:

template <typename T>
class BlobPtr
{
public:
    BlobPtr& operator++(); //前置运算符,这里的BlobPtr等价于BlobPtr<T>
};

类模板和友元

原书写得很烦,我看了半天,其实蛮简单的,看代码就能知道

template <typename T>
class A
{
    friend class Blob<T>; //相同类型的才是友元
    template <typename X>
    friend class B<X>; //B的所有实例全是友元
};
和内置类型做朋友
template <typename T>
class Bar
{
    friend int; //int是这个模板类的朋友,能访问它所有实例的所有成员
};

顺便说一句模板也有类型别名,无非就是所谓的简化,也有static成员,概念跟原来一样

模板参数

唉,原书的作者太啰嗦了。。。
我这里就举几个我觉得有必要说明的

模板类型参数作为返回类型(在类模板外部定义的时候)

//在模板类外定义的时候,如何将模板类型参数作为返回类型
template <typename T>
typename T::value_type top(const T& c) //看懂了没,这个返回类型,就照着它写
{}

默认模板实参

就像我们能为函数参数提供默认实参一样,我们也可以提供默认模板实参
我们先来给函数模板提供默认实参:

template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
    if(f(v1, v2)){return -1;}
    if(f(v2, v1)){return 1;}
    return 0;
}

在这段代码中,我们为模板添加了一个类型参数F,表示可调用对象的类型,并定义了一个新函数参数f,绑定到一个可调用对象上

现在再来个类模板实参:

template <class T = int>
class Numbers
{
public:
    Numbers(T v = 0) : val(v) {}
};
//因为有了默认实参,我们就可以这么干了
Numbers<> a;

成员模板

其实就是类里面的成员函数是模板函数,这个类可以是普通类也可以是类模板,下面就分别来讲

普通(非模板)类的成员模板

class Debugdel
{
public:
    template <typename T>
    void operator()(T *p) const //虽然是这个类的对象,但可以删除任何类型的指针
    {
        delete p;
    }
};

类模板的成员模板

我们要为Blob类定义一个构造函数,接受俩迭代器,表示要拷贝的元素范围,由于我们希望支持不同类型序列的迭代器,因此要将这个构造函数定义为模板:

template <typename T> 
class Blob
{
    template <typename It>
    Blob(It b, It e) : data(make_shared<vector<T>>(b, e)) {}
};

好的,下面我们来调用看看:

int ia[] = {0, 1, 2};
vector<long> v1 = {3, 4, 5};

//实例化Blob<int>类及其接受两个int*参数的构造函数
Blob<int> a1(begin(ia), end(ia));
全部评论
已经不知不觉就橙色啦~~加油~
点赞 回复 分享
发布于 2017-01-10 14:33
马了!
点赞 回复 分享
发布于 2017-01-09 11:54
加油,跟着你后面再看呢
点赞 回复 分享
发布于 2017-01-09 21:16
老哥一个月就看完了,好强啊,能分享下怎么看的吗,一半看书一半做题吗?
点赞 回复 分享
发布于 2017-05-01 17:01
mark
点赞 回复 分享
发布于 2017-05-01 20:22

相关推荐

评论
点赞
7
分享

创作者周榜

更多
牛客网
牛客企业服务