About

videos;

课件,密码kr24;

认识headers、版本、重要资源

以STL为目标探讨泛型编程。

使用一个东西,却不明白它的道理,不高明!

  • level 0: 使用C++标准库;

  • level 1: 认识C++标准库(胸中自有丘壑,体系结构应当建立起来);

  • level 2: 良好使用C++标准库;

  • level 3: 扩充C++标准库;

Standard Template Library (6大部件:Container(容器) 各种基本数据结构;Adapter(适配器) 可改变containers、Iterators或Function object接口的一种组件;Algorithm(算法) 各种基本算法如sort、search…等;Iterator(迭代器) 连接containers和algorithms;Function object(函数对象);Allocator(分配器)) 加上其他一些零碎的东西构成了C++ Standard Library.

STL体系结构基础介绍

STL六大部件(component):

数据在容器(内存的事情我们不必管,由分配器支持),处理数据在算法,连接之间的桥梁是迭代器(泛化的指针)。仿函数可以处理类之间的一些操作,适配器转换。

STL六大部件

int stl_component()
{
int ia[6] = {27, 210, 12, 47, 109, 83};
//用到了容器和分配器定义一个vector
vector <int, allocator<int> > vi(ia, ia+6);
//用到了算法,迭代器,函数适配器,函数对象
// vi.begin()和vi.end()迭代指向vi中的元素
// not1是negator,否定less的作用,变成了大于等于40的数
// bind2nd绑定了less比较的第二个对象,固定在40
cout << count_if(vi.begin(), vi.end(),
not1(bind2nd(less<int>(), 40))) << endl;

return 0;

}

简单提及时间复杂度。

“前闭后开”曲间,标准库规定容器迭代器的最后一个元素取不到,即[iter.begin(), iter.end() ),此外容器也不一定是连续空间,比如链表。

Container<T> c;
...
Container<T>::iterator ite = c.begin();
// for语句的语法规定,括号里面一定要有两个分号,分开三个句子。
//第一个句子是初始化用的,如果没有初始化的必要,就视为空语句,加上分号;
//第二个句子作为判断条件,如果没有判断条件,也视为空语句,后加一个分号。这种情况,会无限循环,相当于while(1)。如果for的执行部分,就是{}之间有break语句,可以退出;
//第三个句子是执行部分执行完毕再执行的语句;无则视为属空语句;此时不用再加分号
for (; ite != ite.end(); ++ite)
...

建议使用range-based for statement (since c++11):

for (decl:coll){
statement
}
for (int i : {2,3,5,7,9,13}) {
std::cout<<i<<std::endl;
}

容器与分类之各种测试(1)

容器-结构与分类

标红的是C++11新加入的特性

1.sequence containers 序列式:

array(不可扩充), vector(分配器动态扩容), deque(前后快速插入), list(双向循环链表), forward-list(单向链表);

2.associative containers 关联式,便于查找:

set/multiset, map/multimap, 两者编译器所带标准库都用红黑树实现。set只带key,map带有key和value,两者意思与python的set和dict类似。multiset/multimap代表key是可以重复的。

3.unordered containers (since c++11):

不定序,hash table,其实也可以属于一种associative containers.内部采用链表法解决哈希冲突。


使用容器array示例:

array必须指定固定大小,不能是变量大小;

array.data()首地址;

clock()得到的是ms;

snprintf()是c标准库函数,在这里是将数字转成字符串,具体可参考

在固定大小的array中随机生成数并出初始化填充,然后快排再二分查找指定的数字,并打印出消耗的时间

容器与分类之各种测试(2)

侯捷老师给的一些建议:

1.通过给每个测试定义不同的namespace,使得每一部分都是独立的,方便测试。

2.用到变量的时候去定义,然后缩排方便寻找。


使用容器vector:

vector.push_back()把元素放到尾巴;

vector 2倍扩容增长空间;

vecotr.size()元素个数;

vector.capacity()容器容量,一般比size的结果大;

try, catch目的是了解当前是否可以抓取到指定的内存大小;

::find中双冒号代表全局的,find()是模板函数(循序查找),返回的是iterator;

定义一个vector,然后不断填充随机数初始化,然后利用模板函数find和二分查找去搜索指定的数,比较两者的时间差异

容器与分类之各种测试(3)

1.使用容器list:

容器的第二个参数是分配器,一般我们不写,使用默认的分配器分配内容;

list.max_size()

list.size()

list.front()

