C++ Primer Plus翻译心得 7~16章(算法前)

44.替代变量:reference variable
创造别名,主要用于函数形参,和指针一样,不会make a copy而是直接访问原数据。
声明如:int a;int & b=a;//此时b就是a的别名,两者使用同一内存地址
类似指针,int *是pointer-to-in,int &是refer-to-in。指代变量必须在声明时就赋值,且不能更改,不同于指针,和常量指针类似。
int func(int &a,int &b);//这样写函数原型,当被呼叫时直接func(a,b)、a和b都是int,不需要额外写指针。若不想改变原数据而只想省下复制的时间,可以写int func(const int &a,const int &b);
有些涉及临时变量的形参需要慎重。临时变量的自动创造有两种情况:(1)实参的类型匹配,但不是左值(lvalue);(2)实参的类型不匹配,但可转化成形参的类型。
左值是指一个数据对象能通过地址来引用,非左值的比如字符常量(除了引起来的字符串)、一些表达式。C的时代还没有出现const,左值是指可以出现在赋值符号左边的值,C++后const称为不可调改的左值。
所以对于int func(int &a,int &b);,如果在调用时输入了char,char可以向int转换,但会生成一个临时的int变量,所以如果需要修改char的值,func会对临时int变量修改,无法达到目的;如果输入int常量如func(1,2);,func也会生成临时int地址储存1、2。
一个&称为左值引用,&&称为右值引用。
返回一个引用变量的函数:int &func(int a,int b);//return时和普通函数一样,不用加&
这样就不会返回一个copy而是直接返回原数据。这样的函数还可以做左值,因为其返回了一个引用变量:func(1,2)=3;,若要避免这样赋值可在函数头最前面加const。返回引用变量的好处是节约时间和空间,被调用函数创建了变量的存储空间并将其返回给调用函数,而普通的调用函数返回一个临时变量,拷贝到调用函数的变量中,然后删除临时变量。
要注意,有些变量在函数调用完后就被删除,不要在返回引用时返回这类变量(指针同理),否则在主函数中调用这个内容已被删除的地址可能导致崩溃。一般可以在调用函数中创建好地址再传给被调函数,或者用指针的new办法并用自动delete。//直接创建指针也可,比如在上述func中int t;…return t;,在使用func时写int &t=func(a,b);,这样t就接住了func中传来的地址。
设想有一个函数:int fun(const string &);,输入一个字符串常量会如何?首先,字符串常量等于其第一个字符的地址即char
类型,char
可以转换为string,于是系统创建一个临时string变量,并将其地址传给fun函数。

45.以基类的引用做形参的函数,可以输入派生类而不用类型转换。

46.setf()状态的存储:
setf()中很多状态用完一次就得重新设置。可以储存设置如:
ios::fmtflags initial;
initial=cout.setf(…);//设置了很多状态,将此时的format储存在initial中
下次使用直接cout.setf(initial);即可

47.默认参数:
int func(int a=1);在原型里写赋值即可,别在定义里写。注意,一个参数有默认值,那它右边的所有参数也都必须有默认值(防止使用时默认值对不上号)。

48.函数重载:
同名函数拥有不同signature即重载。有时函数重载会导致自动类型转换失效,并且要从编译器角度看,变量和变量引用是一样的、不能重载(int func(int a)和int func(int &a)调用时都写为func(x)),有时仅返回类型不同、形参多了个const也不能重载。但一些引用变量类型的const可以重载。如下的函数,在它们不同时出现时:
int func(int &a);//可匹配能修改的左值
int func(const int &a);//可匹配右值,常量左值,能修改的左值
int func(int &&a);//可匹配右值
但当它们同时出现、重载时,就会精确匹配:
int func(int &a);//匹配能修改的左值
int func(const int &a);//仅匹配常量左值(能修改的左值会匹配到上一条函数,右值到下一条)
int func(int &&a);//可匹配右值

49.函数模板,例如:
template (这里typename 也可换成class,但前者可用于老环境。定义、原型都要写。也可以写多种类型)
void swap(T &a,T &b)
{
T temp=a;
a=b;
b=temp;
}
函数模板不会使程序变小,因为编译时按照模板生成了每次调用时的函数,存放在头文件里。函数模板也可以重载,函数模板的实参不一定都是T类型。
如果有未定义的运算符,可以编写运算符重载,也可以针对这种类型写特定函数,运行时先从一般函数中查找,再去函数模板中查找。实际上,C++98标准规定:
(1)对于同一个函数名,可有非模板函数,模板函数,特化模板函数;(2)优先级上,非模板函数>模板函数>特化模板函数。

特化模板函数:
原型如:template<> void swap(classname&,classname&);
其中classname是程序员定义的类名等,swap后跟的可省略,因为后面的signature就暗示了。

如果用本例中的swap函数处理一个int和一个double变量,可以这样:swap(int变量,double变量);这样可强制转换,同时若有匹配的非模板函数也会强制使用模板函数,但由于生成了临时变量,交换的目的没有达到。更多函数优先级参考434页及之前,总的来说,能匹配上的优先,相同匹配时非模板函数优于模板函数,同为模板函数,实参更贴近形参、需要转换少的优先。

decltype关键词:int x;decltype(x) y;//声明y变量,其类型和x变量一样。里面可以是表达式。
主要是为了模板函数中生成新变量时类型不确定准备的。
在此处括号里使用函数,不会调用函数,而只会检查函数返回类型。
见441之前。

考虑一个函数如下:
template <typename T1,typename T2>
?什么类型? swap(T1 &a,T2 &b)
{
return a+b;
}
为了解决这种问题,C++11提出了拖尾函数返回:
auto func(int,int)->double;//定义也要这样写
如此一来,模板函数可以这样写:
template <typename T1,class T2>
auto swap(T1 &a,T2 &b)->decltype(a+b)
{
return a+b;
}

50.自定义头文件:
一般把函数原型、符号常量(define或const定义)、结构体定义、类定义、模板函数和内联函数的定义放在头文件里。把普通函数的定义放在头文件经常导致重复定义。

51.除了automatic、static、dynamic storage duration,还有thread storage duration(C++11),用于多核CPU。

静态声明:
有外部链接的(可以在不同文件间共享)在block外声明;内部链接的在前面加static;无链接的在block内声明且前面加static。

对于在一个文件中定义的外部变量,所有使用之的其他文件,都要有一个变量声明,前面加extern,说明该变量是其他地方声明的。在使用变量前加::也可以说明使用的是外部变量。在block内声明的static变量从程序开始就占有空间,即使跳出block也存在,且多次调用该block不会重复声明而是用已有的空间且保有以前的值,可以用来存放密码等。

