模板和标准模板库(STL)
一、模板的起源
变量四要素:类型、名称、值、地址
数据类型:存储形式、编码格式、运算规则、访问方式
int a = 0; // 补码
float b = 0; // 阶码
char c = ‘A‘; // ASCII
char_t d = ‘A‘; // UCS-4
int i = 0;
i++;
cout << i << endl; // 1
int* j = (int*)0;
j++;
cout << (int)j << endl; // 4
class Student { m_name learn };
Student stu (...);
stu.m_name = ...;
Student* pstu = &stu;
pstu->learn (...);
静态类型:由编译器在编译源代码的过程中,处理有关数据类型的检查、指令等问题,并将处理结果直接反应在所生成的二进制机器指令中。
# Python
i = 10
i = 1.2
i = ‘Hello, World !‘
静态类型的优点:运行性能好,安全。
静态类型的缺点:缺乏灵活性,不具备泛型能力。
代码:typed.cpp
借助于带参宏(宏函数)可以在某种程度上规避源自静态类型系统的限制,但同时也因为类型检查和转换的缺少而引入潜在的风险。
代码:untyped.cpp
通过宏描述类型无关算法的框架,借助预处理器自动生成针对不同数据类型的具体版本,达到类型安全和类型无关的效果。
代码:macro.cpp
显示扩展宏,预处理器缺乏类型分析能力。
宏的调试非常困难。
二、函数模板
1.定义
template<typename 类型形参1,
typename 类型形参2, ...>
返回类型 函数模板名 (调用形参表) {
函数体
}
在函数模板的返回类型、调用形参表乃至函数体中都可以引用该函数模板的类型形参。
template<typename A, typename b,
typename _C>
A fun (b arg) {
_C var;
...
}
2.使用
函数模板名<类型实参1, 类型实参2, ...> (调用实参表);
当编译器"看到"以上调用语句,就会用尖括号中的类型实参表与所调用函数模板尖括号中的类型形参表,按照从前到后,依次结合,把模板化的函数编成一个具体函数,编译成二进制机器指令,满足调用的需要。这个过程就叫做函数模板的实例化。
代码:ftmpl.cpp
3.类型参数
1)类型形参:必须用typename/class关键字修饰,而且必须其名称必须是合法的标识符。
2)类型实参:既可以是基本类型,也可以是标准库、第三方库、或者开发者自己编写的类类型,唯一的条件就是必须满足函数模板内部实现的需要。
代码:targ.cpp
4.延迟编译
在构建过程中一个非模板函数,只被编译一次,而且是在其定义被编译器发现的时候。
在构建过程中一个模板函数,至少要经历两次编译过程,第一次是在其定义被编译器发现时,这时编译器会对该函数模板做与类型无关的语法检查,而后会在其内部生成关于该函数模板的内部表示;第二次是在其调用语句被编译器发现时,这时编译器会将调用语句所提供的类型实参与内部表示中的类型形参结合,做与类型相关的语法检查,而后将该函数模板以具体函数的形式编译成二进制机器指令。
代码:typename.cpp
5.隐式推断
如果函数模板调用参数(圆括号里的参数)的类型相关于该模板的模板参数(尖括号里的参数),即使不显式指定模板参数的类型,编译器也可以根据函数调用虚实参数类型匹配的原则,隐式推断出模板参数的实际类型,这就叫函数模板类型参数的隐式推断。但是,如果隐式推断的类型与代码编写者所期望的类型不一致,可能产生未定义的后果。
代码:deduction.cpp
6.重载
函数模板和普通函数一样,也可以构成重载关系,二者区别在于,对重载版本的解析,如果是函数模板,在类型匹配程度相当的情况下,编译器并不会立即以歧义为由报错,相反会进一步检查不同重载版本的类型针对性,并优先选择针对性较强的版本。
代码:overload.cpp
三、类模板
如果一个类的成员变量、成员函数、成员类型乃至基类中包含参数化的类型,那么这个类就是一个类模板。
1.定义
template<typename 类型形参1,
typename 类型形参2, ...>
class 类模板名 [: 继承方式 基类, ...] {
成员变量
成员函数
成员类型
};
其中成员变量、成员函数、成员类型、基类都可以引用该类模板的类型参数。
class X { ... };
class A : public X { ... };
class B : public X { ... };
class C : public X { ... };
template<typename Base>
class Derived : public Base { ... };
Derived<A>
Derived<B>
Derived<C>
2.使用
类模板名<类型实参1, 类型实参2, ...> 对象 (...);
类模板名<类型实参1, 类型实参2, ...>& 引用 = 对象;
类模板名<类型实参1, 类型实参2, ...>* 指针 = &对象;
|<---------------------------------->|
类
类模板不支持隐式推断,而必须被显式实例化。
编译期 运行期
类模板 -实例化-> 类 -实例化->对象
编译器 处理器
代码:ctmpl.cpp
类模板中的成员函数都是函数模板,同样遵循延迟编译的规则,因此只有那些真正被调用的静态成员变量成员函数才会被二次编译,为该模板所提供的类型实参只要满足这些函数的要求即可,无需考虑那些用不着的函数的要求。另外,作为模板的设计者,要力争降低对类型参数的功能性要求,以简化模板用户的工作。
<
==
3.静态成员变量
语法:需要用static关键字声明,在类外定义初始化。
逻辑:类的一部分而非对象的一部分,被类的所有对象共享。
物理:存在进程的静态存储区(DAT/BSS)。
--------------------------
类型:主词,副词,饰词
int const* p ...;
static/auto/register
--------------------------
类模板中的静态成员变量,是该类模板的每个实例化类的一部分,在该实例化类被实例化为对象时,被这些对象共享。
/ X -> x1, x2, x3
A - Y -> y1, y2, y3
s \ Z -> z1, z2, z3
代码:static.cpp
4.容器
通过数组存放多个类型相同的数据
int a = 10, b = 20;
a = b;
cout << a << endl; // 20
void foo (int a) { ++a; }
foo (a);
cout << a << endl; // 20
int bar (void) { int c = 30; return c; }
c = bar ();
cout << c << endl; // 30
-----------------------------
int a[] = {10}, b[] = {20};
a = b; // ERROR
void foo (int a[1]) { ++a[0]; }
foo (a);
cout << a[0] << endl; // 11
-----------------------------
int* bar (void) { int c[] = 30; return c; }
int* c = bar ();
cout << *c << endl; // 30?
数组容器
递归实例化:用一个类模板的实例化类实例化该类模板自身,以获得在空间上具有递归特征的数据结构。
代码:array.cpp
嵌套实例化:用一种数据结构的模板实例实例化另一种数据结构的模板实例,以获得空间上的复合结构。
template<typename T> Array { ... };
template<typename T> List { ... };
template<typename T> Tree { ... };
Array<Array<int> > a; // 二维数组
List<List<int> > b; // 二维链表
Array<List<int> > c; // 链表数组
List<Array<int> > d; // 数组链表
Tree<List<Array<Tree<List<Array<int>>>...
5.特(例)化
当模板取某些特定类型时,其通用版本的实现未必能输出正确的结果,或者结果虽然正确,但性能不佳。这时,可以通过特化语法,显式指名针对该特殊类型的特殊处理。
1)完全特化:特化类模板的所有参数。
A.全类模板特化
B.成员特化
代码:spec.cpp
2)局部特化:特化类模板的部分参数,或参数的部分属性。
编译器总是优先选择类型约束性强的版本:
完全特化>局部特化>通用版本
代码:part.cpp
int i = 0;
int* p = &i; // p -> i
int* q = p; // q __/
smart_ptr<T>
6.auto_ptr的简化实现
1)利用局部对象的自动析构避免堆对象的内存泄漏;
2)利用类模板提供泛型支持;
3)通过重载*/->操作符模拟通过平凡指针访问内存的方式;
4)通过转移拷贝避免前拷贝引起的double free异常;
5)增加针对数组类型的局部特化,避免因管理堆对象数组而导致非法指针异常。
代码:auto.cpp
四、模板参数的缺省值
1.如果为模板的某个参数提供了缺省值,则编译器会在无法找到调用语句所传入模板实参的前提下,为该模板的形参制定缺省值。
2.若为函数模板:G++ Ver. >= 4.8
代码:defarg.cpp
五、数值参数
int a[5];
int b;
int (*pfun) (int, int);
代码:limit.cpp
能够作为模板非类型参数实参的必须是常量、常量表达式,或者带有常属性(const/C限定)的变量,但是不能同时具有挥发性(volatile/V限定)。
GNU的C++编译器对模板非类型参数的限制:
1.不能用类类型对象作为模板参数;
2.不能用浮点型数据作为模板参数;
3.字符串:字面值、字符指针、字符数组、string对象
---------- ----------
形参 实参且全局可被外部链接
六、typename
1.声明模板的类型参数
template<typename ...> ...
也可以用class
template<class ...> ...
2.解决嵌套依赖
嵌套依赖:在模板中引用依赖于该模板参数的类型的嵌套类型。
T <- A [ B ]
struct \
class - 声明类
\ 声明模板的
/ 类型参数
typename - 解决嵌套依赖
代码:typename.cpp
七、template
1.声明模板
template<...> ...
2.解决嵌套模板
如果在函数模板或类模板中,需要访问某个依赖于该模板类型参数的类型的内部模板,需要通过template关键字告诉编译器如何正确处理其后的“<>”模板参数表。
代码:template.cpp
八、在子类模板中访问基类模板
在子类模板中不能直接访问基类的非私有成员,而需要在被访问成员前面显式添加作用域限定符或this指针,否则编译器将只在子类和全局作用域中搜索被访问的成员标识符,导致编译失败或者逻辑错误。
代码:inherit.cpp
九、模板型模板参数
类型参数:<..., typename T, ...>
数值参数:<..., size_t S, ...>
模板型参数:<..., template<...> class C, ...>
代码:stack.cpp
十、零初始化
局部变量:T var = T ();
成员变量:... : m_var (), ...
明确指示编译器生成初始化代码。对于基本类型,用相应类型的零初始化。对于类则调用该类缺省构造函数初始化。
参见:init.cpp
十一、模板与虚函数
1.类模板中是否可以声明虚函数,是否可以表现出多态?
类模板中可以声明虚函数,只要用户为实例化类模板所提供的类型实参不违背虚函数有效覆盖的条件,就可以表现出多态性。
2.一个类或者类模板中的虚函数不能同时又是模板函数(带有自己的类型参数),其原因就是虚函数表的静态构建和函数模板的延迟编译之间存在矛盾。
代码:vfun.cpp
十二、编译模型
1.单一模型:声明、实现、使用同处一个源代码文件。
优点:可读性好。
缺点:文件过大,难以维护,不利于协作。
2.分离模型:声明、实现、使用分别放在不同的源代码文件和头文件中。
优点:文件规模不大,结构清晰,利于维护,方便协作。
缺点:无法正确执行针对模板的第二次编译,导致链接失败。
3.包含模型:在模板声明文件尾部包含该模板的实现。
优点:保证关于的模板的二次编译顺利完成。
缺点:模板的实现代码必须公开,延长编译时间。
4.实例模型:通过在模板实现文件中对其中的函数模板和类模板做显式实例化。
优点:模板的实现代码不必公开,不延长编译时间。
缺点:显式实例化模板的类型有限,不可能全部通用。
5.导出模型:将希望被其它源代码模块使用的函数或者类声明为导出,编译器会对该模板的内部表示进行缓存,确保其第二次编译的顺利。
优点:。。。
缺点:绝大多数编译器并不支持导出模型。
从C++2011标准以后,不会再从语言级别支持到处模型。
十三、预编译头文件
x.h
iostream
stdio.h
sys/socket.h
...
a.cpp
x.h
b.cpp
x.h
c.cpp
x.h
g++ -c x.h -> x.h.gch // 预编译头文件
十四、容器、迭代器和泛型算法
容器:利用模板语法把常用数据结构进行封装。
泛型算法:利用模板语法把常用非数值算法进行封装。
迭代器:为不同种类的容器提供统一的获取其中数据元素的接口。
双向线性链表、正向顺序可写迭代器、线性查找算法。
代码:list.cpp
十五、标准模板库(STL, Standard Templates Library)
1.十大基本容器
1)线性容器:向量(vector),双端队列(deque),列表(list)
2)适配器容器:堆栈(stack),队列(queue),优先队列(priority_queue)
3)关联容器:映射(map)、多重映射(multimap),集合(set)、多重集合(multiset)
2.向量(vector)
1)基本特性
占用连续的内存空间,支持常数时间的下标计算
支持动态内存管理
2)实例化
#include <vector>
空向量:vector<元素类型> vec;
非空向量:vector<元素类型> vec (初始大小)
指定初值的非空向量
vector<元素类型>vec (初始大小,初值)
vector<int> vi (5, 12);
复制另一个容器中的部分或全部。
int arr[] = {1, 2, 3, 4, 5}
vector<int> vi (arr, arr + 5);
vector<int> vi (&arr[0], &arr[5]);
[]/size/sizeof/capacity
3)迭代器
A.在STL中只有向量和双端队列采用连续内存存放数据元素,因此只有这两个容器提供随即迭代器,其它容器都只提供顺序迭代器。随机迭代器在顺序迭代器的功能之上又增加了:和整数的加减法运算、迭代器之间的大于、小于、相减运算。
B.四种迭代器类型
iterator,正可写
const_iterator,正只读
reverse_iterator,反可写
const_reverse_iterator,反只读
C.八个特征迭代器
begin()
begin() const
end()
end() const
rbegin()
rbegin() const
rend()
rend() const - 反向终止只读迭代器
__ A B C D __
^ ^ ^ ^
| | | |
rend begin rbegin end
begin - 起始
end - 终止
无const - 可写
有const - 只读
无r - 正向
有r - 反向
D.注意任何可能引发容器内部内存布局发生变化的操作(多数都与数据元素的增减有关),之前获得的迭代器可能因此操作而失效,安全起见在使用这些迭代器之前,对其重新定位,以反映当前的内存状态。
代码:vec2.cpp
4)成员函数
front(),没有push_front()和pop_front()
back()/push_back()/pop_back()
insert()/erase(),效率不高
size()/resize()
capacity()/reserve()
empty()/clear()
operator[]
特征迭代器函数
...
5)查找和排序
#include <algorithm>
A.IT find (IT begin, IT end, KEY const& key);
B.void sort (IT begin, IT end); 快速排序:O(NlogN)
void sort (IT begin, IT end, LESS less);
代码:vec3.cpp
6)类类型元素
缺省构造、深拷贝、"=="/"<"操作符。
代码:vec4.cpp
3.双端队列(deque)
双端队列和向量相比几乎完全一样,唯一的差别就是双端队列是两端开放的容器。
push_front()/pop_front()
时空性能略逊于向量。
12 V 345
4.列表(list)
1)唯一化
void unique (void);
对连续的重复出现的元素唯一化。
2)排序
void sort (void)
void sort (LESS less)
3)拆分
void splice (IT pos, list& lst);
void splice (IT pos, list& lst, IT del);
void splice (IT pos, list& lst, IT begin, IT end);
4)合并
void merge (list& lst);
void merge (list& lst, LESS less);
代码:list.cpp
5.堆栈、队列和优先队列
适配器容器<元素类型[,底层容器类型]>
stack vector/deque[缺省]/list
queue deque[缺省]/list
priority_queue vector/deque[缺省]
代码:sqp.cpp
6.映射(map)
键值对,Key-Value
红黑树(平衡有序二叉树),O(logN)
1 2 3 4 5
1
2
3
4
5
3
2 4
1 5
迭代顺序:中序,L-D-R,关于键的升序
键必须唯一且只读
数据元素是封装了键和值的"对"对象。
template<typename FIRST, typename SECOND>
class pair {
public:
pair (FIRST f, SECOND s) : first (f), second (s) {}
FIRST first; // 键
SECOND second; // 值
};
提供以键为参数的下标运算符。
110108198005220055 -> "张三, 37, ..."
^
|
m[‘110108198005220055‘] --+
代码:map.cpp
多重映射
first
|
3 v
2 4->4->4
1 5 <-- second
不支持下标操作符
pair<IT, IT> equal_range (key)