list.back()

list.sort()对其进行排序

2.使用容器forward_list:

forward_list.push_front只提供单向的放数据

forward_list.max_size()

forward_list.front()

没有forward_list.back()forward_list.size()

3.使用容器slist:

非C++11标准库;

#include<ext\list>头文件包含。

4.使用容器deque:

双向开口,可进可出。

分段连续的buffer构成的,但是给使用者感觉是连续的,内部其实不是,分段连续,buffer内部连续。每次扩充一个buffer.

vector扩充方便,但是容易浪费,list每次扩充一个节点,但是查找麻烦,deque每次一个buffer,相对折衷。

deque.size()

deque.front()

deque.back()

deque.max_size()

没有自己的sort()函数

5.使用容器stack:

stack.pop()

stack.push()

stack.size()

stack.top()

6.使用容器queue:

queue.push()

queue.pop()

queue.front()

queue.back()

queue.size()

stack和queue用的deque,因此在技术上称容器的adapter, 不提供iterator

容器之分类与各种测试(4)

关联式容器

1.使用容器multiset:

key就是value, 可插入相同元素,内部红黑树组织数据

multiset.insert()

multiset.size()

multiset.max_size()

multiset.find()容器自带find()函数

2.使用容器multimap:

,内部使用红黑树,key可以重复,指定key和value的数据类型,multimap<long, string> c;

multimap.insert(), c.insert(pair<long,string>(i,buff)),利用pair插入和取出

multimap.find()

3.使用容器unordered_multiset:

内部使用hash_table, 链表法解决哈希冲突,一个篮子bucket后续可以挂链表

load_facotr载重因子

4.使用容器unordered_multimap:

5.使用容器set:

内部用红黑树实现,元素不允许相同

6.使用容器map:

内部用红黑树实现,key不允许相同

c[i] = string(buf),与之前的multimap插入元素不同,可以用这种特殊写法,在内部会合成pair

7.使用容器unordered_set:

内部用hash_table

8.使用容器unordered_map:

内部用hash_table

分配器测试

使用分配器:

不使用标准库自带的std::allocator, 调用GNU c++编译器的allocator

OOP vs. GP

object-oriented programming and generic programming, 面对对象编程与泛型编程

OOP企图将data和methods放在一起,GP却将data和methods分开来

算法通过迭代器操作容器数据

操作符重载和模板(泛化,全特化,偏特化)

再次强调和复习operator overloading,templates(类模板,成员模板,函数模板;模板的泛化,全特化和偏特化)。

1.操作符重载的情况主要是为了满足用户自己定义类的一些操作,可以在相关网站查询,并不是每个操作符都可以重载。

2.模板

类模板,指定类参数类型

函数模板,自动推导传入函数的参数类型并根据作相应的操作符调用

成员模板

在模板中,对于某种类型的做特殊处理(比如此时会有更适合和高效的处理),调用时也会自动调用特化的版本,标志template<>为空声明

部分模板参数指定为某类型时有特殊处理,只绑定其中一个模板参数,标志template<>为非空声明

Allocator

不建议你单独使用分配器。

operator new()和底层的内存分配函数malloc()

malloc()分配给申请内存大小的空间时会额外给一些空间放必要的东西,有一些额外的开销cookie,free释放内存。但是一般使用时,由于申请的size不大,所以这些额外的空间占所有的比例比较大,所以是个缺陷。

P11总结一下,allocator就是用来分配内存的,最重要的两个函数是allocate和deallocate,就是用来申请内存和回收内存的,外部(一般指容器)调用的时候只需要知道这些就够了。内部实现,目前的所有编译器都是直接调用的::operator new()和::operator delete(),说白了就是和直接使用new运算符的效果是一样的,所以老师说它们都没做任何特殊处理。G2.9里的那个更好的allocator,在老师的另一门课《C++内存管理》里讲得特别细,大概从P23开始讲细节,更早的几P也有使用用例。

malloc开辟内存示意

一般的VC等编译器只是将malloc包装成allocator,然后调用,缺陷没有改变

GNU2.9使用了alloc这个函数,利用16个链表来分配申请的size的内存,减少了不必要的cookie

alloc 16个链表示意。每个代表8个字节长度,从8-128个字节

但是到了GNU4.9时候又抛弃了alloc,转而回到了原始的有缺陷的设计。不过依然可以调用GNU命名空间中的_pool_alloc来使用2.9版本中的alloac函数