52.volatile和const相对。前者主要和编译器优化相关,加此关键词可阻止编译器进行此类优化。
mutable关键词用于const结构体或const类,在其中的成员前加mutable以表明该成员可变。
在外部链接的变量前加const会导致其仅有内部链接,这主要是为了避免头文件包含常量定义时出现重复定义的情况。在前面加extern可使其有外部链接,然后其他文件也用extern声明该外部引用的变量。
函数默认有外部链接、静态储存。在函数原型和定义前加static可使函数仅有内部链接。正如block内的本地变量会覆盖同名的外部变量、一个文件内的内部变量会隐藏同名的外部变量,一个本地的内部链接函数会覆盖同名外部函数。类似变量,一个函数也只能定义一次,但用到该函数的文件都要写函数原型。
一个库函数,默认使用C++版本。如果要使用C版本的老函数,可以在函数原型前面加”C”空格。

53.new指定内存位置:
头文件包含new,在new后括号中写希望使用的地址,如:int p=new(buffer1) int[20];即可在buffer1中创建动态空间。buffer1是一个char类型,指向一个地址,重复使用会覆盖之前的数据。同时,buffer1在静态内存区域,不能用delete释放,也不需要。

54.命名空间、using声明和using命令:
486之前。using声明如using std::cout;,using命令如using namespace std;。前者,再声明本地同名变量会报错;后者则会使本地变量覆盖命名空间的变量,可以使用::来操作全局变量,或者命名空间名::来使用特定命名空间的变量。命名空间也可嵌套,其嵌套有传递性,还可以给命名空间起别名:namespace a=b::c;。还可有无名的命名空间,作用类似全局变量但不能在其他文件使用。

55.在类的定义里直接写的都默认是inline函数,在外面实现的函数也可以加inline来变成内联函数。

56.定义一个const的类的对象后,可能不能调用其成员函数,因为系统不知道该函数是否会修改对象。在该成员函数的定义和原型,圆括号最后加const,意为此函数保证不会修改常量对象:
void classname::func(int a) const;

57.如何在类的定义中加一个常量数据成员?直接加const T=12;是不完善的,因为直到创造一个对象时,这个数据才会在内存中存储。可以static const T=12;或者借用enum变量:enum{T=12};,接下来就可以使用T。但在外面使用类定义外使用T时要加 类名::T ,或者在命名空间block里写类定义后写 命名空间::类名::T(类名::可省略)

传统的enum还有一个问题,即不同enum类中的变量不能同名,C++11的解决方法是加class:
enum pants{S,M,L,XL};enum T-shirts{S,M,L,XL};//冲突
enum class pants{S,M,L,XL};enum class T-shirts{S,M,L,XL};//也可以用struct代替class
此时可以用::来指示具体是谁的S,M,L,XL。而且此时的enum变量不会自动转换为int,有时需要(int)来强制转换。C++11同时默认enum基于int,也可以手动选择基于什么类型,比如short来节省空间:
enum class:short pants{S,M,L,XL};//不能是浮点数

58.运算符重载:
返回类型 operator+(参数);
+也可以换成其他运算符。使用时可以只写+,也可以按常规成员函数的方式调用,即.操作符加函数名operator+(参数)。调用+的类在+左边,右边的为参数。+可以连用,写的时候不要返回block内变量的引用。
运算符重载函数不一定要是类的成员函数,但至少有一个操作数是用户定义的类,且不能改变原运算符的语法格式,不能改变运算符的优先级,不能自定义新的运算符,且以下的运算符不能被重载:
sizeof
.
.*
::
?:
typeid
const_cast
dynamic_cast
reinterpret_cast
static_cast

以下的运算符重载时只能写做成员函数:

()
[]
->

可重载的运算符:

59.友元函数,友元类,友元成员函数:
本条只讲友元函数。
声明一个函数是一个类的友元函数,则该函数也可以访问类的private成员。
如果有一个类CLASS,a是CLASS 的一个实例,定义一种a+int的操作,如果只写了一个+的重载成员函数,就不能使用int+a的语法,因为成员函数要通过a来调用。可以再写一个非成员函数的重载:
CLASS operator+(const int t,const CLASS &a);//假设a+int返回一个CLASS对象
它不需要a来调用,但必须要使它获得对a的私有成员的访问权,也就是让它成为友元函数。
在这个函数前加friend,并将其放在类的声明里即可。写函数定义时不用加CLASS::,也不用再写friend了。在该函数中也可直接调用已经定义的成员函数:
CLASS operator+(const int t,const CLASS &a)
{
return a+t;//+已在成员函数中重载,也可以不写成友元函数(没有访问私有成员)
}
这么短的友元函数也可以直接写在类定义里成为内联函数。

类似地,对<<重载也可用友元函数,否则就要用a<<cout;的形式了。
void operator<<(ostream &os,const CLASS &a)
{
os<<…;//不需要是ostream类的友元函数,因为只是整体使用
}
想输出至文件,由于基类的引用可以指向基类及其派生类,所以不用再写函数。
其实由于上面的函数返回void,它不可用连用:cout<<a<<”123”;
返回一个ostream对象就可使之连用。由于这里的参数是引用、不是本地变量,可以返回一个ostream &。

60.给类加上类型转换:
假设有一个类WEIGHT,成员有pound磅(int)和KG公斤(double),有一个构造函数是WEIGHT(int p);,那么此构造函数就可以行使默认类型转换,如:WEIGHT a=10;,实际是WEIGHT a=WEIGHT(10);。如果要停用这种默认类型转换,可以在构造函数前加explicit,这时只能写后面的式子来转换类型。不加explicit的构造函数默认可用于以下几种默认转换:
用int类型初始化一个WEIGHT对象;
将一个int类型赋值给WEIGHT对象;
将int作为WEIGHT参数传递;
函数返回WEIGHT对象,却返回了int;
以上几种情况若使用了可以明确转换为int的基础类型,也成立。

最后一点中的“明确转换”,比如同时有一个WEIGHT(long long p);,那么WEIGHT a=10.0;本来可使用参数为int的构造函数,现在就不知道使用哪个函数。

转换函数:
要将一个其他类型转换为类的对象可使用构造函数,而将一个对象转换为其他类型就需要转换函数。格式:operator typeName();其中typeName是需要转换到的类型。
转换函数必须是成员函数,不能在头部写返回类型,不能有参数。
对于WEIGHT对象,写一个将之转换为int的转换函数:
operator int()
{
return int(KG+0.5);//四舍五入的效果
}
如果定义了不止一个转换函数,注意避免语义模糊。
尽量多用explicit关键词。C++98explicit不能对转换函数使用,C++11移除了该限制。或者定义一个有返回类型的普通成员函数来做类型转换。

59和60总结:想要得出一个类+另外一个类型,可以写一个重载+的函数,再写一个友元函数,这样快一点但写的代码多;或者使用转换函数,每次+时转换类型,这样写的代码少但运行慢一点。

61.57补充:一个类的成员若是static的,则不管对象有几个,该成员只有一个、被所有对象共享。这样的成员不能在类的定义中初始化,应该在主程序中初始化。如:
class CLASS{
static int a;
};

