【读书笔记】c++primer

c++ primer 5e学习笔记
第1章
1.标准库 类型和函数的集合,每个c++编译器都必须支持。
2.()运算符:调用运算符。跟随在函数名后,起调用函数的作用

第2章
1.p32:char在一些机器上是有符号的,在一些机器上是无符号的;
2.p33:赋给无符号类型一个超出表示范围的值,结果是初始值对可表示数值总数取模的结果
-1赋值给unsigned char = -1/256=255;
3.p36:可分行书写字符串字面值
cout<<“a cute dog”
“a cute cat”< 4.p36:转义序列“\”对应的是八进制,“\x对应十六进制
5.p38:变量:具名的可供程序操作的存储空间
6.函数体外部的内置类型变量未被初始化,会默认初始化为0。函数体内未初始化的变量,访问时会编译报错。

    int i;
    cout << "1" << i << "2" << endl;
    return 0;

编译报错,虽然编译器检测出来了,但是编译器并不是一定就能检测出来,调试也会很困难

【读书笔记】c++primer_第1张图片

ASCLL码0对应空格

7.存疑:extern 的使用p41
8.预处理器:运行于编译过程之前的一段程序,NULL是预处理变量
9.指向指针的引用:int *p;int *&s = p;从右往左离变量名最近的对变量的类型有最直接的影响p52
10.const只在文件内有效,解觉多文件内使用的办法就是在变量定义之前加extern
11.对常量的引用:格式:const int ci = 1024;
const int &r1 = ci;const不可缺,让一个非常量引用指向一个常量是不允许的。
12.const int &i = 42;允许,i为常亮引用,允许常量引用绑定到非常量的对象,字面值甚至一般表达式,int &i = 42;不允许,引用不是对象,只能绑定带具体对象。p55
13.类型别名,

typedef char *pstring;
const pstring cstr = 0;
//pstring 是char*的别名,cstr是一个指向char的常量指针。
const char *cstr2 = 0;
//cstr2是一个指向const char的指针。