容器之间的实现关系与分类

容器之间的关联,复合关系,不是继承,只是会有某些容器的功能

深度探索list(上)

最具代表性的容器,双向循环列表,优先介绍。

定义自己的迭代器iterator, 其中要有typedef和operator reloading(操作符重载)

list的iterator定义

提到了迭代器++所体现的智能指针的作用,取出prev或next指针赋给node。其中要特别注意前++和后++的区别,由于C++中整数连续后++是违法的,所以这里操作符重载后++时返回原值,也造成迭代器后++违法的现象

取出list中的data

深度探索list(下)

G2.9和G4.9版本链表源代码的比较,一处修改了iterator模板传入参数个数,一个是修改指针指向自己这个类,而非void然后再强制转换

容器的一般都是满足左闭右开的特点,也就是最后一个值取不到,所以这里最后再环状末尾加了一个空的节点,但是指向begin节点

迭代器的设计原则和Iterator Traits的作用与设计

容器用域存储数据,算法用以处理数据,而迭代器iterator就是沟通容器和算法的桥梁。上一讲提到,容器需要自己定义iterator,并且写出五个规定的typedef,这个就是为了算法询问iterator,得出这样的特性然后进行定制的数据操作。但是如果iterator不是类,无法写出typedef的时候,就需要traits这样的中间变量来让算法得出迭代器的特性。

算法询问迭代器的特性,但是迭代器只是指针的话就无法直接询问

利用traits萃取机来充当中间人的角色

如果迭代器是类,直接询问得出结果,否则利用偏特化替迭代器回答。其中注意const的使用

traits传递完整的五个必须的特性

除了iterator traits之外,还有各式的traits, 包括type traits, char traits, pointer traits等等。

Vector深度探索

vector不在原地扩充,而是请求重新分配2倍大小的空间

申请空间并搬运,这里的insert_aux函数还可能被其他函数调用,所以又重新判断了一次

vector的iterator就是个指针,因为是连续空间,所以利用traits为算法传递typedef. G4.9版本中对vector做了多层推导,所以可读性不高,也被侯老师吐嘈。

array, forward_list深度探索

把array包装成容器,然后利用指针当iterator进行操作。

单向链表与前面的双向循环链表类似,只不过没有prev的指针,因此迭代器没有--操作

deque, queue和stack深度探索(上)

双向队列deque在上层调用时表现是连续的,但是底层实现是用分段连续的buffer来实现的,通过迭代器和操作符重载来营造连续插入或弹出的假象。

一个迭代内部有四个指针:cur,first, last, node,其中前三个指针是针对buffer而言的,而node指的是当前deque中用到了哪个buffer

deque class内部有四个数据,start和finish是类似begin()和end()的两个首尾迭代器,一般的容器都会有,map指向这个包含buffer的deque,map_size表示大小,所以整个deque大小是2x16+2x4=40 bytes. 此外,G2.9还可以预设buff_size大小,否则的话按照默认规则设定。

deque的迭代器设计,其类型是可以随意访问的iterator,map pointer是指向指针的指针,因此deque内部存着buffer的node指针

deque的元素插入,是将左右两边比较短的那一部分进行搬移

deque, queue和stack深度探索(下)

deque内部通过iterator操作符重载并做一些处理来进行表面上连续空间的模拟。

back代表最后一个元素,而容器中的finish是开的,取不到,所以需要减去1得到最后一个元素

dwque元素size计算,前面的buffer后面的buffer元素不一定是满的(双向的特点),只有中间是满的,所以分三步计算

迭代器++和--操作符重载

迭代器跳着加的操作符重载

迭代器跳着减的操作转为跳着加的操作

G4.9版本的deque,其中取消了用户对buffsize的预设

利用deque模拟队列queue

用deque模拟stack

queue和stack也可用list模拟,但是默认是deque,因为速度较快

queue不可用vector模拟,以为vector没有pop_front操作

queue和stack都不可用map或set模拟,因为没有该数据结构弹出元素这一操作

RB-Tree深度探索

关联式查找容器的底层支撑结构。

这里红黑树的header不是根节点,是空的,为了容器前闭后开处理方便

红黑树的五个模板参数,key和value是数据类型,keyofvalue代表如何从value取出key(value可能是key-data组成的pair),compare代表key的比较函数

四个模板参数的一个实例,其中key和value都是int类型,identity函数代表取key函数,不是C++标准库函数,是G2.9的函数