methods.cpp中在外部写int CLASS::a=1;。仅当static成员是一个const整型,或一个枚举常量时,可在类的定义中直接初始化(见57)。
static成员和对象在内存中不放在一起。

61.C++会自动生成一些类的成员函数,有时会引起一些麻烦。以CLASS a,b;为例,以下函数在你没有定义时自动生成:
构造函数;
析构函数;
拷贝构造函数;使得CLASS a=b;等同于CLASS a=CLASS(b);
赋值操作符函数;
地址操作符函数。用于返回this指针的值
后三种只在有对应操作时生成。
(C++11还有move constructor和move assignment operator,见后)

每次声明一个类的对象时都会调用一次默认构造函数。可以不定义、自动生成,若你自己定义了,程序会使用你定义的。自定义的默认构造函数可以无参,也可以通过给所有参数设置默认值来变成无参的默认构造函数;
拷贝构造函数,用于将一个对象赋给新创建的对象。其原型为:CLASS (const CLASS &);。以下几种形式都会调用拷贝构造函数:
CLASS a(b);
CLASS a=b;//可以直接将a赋值,也可能创造临时变量再赋值,根据不同环境
CLASS a=CLASS(b);//同上
CLASS *ap=new CLASS(b);
有几种情况会调用拷贝构造函数:
(1)声明一个对象并用已有对象来初始化这个新对象;
(2)函数将对象通过值传递;//所以应该尽量传递引用
(3)函数返回一个对象;//同上
(4)生成临时对象。
总之,涉及拷贝一个对象的操作,都会调用拷贝构造函数。它的执行,就是将类的非static的数据成员挨个赋值给需要生成的对象。但这时它虽然绕过了构造函数,却会在销毁对象时调用一次析构函数,可能引起麻烦。比如你定义了一个static int来计数对象的个数,使用析构函数就会使计数器-1,数量就不准确。解决办法是提供一个拷贝构造函数,其中要更新计数器。另外,由于其直接赋值,如果类含有指针成员,比如用char *来指代一串字符,拷贝构造函数使用后临时变量也指向同一个地址,然后如果析构函数用delete,临时对象调用析构函数时就会影响到不该删的数据,而且原本的对象再调用析构函数delete,就会导致未定义的操作(两次delete同一个地址)。为了解决这个问题,自定义的拷贝构造函数需要用深拷贝,也就是再new一个char *,这样析构时就不会干扰原来的数据。

赋值操作符函数也可能导致问题。
每当将一个对象赋给另一个对象时就会调用赋值操作符函数。如上所说,有些环境下CLASS a=b;会先用拷贝构造函数再调用赋值操作符函数。同样,这种操作也是对成员挨个赋值,若成员中有指针,也会造成两个不同对象的成员指针指向同一个地址(数据污染)、对同一地址delete两次。那么我们就需要自己写赋值操作符函数了。
比如对于一个有通过new创建指针的类,我们的赋值操作符函数先要delete原来的指针(同时要避免将对象赋给自己,如a=a),然后返回一个调用者的引用(这样就能连环使用)。

对[]重载且返回一个引用,可以造成赋值(虽然违反了私有)。例如string t[1]=’a’;如果要对const对象使用,可以重载一个const版本。

String::String(const String & st)//拷贝构造函数模板
{
num_strings++; // handle static member update if necessary
len = st.len; // same length as copied string
str = new char [len + 1]; // allot space
std::strcpy(str, st.str); // copy string to new location
}

String & String::operator=(const String & st)//赋值操作符函数模板
{
if (this == &st) // object assigned to itself
return *this; // all done
delete [] str; // free old string
len = st.len;
str = new char [len + 1]; // get space for new string
std::strcpy(str, st.str); // copy the string
return *this; // return reference to invoking object
}

62.delete可以对new生成的指针或空指针操作。有时即使只需要一个长度的指针,也用new[1]而直接不用new,以和delete匹配。

63.类的静态成员函数:
只在类的定义中加static,函数实现时头部不加。这种函数不必被对象唤醒,也没有默认的this参数,使用时前面加 类名:: 来调用。而且静态函数只能使用静态数据成员。
静态函数一般用来设置类的flag等等。

64.对类使用指针:
有许多注意事项。有时需要主动调用析构函数,按与声明时相反的顺序。

65.初始化对象时给常量成员赋值:用member initializer list
CLASS::CLASS(int a):CONST(a)//CONST在类中的定义是const int CONST;,多个间用,隔开
{

}
这样还没有进入函数体,CONST便已经被赋值了,此时它还不是const。对普通成员也可以这样赋值。若类的成员中有引用变量,这个引用变量,也必须这样赋值(因为引用变量和const一样,只有声明的时候可以赋值)。有时为了提高效率,若成员数据是另一个类,也这样赋值。这种形式赋值只能用于构造函数,初始化顺序和初始化表无关,而是按类的定义中的出现顺序。或者如果愿意,对常量和非常量都可以在类的定义里就赋值,比如在定义里写int a=10;,但如果构造函数中再用member initializer list赋值,就会覆写这个默认值。

66.创造衍生类时,必须先创造基类对象(用MIL,不用MIL就会调用无参版本构造函数),销毁时则先调用衍生类的析构函数再调用基类析构函数。这里调用的都是关系最近的一个基类的函数。
基类的引用或指针可以不经类型转换就指向衍生类对象,但此时只能用基类的方法。也可以用衍生类对象来初始化一个基类对象,因为copy constructor的参数是基类对象的引用,可以指向衍生类对象。赋值操作符重载函数同理。另外,基类的引用或指针还可以不经类型转换指向衍生类的衍生类对象。
不加virtual的函数,默认根据指针或引用的类型来决定使用同名函数中的哪个;加了virtual,根据指针或引用所指向的对象来决定使用哪个。在基类中的虚函数,默认衍生类中同名函数也是虚函数,但最好还是用virtual指明。只需在原型中写virtual。另外,最好把析构函数也写成虚函数(即使基类的析构函数什么都不做),否则只会调用指针或引用的类型的析构函数。
按值传递的函数传一个衍生类,仅基类的部分会传递,所以用的都是基类的方法;如果传递引用或指针且有虚函数,则会使用类中对应的函数。
在衍生类中重定义基类中的同名方法,会使得衍生类无法使用基类的同名方法(基类的方法被隐藏)。所以衍生类中同名方法的函数原型必须和基类中一样(为了使用基类的方法。如果不需要也可以不用),除非基类的方法返回基类的指针或引用,此时衍生类中的同名方法可以返回衍生类的指针或引用,但参数表还是必须一样。如果不需要更改内容,可以在衍生类的方法实现中直接调用基类的方法。

67.protected的成员,对于外部不可直接访问,对于衍生类可以直接访问。

