步骤/目录:
第一部分 第1-7章
第二部分 第7-16章(算法前)
第三部分 16章-附录前

本文首发于个人博客https://lisper517.top/index.php/archives/4155/,转载请注明出处。
本文的原文写作日期为2020年3月1日,主要目的是总结《C++ Primer Plus》第六版(英文版)。

当年还在疫情期间居家,1438页的英文版,每天看50页,一个月看完。下面的内容有些缩进或者图片缺失,可以参考 原word文档

第一部分 第1-7章

1.变量的声明与赋值 写法
int a=1;int a(1);int a={1};int a{1};int a={};int a{};
最后两个语句中a赋值为0。
{}为initialization list,可用于数组的declaration and definition。

2.不同进制的标识
10进制(decimal)第一个bit为0-9;
8进制(octal)第一个bit为0,第二个bit为0-7;
16进制(hexadecimal)前两个bit为0x或0X。
分别以cout<<dec/hex/oct;改变输出进制,对剩余的输出都有效。

3.数字常量的存储
cout<<1000;
默认存为int。加l或L后缀指定为long:
cout<<1000L;
u为unsigned long,同理有ul、ull等。
八进制、十六进制的数按int,u int,l,ul,ll,ull的优先顺序存放。

4.转义字符
\n    newline
\t    水平制表符
\v    垂直制表符
\b    回车符
\r    carriage return
\a    alert

5.扩展字符集 ISO 10646
\u后跟8个十六进制bit,\U后跟16个十六进制bit。
如ö为\u00F6
â为\u00E2

wchar_t用于更复杂的字符集,占2bytes,加L标识,用wcout函数:
wchar_t bob = L'P';wcout << L"tall" << endl;
C++11引入了char16_t和char32_t,分别占16bits和32bits,加u和U标识。


+5.37E+16
一个科学记数法实例
E/e皆可,e后可为+/-,为指数的符号;为+可省略。

6.setf()函数
C++中cout.setf(ios::left,ios::adjustfield); cout.setf(ios::showpoint,ios::showpint);
cout.setf(ios::scientific,ios::floatfield);
分别表示什么

参考以下:

1.使用控制符控制输出格式
控制符 作用 
dec 设置整数的基数为10 
hex 设置整数的基数为16 
oct 设置整数的基数为8 
setbase(n) 设置整数的基数为n(n只能是16,10,8之一) 
setfill(c) 设置填充字符c,c可以是字符常量或字符变量 
setprecision(n) 设置实数的精度为n位.在以一般十进制小数形式输出时,n代表有效数字.在以fixed(固定小数位数)形式和scientific(指数)形式输出时,n为小数位数.
setw(n) 设置字段宽度为n位.
setiosflags(ios::fixed) 设置浮点数以固定的小数位数显示.
setiosflags(ios::scientific) 设置浮点数以科学计数法(即指数形式)显示.
setiosflags(ios::left) 输出数据左对齐.
setiosflags(ios::right) 输出数据右对齐.
setiosflags(ios::shipws) 忽略前导的空格.
setiosflags(ios::uppercase) 在以科学计数法输出E和十六进制输出字母X时,以大写表示.
setiosflags(ios::showpos) 输出正数时,给出“+”号.
resetiosflags 终止已设置的输出格式状态,在括号中应指定内容.
2.用流对象的成员控制输出格式
流成员函数 与之作用相同的控制符 作用 
precision(n) setprecision(n) 设置实数的精度为n位.
width(n) setw(n) 设置字段宽度为n位.
fill(c) setfill(c) 设置填充字符c.
setf( ) setiosflags( ) 设置输出格式状态,括号中应给出格式状态,内容与控制符setiosflags括号中内容相同.
ubsetf( ) resetiosflags( ) 终止已设置的输出格式状态.
cout.width(10); 
cout.setf(ios::hex);
3.设置格式状态的格式标志
格式标志 作用 
ios::left 输出数据在本域宽范围内左对齐 
ios::right 输出数据在本域宽范围内右对齐 
ios::internal 数值的符号位在域宽内左对齐,数值右对齐,中间由填充字符填充 
ios::dec 设置整数的基数为10 
ios::oct 设置整数的基数为8 
ios::hex 设置整数的基数为16 
ios::showbase 强制输出整数的基数(八进制以0打头,十六进制以0x打头) 
ios::showpoint 强制输出浮点数的小点和尾数0 
ios::uppercase 在以科学计数法输出E和十六进制输出字母X时,以大写表示 
ios::showpos 输出正数时,给出“+”号.
ios::scientific 设置浮点数以科学计数法(即指数形式)显示 
ios::fixed 设置浮点数以固定的小数位数显示 
ios::unitbuf 每次输出后刷新所有流 
ios::stdio 每次输出后清除stdout,stderr 
————————————————
版权声明:本文为CSDN博主「荪荪」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/smf0504/article/details/51469300

7.浮点数标识
后缀f/F表示float,l/L表示long double。默认为double。
小的浮点数赋给整数时截取整数部分而非四舍五入

8.类型转换
short a=1;short b=2;short c=a+b;
a+b先转换为int,再换成short。
表达式转换规则:有ld都转换成ld;有d、f同理。然后是int。
强制类型转换:short a;int (a)或(int) a;不会改变a的值,而是创造一个新的值,可用于表达式中。int a=1.2+2.9;中,先转换成double得4.1,再赋值。
auto关键词可自动分配类型。如auto a=1;a将定义为int。

9.static_cast<>和dynamic_cast<>
static_cast<int> a;将a强制转换为int类型。详见后续

10.数组声明
int a[3]={1,2,3};
int a[3]{1,2};(array a中剩余元素初始化为0,可以快速全部初始化为0。11标准中可省略=)
int a[]{1,2,3};(自动识别a大小为3)
int a[3]{};(全部初始化为0。11标准中可用)
initialization list只能在definition时使用,不可如下:
a[3]={1,2,3};
有些老compiler报错可如下写:
static int a[3]{1};