G4.9与2.9版本的红黑树使用上改了一些命名字母

Set, Multiset深度探索

set和multiset底层利用Rb_tree实现,所以基本上它们的事内部都是交给红黑树去完成,所以又被叫成container adapter. 其中需要注意的是它们中的key和value是一样的,所以不能被改变,否则key就会变化。

set和multiset

set具有三个模板参数,因为value和keyofcompare只有一种默认情况。其中const_iterator的类型只能用于读取容器内的元素,但不能改变其值,对const_iterator类型解引用,得到的是一个指向const对象的引用。

Map, Multimap深度探索

related blog

map和multimap使用红黑树实现key-value的存储,且不允许改变key值

map的模板参数,其中会将key-data打包成pair存入Rb_tree中当作value,且为了防止key不被修改,在key前加了const关键字

map可以使用[]操作符和lower_bound函数插入key-value,如果key不存在会返回一个合适的位置,但是这个性质multimap没有,只能用insert函数插入pair(), 应该是因为multimap可以有多个相同的key,所以用[]操作符可能无法加入相同的key.

Hash Table深度探索

另一个关联式容器哈希表,或散列表,是一种常用的存储较多数据的数据结构(减少查找时间),其中难点在于hasd function的选择和对对哈希碰撞的处理(一般是开放式寻址法或者链表法)。

hash table示意,利用bucket和list来存储数据。其中G2.9硬编码了一系列质数作为扩充后的长度,但是G4.9没有继续沿用了

hashtable的6个模板参数,其中key指定篮子,value是pair,HashFcn是哈希函数,映射要存的object为key,ExtractKey是从存入的value pair中取出key的方式,同Rb_tree类似,EqualKey是判断key是否相等的函数。hashtable的大小为1+1+1+12+4=19个字节,为了对齐取4的倍数,为20 bytes. 其迭代器包含指向当前元素位置的指针和指向当前hashtable的指针。

hashtable使用示例,其中eqstr是自己写的函数,调用C中比较char的函数strcmp,然后根据其返回的int值(1,0,-1)返回bool值

c++标准库中偏特化了一些hash函数

但是G2.9内没有提供针对string的现成的hash函数,只有针对C的字符串的哈希函数。到了G4.9版本,基本的数据类型都有了自己的hash function, 包括string

找到存放的位置key,这里做了取余处理,也就是对编码出的hash值对bucket长度取余

上面的示例程序计算hash key示意,注意还有一个取余操作。在这里哈希表是单向链表存储,实际有的可能是双向链表存储

Unordered容器概念

利用hash table实现unordered_set, unordered_multiset, unordered_map, unordered_multimap容器。

四个unordered容器的模板参数

回顾之前利用unordered_set存储随机数的例子,验证两个事情:1. hashtable的bucket数量比要存的object的数量多;2. 扩容后的长度不一定是之前指定的质数了。

算法的形式

算法通过迭代器联通容器,进行数据处理和函数运行。从语言层面上讲,容器是class template, 而算法是function template, 其中有些算法还可传入你定制的准则criterion, 比如sort函数的比较函数。

STL中算法algorithm的作用

迭代器的分类

STL容器中总共会用到三种种类,分别是random_access_iterator_tag (array, vector, deque), bidirectional_iterator_tag(list, rb_tree), forward_iterator_tag(forward_list, hashtable可能是) 这三种。多出来的两种input_iterator_tag和output_iterator_tag属于istream和ostream. 五种之间是继承关系,同时用这种tag的方式,非简单的12345.

各种容器的迭代器种类

打印出各个容器的迭代器种类。右边的容器(typename)::iterator()表示临时对象,传给display_category()函数作为具体的模板参数,然后利用萃取机traits得出出入的迭代器的category,然后利用函数重载传给具体的_display_category(),打印出结果。istream和ostream利用直接指定的方式打印出。

也可直接利用C++的typeid取出迭代器的name属性,打印出类型名称。

迭代器类别对算法的影响

STL算法内部会利用迭代器的类别开辟出不同的处理分支,想尽方法达到最快的处理速度。

根据不同的迭代器类别计算其距离。主函数部分的inline语句会预先判断该迭代器是否有difference_type,如果有才会进行下一步。主函数利用萃取机得出的category当作临时对象调用次函数,虽然次函数只写了两种情况,但是其他的迭代器类型会根据继承关系进行调用,这样做就是为了方便,不必写出冗余的代码。