68.设计ABC(abstract base class)时关于纯虚函数:(在原型中)virtual前缀,最后有=0,实现写在外面别写里面。包含纯虚函数的类,没有对象,而仅用于继承时作为基类。也就是说把一些过于抽象的共同点提取出来,做成一个基类。当然,一个ABC里也不全是纯虚函数,纯虚函数可以有实现也可以没有实现。

69.基类有new和delete的动态分配成员且写了析构函数、拷贝构造函数、赋值操作符函数,如果衍生类没有动态分配,可以不做特殊函数。衍生类的默认析构函数在自身执行完后会调用基类的析构函数,默认的copy constructor会使用基类的copy constructor来定义基类部分,赋值操作同理。
若衍生类也有动态分配,析构函数只需要delete衍生类里多出来的new,剩下的会自动执行基类析构函数;对衍生类的copy constructor,函数体里处理衍生类多出来的部分,头部还要用初始化表呼叫基类的copy constructor,即
derived::derived(const derived &t):base(t)//derived即衍生类名,base即基类名
{
…//处理衍生类多出来的部分
}
实际上,base里并没有参数为derived类型的copy constructor,但由于基类的引用可以指向衍生类,所以可以使用base::base(const base &t)的方法,只拷贝衍生类中基类的部分。
最后,对于赋值操作符函数,在衍生类中,在if(this==&t) return *this;的下一行加一个base::operator=(t);,这样就调用了基类的赋值操作符函数。对于友元函数,比如重载<<函数,可以在衍生类的重载<<中加os<<(const base &)t;来强制类型转换并指明使用基类的重载<<函数,即可调用基类的<<函数。

70.不被继承的成员函数:
构造函数不被继承,但可通过成员初始化表调用基类的构造函数(不显式调用就会用默认的无参基类构造函数),C++11增加了继承构造函数的机制。析构函数也不继承,但在执行完衍生类的析构函数后会自动执行基类的析构函数,通常来说,基类的析构函数都要定义为虚函数以确保不会只执行基类的析构函数。赋值操作符函数也不被继承,因为参数不同、函数签名不同。

70.继承描述的是is-a关系,如果一个类包含其他类的对象,就是一种has-a关系。这种情况下,构造函数也可以使用member initializer list,但使用成员名而不是类名。如一个类中含string name;,那么要写name(t)来调用对应的构造函数。和继承相同,如果省略MIL,会使用默认的无参构造函数来初始化对象成员;而且是按类定义的顺序而非MIL的顺序来构造,用前面的数据来初始化后面的数据时要注意。
同时,has-a关系也可以通过private继承,这样衍生类就不会继承基类的接口(has-a关系的特点),但衍生类的方法可以使用基类的方法。类的包含(一个类包含另一个类的对象)有一个有名字的成员对象,而私有继承提供了一个无名的成员对象。私有继承时private可以省略。
用私有继承来表示has-a关系,MIL要写类名而不是对象名,衍生类的成员函数调用基类成员函数时要加上 类名:: 来指定。要得到具体的成员,可以使用强制转换,直接return (const string &) *this;
但一半用包含而非私有继承来表示has-a关系,因为更少出错,而且包含可以提供同一个类的不只一个对象成员。但私有继承可以访问基类的protected成员,可以重载虚函数。

71.各种继承关系:

最后一行指能否用基类的指针或引用指向衍生类。
不同继承可以影响基类的衍生类的衍生类。

72.如果私有继承却想使用基类中的私有成员,可以在衍生类的public部分加using命令。如using 成员名;,注意只要成员名,不要括号、签名、返回类型。例如using std::valarray::operator[];

73.MI,multiple inheritence,比如base是基类,derived1_1和derived1_2都公有继承了base,然后需要一个derived2来公有继承1_1和1_2,就会引出问题。首先是2会有两个base成员,那么用base指针指向2就会有歧义:derived2 t; base *p=&t;。可以使用类型转换解决问题:base *p=(derived1_1 *)&t;或base *p=(derived1_2 *)&t;但这只是治标,要治本还得使用虚基类,这样2就只有一个base成员。
具体语法,只需要写定义时derived1_1:public virtual base(也可写virtual public),甚至base和2的定义都不用变。这样2继承1_1和1_2后就只有一个base成员。当然还需要其他调整。
首先构造体:非虚基类,构造函数的MIL只要最近的父类,对于2,就是需要1_1和1_2在MIL中;而虚基类使得1_1和1_2之间公共的部分(即base)不声明,那么我们就需要认为调用base(…)(除非要使用base的默认无参构造函数)。
还有,如果1_1和1_2都从base继承了一个虚函数,我们没有定义该函数而直接用2来调用该函数,理论上程序会寻找最近的祖先的同名函数,但1_1和1_2都有,也导致歧义。可以用 :: 来明确调用的是哪个函数,但最好还是重定义一个同名函数。但这时需要一些接口,比如要定义展示内容的函数,可定义为protected,在base中定义的只会展示base的内容,在1_1和1_2中除了展示函数,还要有一个仅展示多出来的内容的函数,然后在2中把这些组合起来。定义为protected是为了使这些模块仅在衍生类中能调用。
通常,一个衍生类的衍生类,如果继承的衍生类中部分是虚基类,那么这个第三代类的这部分通用一个基类部分,剩下的都是独立的非虚基类。

74.一个类中的成员若与其祖先类中成员同名,就会占支配地位,这时可不用 :: 而直接使用、没有歧义。这种支配与继承方式无关,例如73中,1_1和1_2有同名的public成员,即使2是公有继承_1、私有继承_2,这时理论上不能直接调用_2中的成员,但还是有歧义。

75.类模板:首先在类定义前加template 或者template ,然后涉及的方法都要加template ,且关系标识符都要改,从classname::改成classname::(原型不用改,这两个都是在实现里改)。如果方法写在类定义里,可以加前缀和关系。和函数模板一样,类模板不是真的可执行代码,而是告诉编译器如何生成真正的类。函数模板会使用传入的参数决定类型,而类模板必须要指定类型。

76.使用类模板必须确保知道细节。比如一个类模板重载了两个对象的+操作符,你传入指针来实例化,但指针相加没有意义。

77.类模板可以用来做其他模板的类参数,也可以递归使用来声明多维,比如vector<vector>就是二维数组。

78.类模板中可以使用不止一个类参数,类参数也可以有默认值(函数模板的类参数不能设默认值),还可以加确定类型的参数、设置默认值(函数模板也可以)。

79.隐式实例化:在没有创造具体对象时,程序不会根据类模板生成类。如果需要显式实例化,可以在类前面加template class 。

80.特殊实例化:比如你定义一个类模板,其中有sort函数,对于数值类型可以正常工作,而对于char*类型需要用strcmp。
特殊实例化格式是template <> class Classname { … };
部分特化:如
template <class T1, class T2> class Pair {…};
template class Pair<T1, int> {…};
template <> class Pair<int, int> {…};
那么
Pair<double, double> p1; //用第一个
Pair<double, int> p2; //用第二个
Pair<int, int> p3; //用第三个
用最接近的一个形式。
新的编译器支持在模板里嵌套模板,比如在类模板的成员里定义一个类模板。如果在外部给出子模板的定义,头部需要嵌套。