[cstr是一个常量指针,不可改变指针的值,cstr2是一个指向常量的指针,指向的对象是一个常量](https://img-blog.csdnimg.cn/20210215141409206.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80 MjExOTMyNA==,size_16,color_FFFFFF,t_70)
14.auto类型说明符:编译器判断表达式所属类型
15.decltype类型指示符:得到操作数的类型但不计算其结果,返回的结果包含顶层const和引用。decltype(f()) sum = x。如果表达式的内容是解引用,得到的类型是引用而不是引用的对象。decltype(*p)结果类型是int&,而不是int。
decltype((var))双层括号得到的一定是引用。p63
16.预处理器:确保头文件多次包含仍能安全工作。预处理器是在编译之前处理的一段程序#include;
头文件保护符:#define #ifdef #endif,头文件保护符必须唯一

第三章
17.命名空间的using声明:using std::cin
标准库类型string
18.string 读写:string 的cin操作会忽视空格,保留空格可用getline(cin,line);
19.c++中字符串字面值并不是标准库类型string的对象。
20.处理string对象中的字符,在cctype头文件中。c++标准库兼容c语言标准库,C语言中文件名name.h,改成cname,cname中定义的名字从属于std,但name.h则不是。
21.访问string中每个对象:string s = “hello world” ;for(auto c:s)
22.使用超出范围的下标将引发不可预知的结果。慎用下标访问方式,使用前要判空。
标准库类型vector
23.vector:#include
using std::vector;
vector是一个类模板
24.向vector中添加元素:循环体内部包含向vector添加元素,不能用for循环,原因后面说。p91。p169范围for语句不是传统for语句。
25.不能用下标形式向vector添加元素,只能push_back,因为
vector ivex 是一个空vector,不包含任何元素。也就是,只能对确知已存在的元素执行下标操作。访问不存在下标会产生运行时错误:缓冲区溢出(buffer overflow)
迭代器
26.迭代器的对象是容器(或string)中的元素(或字符)。
27.有迭代器的类型都有begin(),end()的成员,返回的就是迭代器。
28.我们并不知道迭代器的具体类型,使用iterator和const_iterator(常量指针)来表示迭代器的类型。p97
29.谨记,但凡使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
数组
30.不允许拷贝和赋值
31.数组从内向外阅读
int *ptrs[10];//含有10个整形指针的数组;
int (*parray) [10] = &arr;//parray指向一个含有10个整数的数组。
int(&parray)[10] = &arr;//paray引用一个含有10个整数的数组。
32.c++11为数组引入begin()和end(),begin(arr);
33.内置的下标运算符所用的索引值不是无符号类型,这一点与string 和vector不同。
C风格字符串
34.虽然cpp支持,但最好不要使用。
35.c风格字符串与string混用,允许c风格字符串来初始化string,允许加法运算中使用一个c风格字符串。
用string初始化c风格字符串用const char *str = s.c_str();
多维数组
36.用类型别名可简化多维数组的指针:
using int_array = int[4];
typedef int int_array[4];//注意这个写法
typedef int[4] int_array;//错误写法

第四章 表达式
1.运算符:函数调用也是一种特殊的运算符,对运算对象的数量没有限制。
2.重载运算符
3.注意关键字decltype,采用左值右值是不同的。
4.求值顺序:int i = f1() * f2(); *没有规定左右运算的先后顺序。再如cout<<++i< 5.赋值运算符:使用列表初始化不允许损失精度。p129,赋值运算符优先级低于关系运算符。
6.后置递增运算符优先级高于解引用运算符。
7.位运算符:~,<<,>>,&,^(位异或),|,右移运算符的的左端是否保留符号位要视机器而定。
8.sizeof运算符返回所占的字节数,
sizeof(type);
sizeof expr;//返回的是表达式结果类型的大小,并不计算运算对象的值。sizeof运算不会把数组换成指针来处理。
9.有符号数和无符号数相加:
无符号类型不小于带符号类型,则带符号类型会转换为无符号类型后相加,当带符号类型为负数,会计算出错:
计算出错例子
也可能计算正确,当负数的绝对值小于正数的绝对值时,计算是正确的。
10.强制类型转换(cast):
static_cast<>(exp);//具有明确定义的类型转换,只要不包含底层const,都可使用;
const_cast<>(exp);//改变运算对象的底层const,并且也只能用于改变常量属性,不能用于改变表达式的类型。
reinterpret_cast<>(exp);//避免使用
dynamic_cast 后面p730介绍
应避免使用强制类型转换,
旧式强制类型转换:
type(expr);//函数形式的类型转换
(type)expr;//c语言风格
执行的过程与新式差不多,但形式上不够明确。

第五章语句
1.注意空语句也并非都是无害的;
2.switch语句,case标签必须是整形常量表达式,没有break的情况下,匹配case后面的语句都会被执行,不管后面的case有没有匹配上。
当逻辑不需要break,而是希望逐个判断case时,可把case写在同一行p161
swich语句是一个作用域,case语句并没有区分出不同作用域,需要时加花括号分出作用域。p163
3.传统for语句:语句头的初始条件可以定义多个对象,以逗号间隔,但是多个对象必须是同一类型。
4.范围for语句:c++11新标准
for(declaration : expression)
statement
expression必须是一个序列,拥有能返回迭代器的begin和end成员。declaration定义一个变量,序列中的元素转换成该变量的类型。例如:vectorv = {0,1,2,3,4};for(auto &r : v){statement}。p169
前面p91提到的,不能通过范围for语句增加vector对象,因为范围for语句的结束条件是隐含的,实际是判断vector的end的。
5.do while语句,循环条件是不能定义在循环内部的,定义在循环内部则会每次循环都重新定义。循环条件也不能定义在while条件部分,因为要先执行do再判断条件。
6.跳转语句
break语句:只能终止离它最近的while,do while,for,或switch。
continue语句:终止最近的循环中的当前迭代,开始下一次迭代。
goto语句:无条件跳转到函数内的另一条语句。不要使用goto语句。
7.try语句块和异常处理:
典型的异常包括失去数据库连接以及遇到意外输入。
throw表达式:throw runtime_error(“data must …”);//runtime_error是标准库异常类型的一种,定义在stdexcept头文件中。
try
{program_statements;
throw runtime_error}
catch (runtime_error)
{
//处理代码
}
当异常抛出时,首先搜索抛出该异常的函数,没有找到匹配的catch时,终止该函数,并在调用该函数的函数中找匹配的catch,以此类推。如果最终也没有找到匹配的catch子句,程序转到名为terminate的标准库函数,该函数与系统有关,一般导致程序非正常退出。
标准异常p176:
exception头文件定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外信息。
stdexcept头文件
new头文件定义bad_alloc异常类型p407
type_info头文件定义bad_cast异常类型p731
exception、bad_alloc、bad_cast对象不能为这些对象提供初始值,只能默认初始化。其他类型的异常恰好相反,使用string或c风格字符串初始化这些对象,不允许使用默认初始化。
异常类型只提供一个what的成员函数,返回指向c风格字符串的const char*。提供一些提示文本。

第六章 函数
1.调用运算符:就是函数的圆括号。
函数调用其实是两步:用实参初始化形参,控制权转移给被调函数。
为了与c语言兼容,可以使用关键字void表示函数没有形参:
void f2(void){};
2.局部对象:在cpp中,名字有作用域,对象有生命周期。
3.局部静态对象:有些时候,令局部变量的生命周期贯穿函数调用及之后的时间,可将变量定义为static类型。
局部静态对象在程序执行路径第一次经过对象定义语句时初始化,知道程序终止才被销毁。
4.函数说明:
函数的三要素(返回类型,函数名,形参类型),描述了函数的接口。函数声明也称作函数原型。
建议在头文件中声明,在源文件中定义,函数和变量都应该如此。
5.分离式编译:
编译和链接多个源文件:分离式编译每个文件,通常产生一个后缀名是.obj(Windows)或.o(Unix)文件,后缀名的含义是该文件包含对象代码。
6.参数传递:当形参是引用类型时,我们说他对应的实参被引用传递,或者所函数被传引用调用。当实参的值拷贝给形参时,说实参被值传递,或者说函数被传值调用。
7.指针形参:
8.传引用参数,拷贝导致低效,使用引用避免拷贝。使用引用形参,如果函数无需改变形参值,形参最好声明为常量引用,声明为普通引用是一种错误,会给函数调用者一种误导。
9.const形参和实参:当用实参初始化形参时会忽略顶层const,需要主要:同名函数形参列表应该有区别,忽略了顶层const,
void f(const int i){};
void f(int i){};//两个是一样的效果,不能视为同名函数。
可以用非常量初始化一个底层const,反过来不行。
调用普通(非const)引用形参的函数,不能使用字面值,求值表达式,需要转换的对象,或const类型的对象。
函数定义:f(string &s,char c)
函数调用:f(“hello world”, ‘o’);//错误,不能传入字面值常量p192
更难觉察的情况:
bool f(&s);//f为普通引用形参
bool isf(const string &s)
{
return f(s);//错误,isf中s为常量引用,f普通引用形参,错误。
}
解决思路:不能试图修改isf的形参,这样只是将错误转移到了上一层,应该修改f的形参为常量引用,如果不想改,可以在isf中重新定义一个变量,拷贝s的值。
10.数组形参:不允许拷贝数组,因此不允许以值传递的方式使用数组参数。数组会被转换为指针,因此传递数组时传递的是首元素的指针。
传递多维数组:除了首地址外,还需要除第一维的维度。
void print((*matrix)[10]);
void print(int matrix[][10]);//跟上面的等效
注意:
int *matrix[10];//10个指针构成的数组
int (*matrix)[10],//指针指向10个整数的数组。
11.含有可变形参的函数p197:编写能处理不同实参的函数,如果实参类型相同,可以传递一个名为initializer_list的标准库类型。如果类型不同,可编写可变参数模板
initializer_list:是一种标准库类型,用于表示某种特定类型的值的数组。定义在同名头文件中。
initializer_listls
省略符形参:C语言C标准库varargs中的内容,
void foo(parm_list, …);
12.void类型的函数也可以return expression,但是expr必须是另一个返回void的函数。
13.函数也可以返回引用p201,string &shorterString(string &s1, string &s2),调用函数和返回结果都不会拷贝对象,形参也只能是引用类型。函数返回类型是引用类型时,不能返回函数内部定义的局部变量,因为局部变量在函数结束时所占的内存空间也被释放掉了,局部变量的引用或指针指向不再有效的内存区域。
调用一个一个返回引用的函数得到左值。
14.Cpp11标准规定,函数可以返回花括号包围的值的列表,返回类型是vector
15.允许main函数没有return语句直接结束,编译器会隐式地插入return语句。为了使main函数返回值与机器无关,cstdlib头文件定义了两个预处理变量,EXIT_FAILURE,EXIT_SUCCESS,属于预处理变量。
16.返回数组指针p205:因为数组不能被拷贝,所以函数不能返回数组,函数可以返回数组的指针或引用。但是定义一个返回数组指针或引用的函数比较烦琐,可以使用类型别名:
typedef int arrT[10];//或下面
using arrT = int[10];//或上面
arrt *func(int i)
不使用类型别名的函数的声明:
int(func(int i))[10];//int i是函数形参
cpp1标准可以使用尾置返回类型,auto func(int i) -> int (
)[10];
或者用decltype
17.函数重载:
同一作用域内几个函数名字相同但形参不同称为重载函数,参数的类型和数量都可能不同。main函数不能重载。
不允许两个函数只是返回类型不同。
重载和const形参:顶层const不影响函数传入的对象,拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
底层const是可区分的。
const_cast和重载:
定义两个同名函数,参数分别为带const和不带const
18.调用重载的函数:二义性调用,有多个函数可以匹配,但是没有最佳选择,这是发生错误。
19.重载与作用域:
一旦在当前作用域中找到所需的名字,编译器就会忽略掉外层作用域中的同名实体,这里是直接忽略,而不是优先级更低。
cpp中,名字查找优先于类型检查。
20.特殊用途语言特性p211:默认实参,内联函数,constexpr函数
默认实参:string screen(int ht = 24, int wid = 81,char background = ‘’ );
默认实参作为形参的初始值出现在形参列表中,可以为一个或多个形参定义默认值,但是一旦某个形参被赋予默认值,它后面的所有形参都要有默认值。想要使用默认实参,只要在调用函数时省略实参就可,默认实参从右往左填补空缺,调用函数时实参从左往右调用参数。
在给定的作用域中,一个形参只能被赋予一次默认实参。通常应该在函数声明中指定默认实参,并将声明放在头文件中。
局部变量不能作为默认实参,
21.内联函数
内联函数可避免函数调用的开销:编译时在函数调用的地方内联地展开。关键字inline,只是向编译器发出一个请求,编译器可以忽略这个请求。
22.constexpr函数:能用于常量表达式的函数,函数体中有且只有一条return语句,为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数,给constexpr函数传入一个常量表达式时,返回类型也是常量表达式,用一个非常量表达式调用,则返回一个非常量表达式。
内联函数和constexpr函数放在头文件中,和其他函数不同,可以多次定义,但是多个定义必须完全一致。
23.调试帮助:
头文件保护技术,一些代码只用于调试,正式代码屏蔽。:
assert预处理宏:assert(expr);//如果expr为假,输出信息并终止程序的执行,否则什么也不做。定义在cassert头文件中,预处理名字由预处理器而非编译器管理,可直接使用预处理器名字而无需using说明。
NDEBUG预处理变量:
assert的行为依赖于一个名为NDEBUG的预处理器变量的状态,如果定义了NDEBUG,则assert什么也不做。
#define NDEBUG
命令行选项:CC -D NDEBUG main.c
除了用于assert,还可以用于自己的条件调试代码:
#ifndef NDEBUG

#endif
预处理器定义了几个对于程序调试很有用的名字:
__func__输出当前调试的函数的名字
__FILE__存放文件名的字符串字面值
__TIME__存放文件编译时间的字符串字面值
__DATE__存放文件编译日期的字符串字面值

24.函数匹配p217:当几个函数形参数量相等以及某些形参的类型可以转化的时候,不太容易确定调用哪个重载函数:先找可行的,接下来看参数数量和类型,如果没有最佳匹配,编译器报错。
调用重载函数时尽量避免强制类型转换,如果一定需要,说明我们的形参集合不合理。
25.实参类型转换:
26.函数指针:bool pf(const string&, const string &)
在指向不同函数类型的指针间不存在转换规则。
不能定义函数形式的形参,函数类型的形参会被隐式转换为指向函数的指针,可以直接把函数当作实参用,此时是自动转换为指针。
返回指向函数的指针:
using pf= int(
)(int , int);
pf f1(int);
或者直接声明:
int(f1(int))(int, int);
或者尾置返回类型:
auto f1(int) ->int (
)(int *, int);

第七章 类
类的基本思想是数据抽象和封装,数据抽象是一种依赖于接口和实现分离的编程技术。封装实现类的接口和实现分离。
1.定义成员函数:所有成员都必须在类的内部声明,可以在类外定义,
2.this指针:成员函数通过一个名为this的额外的隐式参数来访问调用他的对象的成员。当我们调用一个成员函数时,用请求该函数的对象地址初始化this,this的目的总是指向“这个”对象,this是一个常量指针,不允许改变this中保存的地址。
3.引入const成员函数,
string isbn() const{return bookNo}
const的作用是修改this指针的类型,this指针是一个常量指针,他所指的这个对象本身也是一个常量,因此this应该既是顶层const,也是底层const,但是没有地方可以把this设置成一个底层const,因此就把const放在成员函数后面。这样的成员函数叫做常量成员函数。这样,因为this是指向常量的指针,因此不能用this改变这个对象,也不能改变这个对象的数据成员。到成员函数就是:常量成员函数不能改变对象的数据成员。
常量对象以及常量对象的引用或指针都只能调用常量成员函数。
4.类作用域及成员函数:编译器分两步处理类:首先编译成员的声明,才轮到成员函数体,因此成员函数体可以随意使用类中的其他成员。
可在类的外部定义成员函数,但类内的声明必须完全一致,用类名加作用域运算符再加函数的格式。
函数可返回this对象:return this;//解引用this指针以获得这个对象。
5.定义类相关的非成员函数:
IO类属于被拷贝的类型,只能通过引用来传递。
6.构造函数:类通过特殊的成员函数构造函数来控制其对象的初始化过程。
构造函数不能声明成const类型,类的const对象在构造函数完成初始化后,才取得常量属性。
没有构造函数时类通过默认构造函数执行默认初始化。是由编译器创建的合成的默认构造函数,如果定义在块内的内置类型或复合类型的对象(数组,指针)被默认初始化,他们的值将是未定义的。因此需要类内赋值或手动构造函数。
在Cpp11中可以sales_data() = default;来要求编译器生成构造函数,跟合成构造函数一样的功能。
构造函数初始值列表:
sales_data(const string &s, unsigned n,double p):bookNo(s), units_sold(n), revenue(p
n) {}
冒号和花括号之间的部分称为构造函数初始化列表,负责为新创建的对象的一个或几个数据成员赋初值。
在类的外部定义构造函数。
也可在构造函数的函数体中初始化数据成员。
7.拷贝,赋值和析构
8.访问控制与封装:
访问说明符加强类的封装性:
public说明符之后的成员在整个程序内可被访问,作为接口的部分,构造函数和部分成员函数跟在其后面
private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问。数据成员及作为实现部分的函数跟在其后面。
9.class和struct
struct关键字,定义在第一个访问说明符之前的成员是public,使用class则是private。
10.友元
类可以允许其他类或者函数访问他的非公有成员,方法是令其他类或者函数成为他的友元,在类内增加一条以friend关键字开始的函数声明语句,友元不是类的成员。
尽管当类的定义发生改变时无需更改用户代码,但是使用了该类的源文件必须重新编译。
友员的声明只是指定了访问权限,函数本身需要再次声明。
11.类的其他特性:
在类内定义类型成员:
public: typedef string::size_type pos;
private: pos cursor = 0;
定义类型的成员必须先定义后使用。
令成员作为内联函数:定义在类内部的成员函数是自动内联的,定义在类外部的成员函可以在类内声明时用关键字inline内联。
重载成员函数。
可变数据成员:通过在变量的声明中加入mutable关键字,即使是const成员函数也可以修改可变数据成员的值。
12.返回
this的成员函数:返回引用是左值,返回非引用的话只能通过拷贝。
一个const成员函数如果以引用的形式返回*this,那么他返回类型将是常量引用。
13.类类型
类的声明:可以仅声明类而暂时不定义它,这种声明有时被称为前向声明。但是类只有定义了,编译器才知道存储数据成员需要多大存储空间,这个知识点的意义是:声明过后,类内允许包含指向他自身类型的引用或指针。

14.友元再探p250:
类可以把其他类作为友元:如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的成员。友元关系不存在传递性。
令其他类的成员函数作为友元:必须指明该成员函数属于哪个类。
重载函数和友元:如果一个类想把一组同名成员函数声明成友元,必须每一个单独声明。
再次强调,声明友元只是影响访问权限,并不是声明。
15.类的作用域。
名字查找与类的作用域:类的定义分两步处理:
首先,编译成员的声明,
直到类全部可见后才编译函数体。
但是,在所有声明中使用的名字,必须在使用前确保可见,如果在类内声明中出现在名字在类内该声明之前没有被声明过,则会到类的作用域之外去找,而不是在之后的声明中去找。
类型名要特殊处理:
一般来说,内层作用域可以重新定义外层作用域中的名字,尽管内层已经使用过,但是在类中,如果成员使用了外层作用域中某个名字,则在内中不能再重新定义该名字。因此,类型名的定义通常出现在类的开始处。
16.构造函数再探
构造函数的初始值有时候必不可少,如果成员是const或者引用,必须初始化,当成员属于类类型且该类没有定义默认构造函数时,也必须初始化。但是上述这些情况不能在构造函数内通过拷贝的形式赋值,所以要用初始值列表为这些成员赋值。
初始值列表前后位置关系不会影响实际的初始化顺序。如果用一个成员初始化另一个成员的情况就需要注意:让初始值列表的顺序跟成员声明的顺序一致。
17.委托构造函数:
Cpp11扩展了构造函数初始值的功能,可以定义委托构造函数,
18.使用默认构造函数:
sales_date obj();//我们想要声明一个默认初始化的对象,但是,这里obj会被认为是函数,正确的方法应该是去掉括号,这样就认为是使用默认构造函数初始化对象。
19.隐式的类类型转换:如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时把这种构造函数称为转换构造函数。(其实定义了一个从参数类型向类类型隐式转换的规则):
string null_book = “9-999-999”
item.combine(null_book);
这里我们用一个string自动创建了一个临时的sales_data对象。新生成的这个对象被传递给combine。但是编译器只允许一步类类型转换,
item.combine(“9-999-9999”);//错误
可以抑制上述转换:将构造函数声明为explicit,explicit关键字只能在类内声明出使用,类外定义处不能加。只能用于一个参数的构造函数,多个参数的构造函数也没必要。用explicit关键字声明的构造函数只以直接初始化的形式使用,不能拷贝初始化。
虽然不能对explicit关键字修饰的构造函数进行隐式转换,但是可以为转换显示地使用:
item.combine(sales_data(null_book));//显示转换后调用

20.聚合类:使得用户可以直接访问其成员。
所有成员都是public,
没有定义任何构造函数
没有类内初始值,
没有基类,也没有virtual函数
struct data{
int ival;
string s;
};
初始化数据成员:
data vall = {0, “anna”};//从后往前给成员赋值

21.字面值常量类:
某些类也是字面值类型,字面值类型的类可能含有constexpr的函数成员,数据成员都是字面值类型的聚合类是字面值常量类,不是聚合类如果满足以下条件,也是字面值常量类:
1.数据成员都是字面值类型
2.类必须至少含有一个constexpr构造函数;
3.如果一个数据成员含有类内初始值,则初始值必须是一个常量表达式,或者成员属于某种类类型,初始值必须使用成员自己的constexpr构造函数
4.类必须使用析构函数的默认定义,该成员负责销毁类的对象。
22.constexpr构造函数:尽管构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr函数。因为构造函数没有返回语句,constexpr又要求可执行语句只有返回语句,因此constexpr构造函数一般是空的。
23.p268类的静态成员:静态成员与类本身相关联,而不是跟类的各个对象想关联。static关键字。还有静态成员函数,不包含this指针,不能声明成const的。当在类的外部定义静态成员时,static只出现在类内部的声明语句。静态成员不属于对象,因此不由构造函数初始化,一般来说,在类的外部定义和初始化没有静态成员。
如果静态成员在类的内部初始化,则提供const整数类型的类内初始值,静态成员也必须是字面值常量类型的constexpr。

第二部分 C++标准库

第八章 IO库
Cpp语言不直接处理输入输出,而是通过定义在标准库中的类型来处理IO。
1.IO类
相关的三个独立头文件:
iostream定义了用于读写流的基本类型
fstream定义了读写命名文件的类型
sstream定义了读写内存string对象的类型。
为了支持宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型的数据。wcin,wcout,wcerr,wifstream…
2.IO对象无拷贝或赋值。
3.条件状态p279
4.查询流的状态:IO库定义了一个与机器无关的iostate类型,作为一个位集合来使用:
badbit表示系统级错误
failbit可恢复错误,通常还可以修正
如果到达文件结束为止,failbit和eofbit都会被置位,
goodbit的值为0,表示未发生错误。如果其他三个任一个被置位,则检测流状态的条件会失败。good和fail是确定流的总体状态的正确方法,而eof和bad操作只能表示特定的错误。
5.管理条件状态:
流对象的rdstate成员返回一个iostate值,clear()不接受参数版本复位所有错误标志位,clear()接受参数版本置位单一的条件状态位,
6.管理输出缓冲:每个输出流都管理一个缓冲区,用来保存程序读写的数据。
导致缓冲刷新的原因有很多:程序正常结束,缓冲区满,使用操纵符endl,使用操纵符unitbuf,默认情况下,cerr是设置unitbuf的。cin和cerr都关联到cout,因此cin和cerr会导致cout的缓冲区被刷新。

刷新输出缓冲区:endl操纵符换行并刷新缓冲区,IO库中还有两个类似的操纵符。flush刷新缓冲区但不输出任何额外的字符;ends向缓冲区插入一个空字符,然后刷新缓冲区。
unitbuf操纵符:它告诉流在接下来的每次写操作之后都进行一次flush操作。nounitbuf操作符重置流,
cout< cout< 注意:程序崩溃,输出缓冲区不会被刷新。

关联输入和输出流:当一个输入流跟一个输出流关联,任何试图从输入流读取数据的操作都会先刷新关联的输出流。
tie不带参数版本,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回就是指向这个流的指针,如果未关联流,则返回空指针。tie有参数版本接收一个指向ostream的指针,将自己关联到此ostream。x.tie(&o);//j将x关联到o。
每个流最多关联到一个流,但多个流可以同时关联到一个ostream。解开关联的流,给tie传递一个空指针。

7.文件输入输出
ifstream类型从一个文件读取数据,ofstream向一个给定文件写入数据,fstream可以读写给定文件。
fstream继承自iostream,fstream特有的操作p283:
在新Cpp标准中,文件名既可以是string对象,也可以是C风格字符数组,旧版本标准库只允许C风格字符数组。
因为继承关系,可用fstream代替iostream.
当一个fstream对象离开其作用域时,fstream对象被销毁,与之关联的文件会自动关闭。
8.文件模式:
每个流都有一个关联的文件模式,用来指出如何使用文件p286。
in 以读方式打开
out以写方式打开
app每次写操作前均定位到文件末尾
ate打开文件后立即定位到1文件末尾
trunc截断文件
binary以二进制方式进行IO
与ifstream关联的文件默认以in模式打开,与ofstream关联的文件默认以out打开,与fstream关联的文件默认以in和out模式打开。
以默认out模式打开文件会丢弃已有数据,可指定app模式:
ofstream app(“file11”, ofstream::app);

9.string流:
sstream头文件定义了三个类型来支持内存IO,这些类型可以向string写入数据,从string读取数据,就像string是个IO流一样。
istringstream 从string读取数据,ostringstream向string写入数据,stringstream既可以读也可以写。也是继承自iostream

第九章 顺序容器
1.顺序容器概述:
顺序容器类型:
vector 可变大小数组
deque 双端队列
list 双向链表
forward_list 单项链表
array固定大小数组
string
forward_list和array是新Cpp标准增加的类型,与内置数组相比,array是一种更安全更容易使用的数组类型。array对象的大小是固定的,不允许添加删除,不允许改变大小;forward_list没有size操作。
2.容器库概览
每个容器都定义在一个头文件中,头文件与容器同名。容器均定义为模板类,还需要额外提供元素类型的信息。
vector;//较旧的编译器可能还需要在两个尖括号之间加一个空格。
有些类没有默认构造函数,当我们定义一个保存该类的容器时,需要提供元素初始化器:vector v1(10, inti);
容器操作p295:
3.迭代器:
迭代器范围:
4.类型别名:
iterator容器类型的迭代器类型
const_iterator不能修改元素的迭代器类型
size_type无符号整数类型,足够保存此种容器类型最大可能容器的大小
difference_type带符号整数类型,两个迭代器之间的距离
value_type元素类型
reference元素的左值类型,同value_type&含义相同
const_reference//const value_type&
5.begin和end成员
begin和end有多个版本,带r为反向迭代器,带c为const迭代器。
对const对象调用这些函数时才得到const_iterator.
以c开头的版本是新标准引入,用以支持auto.
6.容器定义和初始化,创建一个容器为另一个容器的拷贝,需要两个容器类型及元素类型相同,但是,用传递迭代器参数拷贝一个范围时,不需要容器类型相同,而且元素类型不相同只要能转换也可以。
顺序容器接收容器大小和元素值来构造:
vector ivec(10,-1);//
标准库array除了制定类型还要指定大小,大小是array类型的一部分。不能对数组类型进行拷贝对象赋值操作,但是array可以:
int digs[10]={0,1,2,3,4,5,6,7,8,9};
int cpy[10]=digs;//错误
7.赋值和swap
array不支持assign(),assign()用于元素拷贝(仅顺序容器)。
list.assign(vector.begin(),vector.end());//
assign()第二个版本:
string.assign(10,“hello”);//接收一个整形值和元素值。
swap并没有交换元素,而是交换两个容器中的数据结构,因此保证在常数时间内完成。
与其他容器不同,对一个string调用swap会导致迭代器引用和指针失效。
与其他容器不同,array调用swap是交换里面的元素。
8.顺序容器操作:
向一个vector,string或deque插入元素会导致所有指向容器的迭代器,指针和引用失效。
insert函数可以接受更多的参数,其中一个版本可接受元素数目和值。insert()返回第一个参数(旧版本标准库中可能会返回void)。
emplace:
emplace_front,emplace,emplace_back,对应push_front,insert,push_back。emplace函数在容器中直接构造元素,因此传递给emplace的参数必须与元素类型的构造函数相匹配,会在容器管理的内存空间中直接创建对象,但是push_back则是创建一个局部临时变量并压入容器中。
9.访问元素:每个顺序容器都有一个front()成员函数,除forward_list外都有back()成员函数,返回的是引用,访问元素的成员函数,返回的都是引用,
下标操作必须保证在范围内,这是程序员的责任。
10.删除元素:
forward_list不支持pop_back,string和vector不支持pop_front,这些操作返回的是void,因此如果需要,要提前保存。erase()返回的是删除元素之后的迭代器,erase(elem1,elem2),删除从elem1开始的后面的元素,也就是结果elem1=elem2,

11.特殊的forward_list操作:
forward_list没有定义insert,emplace,erase,而是定义insert_after(),emplace_after(),erase_after(),还定义了一个before_begin的首前迭代器。
12.改变容器的大小:resize()可增大或缩小容器,但是array不支持resize(),当前大小大于或小于要求大小,array后部的元素会被删除或添加,
在删除或添加的循环中,不要保存end返回的迭代器,应该在每次应该在循环程序中反复调用。
13.vector对象是如何增长大的:
管理容量的成员函数:vector和string的capacity()函数,告诉我们未扩充的当前容器可以容纳多少个元素,reserve(n)分配至少能容纳n个元素的内存空间,shrink_to_fit()将容量减少到size()相同大小。resize()只是改变元素数量而不改变容器容量,
14.额外的string操作p320:
当我们从一个const char*创建string时,指针指向的数组必须以空字符结尾,拷贝遇到空字符时结束,如果还传递给构造函数一个计数值,则不必以空字符结尾。
substr():
可以将以空字符结尾的c风格字符数组insert或assign给一个string,
append()在string末尾插入,replace()是erase()和insert()的一种简写形式,
15.string搜索操作p325:
string::size_type是unsigned类型,
find(),find_first_of(),find_first_not_of(),查找失败返回npos,是size_type类型,初始化为-1。
16.compare()函数
17.数值转换p328:从string中提取数值。
18.容器适配器:容器,迭代器,函数都有适配器,
标准库定义了三个容器适配器:stack,queue,priority_queue,一个容器适配器接收一个已有的容器类型,使其行为看起来像一种不同的类型,适配器是一种机制

第十章 泛型算法

1.概述:大多数算法都定义在头文件algorithm中,标准库还在头文件numeric中定义了一组数值泛型算法,
迭代器令算法不依赖容器,算法永远不会执行容器的操作。
2.初始泛型算法:
一些算法,例如equal,接收三个迭代器,前两个表示第一个序列的范围,第三个表示第二个序列中的首元素,这时,是假定第二个序列至少跟第一个序列一样长,确保算法不会访问第二个序列不存在的元素是程序员的责任。
向目的位置迭代器写入数据的算法是假定容器的位置够大,要能容纳写入的元素,泛型算法不直接操作容器,因此也不能为容器扩容。
插入迭代器:insert_iterator,
3.定制操作p344:
谓词:接受谓词参数的算法对输入序列中的元素调用谓词,谓词参数可以是一个或两个,称为一元谓词,二元谓词。
lambda表达式:结构与函数类型,但是lambda可以定义在函数内部:
[捕获列表](参数)->返回类型 {函数体};
auto f=[]{retrn 42;};
lambda必须使用尾置返回来指定返回类型,如果没有指定类型,可从表达式推断。如果无法推断,返回void类型。lambda不能有默认参数
auto wc = find_if(words.begin(),words.end(), [sz](const string &a){return a.size()>=sz;};//找出words中第一个大于等于sz的元素并返回他的迭代器。捕获了sz,sz可以在函数体中使用。
for_each()函数
当定义一个lambda时,编译器生成一个与lambda对应的新的类类型,当向一个函数传递lambda时,同时定义一个新类型和该类型的对象,从lambda生成一个类都包含一个对应此lambda所捕获的变量的数据成员。
值捕获:被捕获的变量的值是在lambda创建时拷贝的。
引用捕获:必须确保被引用的对象在lambda执行时是存在的,因为值捕获是拷贝,所以想要捕获不能拷贝的对象时(如ostream),需要引用捕获。lambda捕获的都是局部变量,如果函数返回一个lambda,因为在函数结束时局部变量已经失效,因此不能捕获一个引用。
如果我们希望能改变一个被捕获变量的值,需要加上mutable关键字。

参数绑定:
标准库函数bind(),定义在头文件functional中,
auto newCallable = bind(callable,arg_list);
newCallable是一个可调用对象,arg_list是一个逗号隔开的参数列表,参数列表为callable()函数的参数,调用newCallable()时,会调用callable(),arg_list中的参数可能含有_1,_2,_3…这样的参数,是占位符,是newCallabe的参数,他们会传给callable作为在占位符处的参数。_n定义在命名空间placeholders中,placeholders又定义在namespace中,两个都要写出:三std::placeholders::_1;
using namespace std::placeholders;
绑定引用参数:那些不是占位符的参数是被拷贝给bind返回的可调用对象的,有时希望是引用方式传递或者需要绑定的参数不能拷贝,用标准库ref()函数,ref返回一个对象,包含给定的引用,此对象是可以拷贝的,还有cref(),定义在头文件functional中。

4.再探迭代器:
标准库在头文件iterator中定义了几种额外的迭代器:
插入迭代器:被绑定到一个容器上,用来向容器中插入元素
流迭代器:绑定到输入输出流上,可用来遍历IO流
反向迭代器:forward_list没有
移动迭代器:

算法使用迭代器操作来处理数据,可以用某些算法来操作流迭代器,流迭代器不支持递减运算,不能在流中反向移动,

5.泛型算法结构:
迭代器按其功能分为5类,算法的功能要实现对迭代器的能力有要求,向算法传递一个能力更差的迭代器会产生错误,很多编译器不会给出警告或提示。

第十一章 关联容器

关联容器中的元素按关键字保存和访问,两个主要的关联容器类型是map和set。
标准库提供8个关联容器,这8个容器间的不同体现在三个维度上,
1.或者是一个map.或者是一个set
2.是否要求不重复的关键字,允许重复有关键字multi
3.是否按顺序保存元素,不按顺序用关键字unordered

1.使用关联容器
map是关键字-值对集合,map类型通常被称为关联数组,set是关键字的简单组合,关联容器的迭代器都是双向的。
2.关联容器概述:
有序关联容器,关键字类型必须定义元素比较的方法,默认情况下,标准库使用<运算符来比较两个关键字,可以向一个算法提供我们自己定义的关键字比较方法,所提供的操作必须在关键字比较上定义一个严格弱序。在实际编程中,如果一个类型定义了“行为正常”的<运算符,那么他可以作为关键字类型。
pair类型:标准库类型,定义在头文件utility中,一个pair保存两个数据成员,pair的数据成员是public的,两个成员分别命名为first和second。可用make_pair()来生成pair对象。
3.关联容器操作:
key_type:关键字类型
mapped_type:关键字关联的类型
value_type:对于set与key_type一样,对于map,是关键字与值的一对pair,访问方式:map::key_type。
关联容器迭代器:解引用一个关联容器的迭代器,得到的是一个value_type的值的引用,set的迭代器是const的,map与set的关键字都不能改变。
我们通常不对关联容器使用泛型算法,原因主要是关联容器的关键字是const的,关联容器可以使用只读算法,但是很多这类算法都要搜索序列,但是关联容器又不能通过他们的关键字进行快速查找。综上,我们通常不对关联容器使用泛型算法。关联容器有自定义的find,而不用泛型函数find.
关联容器的insert成员向容器中添加一个元素或一个元素范围。
insert()或emplace()返回的值依赖于容器类型和参数,对于不包含重复关键字的容器,添加单一元素的insert()和emplace()返回一个pair,pair的first是一个迭代器,指向具有给定关键字的元素,second是一个bool值,指出是插入成功还是已经在容器中。
multi容器则返回新元素的迭代器,因为总是插入成功的。
删除元素:erase(),
map的下标操作:对一个map使用下标操作,跟数组或vector的下标操作很不一样,使用一个不在容器中的关键字作为下标,会添加一个此关键字的元素到容器中。
对一个map进行下标操作,会获得一个mapped_type对象,解引用迭代器得到的却是value_type对象。
访问元素:find(),count(),lower_bound(),upper_bound(),equal_range()。对map使用find()代替下标,可以避免找不到时向map中添加元素。

4.p394无序容器:
新标准定义了四个无序关联容器,也有find,insert函数,unordered_map,unordered_set,
无序容器提供一组管理桶的函数,
无序容器不是使用比较运算符来组织元素,而是使用一个hash函数和关键字类型的==运算符。无序容器使用一个哈希函数将元素映射到桶,比有序容器更快。标准库为内置类型提供了hash模板,还为一些标准库类型如string提供了hash模板,因此可以以这些类型作为无序容器的关键字,但是自定义类型因为没有hash模板,而不能直接作为无序容器的关键字。可以通过定义将自定义类型作为参数的函数,在函数里使用hash模板,函数里取自定义类型的内置类型或string数据成员来使用hash模板。
无论是有序容器还是无序容器。相同关键字都是相邻存储的。

第十二章 动态内存
C++支持动态分配对象,对应前面的自动对象和static对象,动态分配对象的生存期需要程序员显示释放,对象才会被销毁。为了更安全的使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象,当一个动态对象应该被释放的时候,指向他的智能指针可以确保正确释放它。
静态内存用来保存局部static对象,类static数据成员,以及定义在任何函数之外的变量,
栈内存用来保存定义在函数内的非static对象,分配在静态或栈内存中的对象由编译器自动创建和销毁,static对象在使用前已分配
除了静态和栈内存,每个程序还拥有一个内存池,称作自由空间或堆,程序用堆来存储动态分配的对象。
1.动态内存与智能指针:
通过一对运算符:new,delete,new在动态内存中为对象分配空间并返回一个指向该对象的指针,可以对对象进行初始化,delete接受一个动态对象的指针,销毁该对象,并释放内存。
为了缓解内存泄漏等问题,新的标准库提供两种智能指针,负责自动释放所指向的对象,
shared_ptr允许多个指针指向同一个对象,unique_ptr则独占所指向的对象,还定义看一种伴随类weak_ptr,是一种弱引用,指向shared_ptr所管理的对象。以上三种都定义在memory头文件中。

智能指针也是模板类,默认初始化的智能指针中保存着一个空指针,
p->mem等价于(*p).mem,p.get()返回p中保存的指针swap(p,q),p.swap(q),

shared_ptr独有的操作,make_shared(args),初始化了对象,对于unique_ptr只能创建空指针unique_ptr p,当然shared_ptr也可以采用这种格式sharedptr(q),p是shared_ptr q的拷贝,此操作会递增q中的计数器,
p=q,此操作会递减p的引用计数,递增q的引用计数,若p的引用计数变为0,则将其管理的原内存释放。
p.unique():判断引用次数是否为1,
p.use_count():返回与p共享对象的智能指针数量,可能很慢,主要用于调试。

make_shared()函数,最安全的分配和使用动态内存的方法,
我们可以认为每一个shared_ptr都有一个关联的计数器,通常称其为引用计数,当任何时候存在拷贝时我们增加计数,当给shared_ptr赋予新值或者局部shared_ptr对象离开其作用域,则减少计数。
auto r=make_shared(42);
r=q;
令r指向了另一个地址,r原来指向的对象的引用计数减一,q指向的对象又被r指向,因此q指向的引用计数加一。

shared_ptr自动销毁所管理的对象:
当指向某对象的最后一个shared_ptr被销毁时,shared_ptr自动销毁此对象,通过成员函数析构函数实现,shared_ptr的析构函数会递减它所指向对象的引用计数,当减为0的时候,则销毁对象,释放内存。
如果return一个shared_ptr指向的对象p(返回对象),return语句会向函数调用者返回一个p的拷贝,因此,即使局部函数结束,当前p应该被销毁,但是因为return的拷贝使引用计数加一,因此该对象还不会被销毁。
shared在无用之后仍然保存会导致内存浪费,例如将shared_ptr存放在一个容器中,而后不再需要全部元素了,就要用erase()删除不再需要的元素。

程序使用动态内存出于以下三个原因:
1.程序不知道自己需要使用多少对象。不如容器类
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据。

vector被销毁时,其元素也被销毁了,但是我们希望某些类分配的资源与原对象有相独立的生存期,例如顶一个blob类,希望对象的不同拷贝之间引用同一组底层元素。当一个对象销毁时,底层数据不应该被销毁,

例如让多个blob对象共享一个vector,就不能在一个对象类保存vector,应该将vector保存在动态内存中,就需要为vector设置一个shread_ptr来管理动态分配的vector,此ptr会记录vector的引用计数。

直接管理内存:
new分配 delete释放,
自己直接管理内存的类不能依赖类对象拷贝、赋值和销毁操作的任何默认定义,
在自由空间分配的内存是无名的,只能返回指向该对象的指针,动态分配的对象是默认初始化的,对于内置类型,值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值是未定义的。通常最好应该对动态分配的对象进行值初始化,哪怕就初始化为默认值也好。

动态分配的const对象:
用new分配const对象是合法的,但是跟其他const对象一样,必须进行初始化。new返回的指针是一个指向const的指针,

无法分配所需空间,会抛出异常,bad_alloc,
但可以通过nothrow来不抛出异常,分配失败则返回空指针。称这种形式的new为定位new,nothrow和bad_alloc都定义在头文件new中。
delete释放一块并非new分配的内存,或delete一个对象多次的行为是未定义的,但是通常编译器不会知道该对象是动态分配的还是静态分配的。也不能分辨是否已经释放了。
void use_factory(arg)
{
Foo *p=factory(arg);
return p;//如果这里不释放,一旦use_factory()返回,程序就再也不能释放这块内存了。
}
动态内存管理常犯的三个错误:
1.忘记delete,导致内存泄漏
2.使用已经释放的对象,可以检测出这种错误。
3.同一个内存空间释放两次,会破坏自由空间。

坚持使用智能指针!
delete一个指针后,指向的对象已经被销毁了,但是指针仍然指向那块内存区域,变成空悬指针,避免空悬指针的方法:
在指针即将离开其作用域前释放关联的内存,这样就没机会再使用这个空悬指针了,如果要保留指针,就在delete后将指针置nullptr,这样,指针就不指向任何地址了。
但是!这只是保证当前指针不指向一块无效的内存地址了,因为可以存在多个指针指向同一块内存区域,所以对象被销毁后,其他指针仍然会指向一块无效内存地址。而要找到其他指针是非常困难的。

shared_ptr和new结合使用:
可以用new返回的指针来初始化智能指针,
shared_ptrp2(new int(1024));
接收指针参数的智能指针的构造函数是explicit的(只能直接初始化不能拷贝的形式初始化),因此不能将一个内置指针隐式转换为一个智能指针,shared_ptr 同样,返回一个shared_ptr类型也不能试图返回一个隐式转换的内置指针。但是可以显示绑定到一个shared_ptr上面去。

不要混合使用智能指针和普通指针,
int *x(new int(1024));
prosess(shared_ptr(x));//prosess()函数形参是shared_ptr类型
int j=*x;//错误
这种形式的调用,实参是临时创建的shared_ptr,因此出了函数调用运算符实参的括号后,shared_ptr就被销毁了,再次访问x就会出错,此时x是一个空悬指针。
当我们使用shared_ptr绑定到一个内置指针后,该内置指针的内存管理就交给了shared_ptr,就不应该再使用内置指针来访问其指向的内存了。

get用于将指针的访问权限传递给代码,只有在确定代码不会delete指针的时候才能用get(),更不能用get()的返回去初始化另一个shared_ptr,因为多个shared_ptr绑定到同一个对象,会导致重复delete,或是访问已经delete的对象。

其他shared_ptr操作:
reset()函数将将一个新的指针赋值给shared_ptr,会更新引用次数。

函数内部异常发生时,后导致显示delete的对象也不能被delete,使用智能指针就不会导致这种情况,异常发生,智能指针的引用计数会减一。
智能指针陷阱p417:
1.不适用相同的内置指针值初始化(或reset)多个智能指针
2.不delete get()返货的指针
3.不适用get()初始化或reset另一个智能指针
4.如果使用get()返回的指针,留意最后一个智能指针被销毁后,指针就无效了
5.如果智能指针管理的对象不是new分配的内存,要传递给它一个删除器。

unique_ptr:某个时刻只有一个unique_ptr拥有它指向的对象
不能拷贝或赋值unique_ptr,但是可通过release和reset将指针所有权转移。
不能拷贝或赋值一个unique_ptr,但是在它将要被销毁前可以拷贝,例子就是返回类型是unique_ptr时。此时编译器执行的是一种特殊的拷贝,p473介绍。
标准库老版本有一个auto_ptr,有unique_ptr的部分功能,应避免使用它。

weak_ptr

动态数组:
vector在连续内存中保存他们的元素,容器需要重新分配内存时,必须一次性为很多元素分配内存。C++语言标准库提供两种一次分配一个对象数组的方法。大多数程序应该使用标准库容器而不是动态分配的数组。

new和数组:
int *pia=new int[get_size()];//方括号中必须是整形而不必是常量。也可用类型别名:
typedef int arrT[42];
int *p=new arrT;
动态数组并不是数组类型,因为new分配一格数组并没有得到一个数组对象,只是得到一个指向数组类型的指针。
释放动态数组:
delete [] pa;数组中的元素按逆序销毁。使用类型别名定义了动态数组,delete时也需要加方括号。
也可以用unique_ptr管理动态数组:
unique_ptrup(new int[10]);
up.release();
shared_ptr不支持直接管理动态数组,如果希望使用shared_ptr管理动态数组,必须提供自己管理的删除器。

allocator类:new将内存分配和对象构造组合在一起,delete将对象析构和内存释放组合在一起。我们希望在一块内存上按需构造对象,更重要地局限是没有默认构造函数的类就不能动态分配数组了。
标准库allocator类定义在头文件memory中,将内存分配和对象构造分开来。p428.
deallocate(p,n);释放从p中地址开始的内存,调用前,用户必须对每个在这块内存中创建的对象调用destroy。
p428

第三部分 类设计者的工具

第十三章 拷贝控制
拷贝控制操作:
拷贝构造函数
拷贝赋值运算符
移动构造函数
移动赋值运算符
析构函数

程序员必须定义对象拷贝、移动、赋值和销毁时做什么,如果我们不显式定义,编译器也会为我们定义,但编译器定义的版本的行为可能不是我们想要的。

1.拷贝、赋值和销毁

拷贝构造函数:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此函数为拷贝构造函数。
如果我们没有为一个类定义拷贝构造函数,编译器会为我们合成一个拷贝构造函数。

直接初始化:实质是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
拷贝初始化:要求编译器将右侧对象拷贝到正在创建的对象中,如果需要的话还需要进行类型转换。拷贝初始化通常使用拷贝构造函数来完成。有时会使用移动构造函数来完成而非拷贝构造函数。
拷贝初始化不仅是在等号=定义变量时,还有情况是:
1.将一个对象作为实参传递给一个非引用类型的形参
2.从一个返回类型非引用类型的函数返回一个对象,
3. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
当我们初始化标准库容器或是调用insert(),push()成员时,容器会对其成员进行拷贝初始化,用emplace成员创建的元素则是直接初始化。
参数和返回值:
函数调用时,非引用参数进行的是拷贝初始化。
函数返回值是非引用时,也是拷贝初始化。
这里说明了为什么拷贝构造函数的形参必须是引用类型:
函数调用时,非引用参数进行的是拷贝初始化,拷贝初始化是通过调用拷贝构造函数来完成的,如果拷贝构造函数不是引用类型,调用拷贝函数又需要拷贝函数来完成,将无线循环不会成功。
expicit构造函数只能直接初始化,因此当传递一个实参或从函数返回一个值时,不能隐式使用一个explicit构造函数。
编译器可以跳过拷贝构造函数,将其当做直接初始化。

拷贝赋值运算符:
类可以控制对象如何初始化,也可以控制对象如何赋值,编译器会自己合成一个拷贝赋值运算符。
重载运算符:本质上是函数,赋值运算符是一个名为operate=的函数,也有一个返回类型和一个参数列表
赋值运算符通常返回一个指向其左侧运算对象的引用。

析构函数:
注意构造函数和析构函数都是处理非static成员,因为static成员不属于任何对象。析构函数没有返回值也不接受任何参数。一个给定类只有一个析构函数。
构造函数中,成员的初始化是在函数体执行之前完成的,且按照他们在类中出现的顺序进行初始化,析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序逆序销毁。销毁类类型的成员需要调用成员自己的析构函数。智能指针是类类型,具有析构函数。
析构函数体自身并不直接销毁成员,成员在析构函数体之后隐含的析构阶段中被销毁,析构函数体作为成员销毁步骤之外的另一部分而进行的。

三/五法则:
需要析构函数的类也需要一个拷贝构造函数和一个拷贝赋值运算符。在构造函数中1分配动态内存,合成的析构函数是不会delete一个指针数据成员的,因此需要定义一个析构函数来释放动态分配的内存。
如果用合成的拷贝构造函数和拷贝赋值运算符,函数会简单地拷贝指针成员,以为这多个对象会指向相同的内存,在析构函数中delete,多个成员调用多次析构函数就会导致同一片内存被delete多次。因此,如果自动了析构函数,可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。
如果一个类需要拷贝构造函数,那么它也一定需要拷贝赋值运算符:可以这样理解,如果需要拷贝函数,则意味着合成的统一的构造函数无法满足要求,既不同对象间应该有所不同(如果用合成的构造函数,则不同对象应该是相同的),而要使那些成员不同,就需要拷贝构造函数中使用拷贝赋值运算符。而使用赋值运算符的场景也必然需要拷贝构造函数:这样理解,没什么理解的。都没有构造函数,赋值运算符用在哪里呢。但是它们两者都不依赖于析构函数。

使用=default。
可以将拷贝控制成员定义为=default来显示地要求编译器生成合成的版本。

阻止拷贝:比如说iostream类阻止拷贝,避免多个对象写入或读取相同的缓冲。新标准下可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。虽然声明了它们但是不能以任何方式使用它们。在函数参数列表后面加=delete来指出是删除函数。=delete必须出现在函数第一次声明的时候。
当然,析构函数不能被定义为删除函数。
构造函数如果被定义为删除函数,不允许创建该类型的对象,如果类型的类类型的数据成员的构造函数被定义为删除函数,同样不能定义对象。析构函数也是一样,如果对象的成员不能被销毁,该对象也不能被销毁。
对于删除了析构函数的类型,虽然不能定义这种类型的变量或成员,但可以动态分配这些类型的对象,但无法释放。

编译器可能将一些合成的成员定义为删除的函数p450:
本质上就是一个类有数据成员不能默认构造、拷贝、复制和销毁,则对应的成员函数将被定义为删除的。还应注意,对于具有引用成员或无法默认构造的const的成员的类,编译器不能为其合成默认构造函数。如果类有const成员,不能使用合成的拷贝赋值运算符,此运算符试图赋值所有运算符,但是const对象是不能被赋值的。对于有引用成员的类,合成拷贝赋值运算符被定义为删除的。
新标准之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝的。但是友元和成员函数仍然可以拷贝对象,可以将拷贝控制成员声明为private,但并不定义,来阻止友元拷贝。试图拷贝对象的用户代码编译标记为错误,成员函数或友元试图拷贝会导致连接时错误。
希望阻止拷贝的类应该使用新标准的=delete,而不是声明为private

2.拷贝控制和资源管理
类的行为像一个值,拷贝一个像值的对象时,副本和原对象是完全独立的:如·string 和标准容器
行为像指针的类则是共享状态,拷贝这种类的对象时,副本和原对象使用相同的底层数据:如shared_ptr

类值拷贝赋值运算符,基本上就是这个赋值过程看起来合理就行了。
定义行为像指针的类:令一个类展现类似指针的行为最好的方法是使用shared_ptr来管理类中的资源,如果希望直接管理资源,使用引用计数就很有用了。

引用计数p455:
难题是在哪里存放引用计数:
肯定不能保存在每个对象中,当创建p3时可以增加p1的计数,并将计数拷贝给p3,但是如果更新p2的计数是个问题。
解决这个问题的一个方法是将计数器保存在动态内存中,当创建一个对象时分配一个新的计数器,然后通过拷贝指向计数器的指针,使大家拥有相同的计数器。

3.交换操作
除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap的函数。
我们用一个temp作为中间变量去交换两个对象的值的时候,经历了几轮拷贝,拷贝导致了内存的浪费,这些内存分配是不必要的,我们更希望swap交换指针。标准库的swap就是使用浪费内存的拷贝方式。
在类上定义一个swap函数重载swap()的默认行为,应该将swap定义为友元。还可以声明为inline。
与拷贝控制成员不同,定义swap并不是必要的,但是对于分配了资源的类,定义swap是一种重要的优化手段。就算声明了using std::swap,如果存在类型特定的swap,匹配程度优于std::swap,还是会调用自定义的swap,原因会在后面介绍。

4.拷贝控制示例:
5.动态内存管理类:
新标准引入两种机制,可以避免string的拷贝,
都定义了所谓的移动构造函数,实现机制尚未公开,移动构造函数通常1是将资源移动到正在创建的对象,而不是拷贝。可以假定是一种指针的拷贝。第二个机制是move标准库函数。

6.对象移动
新标准的一个最主要特性就是可以移动而非拷贝对象的能力,
为了支持移动操作,新标准一种新的引用类型右值引用,通过&&而不是&来获得右值引用,右值引用有一个重要的性质:只能绑定到一个将要销毁的对象上,
一般,左值表达的是一个对象的身份,右值表达一个对象的值。左值引用有持久的状态,右值引用要么是字面值常量,要么是在表达式求值过程中创建的一个临时对象。右值引用绑定的对象将要被销毁,没有其他用户。
标准库move函数:
不能将右值引用绑定到左值上,但可以显示地将一个左值转换为对用的右值引用类型。通过move来获得绑定到左值上的右值引用定义在头文件utility中。
int &&rr3=std::move(rr1);//rr1是左值变量。
调用就意味着rr1我们或者是给它赋新值,或者是即将要销毁它。
移动构造函数和移动赋值运算符:
这两个对象对应与拷贝操作,但是它们不是拷贝,而是窃取。
移动构造函数的参数是右值引用,一旦资源完成移动,原对象不再指向被移动的对象,这些资源归新创建的对象所有,所以说是一种窃取,移动。
移动操作窃取,不分配任何资源,移动操作通常不会抛出任何异常,但是我们需要告诉标准库,通过noexcept来承诺不抛出异常,在函数的参数列表后指定,在构造函数中出现在参数列表之后初始化列表的冒号之前。必须在声明和定义都说明。
为什么需要noexcept:
考虑标准库vector,push_back时如果需要重新分配空间,但是在移动过程中发生了异常,但是又还没有移动完,旧的vector已经被窃取1不能再使用,新的vector抛出异常还没有完成,就导致vector被破坏了。但但实际情况是,当push_back有异常发生时,vector自身可以保持不变。另一方面如果vector使用拷贝构造函数,如果异常,原本的vector是不会被破坏的。如果我们希望重新分配内存是是移动而不是拷贝,就需要告诉标准库移动构造函数不会发生异常,这就是noexcept的作用。当我们不能作出不抛出异常的承诺时,就不能这样声明,没有noexcept声明,编译器会去选择是用拷贝还是移动。

移动赋值运算符:

右值和左值引用成员函数:
auto n=(s1+s2).find(‘a’);//在一个右值上调用find函数
甚至是:
s1+s2=“wow”;//对一个右值赋值
上面两个例子是新标准为了兼容旧标准,而没有阻止这种做法,我们不应该这样做。应该强制左侧运算对象(既this指向的对象)是一个左值。
可以在运算符定义时函数参数列表后加一个& 或&&来表示this可以指向一个左值或右值,必须同时出现在函数的声明和定义中。格式跟const函数限定产不多。
引用限定符可以跟const同用,引用限定符必须再const限定符之后。
const可以定义两个参数一样的重载函数,其中一个含有const,编译器会根据实参判断调用哪一个,
但是引用限定符的函数,如果其中一个有引用另一个也必须有,既左值引用和右值引用同时有。

第十四章 重载运算与类型转换
运算符作用域类类型的运算对象时,可以通过运算符重载重新定义运算符的含义。
1.基本概念:
重载运算符是具有特殊名字的函数:名字由关键字operator和其后要定义的运算符号共同组成。参数是该运算符参与运算的对象,左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。重载运算符不能含有默认实参。
当重载运算符是成员函数时,this绑定到左侧运算对象,这时函数参数比运算对象数量少一个。
不能重载的运算符:::,.,.,?:
一般情况下我们是向使用运算符一样去间接地调用重载运算符函数,但是也可以直接调用重载运算符函数。
一般情况下也不应重载逗号,取地址,逻辑与和逻辑或运算符。
重载输入输出运算符必须是非成员函数,定义为成员函数则左侧运算对象将是类的一个对象,但是输入输出的左侧运算对象应该是iostream类,我们也不可能给标准库的iostream添加成员。io运算符一般也是定义成友元。
赋值运算符:
无论形参是怎么样的,赋值运算符都应该定义为成员函数没因为赋值运算符将返回左侧运算对象的引用。
小标运算符必须是成员函数,如果一个类包含下标运算符,则通常会定义两个版本,const版本和非const版本
前置和后置递增递减运算符的区别是后置版本加一个int形参,编译器会给它加一个0的默认形参,但是只是为了区别前后置而已。
后置版本其实是返回值是当前值,但是this指向的对象加一。
前置版本返回的是+1后的
this。
类型转换运算符:类的一种特殊成员函数,它负责将一个类类型的值转换为其他类型。
operator type() const;
避免有二义性的类型转换。

第十五章 面向对象程序设计
面向对象程序设计基于三个基本概念:数据抽象,继承和动态绑定。
通过数据抽象,将类的接口与实现分离;使用继承,可以定义相似的类型并对相似关系建模;使用动态绑定,可以在一定程度上忽视相似类的区别。
对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。
派生类必须通过类派生列表明确指明它是从哪个基类继承而来
class bulk_quote: public queto{};

C++新标准允许派生类显示地注明它将使用哪个成员函数改写基类的虚函数,通过在该函数形参列表之后增加一个override关键字。
基类的指针或引用可以调用派生类的虚成员函数,是在程序运行时由实参类型决定的,动态绑定又称运行时绑定。
作为继承关系中根节点的类通常会定义一个虚析构函数。
基类希望派生类覆盖的函数设置为虚函数。根据基类指针或引用所绑定的对象类型不同。
任何构造函数之外的非静态函数都可以是虚函数。
派生类可以继承定义在基类中的成员,派生类能访问基类的公有成员,不能私有成员,ptotected成员是派生类有权访问但是禁止其他类访问。
成员函数没有被声明成虚函数,则函数调用的解析编译过程就知道了。
新标准允许派生类显示地注明它使用某个成员函数覆盖了他继承的虚函数,override。
尽管派生类对象中含有从基类继承而来的成员,但是派生类并不直接初始化这些成员,派生类必须使用基类的构造函数来出初始化它的基类部分。放在基类的构造函数后面以实参列表的方式为构造函数提供初始值。
首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
派生类应该遵循基类的接口,并且通过调用基类的构造函数来初始化那些从基类继承而来的成员。
如果基类定义了一个静态成员,则整个继承体系中只存在该成员的唯一定义,静态成员遵循public priate规则。
一条声明语句的目的是令程序知晓某个名字的存在以及该名字表示一个什么实体。
新标准可以防止一个类被继承,final关键字在类名后。
可以将基类的指针或引用绑定到派生类对象上,这里我们就不知道绑定的是基类的对象还是派生类的对象了,运行时多态。
静态类型是变量生成时的类型。
动态类型变量或表达式表示的内存中对象的类型。运行时才知。

执行初始化时实质是调用构造函数,执行赋值时实质上是调用赋值运算符,构造函数和赋值运算符都有一个参数,该参数是类类型的const版本的引用
因此,当我们给基类的构造函数传递一个派生类对象时,因为构造函数是基类的,该构造函数只能处理基类自己的成员,如果我们将一个派生类对象赋值给一个基类对象,则运行的赋值运算符也是基类那一个,该运算符只能处理基类的成员,会切掉派生类自有的成员,因此,派生类向基类的自动转换只对指针和引用类型有效,派生类和基类之间对象的转换是不行的。

三点重要:
1.从派生类向基类的类型转换是不存在的
2.基类向派生类不存在类型转换
3.派生类向基类转换也可能由于访问受限而变得不可行。
虽然自动类型转换只对指针和引用类型有效,但是继承体系中的大多数仍然定义了拷贝控制成员,我们通常也可以将派生类对象拷贝移动或赋值给基类对象,但是只能处理基类中有的成员。

3.虚函数
动态绑定,在运行时才知道到底调用了哪个版本的虚函数,所以,所有虚函数都必须有定义。因为编译器也不确定会用哪个虚函数,所以必须都定义。
被调用的函数是绑定到指针或引用上的对象的动态类型相匹配的那一个。

虽然这个动态绑定的问题很早就已经知道了,但是对于编译器为什么不能识别调用的是基类还是父类还是有一些疑惑,毕竟代码里面也给了指针或引用指向的对象,这个问题是这样:
用基类指针指向派生类的对象时,虽然程序员知道绑定的是哪个对象,但是其实编译器的功能没有那么强大,生成一个基类指针时,编译器只是告诉指针去存储这个对象的地址,意会一下就知道了,要存储一个地址,当然只有运行起来,对象实例化时,才能得到对象的地址,因此也只有运行的时候才能知道指针绑定的是哪个对象。而现实问题要求我们基类的指针可以指向派生类的对象,就需要在运行的时候去判断调用谁的成员函数,这就是虚函数的作用了。
虚函数又是怎么实现功能的呢:
具有虚函数的类都有一个虚函数表,因为虚函数这种虚的特性是会继承给派生类的,我们可以认为具有虚函数的基类及它旗下的所有派生类都会有自己的虚函数表,某个对象被实例化的时候,会得到指向虚函数表的指针。结合上面的分析,运行时基类指针得到指向对象的地址,这个地址中当然就有这个指向虚函数表的虚函数指针了,再去这个指针的地址去找到虚函数表,根据虚函数表中存储的该成员函数的地址,从而最终调用到想要的那个成员函数。

你可能感兴趣的:(读书笔记,c++,开发语言,数据结构)