举了advance的例子(前进),与之前的distance计算类似,这里只不过把得出迭代器类型的语句包装成了一个小函数。

举了第三个例子,copy函数。一般的思路是给出三个参数:要拷贝的起点和终点,以及拷贝到的起点,但是这里根据迭代器的类型,极尽所能做了很多的分支,优化到最快。memmove是C语言的底层比较安全的内存复制函数,trivial代表“不重要的‘,由标准库提供的type traits提供,目的为了决定哪一种拷贝赋值比较快。其中对判断语句的first!=last和n<first-last都做了速度衡量,可以说是非常精炼了。

举的第四个例子,destroy函数,与copy类似,也做了很多判断处理。

第五个例子,unique_copy函数

算法源码剖析(几个例子)

举了几个C++标准库中的例子。表明与迭代器结合很紧密。

C++标准库的函数与C函数在形式上有些不同,是一种函数模板,并且与迭代器联系紧密

第一个例子accumulate,累计函数,有两个版本,一种就是传入三个参数进行累加,另一种可以指定二元操作函数,进行定制的累计操作。在右边的代码示例中,利用了自定义函数传入和函数对象/仿函数重载operator(),两者都可以起作用,作为可被调用的对象。

第二个例子for_each,跟第一个例子类似也使用了自定义函数和函数对象,同时强调C++11可以用range-based for statement进行操作

第三个例子replace函数,replace单纯地根据相等替换数值,replace_if可以传入判断条件, replace_copy是在copy过程中替换旧值为新值。

第四个例子count函数,count函数简单判断值相等进行加一操作,而count_if可以传入判断条件。在这里侯老师提到了C++标准库的这些函数虽然是全局的,但是不一定适用于所有的容器,有的容器可能有自己定义的同名成员函数,这个时候就要调用容器自己的对应函数。右边的例子表明不带其同名成员函数的容器可以调用C++标准库全局的这个函数,但是有了同名成员函数就要最好调用自己的。

第五个例子find函数,这个在最开始容器使用测试时候使用过,属于全局函数,有些容器比如那几个关联式容器就有自己定制的find()函数

第六个例子sort函数,其中提到了逆序迭代器。

reverse iterator示意

第七个例子二分搜索binary_search, 内部调用lower_bound函数取出位置。这里侯老师认为在查找时候应该进行一下开头检查,然后再根据结果选择进不进去查找。

仿函数和函数对象

在前面一些的算法的使用中,已经见识到了函数对象/仿函数传入其中进行特殊的操作。在C++的STL中有三大类functors,分别是算术类,逻辑运算类和相对关系类。他们都继承了某个适当者,这样是为了后续利用适配器进行改造,而前提就是像algorithm对iterator一样利用traits询问问题,继承的作用就是为了融入STL,利用被继承者的typedef来回答问题。此外,GNU中也会有一些仿函数供用户使用,比如前面提到的IdentitySelect1st

几个标准库仿函数示例,利用类的对象形式,重载了操作符(),并继承了二元变量操作binary_function

举例前面自定义的仿函数,由于没有继承,所以后续无法融入STL进行适配

仿函数的可适配条件就是需要挑选适当者进行继承

存在多种适配器

仿函数适配器与仿函数类似于算法与迭代器。把仿函数包成adapter给算法使用

适配器可以认为具有一个桥梁作用:把某个底层B包装下做成A交给用户使用,但是内部仍然是调用B来运作。在C++中,适配器采用复合的方式获得某种功能,非继承。

容器适配器方面,典型的是stack和queue,它们内部都是内含了deque作为其底层容器,然后改造成具有自己特性和接口的容器。

Binder2nd/not1/bind

介绍了前面使用过的一个计数语句cout << count_if(vi.begin(), vi.end(), not1(bind2nd(less<int>, 40))),其中用到了算法,函数适配器和函数对象。

bind2ndnot1都是函数适配器,用以修饰后面的函数并将其包装。bind2nd绑定第二实参,限制less函数调用时保持第二参数为40不变。修饰后的函数继续被not1修饰,将结果取反,也就是不小于40的数计数。

bind2nd内部使用binder2nd函数,目的是为了简化用户使用难度,自动帮你推导operation模板参数。侯老师提到binder1st, binder2nd这些函数后来都被bind函数取代,但是旧的函数依旧可以使用。