81.模板头可以嵌套模板,比如template <template class Thing>。

81.友元函数可以特化也可以一般化,一般化时要在类模板前写函数原型。这两种是每一个类有每个类的友元函数,如果在类模板里的友元函数前写template (其中C和T不同),那么所有类共享这个友元函数。

82.别名模板:
如template
using arrtype = std::array<T,12>;
使用时如arrtype gallons; // gallons is type std::array<double, 12>

C++11扩展了using = 的用法,和typedef一样:
typedef const char * pc1; // typedef syntax
using pc2 = const char *; // using = syntax

83.友类:一个类可以是另一个类的友类,这时即可访问另一个类的所有成员。或者也可以只让一个类中的部分方法成为另一个类的友元函数。

让一个类成为另一个类的友类,可以在定义中任意地方写friend class classname;。同时,友类的定义需要写在后面,或者提前声明。

让一个方法成为另一个类的友元函数,友类的定义可以写在前面,而且在友类前面加另一个类的声明class anotherclass;,在友类后面写另一个类的定义,其中声明该方法是友元函数,记得加 :: 表明方法归属。然后在另一个类后面写该方法。
class anotherclass;
含有友元方法的类定义
另一个类定义
涉及的友元方法
想写inline函数就加inline。

让一个函数对两个类都是友元函数需要提前声明一个类,在两个类里都写friend。见888

84.例外:
有时为了避免函数出现预期外的结果,需要调用一些函数。例如一个计算调和平均数(2xy/x+y)的程序,如果x+y=0就有问题,此时可以调用abort()函数(cstdlib中),向标准错误流发送一条信息:程序错误结束,并结束程序(cerr用相同的流)。如果该程序被其他程序打开,会向父程序发送错误值,否则向操作系统发送(不同环境有不同的错误值)。同时根据环境不同,abort()可能刷新文件缓冲区,不确定可以使用exit()刷新文件缓冲区。abort()会导致程序立即终止。
也可以通过例外机制解决这类问题。步骤上,先throw一个例外,再用一个解决器解决它,最后来一个try block。
try{}
catch(type a){}

throw a;
程序从try进入,没遇到异常就执行handler下面的语句,遇到异常就扔出a,然后程序会逐步回溯(这个过程中会释放沿途的自动变量,比如自动分配的对象。所以throw回去的总是一个copy),找到try block,然后看看下面的catch handler有没有匹配的,有就执行这个handler(一般不只一个handler)。a可以是int,char
等,最常用的还是对象,来描述异常情况。如果找不到try block或没有对应的handler,默认会调用abort(),可以更改这个默认设置。

例外特化:出现在函数原型和定义结尾,加throw(…);,括号为空即函数不抛出异常。主要起提醒作用,和注释差不多,C++11不鼓励使用,但增加了noexcept关键词,以说明函数不会抛出异常。

catch一般跟引用,这样可以用基类指针指向衍生类对象,同时catch的顺序要和继承的顺序相反。如果不知道catch什么,可以catch(…),意为匹配所有类型,放在最后相当于默认。

exception头文件中定义了exception类,可以用做基类来写自己的throw对象。其中what()方法可以重载,一般what可以返回字符串来描述错误类型。stdexcept头文件中包含了logic_error和runtime_error两个exception的衍生类,见918。
以前的动态分配,若剩下空间不足会返回null,现在则会throw一个bad_alloc类,它的字符串成员在不同环境中不同。有些编译器支持两种模式切换,格式:
int * pi = new (std::nothrow) int;
int * pa = new (std::nowthrow) int[500];

例外导致问题:如果函数有例外特化,但throw与特化不匹配,默认情况下程序终止。过了这一关,如果回溯以后找不到try区块或者没有匹配的catch handler,也会默认使程序终止。可以修改这两种默认反应。
其实,第二种情况发生时会先调用terminate(),而terminate()默认调用abort(),而set_terminate()可以更改这一设置。这些函数都在exception头文件中。set_terminate()的参数需要是一个函数指针(即函数名),该函数返回值和参数表都应该是void,这样terminate()就会执行这个函数。
对第一种情况,实际上,如果一个函数调用了另一个函数,而这另一个函数可能throw一个类,那么这个类应该在父函数和子函数中都出现。如果throw与特化不匹配,会调用unexpected(),unexpected()调用terminate(),terminate()调用abort()。可以用set_unexpected()来设置unexpected()函数。set_unexpected()的参数也是一个返回和参数都为void的函数地址,其实,不只可以选择调用terminate(),还可以选择再throw一个exception,这次的exception可以和特化表匹配,此时回溯成功;或者不匹配但特化表中有std::bad_exception(exception头文件),此时这个不匹配的exception会转换成std::bad_exception类型;或者不匹配且特化表中没有std::bad_exception类型,那么就会调用terminate()函数。

85.exception也会导致一些问题。比如一个函数中自动分配了一个string并且throw,此时离开前会调用析构函数来清理new出来的字符串空间;但如果new分配了一个东西然后throw,此时就会内存泄漏,因为指针名清理了,但new出来的空间没有释放。这时可以在catch里专门delete,但更好的方法是用智能指针。

86.runtime type identification(RTTI):程序在运行时确定类型。比如,有一个基类,其有许多衍生类,有些衍生类中重定义了基类中的虚函数,而有些没有,那么这种情况下就需要运行时确认类型。有些老编译器不支持RTTI,还有一些编译器默认关闭RTTI。
(1)dynamic_cast操作,可以将基类指针转换为衍生类指针,如果不能转换会返回nullptr;
(2)typeid操作,返回一个值,表明对象类型;
(3)type_info结构体,可储存类型信息。
RTTI只对有虚函数的类有效(这是唯一需要RTTI的地方)。
对于dynamic_cast:一般来说,只有同一类型的转换,或者把衍生类对象转换成直接或间接的基类对象是绝对安全的。对于指针,用法为dynamic_cast<Type >§,当p所属的类是Type直接或间接的子类,就会返回转换后的指针,否则返回nullptr。对于引用,typeinfo头文件有bad_cast例外,dynamic_cast<Type &>§在不能转换时会throw该例外,所以这时需要和try block配合。
对typeid:用法类似sizeof操作符,参数是类名或者结果为对象的表达式。typeid()返回一个type_info对象的引用,type_info的定义在typeinfo头文件中,其重载了==和!=操作符。如果参数是空指针,typeid会throw一个bad_typeid。type_info中定义了name()方法,一般的环境下会返回类名的字符串。typeid通常用来处理dynamic_cast不能处理的场景。