int a[3]={1,2,3.0};不允许narrowing,错误
char a[3]={112,’a’,’b’};允许,112是int但在char范围内
int a=1;int b[2]{a,2};允许
char类型array要注意留出\0的位置。
“s”为string类型,实际上为s和\0,char a=”s”实际上是把s字符串的地址赋给a,错误

11.strlen()返回字符串的长度(不含\0,若字符数组,非实际长度;初始化之前使用,结果不确定)
sizeof()返回实际长度,也不含\0。
char a[10]{“abc”};strlen(a)=3;sizeof(a)=10;

char array用cin输入以whitespace(table,space,newline)结尾。用cin.get()/cin.getline()时以换行符结尾,前者使换行符留在输入流中,后者丢弃换行符。
cin.getline(参数1,参数2),参数1为接收字符串的数组名,参数2为最大接收数,如为20时最多接收19个字符(加\0需一个字符)。参数3后续讨论。
cin.get(参数1,参数2)形式与getline一样。连续两个cin.get()使第二个get()无法接收任何输入。解决办法是在中间再加入一cin.get(),因cin.get(void)可读取任一字符,读取换行符后丢弃。也可写成cin.get(参数1,参数2).get(),因为cin.get(参数1,参数2)返回一个cin对象,可再次调用get()。同理,cin.getline(参数1,参数2).getline(参数1,参数2)也能达到一样的效果。
get()读取empty line(newline)后会设置一个failbit,后续输入将被blocked。可使用cin.clear()。
超出读取限制后,get()和getline()都会将剩余输入留在输入流中,但getline()会设置failbit并关闭输入流。另外,cin读取数字后也会将换行符留在输入流中。由于cin>>int返回cin对象,可使用(cin>>year).get()解决。

12.string对象相比char array有不少优点。
可以直接赋值:str1=str2;str1={“123abc”};
可以相加、增长:str3=str1=str2;str2+=str1;
C中提供了strcpy(str1,str2)、strcat(str2,str1)来对string赋值、附加。str1.size()替代了strlen(),并且初始化str1之前str1.size()为0。为了避免strcpy和cat使用时str1比str2短,可用strncpy()和strncat()并引入第三个代表上限值的参数,如strncpy(a,b,3);但若b比a长,该函数不会自动加\0,后面需要跟a[3]=’\0’;。使用getline(cin,str1)可使string读取space和table。