bin2nd函数内部做了很多工作,包括继承一元参数函数unary_function,询问和操作符重载。注意这里的less()一开始是函数对象,属于typename加()的形式,非函数调用

not1继续修饰bind2nd, 对pred取反

在C++2.0版本中,bind函数取代了bind2nd,可以用来绑定函数/函数对象/成员函数和数据成员,同时可以指定绑定哪个参数,哪个参数不用绑定,利用诸如_1这样下划线加数字的形式(palceholder,占位符)代表第几个参数可以被后续调用再指明。bind可以指定一个模板参数,代表返回类型。更多的例子和示意见下面的图片。

右边举了几个用bind绑定的例子。fn_invert中my_divide的第一参数在后面,所以调用时是_1/_2,fn_rounding就指定了返回类型为int,bound_memfn绑定了函数为Mypair类中的multioly函数,允许用户传递数据,bound_memdata和bound_memdata2绑定函数为传出类中成员数据。最后的例子比较了bind2nd和bind使用的区别,其中cbegin中的c是const的意思

迭代器适配器reverse_iterator/inserter

Adapter在容器,仿函数和迭代器上都有使用。侯老师在这里举了两个例子,一个是前面用到过的reverse_iterator,逆序迭代器遍历方向,另一个是inserter,用来插入数据。在这里侯老师再次强调,adapter是把所有需要的东西系起来,打包好,然后供日后再用,起到一种修饰改造作用。

逆向迭代器内部也是调用正向迭代器,进行操作符重载,包装成想要的样子。

inserter配合copy函数,通过对=操作符进行重载,实现在foo中选定位置进行bar数组的安插,而不是原先的覆盖赋值搬移。其中由于是list容器所以利用advance实现起始迭代器加3

copy函数内部已经把流程写死了,就是按照指定的result迭代器,一步步搬移前面两个迭代器指定范围内的数据,后续的一些adapter改造操作,通过利用操作符重载各自解读,来实现自己定制的特性。

X适配器ostream_iterator/istream_iterator

ostream和istream是两个特殊的迭代器,他们不属于一般的迭代器适配器,容器适配器和仿函数适配器中的任何一个,因此侯老师称之为X适配器,X代表一种未知。

利用out_it迭代器重载操作符=配合copy函数把数据传给cout打印出来

eos可以认为是一个标志,代表cin的结束标志,没有实际参数,iit才是真正的属于输入数据的迭代器,参数为cin,与eos比较判断是否读入完成

如果在例2的两个语句中间加“请输出数字”这样的提示语永远都不会出现,因为这个istream_iterator一旦创建就开始read,读一个传一个

一个万用的Hash Function

hash function的制定可以没有任何规律,但是效果是一定要够乱,尽量减少碰撞。这里侯老师先以一个比较简单的customerhash函数开场,说明这样虽然简单,但是hash code比较容易重复。之后引出了C++标准库中的一个“万用的hash function”, 其中的重点是variadic templates, 这个是C++ 2.0中比较重要的新特性,大意就是可变的模板参数,常用…表示,比如下图中的序号1表示的函数,其中的template<typename...Types>就表示可以接受任意数量的模板参数。

右上角的hash_val函数先调用序号1函数,然后转到了序号2,序号2函数根据4中的hash_combine生成seed,然后再返回2继续拿出seed,不断在序号2和序号4中递归循环,参数会一个一个的拿做当seed,最后只剩一个的时候转去序号3函数,再通过4得到最后的hash code. ^=属于异或操作,0x9e3779b9是根据黄金比例来选取的。

G4.9版本中已经实现对string类型的hash function的偏特化

Tuple用例

Tuple是可以将几个不同的数据结构的变量绑在一起的类型,使用上利用直接声明tuple<>创建,也可以利用make_tuple()实现,或者tie函数快速对变量进行赋值,如下图所示。其中可以对<和=这样的操作符进行重载实现特性上的适配。

tuple的用法示例。最后的tuple_size,::value,tuple_element类似于traits的问答式。此外ppt中tuple的大小为32,侯老师没有给出合适的解释,按照后一页ppt中tuple存在套娃继承的思路,大小应该是4+4+4+16=28,如果最顶的tuple没有被继承,那个就是28+1=29,然后要对齐是4的倍数,结果就是32,但是因为被继承,不是单独的,所以就没有大小,还是28.这里我倾向认为是类似struct的大小计算,最后的大小要是complex大小的倍数,所以是32(但是还没有查证).