87.四种转换:dynamic_cast、const_cast、static_cast、reinterpret_cast
const_cast的格式同dynamic_cast:const_cast < type-name > (expression),当expression与type-name相同时就返回这个值。主要用在一个const指针,有时需要改变其内容的值时,但又不想无意中改变类型。
static_cast的语法相同,当expression和type-name的类型可以从任一方向隐式转换时就返回,否则转换错误。
reinterpret_cast语法一样,它不能转换掉const属性,通常用来干一些法律边缘的活,移植性较差。

88.string类:
string的最大值一般是unsigned int的最大值,用string::npos表示。
输入时,对于字符数组,可以直接cin或者调用cin.get()(\n留在输入流中)、cin.getline()(\n遗弃),同样,string可以用getline(cin,string名)函数,不同的是string的输入不需要限制。>>操作符在遇到第一个空白字符时停止(空白字符留在输入流中)。string之间或者string和字符串间都可以比较大小。
在指定string中寻找子字符串:find()方法,四种原型如下:

找到了返回下标,如果找不到都会返回string::npos。

rfind(), find_first_of(), find_last_of(), find_first_not_of(),find_last_not_of()类似:
rfind()返回最后一次出现的下标;
find_first_of()返回给定字符串中的任一字符第一次出现的下标;
find_last_of()相反,返回给定字符串中的任一字符最后一次出现的下标;
find_first_not_of()返回给定字符串中的所有字符第一次不出现的下标;
find_last_not_of()同理。

许多环境下,string分配的空间比包含的字符长度大,来应对可能超出长度的情况。如果确实超出,环境会再分配两倍的空间。capacity()方法返回现在空间的大小,reserve()方法可以使你请求最小需要的空间。打开文件需要C style的字符串,c_str()方法可将string转换为C式字符串。

实际上,string也是模板生成的,类型参数是char;还可以以wchar_t生成wstring,C++11中可用char_16t生成u16string、char_32t生成u32string。

89.智能指针模板类:
auto_ptr,unique_ptr,shared_ptr:指针类,销毁指针时会调用析构函数自动释放指针指向的空间。C++98采用了auto_ptr,C++11遗弃了auto_ptr并提供unique_ptr和shared_ptr。
这些智能指针在memory头文件中。另外还有weak_ptr,本书不讨论。用法:
template class auto_ptr {
public:
explicit auto_ptr(X* p =0) throw();
…};
auto_ptr模板如上。那么用的时候比如auto_ptr pd(new double);,其中new double是new分配的一块空间,调用构造函数时做参数。注意其用了explicit。
另外,智能指针最后是会delete的,不要用它指向不是new分配的地址。

实际上,智能指针在互相赋值上有些问题。智能指针可以和普通指针互相赋值,智能指针也可以互相赋值,但可能会导致同一地址delete两次。比如:
auto_ptr ps (new string(“I reigned lonely as a cloud.”));
auto_ptr vocation;
vocation = ps;
那么指向的地址在两次析构时都delete。为了解决这个问题,又提出几个方案:
(1)使赋值操作做深拷贝;
(2)引入ownership的概念,只有指针own一个地址时才可以删除,赋值时会导致ownership转移。auto和unique用的就是这种方式,unique更严格一些;
(3)引入一个计数,每有一个智能指针类指向同一个地址就+1,每有一个智能指针析构时计数-1,只有最后一个智能指针析构时才delete。这个概念就是shared_ptr。
auto在转移拥有权后就不能通过没有拥有权的指针访问该地址。在vocation = ps;一行执行后,如果再用*ps就可能崩溃;而unique会在这一行出现编译报错。其实unique也不是完全阻止赋值,如果=右边的是临时右值就可以赋值(马上被销毁),如果是能存活一段时间的变量就不能赋值。但一个unique指针是可以重复使用的,用move()函数,比如p1和p2,p2=move(p1);,然后再给p1赋新的值。如果一个unique是临时右值,那么也可以赋值给shared。
另外,shared和auto用于new,而unique还可以用于new[],比如std::unique_ptr< double[]>pda(new double(5));。
一般来说,如果不需要数组用,有多个指针指向一个对象,或者涉及一些STL算法时要用shared,其他的可以用unique。

90.所有的STL容器类都提供了一些相同的方法:size()返回个数,swap()交换两个位置的元素,以及begin()、end()迭代器。迭代器是泛化的指针,对于一些容器来说只能用迭代器。定义迭代器可以如:vector::iterator pd; // pd an iterator。*操作符和++等操作符对迭代器也有效。

91.对于vector或者其他一些类模板的方法:push_back(),在末尾添加一个新元素;erase(起始迭代器,终止迭代器);insert(插入位置之前,插入起始迭代器,插入终止迭代器)。另外,STL还定义了一些大家都可以用的函数,比如find()。其实有些类模板内重定义了一些标准算法,一般是为了效率,比如vector内的swap()。同时,普通的swap()还允许在不同容器内交换值。

下面介绍一些对许多容器有用的函数:for_each(起始迭代器,终止迭代器,函数地址(函数对象)),对范围内每一个元素执行一次函数,该函数不能改变元素值;random_shuffle(起始迭代器,终止迭代器),将范围内的元素顺序打乱,该函数仅支持可以随机访问的容器;sort(起始迭代器,终止迭代器),需要<操作符定义,将范围内元素升序排列,另外sort还有另一种形式即sort(起始迭代器,终止迭代器,函数对象),这里的函数对象用于替代<操作符,其返回值应该可以转换为bool型。

基于范围的for循环:比如for(auto x: 容器名){},对于容器内的元素,使用x变量来依次访问,执行循环体内的操作。容器可以是数组,或者vector等。相比for_each(),这种for循环可以方便地修改元素值。

92.operator++()为前置++,operator++(int)为后置++。这里的int不需要给名字,因为不会用到。

93.迭代器:每种容器都有自己对应的迭代器。STL定义了五种迭代器:输入,输出,前向,双向,随机访问迭代器。
输入迭代器,从容器向程序为输入。它允许程序读取,没有允许修改元素。输入迭代器定义了++操作,但没有向前的操作。使用输入迭代器的算法应该遍历容器或部分容器一次,因为第二次遍历时元素顺序不一定相同,或者前一个地址不一定还能读取。
输出迭代器,同样,用于单次遍历、只读的算法。
前向迭代器,也定义了++,可以读写元素(声明为const则只能读),额外地,前向迭代器保证了每次遍历时元素顺序一样,而且前一个地址指向的元素能读取。
双向迭代器,相比前向迭代器,其增加了–操作。
随机访问迭代器,在双向迭代器的基础上增加了一些定义。
可以看到,迭代器具有一种“继承”的概念。

指针可以作为一种迭代器,而且是随机访问迭代器。所以许多算法可以对数组使用。

94.copy(起始迭代器,终止迭代器,需拷贝的第一个位置);可以在不同容器或者容器与数组间复制数值。前两个应该至少为输入迭代器,最后一个至少为输出迭代器。一般来说,目标容器或数组内应该有充足的空间来容纳数据,除非运用一些诡计。

