1 演进、环境与资源
dev-c++5.11是开发平台,可以外挂编译器比如MinGW with GCC4.9.2,GCC是Unix家族的,搬到Windows下就是MinGW,挂到dev-c++平台上
2 Variadic Templates
设计函数print可以接受任意个数的参数,并且每一个参数的类型也是任意的,只有typename是关键字,args和Types是任意命名的,为了处理最后的状态还要再写一个上面的函数,最后没有参数的时候会调用上面的函数①版本,如果没有①版本就会编译错误,所以使用Variadic Templates的时候,一定需要写一个处理最后情况的东西,这个东西针对函数就是函数,针对类就是一个类,递归分解,
①是泛化,②是特化,所以①执行到hash_val(seed,args…)会调用②而不是①自己,
然后②会递归recursive调用自己,调用别的同名function不是递归,如果包pack变成0,只剩下种子和0个参数会到③版本,边界条件,
3 Spaces in Template Expression模板表达式的空格、nullptr and std–nullptr_t、Automatic Type Deduction with auto
模板参数仍然是一个模板,可以不用空格隔开两个>符号,
nullptr是对象object或者关键字,它的类型是std::nullptr_t,NULL其实就是0,auto用于变量很长或者类型复杂,比如lambda
4 Unifrom Initialization一致性初始化
现在一致用大括号进行初始化,即在变量后面直接放大括号,就是直接设初值,
大括号的初始化。编译器看到{t1,t2,…,tn}便做出一个initializer_list,它关联至一个array<T,n>。调用函数(例如构造函数)时,该array内的元素可被编译器分解逐一传给函数,但若函数参数是个initializer_list,调用者却不能给予数个T参数然后以为它们会被自动转为一个initializer_list传入。
vector有一个构造函数是接受initializer_list的。其实所有容器都有这种构造函数。
5 Initializer_list
{}大括号里面不允许窄化的转换,
设置默认的初值:
`int n1; // undefined value
int n2{}; // 0
int *p1{}; // nullptr
int x1 = 1.2; // ok
int x2(1.2); // ok
int x3{1.2}; // 警告`
如果想要接受任意个数的东西,就写std::initializer_list,使用者就可以用大括号调用,形成一个array数组,调用的时候传入的一堆东西,编译器会把这些东西形成std::initializer_list这种class传进来,可以接受任意个数参数。
可以把std::initializer_list当成一种容器,
P s={77,5}调用构造函数接受一包的版本,②版本的构造函数和①版本的构造函数,
编译器在看到大括号{}的时候就会制造出initializer_list这种类class的对象,制造出initializer_list就需要编译器调用类initializer_list私有的构造函数,调用构造函数的时候传入一个迭代器和大小,即编译器之前准备好一个array容器,把array的头和长度传入私有的构造函数,
array是数组,变成容器就可以提供迭代器,用算法进行处理,
initializer_list的拷贝是浅拷贝,只是把指针拷贝过去,它的构造函数只是传了一个array的头部指针和一个array的长度。initializer_list类产生的对象并没有包含数组array,只是有一个指针指向array头部,
算法和容器都用到了initializer_list,都可以放很多个元素,
6 Explicit for ctors taking more than one argument
explicit关键字修饰构造函数里接受一个以上的实参,explicit关键字主要用在构造函数上,
如果不想让编译器自动进行隐式转换把5转换成复数类型,就加上explicit关键字修饰构造函数,因为c1+5,加号会作用在c1上,会把5变成复数类型,然后相加,加上explicit表示明确调用构造函数的时候,再来调用构造函数,此时c1+5会失败,
只有non explicit one argument constructor单一实参这种构造函数才能做隐式转换,只要指定一个实参就可以了,因为第二个实参是默认的,
两个参数以上也禁止隐式转换,
7 Range-based for statement
elem是迭代器,vec是容器,{}是initializer_list,auto& elem用引用速度更快,且改变时还能改变原来容器的元素,关联式容器都不支持迭代器改变元素内容,
for loop拿到每一个元素做赋值动作的时候如果类型不一样就要做转换,如果当初源头的地方不允许转换,就不能转换,
8 =default, =delete
默认构造函数就是不需要任何实参就可以调用,编译器给的默认构造函数是空函数,如果class有继承其他的父类,这个class的构造函数必须要调用父类的构造函数,这个调用动作放在这个class的构造函数里,编译器会放一些隐藏代码,
如果你自己定义了一个ctor,那么编译器就不会再给你一个default ctor。如果你强制加上=default,就可以重新获得并使用default ctor。
构造函数可以有多个,但是拷贝构造函数只能有一个版本,不能被重载。拷贝赋值函数也是只能有一个版本。
一般的函数可以修饰为=delete,但是不可以修饰为=default,因为无意义。
=delete可以用于任何函数身上。
=0只能用于virtual虚函数上。
9 Alias Template模板化名
alias别名,using表示使用Vec名称代表等号后面的东西, 别名模板里的参数就会变成上面的T
别名模板。可用于模板的模板参数。Template template parameter模板模板参数
不能对别名模板进行特化。
传给一个函数必须是object,要取出传进来的容器里面的元素是什么类型,
容器一定带着这样一个typedef,所以这样可以得到容器Container的迭代器,然后因为迭代器一定带着value_type,所以把迭代器再丢到iterator_traits萃取机里,得到value_type即每个元素的类型,所以把typename iterator_traitsvalue_type这个名称换成Valtype类型,
10 Type Alias, noexcept, override, final
别名类型,类似于typedef。
这两句是相等的:
typedef void (func)(int, int);
using func = void ()(int, int);
func当成一个类型,就可以创建对象,函数的名称就是函数的指针,就是函数的地址,
using directives即 using 的命令,把命名空间打开,或者一个一个打开
在函数的后面写上关键字noexcept关键字,就是保证函数不会丢出异常,void func() noexcept;即void func() noexcept(true);满足后面的小括号里的条件的时候,该函数func不会抛出异常,
程序有异常没有处理就会一直往上传,最终程序停止,A调用B,B发生异常,就会传给A处理,
自定义的class如果有move ctor和move assignment的时候,这两个函数要保证不会丢出异常,因为如果这个类被存放在vector的时候,万一移动构造函数和移动赋值函数抛出了异常,vector不知道如何处理。
override是覆写,override要保证复写的函数与父类被复写的虚函数的函数签名一致。即大括号前面的部分是函数签名,现在可以在大括号前加override,告诉编译器子类要覆写父类的虚函数,
final表示是继承体系下最后一个,不要再有其他类继承自己了,final在大括号前面,
final用在类不想被继承的时候,或者虚函数不想被子类复写的时候。
11 decltype
用途①声明返回类型,尾置返回类型。和lambdas类似。
decltype是关键字,decltype(coll)::value_type 是一个类型,可以声明一个变量elem,decltype可以让编译器去找出一个表达式的类型,decltype(x+y)让编译器找出x+y的类型,只是让模板先编译通过,实际使用可能编译不通过,而此处因为x和y先出现,但是后面x和y才声明,所以编译不通过,
用途②用于模板里的各种操作
用途③lambda函数里,传递一个lambda类型,
cmp就是对象,面对lambda,往往只有object,没有object的type类型。
要获得其type,就得借助于decltype。
auto cmp = [](const Person &p1, const Person &p2) { … };
std::set<Person, decltype(cmp)> coll(cmp);
12 lambdas
lambda表达式。lambda表达式其实是一个inline函数。lambda本身是一个函数对象object,作用像是一个function函数对象这个类做出的对象,可以用小括号直接调用,
中括号[]是introducer导入器,后面小括号里面放参数,因为行为像函数,mutable表示中括号里面的数据是否可以被改写,由于是中括号开始,没法写返回类型,所以在最后加上->,这三个都是optional可选的,可写可不写,但是只有有一个存在,就一定写小括号,如果三个都没有,也没有参数,则小括号可写可不写,最后大括号{}是函数本体, 中括号里面可以放lambda本身外部的变量,外部的变量可以以传值或者引用的方式传进来,
有一个lambda取用外部的变量id,没有参数,有mutable,lambda相当于仿函数或函数对象这种class并且做出一个对象f,函数对象就是class里面有重载的小括号,表示可以调用,lambda想要取得外部的变量相当于函数对象里面带着一个id变量,
里面变化的相当于是自己的id,里面的id变化不影响外面的id,mutable表示里面的id是可以加加的,不写mutable就不能改变id,lambda相当于一个匿名的函数对象,
cmp这个lambda相当于仿函数或函数对象这种class并且做出一个对象为cmp,用auto修饰cmp,或者把cmp传给一个模板,模板可以推导出模板参数,decltype(cmp)就可以拿到cmp的类型,std::set<Person,decltype(cmp)>coll(cmp),如果需要传一个lambda当作hash function或者排序的准则传给一个不定序的unordered的容器container,std::set<Person,decltype(cmp)>coll是类创建的对象coll,后面(cmp)是调用构造函数,并且给与初值cmp,右边set有一个构造函数,
如果不给coll的构造函数传cmp对象就会调用set的不含参数的构造函数,即set():t(Compare(){},就会调用Compare(),而Compare就是传进set的第二模板参数,decltype(cmp),而lambda没有默认构造函数也没有赋值操作,即容器coll会调用传进去的decltype(cmp)类型的对象decltype(cmp)()默认构造函数,Compare()是产生临时对象,没给参数所以调用lambda的默认构造函数,所以对于排序准则写一个functor,最好写成一个class而不是lambda,因为lambda没有默认构造函数,可能会出错,
13 Variadic Templates
可变参数模板,变化的是参数个数和参数类型。
利用参数个数逐一递减的特性,实现递归函数调用.使用函数模板完成。
利用参数个数逐一递减导致参数类型也逐一递减的特性,实现递归继承或递归复合,以类模板完成。
递归调用处理的都是参数,使用函数模板。
递归继承处理的是类型,使用类模板。
①和③可以共同存在,①比较特化,则③永远不会被调用,因为会被①取代,因为①是特化的版本,
调用cout<<max({57,48,60,100,20,18})<<endl;的时候放大括号{},编译器看到大括号{}就会自动形成一个initializer_list,背后是array容器,即这六个数放到一个array容器里,进去max函数里调用max_element函数,要把initializer_list即array的头尾迭代器,传到max_element函数,再调用_max_element函数,而给_max_element函数的参数里,比大小给的条件为_iter_less_iter(),_iter_less_iter是一个function,此处_iter_less_iter()为调用一个函数,需要收到此函数的返回值,而此_iter_less_iter()函数又返回_Iter_less_iter(),此时_Iter_less_iter就是一个类型,而_Iter_less_iter()就是类产生的临时对象,
如果不用大括号{}就不借用initializer_list,而使用Variadic Templates
把东西组合起来丢给cout,让cout解析打印,所以重载<<操作符,接受的第一个参数是cout这种对象,第二个参数是tuple,
利用可变参数模板实现 cout << maximum(5,2,0,1,3,1,4); 的功能。其实也可以用 cout << std::max( {5,2,0,1,3,1,4} );
tuple是一个类,
子类的对象里面有一个父类的part,head()的返回类型就是m_head的类型,即Head去问type是什么,有::前面就应该加typename,tail()把*this转型为inherited即tuple<Tail…>类型,左边中间inherited(vtail…){}是初值设定,此处不是创建临时对象,而是调用父类的构造函数,所以能一直向上调用,每次都拿出头部一个元素,继续调用父类构造函数,但是下面的Head::type编译不过,因为有的类型无法回答type,
14 Rvalue references右值引用 and Move Semantics
右值引用为了解决非必要的拷贝,当来源端即右边是右值,左值可以出现于operator=左侧。右值只能出现于operator=右侧
左值是变量,变量在赋值符号的左边,左值也可以出现在右边,右值只能在右边,不可以放左边,临时对象就是一种右值,
foo()是函数返回的东西,对函数返回的东西取地址&,但是函数返回的东西是个右值,不可以对右值取地址,foo才是函数的名称即函数的地址,新语法对右值取地址用引用,所以右值放在赋值符号的右边,以便放右边的时候为了能够取它的reference
c的类型是vector,用随机数把buf变成一个字符串,Vtype(buf)是创建临时对象,通过insert插入vector, Vtype(buf)就是一个右值,
vector的insert有两个版本,如果传进来的是rvalue reference右值引用,就会去版本②,否则去版本①,因为Vtype(buf)就是一个右值,编译器面对临时对象,一定把临时对象当成右值引用放版本②,insert为了搬,会调用Vtype(buf)这个临时对象元素的类型的拷贝构造,也需要写两个版本,一个是深拷贝,
Vtype(buf)传到左边进行浅拷贝后,指针被打断,并且因为临时对象,生命会在这一行结束后消失,消失的时候会调用析构函数,而析构函数会判断指针是null,就不会再次delete删除了,Vtype(buf)本身是方块的生命消失掉,但是被打断的指针是null,所以并不会影响放字符串的长方形的那一块内存,
被move后的原来的东西即临时对象不能再用,因为原来的东西即临时对象的指针已经去掉了,而临时对象没有名称,所以之后不会用到它,所以编译器看到临时对象,一定把临时对象当作右值,去搬移拷贝,
如果是左值,也想快速拷贝,可以把左值放到move里,就相当于拿到了它的rvalue reference,
临时对象转交给insert右值引用,然后再转交move构造会丢失一些东西,所以使用标准库的std::forward就可以达到完美的转交,
a是左值,调用process后打印第一种版本,1不是变量,是右值,调用process后为右值引用,左值a加上move后就把a当成右值了,而调用forward ,forward里再调用process即转接的过程,forward(2)和forward(move(a))这两个都调用错了, forward(a)也调用不了,
完美转交的forward都是模板,
15 写一个 Move-aware class
设计一个class,当成元素的类型,带着move意识,之后容器进行copy的时候,发现有move意识,就会用move取代拷贝,
浅拷贝:来源端的长度设到目的端,来源端的指针设到目的端,相当于目的端steal了来源端的东西,steal后,来源端就把自己的长度设为0,指针设为null,析构函数会检查,不是null情况下才会delete,
移动构造函数和移动赋值函数要把传入对象的成员变量的指针置为nullptr,并且在析构函数的时候要判断-不为空的时候进行delete。
只要容器是以节点的形式存在,也没有move constructor或copy construct,到底调用哪一个差距影响不大,
swap_data函数把指针交换了,
M c2(std::move(c));把整个容器c当作右值,只要被当成右值,就是资源resource被偷steal了,所以往下就不能再用c容器了,
16 array,hashtable,hash function
array的数据是_M_instance,后面中括号[]表示数组,类型是value_type,是_Tp,即模板参数,_Nm指定0就是1,指定非0就是非0,因为array要表现c++的数组,数组没有构造函数和析构函数,所以array这个容器也没有,
4.9版本把array做出右上角的第三四行,typedef int T[100] ,T c,T就变成一个类型,用T类型声明对象, 此版本array里的变量即_M_elems,这个变量就像c,_M_elems的类型_AT_Type::_Type就像T,而_Type又是_Tp,
针对基本类型有hash function,指定后形成一个type ,hash是type类型,加上小括号就是临时对象,这个对象是function object,行为像function,所以调用小括号,传入一些数值,
生成撒网继承 genscatterhiderachy