tuple内部实现了自动递归套娃继承,也是通过使用variadic templats可变模板参数完成。不断地划分head-tail构建父类,而且取相应tail地址的时候也是指向一整个模块,如右下角示意。

type traits

type traits主要是为了给用户自己写的类提供一些特征信息,用户自己不需要对其进行偏特化处理就可以自动获取,节省代码工作量,广泛用于C++泛型编程。这些特征信息通过询问的方式进行获取,指导该类后续的相应操作。

type_traits部分内容示例(G2.9版本)

上图出现的Plain Old Data (POD)是一个类型(比如class或者struct),但是没有构造函数,析构函数,虚函数等成分。维基百科的解释是:

POD类类型就是指class、struct、union,且不具有用户定义的构造函数、析构函数、拷贝算子、赋值算子;不具有继承关系,因此没有基类;不具有虚函数,所以就没有虚表;非静态数据成员没有私有或保护属性的、没有引用类型的、没有非POD类类型的(即嵌套类都必须是POD)、没有指针到成员类型的(因为这个类型内含了this指针)。

stackoverflow有也相关问题

In short, it is all built-in data types (e.g. int, char, float, long, unsigned char, double, etc.) and all aggregation of POD data. Yes, it’s a recursive definition. ;)

To be more clear, a POD is what we call “a struct”: a unit or a group of units that just store data.

C++2.0的type_traits非常强大,包含不少特性信息,更多见:http://www.cplusplus.com/reference/type_traits/

一个类如果没有没有涉及到指针,就可以不必写析构函数,使用编译器分配的默认的即可。像string内部带了指针,就必须写析构函数。此外,如果不是父类base,没有覆写的虚函数,也不必写virtual destructor.

利用自定义的类Zoo来测试type traits, 其中刚开始的冒号语句为初始化列,后面四句分别为拷贝构造,搬移构造,拷贝赋值,搬移赋值,&&代表C++2.0新特性move,后面会专门谈到。delete也是2.0新特性,表示写出来但是摧毁掉,default表示使用默认方式,即使你不写也是使用编译器给的默认方式。中间的打印结果中,is_polymorphic为1代表这个类继承了虚函数,所以左边写了虚函数destructor, 相应的右边的结果也显示为1.

type traits通过模板实现,对类型进行操作,比如下面的例子,利用泛化,偏特化,传回真假值。

type traits实现is_void,拿掉cv,再借助helper,给出0/1值

此外有些type traits实现的特性未曾出现于C++源码标准库。侯老师说这种情况可能就是编译器编译的时候来回答支持,不以源代码的方式呈现。

traits的理解

type traits 理解

cout的操作符重载

cout为何可以接收各式各样的object?因为其内部对各类型都做了操作符重载。注意其返回类型为ostream&,为了可以继续接收,非void返回类型。

G4.9版本中cout的操作符重载示例。sub_match代表正则表达式(字符串处理),bitset代表处理二进制对象的类型,类似位图。

movable的使用

move是C++2.0一个比较重要的特性,其内部就是拷贝指针,属于浅拷贝,在type traits中见到的&&符号就代表move功能,而拷贝构造则是深拷贝。move的速度很快,侯捷老师也在课程中比较了加不加move功能对容器元素拷贝速度的影响,同时也给出了一些测试例子,但是在使用move时需要自己对元素的使用有清晰的认识,因为move是拷贝指针,等于是两个指针指向同一个位置,这个是危险的,C++内部会在move时做一些处理去规避这个行为带来的危险情况,所以move完之后的原始对象就最好不要再用了。

move的速度一般比拷贝构造快很多。这里侯捷老师测试了不少容器,他提到自己都是用insert函数来完成插值,但是像红黑树这样的容器是自动选择位置的,不允许用户手动指定位置。在这样的情况下insert函数也可以使用,但是具体的位置不一定是用户指定的。

从下面的程序中可以看出,为了避免delete错误,move的对象在执行时会指向NULL,所以最好不要再去使用之前被拷贝的对象了。

对于临时创建的对象,如果有move函数操作,编译器就会自动调用,像Mc11(c1)这样的语句,由于c1不是临时对象,编译器还是老老实实用的深拷贝,但是用户自己可以利用下一行的std::move进行安排编译器去用move操作,但是move之后,用户就要确保不要用到c1这个对象了。

vector的move拷贝动作,利用swap函数对三个指针操作,所以相比较默认的拷贝构造速度会快上很多。

C++的左值和右值