输出流迭代器:包含在头文件iterator中,使用时需要声明,比如:
ostream_iterator<int, char> out_iter(cout, " ");
int表示传递给输出流的数据类型,char表示输出流使用的字符类型(也可用wchar_t),构造函数的第一个参数是当前使用的输出流,第二个参数是每次输出一个东西后用来分隔的字符串。可以这样使用:
*out_iter++ = 15; // works like cout << 15 << " ";
其意思为将15传递给输出流,然后准备好下一次输出。上面的copy函数中,最后一个参数也可为输出流迭代器,即显示范围内的元素。可以直接用out_iter,也可以使用无名的输出流迭代器:copy(dice.begin(), dice.end(), ostream_iterator<int, char>(cout, " ") );
类似地,输入流迭代器可以这样用:
copy(istream_iterator<int, char>(cin), istream_iterator<int, char>(), dice.begin());
忽略构造函数的参数意为输入失败。

iterator头文件还定义了一些迭代器,比如reverse_iterator, back_insert_iterator, front_insert_iterator和insert_iterator。
reverse_iterator,增加它的值会导致它减少,这主要是为了省事,比如用刚才的copy函数倒序输出。例如vector,其rbegin()返回一个reverse_iterator,指向最后一个元素后的位置;rend()返回一个reverse_iterator,指向第一个元素,那么就可以copy(dice.rbegin(), dice.rend(), out_iter); // display in reverse order。实际上,严格按照刚才的格式会有一点小问题,不过好在对一个reverse_iterator进行*操作会返回其前一个位置存储的值。
刚才还说到copy函数一般需要拷贝到的容器中有足够的空间,而后三种迭代器,back_insert_iterator,front_insert_iterator和insert_iterator通过将copy操作变为insert操作规避了这种问题。back_insert_iterator将新增元素加在容器尾部,front_insert_iterator加载容器前,而insert_iterator则通过其构造函数的一个参数,在该参数指明的位置前加入新增元素。但也有限制,back_insert_iterator要求容器在尾部加元素的时间复杂度为O(1),比如vector;front_insert_iterator类似,vector不行,但是queue可以;insert_iterator则没有这种限制,但有时可能要慢很多。
这三种插入迭代器的模板参数为容器类型,构造函数的参数为容器实际的标识符,如:back_insert_iterator<vector > back_iter(dice);
实际上,back_insert_iterator需要容器存在push_back()方法,它有重新确定容器大小的权力(用push_back()),copy()则不能改变容器的大小。front类似。insert的构造函数还需要一个参数,即插入位置(在该位置前插入):insert_iterator<vector > insert_iter(dice, dice.begin() );

95.容器类型:一开始有11中容器,即deque, list, queue, priority_queue, stack, vector, map,
multimap, set, multiset和bitset。C++11将bitset单独划为一类,因为它在bit的水平处理数据,而且C++11新增了几种容器:forward_list, unordered_map, unordered_multimap, unordered_set和unordered_multiset。
容器是一种对象,用于储存对象。它储存的对象可以是类的实例化,也可以是基础数据类型。容器占有其中储存的对象,即容器消失时其存储的内容也消失(如果装的是指针则指针指向的数据不一定消失)。不是所有对象都可以装在容器中,能装在容器中的对象必须有拷贝构造函数和赋值操作函数。
基础的容器不能保证数据以一定的顺序存储,也不能保证顺序不会变,一点改变就可克服这种缺陷。

96.(&a)->~X();返回void,X如vector,a为标识符。意为对a中的所有对象使用析构函数。C++11添加了一些对所有容器都可使用的操作,比如用右值对容器初始化或赋值(这里涉及到move构造或赋值,控制权的转移,比普通复制可能快一点),和cbegin()、cend(),返回常量迭代器。

97.序列的操作:

deque, list, queue, priority_queue, stack和vector都是序列,都可执行以上操作。下面的操作对6种中的一些可执行:

其中,a.at(n)区别于a[n]在其会在n超界时throw一个out_of_range的exception;

以下6种序列都需要包含各自的头文件。
vector:最常用的序列,支持随机访问。在尾部增删元素需要常量时间,但在头部或中间需要线性时间。可有reverse_iterator。
deque:双端队列,读音同deck。相比vector,deque在头部增删的时间为常量,但deque更复杂一些,执行同样的操作一般比vector更慢。
list:双向链表,相比vector,在list中任意位置增删需要的时间都是常量,list更强调快速增删而vector强调快速随机访问。list也支持reverse,但不支持[]和随机访问,而且list的迭代器只要指向不变,其内容也不会变,因为增删修改的是元素上一个和下一个指针。以下是一些list方法:

除了splice,其他四个方法还可以接受一个额外的参数来决定要执行的操作。
forward_list:单向链表,只有前向迭代器。
queue:一种类适配器,默认使用deque来展示接口。queue不允许随机访问,甚至不允许依次迭代元素。queue仅保留了队列的一些基本概念,比如在尾部添加元素,在头部去掉元素,查看头尾元素,检查元素个数或队列是否为空。以下是这些方法:

注意查看头尾元素和增删头尾元素的方法是分开的。
priority_queue:在queue头文件中,也是类适配器。提供的功能与queue一样,与queue的区别是值最大的元素会被移到头部,内在的区别是内部通过vector实现。可以通过提供构造函数的参数来修改排序规则:
priority_queue pq1; // default version
priority_queue pq2(greater); // use greater to order
greater是一个预定义的函数对象,后面提到。
stack:类适配器,默认提供vector的接口。类似地,stack比vector更严格,不允许随机访问,也不允许依次迭代,而是提供了栈的基本概念操作,如入栈、出栈,查看栈顶,查看栈是否为空。以下为这些方法:

类似地,栈的查看和增删是分开的。

array:C++11新增,不算STL容器(因为大小固定),insert或push_back等改变大小的操作不能使用,但一些有意义的操作可以执行,比如[],或者copy()。

98.关联容器:另一种容器概念的提炼,将键key与值value关联,使用键来找到值,一个键可能对应多个值。对于刚才的几种序列(sequence)容器,X::value_type表示的是容器中的值的类型;对于关联容器,X::key_type则表示了键的类型。
关联容器的优势在于快速访问。它像序列容器一样可以插入元素,但不能指定元素的位置,而是通过容器的算法来决定新增元素的位置,以便快速访问。
关联容器通常使用树形的分支结构来实现。这种类似链表的结构方便增删元素,但却可提供更快的检索。
STL提供了四种关联容器:set,multiset,map,multimap。前两者在set头文件(以前分开在不同的头文件),后两者在map中(也是以前分开)。set是最简单的,其键和值的类型相同,且键就是值,但其中的元素不能重复;multiset与set类似,只是其中一个键可以对应多个值。对于map,其key和value的类型不同,一键一值;而multimap则可一键多值。