raw string:加R后,不需转义字符。若输出有”,需加parenthesis。
cout << R"(Jim "King" Tutt uses "\n" instead of endl.)" << '\n';可将括号中的内容输出。
若要输出括号,可使用cout << R"+*("(Who wouldn't?)", she whispered.)+*" << endl;其中"+*(和)+*" 相匹配。R还可与L、U使用来表达wchar_t和char32_t等。R必须大写。

13.结构体
先声明结构体的格式,再声明结构体变量(可外部声明):
struct human
{
double weight;
int age;
char name[20];
};//结构体的格式
human xiaoming;//xiaoming是一个结构体变量,有weight、age、name等成员。
用.运算符操作数据成员,如cin>>xiaoming.name。声明结构体变量时可使用初始化链表(initialization list),可以不初始化完所有成员,可用xiaoming{}的格式使成员全部初始化为0。结构体变量可以做函数的参数或返回值,可以用赋值符号。声明结构体和声明结构体变量可连用如下:
struct human
{
double weight;
int age;
char name[20];
}xiaoming1,xiaoming2
{
60.0,
20
};
还可以省略human(结构体的tag),只声明结构体变量。
C++的结构体变量中的成员还能有函数。
结构体变量较多时,可创造结构体数组。如human xiaoming[100];其可初始化如下:
human xiaoming[100]
{
{60.0,20,”xiaoming”},
{120.0,40,”xiaomingming”},
{130.0}
};
结构体中可使用bit fields来节省空间:
struct human
{ 
double weight;
int age:8;
char name[20];
};
其中指定age只占8bits,可以表示2^8即-128到127范围,足够表示age了。
但是由于内存对齐原则,位域多用于成员单一的结构体,如:
struct time
{
int year:14,//0-9999年
month:4,//12个月
day:5;//31天
};//此结构体的一个变量大小为4bytes,因为内存对齐
可用sizeof显示结构体变量的大小。

14.union类型
声明和结构体一样,大小为其中最大成员的大小。可用来存放各种值,但一次只能存一个值,实质是不同的基本类型共用一段内存空间:
union a
{
int int_value;
double double_value;
char char_value;
};
a可用于存放int,double或char,长度为8bytes。
union主要目的是节省空间。具体用法可如下:
struct time
{
int year;
int month;
union UNION
{
double day1;
int day2;
}day;
}today;
那么就可以调用today.day.day1。也可以省略union的tag和变量名,直接调用:
struct time
{
int year;
int month;
union
{
double day1;
int day2;
};
}today;
此时调用today.day1

15.枚举变量
格式:enum spectrum {red, orange, yellow, green, blue, violet, indigo, ultraviolet};
定义枚举类型spectrum ,成员有red、orange等。默认red=0,orange=1依次累计。若在中间赋值,如:
enum spectrum {red, orange, yellow=9, green, blue, violet, indigo, ultraviolet};
那么red=0,orange=1,但从blue开始,在yellow的基础上累计,后面blue=10,violet=11等。
spectrum band;
声明一个枚举变量后,它的值只能在枚举的几个数里选。
band=bule可以,band=2就不行(int不会自动转换为enum)。实际上,赋值符号可用于枚举类型,其他符号如++、+不行:
band=blue+red;是不行的,虽然先转换为int后得出结果,但int不能自动转换为enum,需强制类型转换:
band=spectrum(3);

定义枚举类型时tag也可省略。enum可用于switch语句。
enum类型的范围:
enum color{red,blue=100,yellow};
上界:100,比100大又最接近100的2的次方是128,则上界为127;
下界:最小值大于等于0时都为0;小于0时,和上界类似,如red=-6,则找到-8,下界为-7。
上下界主要用于确定存储enum需要的bit数,也和节省空间有关。同时,一个在范围内的数都可赋值给enum变量(需强制类型转换):
color color_of_sky;
color_of_sky=color(102);//此时color_of_sky为120

16.指针
int *a,b;a=&b;//定义int指针a和int类型变量b,并将b取位赋给a
int b;int *a=&b;//定义的同时赋值
指针初始化后,系统在内存中为指针指定一块存储空间,但并未给其指向的值指定空间。如下的语句:
int *a;*a=1;
a指向的位置不确定,强行用1覆盖a指向的内存很可能导致error。

系统通常将指针像int一样处理,但直接将一个int赋给指针是不行的(C99之前可以),需要强制类型转换:
int *a;a=0xB8000000;//错误
int *a;a=(int *)0xB8000000;//可行
在C中可用malloc()的memory allocate函数分配内存,C++中可用new和delete关键词动态管理内存。这样得到的变量没有变量名,只能通过指针访问。
int *p=new int;//得到一个int
int *p=new int[x];//得到一个int数组。其中x可以是表达式、变量、常量。
每new一个指针,必须在使用完后delete之:
delete p;
delete []p;
一个new对用一个delete,一个delete也只能对应一个new。不能将一个地址delete两次,也不能对不是用new定义的指针delete。delete后,指针名可用于下一次new。对空指针delete可以。
int a=1;int *p1=&a;int *p2=p1;delete p2;//p1和p2指向同一个地址,delete一次就够了。过程中p1和p2进行了浅拷贝。
指定动态数组后需要编程者操心使用中有无过界。用sizeof无法得出动态数组的大小。系统知道动态数组的大小,但对编程者不可用。
对动态数组的指针名+1会使其移向下一个元素(不是下一个byte或bit),如:
int *p=new int[10];p=p+1;//此时再输出p[0]就是原来的p[1]。在delete之前应将p减1。
可以像使用普通数组名一样使用动态数组的指针名,C++使用数组就是通过指针完成的。
int a[3];int *p=a;//和int *p=&a[0];等价,p[1]和*(p+1)等价,a实际上就是&a[0],数组名就是指针,也可以当作指针使用。但不能像p=p+1一样a=a+1,也不能用sizeof(p)求a数组的大小。其实a等价于&a[0],而&a是整个数组的指针,所以*(a+1)是数组第二个元素,而&a+1是在a[0]的地址上加整个数组的bytes数,本例中为12bytes,跳过了整个数组。
int a[3];int *p1=a;int (*p2)[5]=&a;
上面的p1是a[0]的地址,即a或&a[0],类型是int *;而p2的类型是int(*)[5],即含有5个元素的int数组的指针,在这里a有3个元素,显然不匹配。如果不加括号:int *p2[5];则定义了一个指针数组,该数组的成员都是指针,每一个成员可指向一个int类型。
两个指针相减得到一个int,即两个元素间相差多少个元素,只有同一数组内的元素地址相减有意义。
char a[10]=”abc”;cout<<a<<”123”;实际上只把a[0]的地址和’1’的地址传给cout,cout在遇到\0后自动停止输出。和int*不同,可以有char *b=”123”;的语句,因为”123”为字符串常量,在内存中有地址,但由于b存放了常量的地址、常量不能修改,最好在前面加const以防修改。要输出&a[0]可以cout<<(double*)a或cout<<(int*)a,即转换为非char*类型。cout<<&a[0]仍然会输出整个字符串,因为cout遇到一个char*类型就会当作string输出。

浅拷贝与深拷贝:前面说到,
Int a;int *p1=&a;int *p2=p1;并不会创造新的地址,p1和p2都指向同一个int,操作其中一个就会改变a的值。要达到深拷贝,需要创造新的地址。比如:
char a[4]=”abc”;char *p=new char[strlen(a)+1];strcpy(p,a);//此处不能直接p=a,而要对a数组中的值一一拷贝。此时指针a和指针p中储存的值相同,但地址不同,可以独立操作。

用指针声明一个结构体后,可以用->操作符访问成员,或者(*).如下:
struct human{int age;double weight;}a;
human *b=&a;//然后可b->age或(*b).age来访问数据成员。

自动分配、静态分配、动态分配:
一个block中声明的变量会自动分配一个内存位置,并在block结束后收回;如果返回该变量的地址给外部,它随时可能被其他值覆盖;一个block就是{}内的部分,比如for循环中定义的局部变量就不能在外面用,一个函数中的变量也不能给其他函数用。实际上,执行时,变量的内存在一个stack中,类似放羽毛球的球筒,按顺序创建,block执行完后按与创建时相反的顺序销毁(栈只能在尾部操作)。在block内部重新定义外部已有的变量,会使得该外部变量“隐形”,只能用内部定义的同名变量。

静态分配的变量,在程序执行时始终存在。可以在所有函数外部声明变量,或者用static关键词:static int a;。有些老编译环境不允许数组和结构体自动分配,这时也要static声明。

通过new动态分配的变量则超出了程序的生命周期,程序结束后仍然存在。所以必须用好delete,不然此区域将持续不可用,因为程序结束后指针丢了。

17.vector和array类的使用
vector<类型名> 对象名(个数)
如vector<int> temp(n);其中n必须是整型变量、常量或表达式,意义为创造了一个有n个成员的整型vector类对象temp。vector可以看成数组的替代,头文件vector。
array类包含在array头文件里,用法:array<int,n> temp;,其中n不能是变量。
C++11后,vector和array类的对象可使用链表来定义并赋值。vector是使用new创建的,内存地址在heap里;array类对象在自动分配区,元素个数相同的array对象可用=赋值。一般的数组、vector类对象、array类对象都不会检查下标是否超出范围,后两者可用at()函数代替下标操作,at()函数会舍弃超出范围的下标。

18.表达式的值
赋值语句的值为左边的变量值,如x=1,整个表达式的值为1;比较表达式的值为bool型。

19.cout.setf(ios::boolalpha);可设置bool型输出true和false。

20.a++:值为后加;++a:值为先加。C++不会保证局部表达式执行完后就立即进行里面的++操作,y=(1+x++)+(2+x++);的值为6(x=1),y=1+x++的值为2。对于内置类型,前置、后置的效率差不多;对于类,前置是用类的方法操作,更快;后置是将值取出、+1再放回原位置。

21.,可在for循环头部使用。i=20,y=120;整个表达式的值为右边的值,即120。,的优先级最低,如i=20,120;会使i=20;i=(20,120);则使i=120。

22.strcmp()比较两个字符串,相同返回0。前者比后者小返回负数,否则返回正数。大小按照ASCII顺序。只要比较的一方是string类对象,即可直接用比较操作符达到相同的效果。string类的对象不用\0来标志结尾,而是其他技术。

23.在程序内延迟时间:
ctime头文件包含了clock()函数和CLOCKS_PER_SEC,前者可返回程序运行到当前经过的系统时间(根据系统不同,可为long、unsigned long或其他类型),为了保证程序可移植性,可用clock_t统一指代clock()函数返回的变量,称之为别名,或者用auto;CLOCKS_PER_SEC为系统中标准的1秒。为了达到延迟一定时间的效果,可如下操作:
int a=10;
clock_t time_delay=a*CLOCKS_PER_SEC;//或者auto time_delay=...
clock_t start=clock();
while(clock()-start<time_delay)
;
这样即可延迟a秒。a也可定义为float、double。

如何自己定义别名:
可以:#define byte char//编译器将把byte替换为char
可以用C++新增的:typedef char byte;//同上
但#define中,如果类型是指针,进行替换后,若有多个变量同时定义,就会出现:
char *a,b;//可以看到,b实际上被定义为了char类型
这种情况只能用typedef。

24.C++11新增的range-based循环:
例1:double a[]{1,2,3};for(double x:a) cout<<x<<endl;
例2:double a[]{1,2,3};for(double &x:a) x=x*0.1;
例3:for(double x:{1,2,3}) cout<<x<<endl;
例1可展示a数组的所有变量;2可通过&操作符对a数组进行操作;3中可在for中定义数组。

25.EOF:
EOF是命令行时代的遗产。cin检测到EOF时,会生成eofbit和failbit并设置值为1。cin.eof()在EOF检测到时返回true;cin.fail()在eofbit或failbit之一设为1时返回true,即到达EOF或读取失败。两者都反映了上一次读入尝试,fail()比eof()在不同的环境更有通用性。在windows命令行环境中ctrl+z可模拟eof输入(然后再次按enter使得输入从缓冲区进入程序)。cin遇到EOF后会设置flag并不再读取输入,cin.clear()可以清除cin的flag并再次读取。

26.cin类提供了函数,能将cin等istream类的对象转换为bool型。所以while(cin)可以检测是否成功输入,替代25中用EOF检测的方法。并且while(cin)还能检测其他导致输入失败的情况,如磁盘错误。由于cin.get()返回的也是cin,所以可改写为while(cin.get(char)),一开始就输入一个char。
ch=cin.get()的写法是可以的,但记住get()返回的是一个cin对象。
cin.get()遇到EOF时返回一个特殊值,一般为-1(没有其他ASCII码为-1)。不需要知道具体值,可以用EOF代表:ch=cin.get();while(ch!=EOF)...但有些环境ch为unsigned char不可能为-1,需要转换ch为int
cout.put()可展示一个字符,接收的参数为char。有些环境中其有3种同名函数,可cout.put(char(int))来指定。
cin.get(ch)和ch=cin.get()的区别:前者返回istream类的对象,正常输入时转换为bool为1,遇到EOF时为0;后者将返回值赋值给ch,ch的类型为int,遇到EOF时ch被赋值EOF。但两者都应该用于字符的输入而非数字。

27.小技巧:写赋值时可以写变量=常量,写条件判断时写常量==变量。这样,若条件判断误写为常量=变量,编译器可立即给出错误。

28.逻辑运算符:
||先算左边再算右边,若左边为真则不执行右边。
&&同样,先左后右,若左边为假则不执行右边。
<的连写是从左至右的。如1<i<3等于(1<i)<3。1<i可能为0或1,整个式子始终为1。
!的优先级较高,&&和||较低,&&优先级比||高。
对于没有这些符号的编译环境,可用or,and,not替代。

29.C++的cctype(或ctype.h)头文件中包含了一些处理字符的标准函数。如isalpha(ch)在ch为字母时返回非0值;ispunct(ch)则在ch为标点时返回非0值。同样,isspace()检测是否空格、制表符、换行符,isdigit()检测单个字符是否数字。在有些字符编码不同的环境,字母的顺序中a-z可能不连续,这时需要用isalpha()。下面包含一些cctype中的函数:
Isalnum()检测是否字母或数字;isalpha()检测是否字母;isblank()空格或水平制表符;iscntrl()控制字符;isdigit()数字;ifgraph()可打印字符(除空格);islower()小写字母;isprint()可打印字符(包括空格);ispunct()标点符号;isspace()空格、进纸键、换行、回车、水平制表符、垂直制表符;isupper()大写字母;isxdigit()0-9或A-F或a-f;tolower()返回一个大写字母的小写字母,若非大写字母则返回原值;toupper()同理。

30.三字运算符:
表达式1?表达式2:表达式3
若表达式1为真,整个表达式的值为表达式2;否则为3。

31.switch的label只能为整型常量,通常用int、char、enum。switch总是比if分支结构效率更高。

32.continue对for循环的作用是直接跳到update expression。

33.goto语句:
设置一个标签,比如abc:
然后应用goto abc;就可跳至该标签的地方继续执行。

34.cin对数字的特性:
int n;cin>>n;如果输入一个字符,会发生:
n的值不变,错误的输入留在输入流中;cin对象设置一个error flag,并且call to cin method, if converted to type bool, returns false.
double等也同理。

35.文件读写:
包含fstream头文件;先定义对象;和cin、cout一样。
读取完后要fin.close()、fout.close()(最好在前面加fin.clear()、fout.clear())。
判断是否读取成功:
If(!fin.is_open()) exit(EXIT_FAILURE);
is_open()函数在文件读取失败时返回0;exit()函数包含在cstdlib中,该命令可与操作系统交流、终止程序。若编译器较老,可用good()方法,但它考虑的情况不如is_open()全面。

36.C++函数原型中不想写实参表可以在括号中用...代替。如void func1(...);
C++中参数与函数实参表中不同,编译器可自动转换类型。
argument:实参,被呼叫函数中的参数;parameter:形参,呼叫函数中的参数
只有在函数声明和原型的参数表中,int a[]和int *a才是等价的。
将数组传给被呼叫函数后便不能用sizeof()求数组长度了,若用到应一并传入。或者也可将数组最后一个元素的指针传入,但一般使用最后一个元素再后一个地址的指针。
将指针传给其他函数时,为了保护原数据,可在定义函数时加const,如void func1(const int *a)

37.const的用法:
int A;const int *a=&A;//不能通过a来修改A,但可有A++;,也可改变a的值;int * const a;则不能改变a指向的地址。
const int A;const int *a=&A;可以,但const int A;int *a=&A;不行(为了防止通过指针修改A值)。也不能通过二级指针来绕过这种限制:
const int a;int *p1;const int **pp2;这时不能有pp2=&p1;。
以上总结:可以把常量/非常量数据的地址赋给常量指针(除非这个数据自己就是指针),但不能通过常量指针来修改非常量的值;常量数据的地址只能赋给常量指针。

常量数据传递给函数时在函数声明与定义中也要有const。

38.二维数组的传参:
设想以下语句:
int a[3][4]={.......};int total=sum(a,3);
那么函数声明应该这样写:
int sum(int (*array)[4],int size)或者int sum(int array[][4],int size)
int total=sum(a,3);中a代表有四个元素的数组,是指向有4个元素的int数组的指针。
这里的sum函数只能用于有四列的二维数组。实际上,二维数组可作为二级指针对待。array[i][j]等于*(*(array+i)+j)

39.结构体做参数传递时就是结构体(不是指针),被调用的函数会做出一份copy而不动原数据(就像int、double做参数)。为了提高效率,可以使用结构体指针做实参。也可以使用C++的新方式(下文提到)。

40.用array类做参数时函数原型写:
void func1(array<int,4> *p);
可以用指针。注意array也可以用作结构体或类的数组。

41.C++不允许main()函数自我调用,C允许。

42.函数也有指针。一般可以用来写同名函数。去掉函数后的括号即函数的指针。注意区别:
FUNCTION(func());
FUNCTION(func);
要声明一个函数指针,格式如下:
double func1(int n);
那么double (*pf)(int n);//double *pf(int n);意为函数pf返回一个double *类型
pf指向一种函数,其返回值为double,实参为一个int类型。
还可以声明并初始化函数指针数组:double (*pf[3])(int n)={f1,f2,f3};//f1,f2,f3是三个函数
或者用auto:auto pf=f1;
auto pf2=pf;//这时pf2也成为和pf一样的函数指针数组
但auto不能用于链表初始化:auto pf2[3]={f1,f2,f3};//错误
现在假设一个函数func原型就是:double func(int n);,这时可pf=func;来将func函数的地址传给函数指针。一个调用此种函数的函数原型应该是:
void FUNCTION(int n,double (*p)(int n));
这样,就可以通过FUNCTION函数调用func函数了:
FUNCTION(123,pf);或FUNCTION(123,func);
pf在使用时可以用(*pf)的形式,C++也允许直接用pf:(*pf)(5)等价于pf(5)。

还可以用typedef来定义函数指针:
void func(int);//函数原型
typedef void (*p_to_func)(int);//只需要将函数名替换为类型名
p_to_func p1;//p1就是指向该类型函数的指针
p_to_func p_array[10];//还可以方便地定义函数指针数组

43.内联函数:
普通函数的地址在其他地方,执行时跳到函数地址再跳回原函数,可能有时间损耗;内联函数在编译时在每次call的地方用函数代码替代了call,形成一种嵌套结构,增大了内存消耗但节约了时间。在declaration或definition时前面加inline即可成为内联函数,但编译器不一定会执行,比如递归函数不能是内联函数,或者函数太大。一般把只有一两行的函数做成内联函数。inline double square(double x){return x*x;}

第二部分 第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 T>(这里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&);
其中classname是程序员定义的类名等,swap后跟的<classname>可省略,因为后面的signature就暗示了。

如果用本例中的swap函数处理一个int和一个double变量,可以这样:swap<double>(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<double>::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 <typename T>或者template <class T>,然后涉及的方法都要加template <typename T>,且关系标识符都要改,从classname::改成classname<T>::(原型不用改,这两个都是在实现里改)。如果方法写在类定义里,可以加前缀和关系。和函数模板一样,类模板不是真的可执行代码,而是告诉编译器如何生成真正的类。函数模板会使用传入的参数决定类型,而类模板必须要指定类型。

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

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

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

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

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

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

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

82.别名模板:
如template<typename T> 
using arrtype = std::array<T,12>;
使用时如arrtype<double> 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.例外:
有时为了避免函数出现预期外的结果,需要调用一些函数。例如一个计算调和平均数(2*xy/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),当*p所属的类是Type直接或间接的子类,就会返回转换后的指针,否则返回nullptr。对于引用,typeinfo头文件有bad_cast例外,dynamic_cast<Type &>(p)在不能转换时会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 X> class auto_ptr { 
public: 
explicit auto_ptr(X* p =0) throw(); 
...};
auto_ptr模板如上。那么用的时候比如auto_ptr<double> pd(new double);,其中new double是new分配的一块空间,调用构造函数时做参数。注意其用了explicit。
另外,智能指针最后是会delete的,不要用它指向不是new分配的地址。

实际上,智能指针在互相赋值上有些问题。智能指针可以和普通指针互相赋值,智能指针也可以互相赋值,但可能会导致同一地址delete两次。比如:
auto_ptr<string> ps (new string("I reigned lonely as a cloud.")); 
auto_ptr<string> 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<double>::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<int> > back_iter(dice);
实际上,back_insert_iterator需要容器存在push_back()方法,它有重新确定容器大小的权力(用push_back()),copy()则不能改变容器的大小。front类似。insert的构造函数还需要一个参数,即插入位置(在该位置前插入):insert_iterator<vector<int> > 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<int>,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<int> pq1; // default version 
priority_queue<int> pq2(greater<int>); // use greater<int> to order
greater<int>是一个预定义的函数对象,后面提到。
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<string> A; // a set of string objects
另外还可以提供一个参数(函数或对象)来提供排序键的算法,默认使用less<>模板。set<string, less<string> > 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<string> >(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<double> 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<double> add; // create a plus<double> object
double y = add(2.2, 3.4); // using plus<double>::operator()()
有点小题大做。对刚才提到的transform()第二种原型,可以这样用:
transform(gr8.begin(), gr8.end(), m8.begin(), out, plus<double>() );
另外functional头文件中还定义了其他的基本运算:

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

100.适配性函数对象与函数适配器:上文中的functor equivalent都是可调配的。一个函数对象,若其typedef成员的来标明参数类型或返回类型,即为adaptable。这些typedef成员为result_type, first_argument_type和second_argument_type。比如一个plus<int>对象会被标识为plus<int>::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<double>(), 2.5));
上述语句意为将范围内的值都乘以2.5,然后输出。
binder2nd同理。

第三部分 16章-附录前

101.STL算法:不同容器,比如vector和list间,如果其内容的值和顺序相同,那么其运用==后值为true。
STL将算法分为了四类:不修改的序列操作;修改的序列操作;排序相关操作;泛化的数值操作。前三种在algorithm头文件中,最后一种在numeric头文件中。
不修改的序列操作,对一定范围内的元素执行操作,操作不会改变容器。for_each()就属于这类。
修改的序列操作,也对一定范围内的元素执行操作,但能改变容器(元素的值或顺序)。
排序相关操作,比如sort()。
泛化的数值操作,操作数值,主要用于vector。
算法可以按照需不需要额外空间分为三类。sort是原地的,copy是拷贝性的,transform则二者都可。一些函数可以原地也可以拷贝,STL的习惯是将拷贝的版本加上_copy,拷贝的形式需要一个迭代器参数来指明结果放哪儿,比如replace函数的原型如下:
template<class ForwardIterator, class T> 
void replace(ForwardIterator first, ForwardIterator last, const T& old_value, const T& new_value);
而它的拷贝版本,原型如下:
template<class InputIterator, class OutputIterator, class T> 
OutputIterator replace_copy(InputIterator first, InputIterator last, OutputIterator result, const T& old_value, const T& new_value);
其返回类型为输出迭代器。一般_copy版本都会返回一个输出迭代器,指向最后一个拷贝的值的地方。
还有一些函数有_if版本。比如replace的if版本:
template<class ForwardIterator, class Predicate class T> 
void replace_if(ForwardIterator first, ForwardIterator last, Predicate pred, const T& new_value);
如果将范围内的某一个元素应用于Predicate后返回true,则会被替换。
replace也有replace_copy_if()版本。

102.string虽然不属于STL,却在设计上有STL的风格,比如它有begin(), end(), rbegin()和rend()方法,因此也可以用一些STL的接口,比如sort()。string还可以使用next_permutation()算法。

103.除了STL,C++还提供了一些更细化的库,比如complex,提供复数相关的方法。

104.vector,valarray和array:vector是STL中的一种容器,可与其他容器交互;valarray是对数值处理方面特别加强的类模板,不属于STL;array是替代基础类型数组的,提供了begin()等,可使用STL的一些接口。
比如valarray的apply()函数,或者各种重载的操作符,非常有用。其没有begin方法,但C++11提供了一些函数模板,比如sort(begin(vad), end(vad)); // C++11 fix!
尽量不要直接访问valarray中最后一个元素后的地址,可能导致不明行为。而且其有resize()方法来重新确定大小,但不像vector一样可以自动调整大小。valarray还可以使用slice类(valarray头文件中)来代替下标,其构造函数为slice(start,number,stride),比如valarray_example[slice(1,4,3)]=10;即将下标为1,4,7,10的元素赋值为10。slice和gslice可以方便地表示多维数组中的一行、一列等。

105.initializer_list:C++11新增。除非类处理的是大小变化的一组数据,否则不需要特别写一个initializer_list的构造函数。需要的话,使用initializer_list头文件,其提供了begin、end、size方法,比如一个initializer_list<double>对象a,a.begin()等可以访问其中的元素。需要注意,返回的是const迭代器,不能单个修改,但可以将另一个初始化链表赋值给a。

106.为了匹配程序处理速度和硬件读取速度,用缓存区。键盘输入时每次enter后缓存区中的数据才传递给程序。一般地,程序到输入语句后会清空输出缓存。

107.iostream中提供了一些类来管理。streambuf类为缓冲区提供内存,还提供了方法来填满缓冲区、访问缓冲区内容、刷新缓冲区、管理缓冲区内存;ios_base类表示了输入输出流的一些成分,比如开/闭、二进制形式/文本形式;ios类基于ios_base类,其中有一个streambuf对象的指针成员;ostream类从ios衍生,istream类同理;iostream则继承了两者。

108.包含iostream头文件就会自动创建8个流对象(4个给1byte的narrow字符,4个给宽字符):cin和wcin(针对wchar_t);cout与wcout;cerr与wcerr,错误流,一般用于展示错误信息,默认连到标准输出设备如显示器,该流没有缓冲区,也就是说可以直接到屏幕;clog与wclog,与cerr的区别在于其是有缓冲区的。

109.操作系统的重定向:windows命令行下:
counter <a >b
将a文件内容输入程序counter,结果输出到b文件中。
cerr和clog即使在重定向后也会输出到屏幕。一些操作系统允许对cerr和clog重定向,如unix和linux的>2操作。

110.ostream对象认得所有基础类型,并且对<<操作符重载。insertiong操作符还可以用于输入4种指针:const signed char * ,const unsigned char * ,const char * 和void *,因为字符串就是用指针表示的。void *是用来匹配其他类型的指针的。
ostream还提供了put()方法来展示字符,write()方法来展示字符串。put也支持wchar_t,这样用:cout.put('W');,其返回cout的引用,可以连用。write()的原型为basic_ostream<charT,traits>& write(const char_type* s, streamsize n);,如果用cout来调用会导致其用char来特化,并返回cout引用,其在遇到终止符后不会停止;write也可以用于打印数值,但需要转换成char*,此时会将其二进制形式认作ascii等编码并打印。write还可以用于在文件种保存数值。

111.刷新缓冲区:读取时缓冲区很有必要,每次要先填满缓冲区再发送;但输入时最好不要等缓冲区填满,而是再输入前立即刷新(清空)缓冲区。flush可以清空缓冲区,endl也可以而且还会输出一个换行,二者使用方式一样,或者可以使用flush(cout)。

112.cout输出格式:char,若表示可打印字符,默认占一个字符宽度;数值整型默认为10进制,一个字符占一个字符宽度(有负号带-);字符串同样。浮点数的格式与过去有区别:
现在的浮点数默认打印精度为6,若以0结尾则不打印;根据数值,可能以固小数点或科学计数法打印(指数大于等于6或小于等于-5时)。具体习惯上,比如用逗号代替小数点(欧洲习惯),在locale头文件中。
ios_base,ios,ostream的继承关系,ios_base中储存了一些描述格式的信息,并提供了一些方法来改变格式。
dec,hex,oct分别表示10进制,16进制,8进制输出格式,它们是函数,比如hex(cout),然后直到下次指定进制,都会一直以16进制输出。也可写cout<<hex。
width方法可以调整占据的宽度。原型int width()返回当前宽度值,原型int width(int i)则将宽度设为i并返回刚才的宽度值。width方法只会影响下一个输出的东西,然后就会回复为默认值。调整宽度后,默认输出的东西在最右边,前面用空格填充;若宽度小于输出的东西,则会按原样输出。cout.fill('*');可以设置用*填充,这项设置持续有效。
浮点数输出格式:对于固定小数点和科学计数法,精度指小数点右边的数位。cout.precision(2);可设置精度为2,也是持续有效的。ios_base类提供了setf()方法来设置一些格式,比如cout.setf(ios_base::showpoint);可以保证小数点和默认不显示的0一定显示。
setf有两个原型:一是fmtflags setf(fmtflags);,用于设置由一bit控制的一些格式,返回先前的格式,fmtflags 是typedef的bitmask类型(一种可以访问每一bit的类型,可为int、enum或STL的bitset容器)。以下是该原型常用的参数:

C++将8进制和16进制作为无符号处理,所以最后一个参数只对10进制有效(一些编译器可能有效)。
setf的第二个原型为fmtflags setf(fmtflags , fmtflags );,用于由不止一个bit控制的格式。同样也返回先前的格式,第一个参数同上一个原型一样,第二个参数则是一个首先清除某些bit的值,比如设置16进制:cout.setf(ios_base::hex, ios_base::basefield);,先用第二个参数将控制进制的其他bit设置为0,再用第一个参数将控制16进制的bit单独打开。以下为三种第二个参数:

internal指将符号和前缀放在左边,数值放在右边。当使用internal时,精度转而控制总数位。
C++中的浮点格式用F还是E,默认对应C中的%g,fixed对应%f,科学计数法对应%e。

setf改变的可以用unsetf来变回原值,其原型为void unsetf(fmtflags mask);,可以将一个或多个bit设置为默认值0。

较新的C++提供了flag的替代:



iomanip头文件:提供了上面提到的格式设置,但更方便使用,如setprecision()
、setfill()和setw()。

113.cin和cout一样认得各种基础类型,但其参数一般为引用变量。cin可cin>>hex。cin还认得几种指针:signed char * ,char * 和unsigned char *。cin会跳过white space,遇到正确的类型后从输入流中将其提取,遇到非预期类型后停止提取,将其留在输入流中。如果一开始就是非预期类型,则会保持变量值不变并在用于判断时返回0。实际上,cin和cout从ios_base继承了类型iostate(也是bitmask)来表明输入输出的状态,iostate包含了3种ios_base元素:eofbit,badbit和failbit,0为初始值,不同的环境在设置badbit和failbit上有些出入。以下是一些说明:


clear()中的参数bit不会变,其他bit设置为0;而setstate则是仅改变参数bit,其他bit不变,但setstate用clear实现。
eofbit、failbit设置为1时默认不会throw exception,但可通过exceptions方法调整,其返回一个表示3个bit的bitfield,改变流状态一定会用到clear,clear会将当前值与exceptions返回的值比较,如果需要就会throw一个ios_base::failure的例外。比如:cin.exceptions(badbit); // setting badbit causes exception to be thrown
多个设置可以用|:cin.exceptions(badbit | eofbit);
由于历史原因,fail方法在failbit或eofbit为1时都回返回true。

114.get和getline方法:
单个字符输入:
get(char &ch);将读到的字符赋值给ch,不跳过white space,其返回cin的引用,如果遇到EOF则调用setstate(failbit),并不会赋给ch任何值;get(void);则将读到的字符转换为int(或其他整型)并返回,不能连用,遇到EOF会返回值EOF(EOF不用char表示)。get(void)可以用来替换C中的getchar()。

字符串输入:
istream & get(char *, int, char); 
istream & get(char *, int); 
istream & getline(char *, int, char); 
istream & getline(char *, int);
第二个参数需要比待读取的字符个数大1。get将终止符留在流中,getline则将其丢弃。ignore()方法有两个参数,第一个是表明最多读取字符数的int(默认为1),第二个参数是char的终止符(默认为EOF),用于读取到指定个数的字符处或遇到终止符为止,这段字符被丢弃,ignore返回cin的引用。
如果以上方法没有从流中抓取任何一个字符,就会用setstate设置failbit,比如立即遇到EOF,或输入空行(getline输入空行时不会fail,因为它提取了换行并丢弃)。而如果字符达到或超出上限:
如果是getline,比如char temp[30]; while (cin.getline(temp,30)),读了29个字符后,如果遇到EOF,会设置eofbit;如果遇到换行符,会读取并丢弃之;如果是其他字符,就会设置failbit。
如果是get(char *, int);,其先测试字符数目,然后看看是不是到EOF,最后检测是否换行符。超出读取上限时其不会设置failbit,但可以用peek()方法检测下一个字符,若是换行则get(char *, int);读完了一整行,否则get(char *, int);在读完前就停止了。

其他输入流方法:
read(),参数同istream & get(char *, int); ,不同的是末尾不加\0,通常与write()配合用于文件读写。其返回cin引用。
peek()读取下一个字符,但不会从流中提取,用法同ch=cin.get(void)。
gcount()返回上一次非格式提取方法所读取的字符个数,即get、getline、ignore、read等方法,而不能用于>>。
putback()将一个字符放回到输入流,参数为需要放入的字符,返回cin引用。

115.文件输入输出:fstream对象继承了iostream,fin和fout可以使用fail()等方法来检测是否正常,但最好使用is_open(),其还可以检测是否以错误的方式打开文件。
命令行参数,见1120,int main(int argc, char *argv[])。
打开模式的参数(ios_base中):

app模式仅允许在EOF后写入,而ate模式仅仅把读指针放在EOF。
下表给出C中fopen函数的对应:


以二进制读写更快,且数值更精确。比如写一个pl结构体,fout.write( (char *) &pl, sizeof pl);,read同理。但不同的环境,二进制表示可能不同。
对于有虚函数的类,其有一个指向虚函数指针表的指针,将其写入后读取会造成严重后果,可以自己写write和read的方法。同样地,比如string类存储的是指向new字符串的指针,将其读写没有太大意义。
也可以用一个fstream对象处理读写,比如finout.open(file,ios_base::in | ios_base::out | ios_base::binary);,fstram的读、写指针的位置总是相同的。另外,seekg可将写指针移动到参数位置,seekp可将读指针移动到指定位置,seekg原型如下:
basic_istream<charT,traits>& seekg(off_type, ios_base::seekdir); 
basic_istream<charT,traits>& seekg(pos_type);
进行char特化,原型等同于:
istream & seekg(streamoff, ios_base::seekdir); //从第二个写指针参数开始算
istream & seekg(streampos);//从文件头开始算
比如fin.seekg(30, ios_base::beg);就是从开始往后30bytes,第二个参数可以有beg、cur和end。
tellg和tellp分别可以获取读、写指针的位置,其都返回一个streampos(从文件头算)来表明位置。
可以用fstream对象读然后修改对象,一般在write后加flush来确保完成修改。

116.需要创建临时文件、需要不重复的文件名,可以用cstdio中的tmpnam函数:
char* tmpnam( char* pszName );,给其一个char*,其会在char*里装一个字符串。L_tmpnam表示字符串最多有几个字符,TMP_MAX则表示最多能创建多少个不重复的文件名,都是cstdio中的常量。创建的名字根据编译器而不同。

117.sstream可提供程序和string对象间的I/O。ostringstream对象提供输出,和ostream格式一样。其输出进入缓冲区(可动态分配空间),然后使用str()方法,返回一个存有缓冲区内容的string对象,然后该ostringstream关闭并且不能再用于写。
要将istringstream对象和一个string对象关联,可以该string对象构造这个istringstream对象。(istringstream对象能否重复使用?有没有open方法?)

最后一章(涉及C++11的新特性和一些boost开源库)
118.移动语义:涉及大量数据的拷贝,可以不需频繁删除,而是更改数据的“所有权”,就像同一盘符更改文件位置一样。这就需要右值引用,一般的构造函数用左值引用,而move constructor用右值引用,移动语义中右值不能是const(通常会将右值在交出所有权,即直接赋值后清零)。除了构造函数,赋值操作符也要使用移动语义。
在一些情况下,可能想使左值当右值使用。这时可以用static_cast<>来转换,或者用std::move()函数(utility头文件)。

119.C++11新增移动构造函数和移动赋值,在原来默认构造、拷贝构造、赋值操作符、析构函数的基础上,但有时定义了其中一个会导致不提供其他几个的默认版本,classname()=default;使用默认构造函数。或者不想让对象有拷贝操作,可以...=delete;。或者对其他成员函数也可以delete,比如一个成员函数使用double为参数,当参数为int时也可以调用,但如果delete掉int版本,那么同样的操作就会编译错误。实际上,delete后成员函数依然存在,只是不能调用了。

120.C++11提供了override关键词,加在函数签名后面。当基类中的虚函数签名与衍生类中对应函数签名不同时,基类的虚函数会被隐藏(无法调用),用override后,如果虚函数没有被override,就会编译时报错。相反,加final关键词则会阻止衍生类重载虚函数。这两个实际上不是关键词,可以用作标识符,仅当出现在特定位置时才有类似关键词的作用。

121.lambda表达式/lambda函数:
generate(起迭代器,止迭代器,数据生成的函数对象)
和函数指针,functor功能一样。lambda表达式可以生成无名函数,可自动推断返回类型,或者可用尾部返回类型来指定:[](double x)->double{int y = x; return x – y;}

122.functional头文件中的function(wrapper),优化相关,减少相同函数签名的模板实例化个数。

123.Variadic template,模板的参数包:接受个数不确定的参数和类型参数。用左边的 ...来打包,用右边的...来解包,解包涉及递归。1197

多线程:参见其他书籍

标签: none

添加新评论