1、 C++可称为强类型语言,凡值必有类型,凡变量声明时必声明其类型,且变量类型”一生”不变。 C++中变量的类型不可变。
2、 根据所给定的模板参数值以及完整的函数模板声明,编译器可自动生成一个对所需数据类型进行操作的函数,称为函数模板实例。
template<typename T>
T const& max_element(T const *l, unsigned sz)
{
T const *max_value(l);
for (unsigned i = 1; i < sz; ++i)
{
if (l[i]>*max_value)
max_value = &(l[i]);
return *max_value;
}
}
max_element<char>(cl, 6);
3、 模板参数推导:(1) 编译器只根据函数调用时给出的实参列表来推导模板参数值,与函数参数类型无关的模板参数其值无法推导;
(2) 与参数返回值相关的模板参数其值也无法推导;
(3) 所有可推导模板参数必须是连续位于模板参数列表尾部,中间不能有不可推导的模板参数。
2、类亦模板
1、 实现一个栈模板my_stack。在类模板内首先用typedef为list_node<T>赋一个别名node_type这样在随后用到list_node模板时就无须一一给出实参。在需要用到大量模板实例类型时,这样做可以使得代码看起来更简洁。
2、 异质链表的每个节点类型都不同。异质链表的作用更类似于元组(tuple),可将一组数量已知、类型已知且不同的数据组合在一起并在不同函数间传递。
template<typename T, typename N>
struct hetero_node
{
T value;
N *next;
hetero_node(T const &v, N *n) :value(v), next(n){}
};
4、特例
1、 以关键字class或typename标记类型的模板参数被称为类型模板参数(type template parameter)
2、非类型模板参数的作用相当于为函数模板或类模板预定义一些常量,在生成模板实例时,也要求必须以常量(即编译期已知的值)为非类型模板参数赋值。
3、只有指向全局变量及外部变量(以extern修饰)及类静态变量的指针和引用才可作为模板参数。函数的局部变量、类成员变量等均不能作为模板参数,这是因为模板参数值必须是编译时已知的。
4、模板特例:现实总是比想象更加复杂,单一模板很难兼顾各种情况。为此,C++提供了一种特例机制。对于某个已有模板,可以为某个或者某组模板参数类型另外指定一种变体,以应付原模板无法处理的情况,或者提供更高效的实现方案。
5、每个特例都需要有模板参数匹配式以表明其使用范围。在声明特例时,匹配式写在模板名之后并用尖括号括起。而原本的模板参数列表则改为匹配式所用模板参数列表。具体到my_vector保存布尔值的特例,由于只匹配模板参数T为布尔值的情况,其匹配式就是<bool>。由于匹配式中没有再引入模板参数,故原模板参数列表为空,但尖括号不能省。所以,为my_vector<bool>声明特例就有如下形式:
template<typename T> class my_vector;//my_vector模板通例
template<> class my_vector < bool > ;//my_vector模板特例
6、要保证通例声明在特例之前。
7、C++中对于特例和通例并没有任何约束,理论上完全可以把特例设计成一个与通例毫无共同点的类。但毕竟通例与特例共享同一模板名,为了可读易用,特例总应该尽量保持与通例接口一致,只有在万不得已时才做些妥协。
8、特例匹配规则:同一模板可以有多个特例。这就带来一个问题:如果某套模板参数值能与多个特例匹配时怎么办?对此,C++的原则简言之是与最“特殊”的特例匹配。
9、同一模板可以存在有歧义的特例,但不能出现会引起歧义的实例。
10、 类模板允许部分特例。 由于特例与重载这两种既相干又不完全等价的机制并存,使得处理函数模板变得异常复杂且潜藏众多冲突。为简化问题,C++标准中只允许为函数模板声明完全特例,而禁止为其声明部分特例。
11、 分辨重载: 准则1 两候选函数中如果有一方其形参列表各类型与调用实参列表各类型更匹配,则淘汰另一方。
准则2两函数如果其形参列表类型同等匹配实参列表类型时,若一方为函数模版实例而另一方为非模版函数,则取非模版函数而淘汰函数模版实例。
准则3两函数如果其形参列表类型同等匹配实参列表类型时,若两者均为函数模版实例,则取更为“特殊”的一方而淘汰另一方。
5、容器、迭代器与算法
1、 封装了对容器虚拟数据序列的操作并按约定提供统一界面以遍历容器内容的代理类即为迭代器(iterator)。
2、 集合容器并不保证存于其中的数据有序排列,但是可以快速判断给定值是否已经在集合中。通常采用红黑树等平衡二叉搜索树结构来实现集合,以便快速定位给定值在树中的位置。
3、 容器的两个重要分类:
1 数据的存储位置与其内容完全无关,通常其数据也是按照一定的序列组织的,如数组、链表、向量等,统称为序列型容器(sequence container).
2 数据存储位置与其内容有关,如集合、字典、散列表等,统称为关系型容器(associative container)。
4、序列型容器可以提供允许修改数据的迭代器,而关系型容器只能提供“只读”迭代器以保证容器内数据结构的完整正确。
5、容器、迭代器与算法的设计模式是C++中重要的通用代码实现方法。标准库中容器、迭代器以及算法的最主要设计思想—以迭代器为界,分隔容器与算法。
5、标准库中的容器
1、标准库中的容器:
序列型容器有双端序列deque、双向列表list以及变长数组vector.
此外还有三种转换类:栈stack、队列queue以及优先队列priority_stack。转换类可以将任何符合要求的序列容器(无论是否标准库容器)再包装成所需数据结构。
关联型容器有集合set、映射map、二者的多重值变体multiset和multimap,以及整数值专用集合bitset。
2、 容器模板对所存数据类型有一个基本要求—可复制构造。将数据放入容器的过程就是通过数据的复制构造函数在容器内创建数据的一个副本的过程。
3、 关联型容器的数据值与其存储位置相关,不能随意修改,所以关联型容器中嵌套定义的引用类型也为数据的只读引用类型。
4、 (1)vector是一个变长数组容器类模板。正如数组那样,数据在vector中连续存储。
(2)为此,vector会预先申请一段内存空间以保存数据。随着数据不断增加,当预留的内存空间不够用时,vector会再申请一段更大的空间并将现有数据“搬”到新空间中后再继续接受新数据
(3)由于数据在内存空间内连续存放,所以vector可提供快速随机访问,具体方式有两种:重载下标操作符“[]”以及成员函数at(i)。成员函数at(i)会做越界检查,当访问越界时会抛出std::out_of_range异常,而下标操作符不做越界检查。
(4)vector适用于数据增删不频繁但需要高效随机存取的场合,而不适用于需要频繁在序列中增删数据的场合。
(5)vector初始内存容量只能容纳一个数据,随后每次申请空间大小翻倍。Vector可以调用reserve()函数申请足够的空间。
(6)数据增加会促使vector申请更大的空间,但是数据减少一般并不会导致vector自动缩减内存空间。
5、deque的特点是不仅可以快速在队列前后两端增删数据,还可以比较快速的随机访问数据。整个deque的数据结构是由若干个称为node的固定长度的连续空间及一个称为map的保存个node起始地址的可变长度的连续空间构成。
6、 标准中的容器转换器(containeradapter)是利用容器实现特定数据结构的类模板。
Stack与queue的模板有两个参数,前一参数定义stack及queue要保存的数据类型,后一参数则定义所用容器类型。后一参数可以省略,省略时二者都将用deque作为底层容器。例如:
std::stack<int, std::vector<int>>vector_stack;
std::stack<int> deque_stack;
std::queue<char, std::list<char>> list_queue;
std::queue<char> deque_queue;
7、 标准库中的关联型容器包括集合set、多值集合multiset、映射map以及多值映射multimap。在大多数标准库的实现中,四种容器都以红黑树保存数据。
8、 散列函数:解决地址冲突最常用的方法是将地址与链表对应,所有映射到同一地址的数据都保存到该地址的链表内。
9、 在散列表容器中有个重要的概念—桶(bucket)。与某个地址(即散列函数的某个返回值)对应的链表称为一个桶,散列表容器可以看成是一个以指向桶的指针为元素的动态数组。
10、 数据在桶中是无序排列的,既不按照键值大小排序,也不保证按照进入容器的先后顺序排列。(但如果再仔细一看,又会发现键值相同的数据总是相邻的,并且都按照后进入容器者在前的顺序排列。)
6、其他
1、模板最为人诟病的缺点有两个:1)晦涩难懂的编译出错信息;2)膨胀的代码。
2、编译器无法在第一时间觉察到所赋类型与对模板参数要求的不匹配,只有在编译到容器或算法内部的某个对模板参数有要求的实际语句时,才会发现这一失配。(通常算法或容器的内部实现会有多重模板函数调用,编译器有可能编译到很深层的调用时才会出错。如此一来,一个最直接的后果便是一长串晦涩难懂的出错信息。对于一个不熟悉该模板库的程序员来说,只有一种反应—茫然。)
3、代码膨胀(code bloat):可分为两种情况:1)源代码的增加;2)编译生成目标代码尺寸的增加。 源代码的增加主要是由于模板特例造成的。
4、合理利用模板,有两层含义:
1)要预估模板可能生成类型的数量级。对于能自动生成新模板实例类型的函数模板,一定要慎重考虑其使用频度,防止新类型泛滥。
2) 要最大限度降低与模板参数有关的操作量。因为每当从某类模板生成新的实例类型时,与其相关联的被调用到的函数模板也都会生成新的函数实例,增加真实的目标代码量。
5、标签(tag)常被写成一个空类,其目的只为声明一种独特的类型用于分辨函数重载。
6、通过在模板中嵌套调用自身定义类型,实际上实现了一种在编译期的“递归”逻辑,并且利用模板特例来实现“递归”终止。对于没有明显的循环控制语句的编译期控制逻辑来说,这是一种非常重要的用于实现某种编译期“遍历”逻辑的方式。
7、为各种基本类型操作提供可重用代码支持,是C++元编程(Meta-Programming)的一个重要课题。