set:STL的set,是关联集合,有reverse_iterator,有序,键/值唯一。初始化如下:
set A; // a set of string objects
另外还可以提供一个参数(函数或对象)来提供排序键的算法,默认使用less<>模板。set<string, less > A; // older implementation
老环境可能必须提供两个参数。
set数学上可以求交集、并集,差则是第一个集合减两个集合的交集。STL提供了一些算法(不是set的方法)来实现这些操作,但执行这些算法的条件是集合有序。set_union提供交集运算,有5个迭代器参数,前四个指明范围,最后一个为output iterator:set_union(A.begin(), A.end(), B.begin(), B.end(), ostream_iterator<string, char> out(cout, " "));
如果想将结果输出到另一个set,需要知道:关联集合中键是常量,所以迭代器也是常量;而且set_union类似copy,会覆盖原有内容,并且需要足够空间。所以这里需要insert_iterator解决这个问题:set_union(A.begin(), A.end(), B.begin(), B.end(),insert_iterator<set >(C, C.begin()));
这里通过一个无名的插入迭代器进行了插入操作。
set_intersection() 和set_difference()分别提供了求交集和求差的接口,参数和set_union一样。
set还有两种有用的方法,lower_bound() 和upper_bound()。前者以一个键/值为参数,返回一个迭代器,指向第一个>=该参数的set成员;后者返回的迭代器则指向第一个大于参数的成员。
因为set不能指明插入位置,set的insert方法不需要插入位置的参数,而只需要目标元素或目标范围。

multimap也是有序,支持reverse_iterator的关联容器,与set不同,键与值的类型不同,且可以一键多值。声明如:multimap<int,string> codes;,int为键,string为值。第三个参数可选,为排序用的函数或对象,同样也默认为less<>模板。
multimap将键类型和数据类型组合得到实际的值类型,其中用到了pair<class T, class U>来将两种类型作为一个对象存储。实际上,得到的类型为pair<const keytype, datatype>,在刚才的例子中为pair<const int, string>。
插入元素:pair<const int, string> item(213, “Los Angeles”); codes.insert(item);或者用无名对象:codes.insert(pair<const int, string> (213, “Los Angeles”));。因为通过键排序,所以insert也不需要指定插入位置。另外还可以通过first和second访问键与值:pair<const int, string> item(213, “Los Angeles”); cout << item.first << ’ ’ << item.second << endl;。
count()方法以一个key为参数,返回该key对应的值的个数;lower_bound() 和upper_bound()方法也以一个key为参数,返回同set的方法;另外还有equal_range()方法,以一个键为参数,返回与键匹配的范围(以迭代器表示),两个迭代器是以pair对象表示(类型参数都是迭代器),使用例如下:
pair<multimap<KeyType, string>::iterator, multimap<KeyType, string>::iterator> range= codes.equal_range(718);
cout << “Cities with area code 718:\n”;
std::multimap<KeyType, std::string>::iterator it;
for (it = range.first; it != range.second; ++it)
cout << (*it).second << endl;
这里也可以使用auto。

无序关联容器:关联容器基于树形结构,而无序关联容器基于哈希表,目的是高速增删和高效查找。四种无序关联容器:unordered_set, unordered_multiset, unordered_map和unordered_multimap,详见附录G。

99.函数对象,也称functor,它像函数一样使用()操作符。函数对象包括普通函数名,函数指针,一些重载了()的对象。比如for_each函数,原型:template<class InputIterator, class Function> Function for_each(InputIterator first, InputIterator last, Function f);
比如void ShowReview(const Review &);函数,则ShowReview的类型是void(*)(const Review &),这个类型参数就会传递给Function。

STL规定了容器和迭代器,同样也规定了函数对象。函数对象可以无参数(生成器)、有一个参数(一元)、两个参数(二元);返回bool的一元函数对象称为predicate,而返回bool的二元函数对象称为binary predicate,比如sort的第三个参数就可选择binary predicate;或者list的方法remove_if(),参数就是一个functor,如果该元素使用functor后返回值为true就会移除该元素。
binary predicate和predicate有时可以互相转换。如果我们将一个一元转换成二元,那么这个二元functor就是这个一元functor的适配器(改变了接口)。

STL规定了几种基础的functor,比如两值相加、比较,主要是为了支持一些以函数为参数的STL函数。比如transform()函数有两种原型,第一种原型有四个参数,前两个为迭代器、指明容器中的范围,第三个参数指明结果会copy到哪里,最后一个参数为functor,用于执行某种变换。比如:
const int LIM = 5;
double arr1[LIM] = {36, 39, 42, 45, 48};
vector gr8(arr1, arr1 + LIM);
ostream_iterator<double, char> out(cout, " ");
transform(gr8.begin(), gr8.end(), out, sqrt);
这种情况下,目标迭代器需要和原范围至少一样大,因为这是种一一对应的关系;
第二种transform()原型,在第一个范围后再加了一个迭代器参数来指明第二个范围,而且其functor参数是二元的。此时的变换会将第一个范围和第二个范围中的值依次用binary predicate操作,然后输出:
transform(gr8.begin(), gr8.end(), m8.begin(), out, add);//将两个范围中的值相加后输出
比如两个范围内都是double类型的值,那么就需要自己定义一个add的二元函数对象。为了泛用性,还需要用模板(比如对int相加)。但也可以用functional头文件,其中定义了几种运用了类模板的函数对象,比如plus<>()。对double类型可以这样使用:
plus add; // create a plus object
double y = add(2.2, 3.4); // using plus::operator()()
有点小题大做。对刚才提到的transform()第二种原型,可以这样用:
transform(gr8.begin(), gr8.end(), m8.begin(), out, plus() );
另外functional头文件中还定义了其他的基本运算:

可以对自定义的类提供这些运算。

100.适配性函数对象与函数适配器:上文中的functor equivalent都是可调配的。一个函数对象,若其typedef成员的来标明参数类型或返回类型,即为adaptable。这些typedef成员为result_type, first_argument_type和second_argument_type。比如一个plus对象会被标识为plus::result_type,其应该被typedef为int。
一个可调配的functor可以被函数调配对象(会用到这些typedef)来调配。比如STL提供了binder1st 和binder2nd来将二元可调配函数对象体=调配成一元可调配函数对象。比如:
binder1st(f2,val) f1;
其中f2是一个二元functor,val是自己指定的一个可被f2接受为参数的对象,那么以后f1(t)就等同于f2(val,t)了,f1是f2转换过来的一元functor。
为了简化,STL还提供了bind1st()函数,它的参数是函数名和val,返回一个binder1st对象,可以直接使用。比如:
transform(gr8.begin(), gr8.end(), out, bind1st(multiplies(), 2.5));
上述语句意为将范围内的值都乘以2.5,然后输出。
binder2nd同理。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值