- unsigned int 可以缩写为unsigned
- 使用int执行整数运算。在实际应用中,short常常显得太小而long一般和int有一样的尺寸。如果你的数值超过了int的表示范围,选用long long
- 执行浮点数运算选用double,这是因为float通常因为精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。
- 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char 可以表示0-255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此把-1赋给8比特大小的unsigned char所得的结果是255。
- 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的。此时,程序可能继续工作,可能崩溃,也可能生成垃圾数据。
- 当一个算数表达式中既有无符号数又有int值时,那个int值就会转换成无符号数。
- 如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:
long double ld = 3.1415926536;
int a{ld} , b = {ld}; //错误:转换未执行,因为存在丢失信息的风险
Int c(ld) , d = ld; //正确:转换执行,且确实丢失了部分值
- 内置类型的变量未被显示初始化,他的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量的值时未定义的。如果试图拷贝或是以其他形式访问此类值将引发错误。
- 变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。任何包含了显示初始化的声明即成为定义。我们能给由extern关键字标记的变量赋一个初始值,但是这么做也就抵消了extern的作用。extern语句如果包含初始值就不再是声明,而变成定义了:
extern double pi = 3.1415926; //定义
在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。
变量能且只能被定义一次,但是可以被多次声明。
声明和定义的区别看起来也许微不足道,但实际上却非常重要。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
- 因为全局作用域本身没有名字,所以当作用域操作符(::)的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。
- 一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。引用并非对象,相反的,他只是为一个已经存在的对象所起的另一个名字。定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。
- 引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起:
int& refVal = 10; //错误:引用类型的初始值必须是一个对象
Double dval = 3.14;
Int& refVal2 = dval; //错误:此处引用类型的初始值必须是int型对象
- 初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个一般表达式:
int i = 42;
const int &r1 = i; //允许将const int& 绑定到一个普通的int对象上
const int &r2 = 42; //正确:r1是一个常量引用
const int &r3 = r1 * 2; //正确:r3是一个常量引用
int &r4 = r1 * 2; //错误:r4是一个普通的非常量引用
- 常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变他的值:
int i = 42;
int& r1 = i; //引用ri绑定对象i
const int& r2 = i; //r2也绑定对象i,但是不允许通过r2修改i的值
r1 = 0; //r1并非常量,i的值修改为0
r2 = 0; //错误:r2是一个常量引用
- 因为引用本身不是一个对象,所以不能定义引用的引用。也不能定义指向引用的指针。
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。指针是对象,所以存在对指针的引用。
int i = 42;
int *p; //p是一个int型指针
int *&r = p; //r是一个对指针p的引用
r = &i; //r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0; //解引用r得到i,也就是p指向的对象,将i的值改为0
面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。eg:int*& r,从右向左阅读r的定义。距离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,生命的基本数据类型部分指出r引用的是一个int指针。
- 指向常量的指针不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针:
const double pi = 3.14; //pi是个常量,它的值不能改变
double *ptr = π //错误:ptr是一个普通指针
const double *cptr = π //cptr可以指向一个双精度常量
*cptr = 42; //错误:不能给*cptr赋值
允许令一个指向常量的指针指向一个非常量对象:
double dval = 3.14; //dval是一个双精度浮点数,它的值可以改变
cptr = &dval; //正确:但是不能通过cptr改变dval的值
和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
- 常量指针:把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb = 0;
int *const curErr = &errNumb; //curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = &pi //pip是一个指向常量对象的常量指针
- 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
- 指针的值是0,则条件取false。任何非0指针对应的条件值都是true。
int ival = 1024;
int *pi = 0; //pi合法,是一个空指针
int *pi2 = &ival; //pi2是一个合法的指针,存放着ival的地址
if(pi) //pi的值是0,因此条件的值是false
//...
if(pi2) //pi2指向ival,因此它的值不是0,条件的值是true
//...
- void* 是一种特殊的指针类型,可用于存放任意对象的地址。
doulbe obj = 3.14,*pd = &obj;
void *pv = &obj; //正确:void*能存放任意类型对象的地址 obj可以是任意类型的对象
pv = pd; //pv可以存放任意类型的指针
- 利用void*指针能做的事情比较有限:拿它和别的指针比较、作为函数的输入或输出、或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
- const对象必须初始化。
const int i = get_size(); //正确:运行时初始化
const int j = 42; //正确:编译时初始化。编译器会在编译过程中把用到该变量的地方都替换成对应的值。
const int k; //错误:k是一个未经初始化的常量
- 默认情况下const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
- 某些时候有这样一种const变量,他的初始值不是一个常量表达式,但又确实有必要在文件间共享。我们不希望编译器为每个文件分别生成独立的变量。如果想让const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中定义const,而在其他多个文件中声明并使用它。解决的方法是,对于const变量不管声名还是定义都添加extern关键字,这样只需定义一次就可以了:
//file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
//file_1.h 头文件中的声明也由extern做了限定,其作用是指明bufSize并非本文件所独有,他的定义将在别处出现。
extern const int bufSize; //与file_1.cc中定义的bufSize是同一个
- 用名词 顶层const 表示指针本身是个常量,而用名词 底层const 表示指针所指的对象是一个常量。更一般的,顶层const可以表示任意的对象是常量,底层const则与指针和引用等复合类型的基本类型部分有关。用于声明引用的const都是底层const。
- 常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
一个对象(或表达式)是不是常量表达式有他的数据类型和初始值共同决定:
const int max_files = 20; //max_files是常量表达式
const int limit = max_files + 1; //limit是常量表达式
int staff_size = 27; //staff_size不是常量表达式 因为数据类型是int不是const int
const int sz = get_size(); //sz不是常量表达式,因为具体值要到运行时才能获取到
- 允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20; //20是常量表达式
constexpr int limit = mf + 1; //mf + 1是常量表达式
constexpr int sz = size(); //只有当size是一个constexpr函数时才是一条正确的声明语句
- constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
const int *p = nullptr; //p是一个指向整型常量的指针
constexpr int *q = nullptr; //q是一个指向帧数的常量指针
其中的关键在于constexpr把它所定义的对象置为了顶层const
constexpr指针既可以指向常量也可以指向一个非常量。
- 类型别名
方法一:使用关键字typedef
typedef double wages; //wages是double的同义词
typedef wages base,*p; //base是double的同义词,p是double*的同义词
方法二:使用关键字using
using SI = Sales_item; //SI是Sale_item的同义词
- typedef char *pstring;
const pstring cstr = 0; //cstr是指向char的常量指针
const pstring *ps; //ps是一个指针,他的对象是指向char的常量指针
上述两条声明语句的基本数据类型都是const pstring,和过去一样,const是对类型的修饰。pstring实际上是指向char的指针,因此,const pstring就是指向char的常量指针,而非常量字符的指针。
不要替换成const char *cstr = 0;//是对const pstring cstr 的错误理解
声明语句中用到pstring时,其基本数据类型是指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两中声明含义截然不同,前者声明了指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
- 使用auto也能在一条语句中声明多个变量。因为在一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i = 0, *p = &i; //正确:i是整数、p是整型指针
auto sz = 0, pi = 3.14; //错误:sz和pi的类型不一致
- auto一般会忽略掉顶层const,同时底层const则会保留下来。
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。
- decltype:选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype(f()) sum = x; //sum的类型就是函数f的返回类型
如果decltype使用的表达式是一个变量,则decltype返回该变量的类型包括顶层const和引用在内。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
- 如果给变量加上了一层或多层括号,编译器就会把它当作是一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这养的decltype就会得到引用类型:
//decltype的表达式如果是加上了括号的变量,结果将是引用
int i = 42;
decltype((i)) d; //错误:d是int&,必须初始化
decltype(i) e; //正确:e是一个(未初始化)的int
切记:decltype((variable))(注意是双层括号)的结果永远都是引用,而decltype(varibale)结果只有当variable本身是一个引用时才是引用。
- 用cin>>string的方式,string对象会自动忽略开头的空白(即空格符、换行符、制表符)并从第一个真正的字符开始读起,知道遇到下一处空白为止。
有时我们希望能在最终得到的字符串中保留输入时的空白字符,这时应该用getline函数代替原来的>>运算符。getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)。因为string对象中不包含换行符,所以我们手动加上换行符。
- 当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string。
- 因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象。切记,字符串字面值与string是不同的类型。
- vector能容纳绝大多数的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector。
- 范围for语句体内不应改变其所遍历序列的大小。
- 但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
- 如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
- cbegin和cend: auto it3 = v.cbegin(); //it3的类型是vector<int>::const_iterator
- 数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。如果不清楚元素的确切个数,请使用vector。
- 数组的元素应为对象,因此不存在引用的数组。
- 字符数组有一种额外的初始化形式,我们可以用字符串字面值对此类数组初始化。当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去:
char a1[] = {‘c’,’+’,’+’}; //列表初始化,没有空字符
char a2[] = {‘c’,’+’,’+’,’\0’}; //列表初始化,含有显示的空字符
char a3[] = “c++”; //自动添加表示字符串结束的空字符
const char a4[6]=”Daniel”; //错误:没有空间可存放空字符
- int *ptrs[10]; //ptrs时含有10个整形指针的数组
int &refs[10] = /*?*/; //错误:不存在引用的数组
int (*Parray)[10] = &arr; //Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
- int a[10]{}; 获取数组尾后指针:int* e = &a[10];
这里显然使用下标运算符索引了一个不存在的元素,a有10个元素,尾元素所在位置的索引是9,接下来那个不存在的元素唯一的用处就是提供其地址用于初始化e。就像尾后迭代器一样,尾后指针也不指向具体的元素。因此,不能对尾后指针执行解引用或递增的操作。
- begin,end返回数组的首元素指针和数组的尾元素下一位置的指针。这两个函数定义在iterator头文件中。
因为数组不是类类型,所以这两个函数不是成员函数,正确的使用形式是将数组作为他们的参数:begin(arr),end(arr)。
- 不允许使用一个数组为另一个内置类型的数组赋初值,也不许使用vector对象初始化数组。相反的,允许使用数组来初始化vector对象。要实现这一目的,只需指明要拷贝区域的首元素地址和尾后地址就可以了:
int int_arr[] = {0,1,2,3,4,5};
vector<int> ivec(begin(int_arr),end(int_arr)); //ivec有6个元素,分别是int_arr中对应元素的副本
- 要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
- 左值可以位于赋值语句的左侧,右值则不能。一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。可以做一个简单的归纳:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
- 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
- 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。
- 使用关键字decltype的时候,左值和右值也有所不同。如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。举个例子,假定p的类型是int*,因为解引用运算符生成左值,所以decltype(*p)的结果是int&。另一方面,因为取地址符生成右值,所以decltype(&p)的结果是int**,也就是说,结果是一个指向整型指针的指针。
- (-m)/n和m/(-n)都等于-(m/n)
m%(-n) = m%n
(-m)%n = -(m%n)
- 赋值运算满足右结合律:int i,j; i = j =0;//正确,都被赋值为0
因为赋值运算符满足右结合律,所以靠右的赋值运算j=0作为靠左的赋值运算符的右侧运算对象。又因为赋值运算返回的是其左侧运算对象,所以靠有的赋值运算的结果(即j)被赋给了i。
- 箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。
- sizeof(type)//类型
sizeof expr //表达式
在第二种形式中,sizeof返回的是表达式结果类型的大小。与众不同的一点是,sizeof并不实际计算其运算对象的值。
- 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理。
- 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。
- 逗号运算符含有两个运算对象,按照从左向右的顺序依次求值。对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值。
- 允许将指向非常量的指针转换成指向相应的常量类型的指针,对于引用也是这样:
int i;
const int& j = i; //非常量转换成const int的引用
const int* p = &i; //非常量的地址转换成const的地址
int& r = j , *q=p; //错误:不允许const转换成非常量
相反的转换并不存在,因为它试图删除掉底层const。
- 强制类型转换:cast-name<type>(expression);
其中,type是转换的目标类型而expression是要转换的值。如果type是引用类型,则结果是左值。cast-name是static_cast、dynamic_cast、const_cast、reinterpret_cast中的一种。dynamic_cast支持运行时类型识别。
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。static_cast对于编译器无法自动执行的类型转换也非常有用。
const_cast只能改变运算对象的底层const。
reinterpret_cast通常作为运算对象的位模式提供较低层次上的重新解释。
- exception头文件定义了最通用的异常类exception。他只报告异常的发生,不提供任何额外信息。
- stdexcept头文件定义了几种常用的异常类:
exception 最常见的问题
runtime_error 只有在运行时才能检测出的问题
domain_error 逻辑错误:参数对应的结果值不存在
invalid_argument 逻辑错误:无效参数
length_error 逻辑错误:试图创建一个超出该类型最大长度的对象
out_of_range 逻辑错误:使用一个超出有效范围的值
异常类型只定义一个名为what的成员函数,该函数没有任何参数,返回值是一个指向C风格字符串的const char*。
- 函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
- 如果一个函数永远不会被我们用到,那么它可以只有声明没有定义。
- 局部静态对象:某些时候,有必要令局部变量的生命周期贯穿函数调用以及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象在程序的执行路径第一次经过定义语句时初始化,并且知道程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
- 我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。
- 我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。
- 数组第二位(以及后面所有维度)的大小都是数组类型的一部分,不能省略。
- 如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。initializer_list类型定义在同名的头文件中:
initializer_list<T> lst; 默认初始化:T类型元素的空列表
initializer_list<T> lst{a,b,b...}; lst的元素数量和初始值一样多;lst的元素时对应初始值的副本;列表中的元素是const
lst2(lst),lst2 = lst 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素
lst.size() 列表中的元素数量
lst.begin() 返回指向lst中首元素的指针
lst.end() 返回指向lst中尾元素下一位置的指针
- 和vector一样,initializer_list也是一种模板类型。定义initializer_list对象时,必须说明列表中所含元素的类型:
initializer_list<string> ls; //initializer_list的元素类型是string
initializer_list<int> li; //initializer_list的元素类型是int
和vector不一样的是,initializer_list对象的元素永远是常量值,我们无法改变initializer_list对象中元素的值。
void error_msg(initializer_list<string> il)
{
for(auto beg = il.begin();beg != il.end();++beg)
cout<<*beg<< ‘ ’;
cout<<endl;
}
//假设expected和actual是string对象
if(expected != actual)
error_msg({“functionX”,expected,actual}); //括号里有大括号别忘了!!!!
else
error_msg({“functionX”,”okay”}); //括号里有大括号别忘了!!!!
- 返回局部对象的引用是错误的;同样返回局部对象的指针也是错误的。一旦函数完成,局部对象被释放,指针将指向一个不存在的对象。
- 引用返回左值。函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值:
char& get_val(string& str , string::size_type ix)
{
return str[ix]; //get_val假定索引值是有效的
}
int main()
{
string s(“a value”);
cout<<s<<endl; //输出 a value
get_val(s,0)=’A’; //将s[0]的值改为A
cout<<s<<endl; //输出A value
return 0;
}
如果返回类型是常量引用,我们不能给调用的结果赋值,这一点和我们熟悉的情况是一样的。
- 函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。
如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。
- cstdlib头文件定义了两个预处理量,我们可以使用这两个变量分别表示成功与失败:EXIT_FALLURE,EXIT_SUCCESS 因为他们是预处理变量,所以既不能在前面加上std::,也不能在using声明中出现。
- 因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名:
typedef int arrT[10]; //arrT是一个类型别名,他表示的类型是含有10个整数的数组
using arrT = int[10]; //arrT的等价声明
arrT* func(int i); //func返回一个指向含有10个整数的数组的指针
- 要想在声明func时不适用类型别名,我们必须牢记被定义的名字后数组的维度:
int arr[10]; //arr是一个含有10个整数的数组
int *p1[10]; //p1是一个含有10个指针的数组
int (*p2)[10] = &arr; //p2是一个指针,它指向含有10个整数的数组
和这些声明一样,如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该优先于数组的维度。因此,返回数组指针的函数形式如下所示:
Type (*function(parameter_list))[dimension]
类似于其他数组的声明,Type表示元素的类型,dimension表示数组的大小。(*function(parameter_list))两端的括号必须存在,就像我们定义p2时两端必须有括号一样。如果没有这对括号,函数的返回类型将是指针的数组。
举个例子,下面这个func函数的声明没有使用类型别名:
func(int i)表示调用func函数时需要一个int类型的实参。
(*func(int i))意味着我们可以对函数调用的结果执行解引用操作。
(*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组。
int (*func(int i))[10]表示数组中的元素是int类型
- 尾置返回类型。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用:
auto func(int i) -> int(*)[10];
因为我们把函数的返回类型放在了形参列表之后,所以可以清楚的看到func函数返回的是一个指针,并且该指针指向了含有10个整数的数组。
- 还有一种情况,如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。
- 顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:
Record lookup(Phone);
Record lookup(const Phone); //重复声明了Record lookup(Phone)
另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:
Record lookup(Account&); //函数作用于Account的引用
Record lookup(const Account&); //新函数,作用于常量引用
Record lookup(Account*); //新函数,作用于指向Account的指针
Record lookup(const Account*); //新函数,作用于指向常量的指针
上面4个函数都能作用于非常量对象或者指向非常量对象的指针,不过当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
- 在给定的作用于中一个形参只能被赋予一次默认实参,换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
- 定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:
- 函数的返回类型及所有形参的类型都得是字面值类型
- 而且函数体中必须有且仅有一条return语句
constexpr int new_sz() {return 42};
constexpr int foo = new_sz(); //正确:foo是一个常量表达式
- constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr函数中可以有空语句、类型别名以及using声明。
- 内联函数和constexpr函数可以在程序中多次定义,毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者constexpr函数来说,他的多个定义必须完全一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中。
- 定义在类内部的函数是隐式的inline函数。
- 函数指针指向的是函数而非对象,和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关:
bool lengthCompare(const string& , const string&);
该函数的类型是bool(const string& , const string&)。想要声明一个指向给函数的指针,只需要用指针替换函数名即可:
//pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf)(const string& , const string&); //未初始化
从声明的名字开始观察,pf前面有个*,因此pf是指针;右侧是形参列表,表示pf指向的是函数;再观察左侧,发现函数的返回类型是布尔值。因此,pf就是一个指向函数的指针,其中该函数的参数是两个const string的引用,返回值是bool类型。
*pf两端的括号必不可少。如果不写这对括号,则pf是一个返回值为bool指针的函数。
- 当我们把函数名作为一个值使用时,该函数自动地转换成指针。例如,按照如下形式我们可以将lengthCompare的地址赋给pf:
pf = lengthCompare; //pf指向名为lengthCompare的函数
pf = &lengthCompare; //等价的赋值语句:取地址符是可选的
此外,我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针:
bool b1 = pf(“hello”,”goodbye”); //调用lengthCompare函数
bool b2 = (*pf)(“hello”,”goodbye”); //一个等价的调用
bool b3 = lengthCompare(“hello”,”goodbye”); //另一个等价的调用
在指向不同函数类型的指针间不存在转换规则。但是和往常一样,我们可以为函数指针赋一个nullptr或者值为0的整型常量表达式,表示该指针没有指向任何一个函数。
- 虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是参数类型,实际上确实当成指针使用:
void useBigger(const string& s1 , const string& s2 , bool pf(const string& , const string&));
我们可以直接把函数作为实参使用,此时他会自动转换成指针:
useBigger(s1 , s2 , lengthCompare);
类型别名和decltype能让我们简化使用了函数指针的代码:
//Func和Func2是函数类型
typedef bool Func(const string& , const string&);
typedef decltype(lengthCompare) Func2; //等价的类型
//FuncP和FuncP2是指向函数的指针
typedef bool(*FuncP)(const string& , const string&);
typedef decltype(lengthCompare) *FuncP2; //等价的类型
需要注意的是,decltype返回函数类型,此时不会将函数类型自动转换成指针类型。因为decltype的结果是函数类型,所以只有在结果前面加上*才能得到指针。
- 和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。与往常一样,要想声明一个返回函数指针的函数,最简单的方法是使用类型别名:
using F = int(int* , int); //F是函数类型吗,不是指针
using PF = int(*)(int* , int); //PF是指针类型
我们必须显示地将返回类型指定为指针。
- 在抽象数据类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无需了解类型的工作细节。
- 默认情况下,this的类型是指向类类型非常量版本的常量指针。例如在Sales_data成员函数中,this的类型是Sales_data *const。这意味着我们不能把this绑定到一个常量对象上。这一情况也就使得我们不能在一个常量对象上调用普通的成员函数。
紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数。
- 常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
- 编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)。因此,成员函数体可以随意使用类中的其他成员而无需在意这些成员出现的次序。
- IO属于不能被拷贝的类型,因此我们只能通过引用来传递。
- 不同于其他成员函数,构造函数不能被声明成const的。当我们创建类的一个const对象时,知道构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
- 构造函数可以通过在参数列表后面写上 =default来要求编译器生成默认构造函数。
- 使用class和struct定义类唯一的区别就是默认的访问权限。类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式。如果我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的;相反,如果我们使用class关键字,则这些成员是private的。
- 友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域访问控制级别的约束。
一般来说,最好在类定义开始或结束的位置集中声明友元。
- 类除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名。由类定义的类型名字和其他成员一样存在访问限制,可以是public或者private中的一种。其次,用来定义类型的成员必须先定义后使用,这一点与普通成员有所区别。
- 有时会发生这样一种情况,我们希望能修改类的某个数据成员,即使是在一个const成员函数内。可以通过在变量的声明中加入mutable关键字做到这一点。
class Screen
{
public:
void some_member() const;
private:
mutable size_t access_ctr; //即使在一个const对象内也能被修改
};
void Screen::some_member() const
{
++access_ctr;
}
尽管some_member是一个const成员函数,他仍然能够改变access_ctr的值。该成员是个可变成员,因此任何成员函数,包括const函数在内都能改变它的值。
- 类内初始值必须使用=的初始化形式或者花括号括起来的直接初始化形式
- 返回引用的函数是左值的,意味着这些函数返回的是对象本身而不是对象的副本。
- 每个类定义了唯一的类型。对于两个类来说,即使它们的成员完全一样,这两个类也是两个不同的类型。
- 我们首先完成类的定义,然后编译器才能知道存储该数据成员需要多少空间。因为只有当类完全完成后类才算被定义,所以一个类的成员类型不能是该类自己。然而,一旦一个类的名字出现后,他就被认为是声明过了(但尚未定义),因此类允许包含指向它自身类型的引用或指针:
class Link_screen
{
Screen window;
Link_screen* next;
Link_screen* prev;
}
- 友元关系不存在传递性。每个类负责控制自己的友元类或友元函数。
- 要想令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系:
- 首先定义Window_mgr类,其中声明clear函数,但是不能定义它。在clear使用Screen的成员之前必须先声明Screen。
- 接下来定义Screen,包括对于clear的友元声明。
- 最后定义clear,此时它才可以使用Screen的成员。
- 一般来说,内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过。然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
- 对构造函数来说:如果成员是const或者引用的话,必须将其初始化。类似的,当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。
- 我们初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值。
- 在很多类中,初始化和赋值的区别事关底层效率问题:前者直接初始化数据成员,后者则先初始化再赋值。
- 成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
- 一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。
当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。假如函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。
- 编译器只会自动地执行一步类型转换。
- 我们可以通过将构造函数声明为explicit从而抑制构造函数定义的隐式转换。
关键字explicit只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换。
只能在类内声明构造函数时使用explicit关键字,在类外定义时不应重复。
explicit构造函数只能用于直接初始化,不能将explicit构造函数用于拷贝形式的初始化过程。
- 尽管构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr函数。事实上,一个字面值常量类必须至少提供一个constexpr构造函数。
- constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式。
constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型。
- 静态成员可以是public的或private的。静态数据成员的类型可以是常量、引用、指针、类类型等。
- 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
- 静态成员函数不能声明成const的,而且我们不能在static函数体内使用this指针。
- static只出现在类内部的声明语句中
- 一般来说,我们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。和其它对象一样,一个静态数据成员只能定义一次。
- 通常情况下,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式,因为这些成员本身就是常量表达式,所以它们能用在所有适合于常量表达式的地方。
static constexpr int period = 30; //period是常量表达式
- 即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。如果在类的内部提供了一个初始值,则成员的定义不能再指定一个初始值了。
- 静态成员可以是不完全类型。特别的,静态数据成员的类型可以就是它所属的类类型。而非静态数据成员则受到限制,只能声明成它所属类的指针或引用:
class Bar
{
static Bar mem1; //正确:静态成员可以是不完全类型
Bar *mem2; //正确:指针成员可以是不完全类型
Bar *mem3; //错误:数据成员必须是完全类型
};
- 静态成员和普通成员的另外一个区别是我们可以使用静态成员作为默认实参。非静态数据成员不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终将引发错误。
- 确定一个流对象的状态的最简单的方法是将它当作一个条件来使用:
while(cin>>word)
//ok:读操作成功
while循环检查>>表达式返回的流的状态。如果输入操作成功,流保持有效状态,则条件为真。不过流作为条件使用,只能告诉我们流是否有效,而无法告诉我们具体发生了什么。
- 如果到达文件结束位置,eofbit和failbit都会被置位。
- endl完成换行并刷新缓冲区的工作。flush刷新缓冲区,但不输出任何额外的字符。ends向缓冲区插入一个空字符,然后刷新缓冲区。
- 当一个fstream对象被销毁时,close会自动被调用。
- 默认情况下,即使我们没有指定trunc,以out模式打开的文件也会被截断。为了保留以out模式打开的文件的内容,我们必须同时指定app模式,这样只会将数据追加写到文件末尾,或者同时指定in模式,即打开文件的同时进行读写操作。
- 默认情况下,当我们打开一个ofstream时,文件的内容会被丢弃。阻止一个ofstream清空给定文件内容的方法是同时指定app模式。
- 顺序容器:所有顺序容器都提供了快速顺序访问的能力。
vector、deque、list、forward_list、array、string
一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。容器均定义为模板类。
- c.emplace(inits) 使用inits构造c中的一个元素
- 通过类型别名我们在不了解容器中元素类型的情况下使用它:
size_type无符号整数类型,足够保存此种容器类型最大可能容器的大小。
difference_type带符号整数类型,足够保存两个迭代器之间的距离
value_type元素类型
reference元素的左值类型,与value_type&含义相同
const_reference元素的const左值类型(即const value_type&)
- 与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小:
array< int , 42 > //类型为:保存42个int的数组
array< string , 10 > //类型为:保存10个string的数组
- 由于大小是array类型的一部分,array不支持普通的容器构造函数。这些构造函数都会确定容器的大小,要么是隐式地,要么是显示地。
- 我们不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制:
int digs[10] = {0,1,2,3,4,5,6,7,8,9};
int cpy[10] = digs; //错误:内置数组不支持拷贝或赋值
array<int , 10> digits = {0,1,2,3,4,5,6,7,8,9};
array<int , 10> copy = digits; //正确:只要数组类型匹配即合法
与其他容器一样,array也要求初始值的类型必须与要创建的容器类型相同,此外,array还要求元素类型和大小也都一样,因为大小是array类型的一部分。
- 除array外,交换两个容器内容的操作保证会很快——元素本身并未交换,swap只是交换了两个容器的内部数据结构。除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。
元素不会被移动的事实意味着,除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效。它们仍然指向swap之前所指向的哪些元素。但是,在swap之后,这些元素已经属于不同的容器了。例如,假定iter在swap之前指向svecl[3]的string,那么在swap之后它指向svec2[3]的元素。与其他容器不同,对一个string调用swap会导致迭代器、引用、指针失效。
与其他容器不同,swap两个array会真正交换他们的元素。因此,交换两个array所需的时间与array中元素的数目成正比。
因此,对于array,在swap操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素值已经与另一个array中对应元素的值进行了交换。
- 只有当容器中的元素也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。
- 当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。随后对容器中元素的任何改变都不会影响到原始对象。
- 虽然某些容器不支持push_front操作,但他们对于insert操作并无类似的限制(插入开始位置)。因此我们可以将元素插入到容器的开始位置,而不必担心容器是否支持push_front。
- 将元素插入到vector、deque和string中的任何位置都是合法的。然而,这样做可能很耗时。
- 新标准引入了三个新成员——emplace_front、emplace和emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。
当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。
- 包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有一个back成员函数。这两个操作分别返回首元素和尾元素的引用。
- at和下标操作只适用于string、vector、deque和array。
back不适用于forward_list。
- 在容器中访问元素的成员函数(即front、back、下标和at)返回的都是引用。如果容器是一个const对象,则返回值是const的引用。
- 如果我们使用auto变量来保存这些函数的返回值,并且希望使用此变量来改变元素的值,必须记得将变量定义为引用类型。
- 如果我们希望下标是合法的,可以使用at成员函数。at成员函数类似下标运算符,但如果下标越界,at会抛出一个out_of_range异常。
- 删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向vector或string中的删除点之后位置的迭代器、引用和指针都会失效。
- 在forward_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。由于这些操作与其他容器上的操作的实现方式不同,forward_list并未定义insert、emplace和erase,而是定义了名为insert_after、emplace_after和erase_after的操作。forward_list也定义了before_begin,它返回一个首前迭代器。这个迭代器允许我们在链表首元素之前并不存在的元素“之后”添加或删除元素(即在链表首元素之前添加删除元素)。
- 我们可以用resize来增大或减小容器,array不支持resize。如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部,任何新添加的元素都被初始化。
list<int>ilist(10,42); //10个int;每个的值都是42
ilist.resize(15); //将5个值为0的元素添加到ilist的末尾
ilist.resize(25,-1); //将10个值为-1的元素添加到ilist的末尾
ilist.resize(5); //从ilist末尾删除20个元素
- 对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针、引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
- 对于list和forward_list添加元素后,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。
- 当我们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效。
- 由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对vector、string和deque尤为重要。
- 不要保存end返回的迭代器:因为当我们添加/删除vector或string的元素后,或在deque中首元素之外任何位置添加/删除元素后,原来的end返回的迭代器总会失效。
- 容器的size是指它已经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。
- 当我们遇到一个从const char*创建string时,指针指向的数组必须以空字符结尾,拷贝操作遇到空字符时停止。如果我们还传递给构造函数一个计数值,数组就不必以空字符结尾。如果我们未传递计数值且数组也未以空字符结尾,或者给定计数值大于数组大小,则构造函数的行为是未定义的。
- append操作是在string末尾进行插入操作。
- 除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。容器、迭代器和函数都有适配器。本质上,一个适配器是一种机制,能使某种事物的行为看起来像另一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
- 对于一个给定的适配器,可以使用哪些容器是有限制的。所有适配器都要求容器具有添加和删除元素的能力。因此,适配器不能构造在array上。
- 我们也不能用forward_list来构造适配器,因为所有适配器都要求容器具有添加、删除以及访问尾元素的能力。
- 泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。泛型算法运行于迭代器之上而不会执行容器操作的特性带来了一个令人惊讶但非常必要的假定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加或删除元素。
- 除了少数例外,标准库算法都对一个范围内的元素进行操作,我们将此元素范围称为“输入范围”。接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向处理的第一个元素和后元素之后位置的迭代器。
- accumulate函数定义在头文件numeric中。accumulate函数接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。
accumulate的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。
- 那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。如果第二个序列是第一个序列的一个子集,则程序会产生一个严重错误——算法会试图访问第二个序列中末尾之后(不存在)的元素。
- back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。
- unique算法重排输入序列,将相邻的重复项“消除”,并返回一指向不重复值范围末尾的迭代器。
- 谓词是一个可调用的表达式,其返回结果是一个能用做条件的值。标准库算法所使用的谓词分为两类:一元谓词(意味着它们只接受单一参数)和二元谓词(意味着他们有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。
- 一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。一个lambda表达式具有如下形式:
[capture list](parameter list) -> return type {function body}
其中capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);return type、parameter list和function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda必须使用尾置返回来指定返回类型。
我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体:
[]{return 42;}
与一个普通函数调用类似,调用一个lambda时给定的实参被用来初始化lambda的形参。通常,实参和形参的类型必须匹配。但与普通函数不同,lambda不能有默认参数,因此,一个lambda调用的实参数目必须永远与形参数目相等。一旦形参初始化完毕,就可以执行函数体了。
空捕获列表表明此lambda表达式不适用他所在函数中的任何局部变量。虽然一个lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。一个lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获列表指引lambda在其内部包含访问局部变量所需的信息。
一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
捕获列表只能用于局部非static变量,lambda可以直接使用局部static变量和在他所在函数之外声明的名字。
变量的捕获方式也可以是值或引用。
采用值捕获的前提是变量可以拷贝。被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。
一个引用方式捕获的变量与其他任何类型的引用的行为类似。当我们在lambda函数体内使用此变量时,实际上使用的是引用所绑定的对象。
除了显示列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量,为了指示编译器捕获列表,应在捕获列表中写一个&或=。&告诉编译器应采用捕获引用方式,=则表示采用值捕获方式。
当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。此符号制定了默认捕获方式为引用或值。
- 我们不能拷贝ostream对象,因此捕获os的唯一方式就是捕获其引用(或指向os的指针)。
- 创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。因此,istream_iterator要读取的类型必须定义了输入运算符。当创建一个istream_iterator时,我们可以将它绑定到一个流。当然,我们还可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器。对于绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。
istream_iterator更有用的地方:
istream_iterator<int>in_iter(cin),eof; //从cin读取int
vector<int>vec(in_iter,eof); //从迭代器范围构造vec
我们用一对表示元素范围的迭代器来构造vec。这两个迭代器是istream_iterator,这意味着元素范围是通过从关联的流中读取数据获得的。这个构造函数从cin中读取数据,直至遇到文件尾或者遇到一个不是int的数据为止。从流中读取的数据被用来构造vec。
- 我们可以对任何具有输出运算符(<<运算符)的类型定义ostream_iterator。创建一个ostream_iterator时,我们可以提供(可选的)第二参数,它是一个字符串,在输出每个元素之后都会打印此字符串。此字符串必须是一个C风格字符串(即,一个字符串常量或者一个指向以空字符结尾的字符数组的指针)。必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator。 out = val;用<<运算符将val写入到out所绑定的ostream中。val的类型必须与out可写的类型兼容。
- 反向迭代器需要递减运算符。我们只能从即支持++也支持--的迭代器来定义反向迭代器。毕竟反向迭代器的目的是在序列中反向移动。除了forward_list之外,标准容器上的其他迭代器即支持递增运算又支持递减运算。但是,流迭代器不支持递减运算,因为不能在一个流中反向移动。因此,不能从一个forward_list或一个流迭代器创建反向迭代器。
- base成员函数可以将反向迭代器转换为普通迭代器。注意反向迭代器和普通迭代器指向的是不同的元素。这些不同保证了元素范围无论是正向处理还是反向处理都是相同的。因为他们都是左闭右开区间的特性。
- 对于list和forward_list,应该优先使用成员函数版本的算法而不是通用版本的算法。
- 我们可以为任何定义了输入运算符(>>)的类型创建istream_iterator对象。类似的,只要类型有输入运算符(<<),我们就可以为其定义ostream_iterator。
- 关联容器支持高效的关键字查找和访问。两个主要的关联容器类型是map和set。map中的元素是一些关键字-值对:关键字起到了索引的作用,值则表示与索引相关联的数据。set(关键字即值)中每个元素只包含一个关键字(set要求不重复关键字):set支持高效的关键字查询操作——检查一个给定关键字是否在set中。
- 类型map和mutimap定义在map中;set和multiset定义在头文件set中;无序容器则定义在头文件unordered_map和unordered_set中。unordered_map和unordered_set是用哈希函数组织的。
- map类型通常被称为关联数组。关联数组与“正常”数组类似,不同之处在于其下标不必是整数。
- set就是关键字的简单集合。当只是想知道一个值是否存在时,set是最有用的。
- 当从map中提取一个元素时,会得到一个pair类型的对象,pair是一个模板,保存两个名为first和second的(共有)数据成员。map所使用的pair用first成员保存关键字,用second成员保存对应的值。
- 关联容器的迭代器都是双向的。
- 每个关联容器都定义了一个默认构造函数,它创建一个指定类型的空容器。我们也可以将关联容器初始化为另一个同类型容器的拷贝,或是从一个值范围来初始化关联容器,只要这些值可以转化为容器所需类型就可以。
map<string , size_t>word_count; //空容器
set<string>exclude = {“the” , “but” , “and” , “or”}; //初始化列表
//三个元素;authors将姓映射为名
map<string , string>authors = {{“Joyce” , “James”} , {“Austen” , “Jane”} , {“Dickens” , “Charles”}};
当初始化一个map时,必须提供关键字类型和值类型。我们将每个关键字-值对包围在花括号中:{key ,value}来指定他们一起构成了map中的一个元素。在每个花括号中,关键字是第一个元素,值是第二个。因此author将姓映射到名,初始化后它包含三个元素。
- 用来组织一个容器中元素的操作的类型也是该容器类型的一部分。为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的类型。当定义此容器类型的对象时,需要提供想要使用的操作的指针。
- pair类型定义在头文件utility中。make_pair(v1 , v2)返回一个用v1和v2初始化的pair。pair的类型从v1和v2的类型推断出来。
- key_type 此容器类型的关键字类型
mapped_type 每个关键字关联的类型;只适用于map
value_type 对于set,与key_type相同;对于map,为pair<const key_type , mapped_type>因为我们不能改变一个元素的关键字,因此这些pair的关键字部分是const的。
- set中的关键字也是const的。可以用一个set迭代器来读取元素的值,但不能修改。
- 当使用一个迭代器遍历一个map、multimap、set或multiset时,迭代器按关键字升序遍历元素。
- map和unordered_map容器提供了下标运算符和一个对应的at函数。不能对一个multimap或一个multimap或一个unordered_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。set类型不支持下标,因为set中没有与关键字相关联的“值”。元素本身就是关键字,因此“获取与一个关键字相关联的值”的操作就没有意义了。
- 如果关键字不在map中,会为它创建一个元素并插入到map中,关键值将进行值初始化。
map<string , size_t>word_count; //empty map
//插入一个关键字为Anna的元素,关键值进行值初始化;然后将1赋予它
word_count[“Anna”] = 1;
- 由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作。
c[k] //返回关键字为k的元素,如果k不在c中,添加一个关键字为k的元素,对其进行值初始化
c.at(k) //访问关键字为k的元素,带参数检查,若k不在c中,抛出一个out_of_range异常
- 当对一个map进行下标操作时,会获得一个mapped_type对象;但当解引用一个map迭代器时,会得到一个value_type对象。
- c.count(k) 返回关键字等于k的元素的数量,对于不允许重复关键字的容器,返回值永远是0或1
c.find(k) 返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器
- 如果一个multimap或multiset中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。
- 无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
- 智能指针负责自动释放所指向的对象。定义在memory头文件中
shared_ptr允许多个指针指向同一个对象
unique_ptr则“独占”所指向的对象
标准库还定义了一个名为weak_ptr的伴随类,他是一种弱引用,指向shared_ptr所管理的对象。
- 智能指针也是模板,当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。
- 最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。
shared_ptr<int>p3 = make_shared<int>(42);
当然我们通常用auto定义一个对象来保存make_shared的结果,这种方式较为简单:
auto p6 = make_shared<vector<string>>(); //p6指向一个动态分配的空vector<string>
- 一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。它是通过另一个特殊的成员函数——析构函数完成销毁工作的。
- 如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
- 程序使用动态内存出于以下三种原因之一:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
- 一般而言,如果两个对象共享底层的数据,当某个对象被销毁时,我们不能单方面地销毁底层数据。
- int* pi1 = new int; //默认初始化,*pi1的值未定义
int* pi2 = new int(); //值初始化,*pi2为0
string* ps = new string; //初始化为空string
string* ps1 = new string(); //值初始化为空string
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。
也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可。
对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的,不管采用什么形式,对象都会通过默认构造函数来初始化。
- 用new分配const对象是合法的:
//分配并初始化一个const int
const int* pci = new const int(1024);
//分配并默认初始化一个const的空string
const string* pcs = new const string;
一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显示初始化。
- 在delete之后,指针就变成了人们所说的空悬指针,即,指向一块曾经保存数据对象但现在已经无效的内存的指针。
有一种方法可以避免空悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以再delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。
- 我们可以用new返回的指针来初始化智能指针:
shared_ptr<int>p2(new int(42)); //p2指向一个值为42的int
接受指针参数的智能指针构造函数是explicit的,因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
shared_ptr<int>p1 = new int(1024); //错误:必须使用直接初始化形式
shared_ptr<int>p2(new int(1024)); //正确:使用了直接初始化形式
- 如果你使用的智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
- 我们不能拷贝或赋值一个unique_ptr,但是我们可以拷贝或赋值一个将要被销毁的unique_ptr:
unique_ptr<int>clone(int p)
{
return unique_ptr<int>(new int(p)); //正确:从int*创建一个unique_ptr<int>
}
还可以返回一个局部对象的拷贝:
unique_ptr<int>clone(int p)
{
unique_ptr<int>ret(new int(p));
//....
return ret;
}
- 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。
- 当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。同样不能用范围for语句来处理动态数组中的元素。
要记住我们所说的动态数组并不是数组类型,这是很重要的。
- delete[] pa; 销毁pa指向的数组中的元素,并释放对应的内存,数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,以此类推。
- 标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:
//up指向一个包含10个未初始化int的数组
unique_ptr<int[]>up(new int[10]);
up.release(); //自动用delete[]销毁其指针
- 指向数组的unique_ptr不支持成员访问运算符(点和箭头运算符)。
- 与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。
//为了使用shared_ptr,必须提供一个删除器
shared_ptr<int>sp(new int[10] , [](int* p){delete[] p;});
sp.reset(); //提供我们提供的lambda释放数组,它使用delete[]
- shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素:
//shared_ptr未定义下标运算符,并且不支持指针的算术运算
for(size_t i = 0; i !=10; ++i)
{
*(sp.get() + 1) = i; //使用get获取一个内置指针
}
- allocator类定义在头文件memory中,他帮助我们将内存分配和对象构造分离开来。它分配的内存是原始的、未构造的。我们按需要在此内存中构造对象。在还未构造对象的情况下就使用原始内存时错误的。
- 在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
- 如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。
- 如果一个类需要一个拷贝构造函数,几乎可以肯定他也需要一个拷贝赋值运算符。反之亦然。
- 我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。删除的函数是这样一种函数:我们虽然声明了它们,但不能以任何方式使用它们。在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的。
=delete必须出现在函数第一次声明的时候。
- 如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。
- 标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。
- 右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。右值引用只能绑定到一个将要销毁的对象。因此我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
- 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。
我们在一个函数的参数列表后指定noexcept,在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间。
- 在移动操作之后,移后源对象必须保持有效的、可析构的状态,但用户不能对其值进行任何假设。
- 定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地定义为删除的。
- 如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”的。
- 移动迭代器的解引用运算符生成一个右值引用。
- 除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
- 通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符。
- 通常情况下,我们把算数和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参都是常量的引用。
- 如果类定义了调用运算符,则该类的对象称作函数对象。因为可以调用这种对象,所以我们说这些对象的“行为像函数一样”。
- 类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下所示:operator type() const;其中type表示某种类型。类型转换运算符可以面向任意类型(除了void之外)进行定义,只要该类型能作为函数的返回类型。因此,我们不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型。类型转换运算符通常不应该改变待转换对象的内容,因此,类型转换运算符一般被定义成const成员。它不能声明返回类型,形参列表也必须为空。
- 编译器一次只能执行一个用户定义的类型转换,但是隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用。
- 在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。
- 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
- 关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。
- 成员函数如果没有被声明为虚函数,则其解析过程发生在编译时而非运行时。
- 在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的。
- 如果基类定义了一个静态成员,则在整个集成体系中只存在该成员的唯一定义。
- 派生类的声明与其他类差别不大,声明中包含类名但是不包含它的派生列表。派生类列表以及定义有关的其他细节必须与类的主体一起出现。
- 如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。
一个类不能派生它本身。
- 在类名后跟一个关键字final可以防止继承发生。
- 动态绑定只有当我们通过指针或引用调用虚函数时才会发生。
- 我们还能把某个函数指定为final,如果我们已经把函数定义成final了,则之后的任何尝试覆盖该函数的操作都将引发错误。
- 我们不能(直接)创建一个抽象基类的对象。
- 派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员;对于普通的基类对象中的成员不具有特殊的访问权限。
- 友元关系不能传递、也不能继承。基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员。
- 对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此。
例如,Pal是Base的友元,所以Pal能访问Base对象的成员,这种可访问性包括了Base对象内嵌在其派生类对象中的情况。
- 在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员(例如,非私有成员)标记出来。using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。
派生类只能为那些它可以访问的名字提供using声明。
- 当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
如果一个名字在派生类的作用于内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
恰恰因为类作用域有这种继承嵌套的关系,过亿派生类才能像使用自己的成员一样使用基类的成员。
- 声明在内层作用域的函数并不会重载声明在外层作用域的函数。因为如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。
- 虚析构函数将阻止合成移动操作。因此当我们移动对象时实际使用的是合成的拷贝操作。
- 在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显示地使用基类的拷贝(或移动)构造函数。
- 与拷贝和移动构造函数一样,派生类的赋值运算符也必须显示地为其基类部分赋值。
- 一个类只初始化它的直接基类,一个类也只能继承其直接基类的构造函数。
- 在模板参数列表中,typename和class没有什么不同。
- 一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式,绑定到指针或引用非类型参数的实参必须具有静态的生存期。我们不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数也可以用nullptr或一个值为0的常量表达式来实例化。
在模板定义内,模板非类型参数是一个常量值,在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小。
非类型模板参数的模板实参必须是常量表达式。
template<unsigned N , unsigned M>
int compare(const char (&p1)[N] , const char (&p2)[M])
{
return strcmp(p1 , p2);
}
当我们调用这个版本的compare时:
compare(“hi” , “mom”);
编译器会使用字面常量的大小来代替N和M,从而实例化模板。编译器在一个字符串字面常量的末尾插入一个空字符作为结束符,因此编译器会实例化出如下版本:
int compare(const char (&p1)[3] , const char (&p2)[4])
- 为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。
模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。
- 定义在类模板之外的成员函数必须以关键字template开始,后接类模板参数列表。
- 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
- 模板声明必须包含模板参数。与函数参数相同,声明中的模板参数的名字不必与定义中相同。但是,一个给定模板的每个声明和定义必须有相同数量和种类(即,类型或非类型)的参数。
- 一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
- DebugDelete()(ip) //在一个临时DebugDelete对象上调用ioerator()(int *)
- 与类模板的普通函数成员不同,成员模板是函数模板。当我们在类模板外定义一个成员函数时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后面跟成员自己的参数列表:
template <typename T>
template <typename It>
Blob<T> ::Blob(It b , It e) {}
我们定义了一个类模板的成员,类模板有一个模板类型参数,命名为T。而成员自身是一个函数模板,它有一个名为It的类型参数。
为了实例化一个类模板的成员模板,我们必须同时提供类和函数模板的实例。
- 多个文件中实例化相同模板的额外开销可能非常严重。我们可以通过显示实例化来避免这种开销。一个显示实例化有如下形式:
extern template declaration; //实例化声明
template declaration; //实例化定义
declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参。
//实例化声明与定义
extern template class Blob<string>; //声明
template int compare(const int& , const int&); //定义
对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前。
- 一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。
- 将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换。
- 显示模板实参按由左至右的顺序与对应的模板参数匹配:第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配。只有尾部(最右)参数的显示模板实参才可以忽略,前提是它们可以从函数参数推断出来。
- 虽然不能隐式地将一个左值转换为右值引用,但从一个左值static_cast到一个右值引用是允许的。
- 通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。而使用引用参数(无论是左值还是右值)使得我们可以保持const属性,因为在引用类型中的const是底层的。如果我们将函数参数定义为T1&&,通过引用折叠就可以保持翻转实参的左值/右值属性。
- 我们可以使用一个initializer_list来定义一个可接受可变数目实参的函数,但是,所有实参必须具有相同的类型(或它们的类型可以转换为同一个公共类型)。
当我们即不知道想要处理的实参的数目也不知道它的类型时,可变参数的函数时很有用的。
- 当我们希望将一些数据组合成单一对象,但又不想麻烦地定义一个新数据结构来表示这些数据时,tuple是非常有用的。
- string的下标编号习惯与bitset恰好相反:string中下标最大的字符(最右字符)用来初始化bitset中的低位(下标为0的二进制位)。
- 在bitset中,下标运算符对const属性进行了重载。const版本的下标运算符在指定位 置位 时返回true,否则返回false。非const版本返回bitset定义的一个特殊类型,他允许我们操纵指定位的值。
- 一个正则表达式的语法是否正确是在运行时解析的。
- 匹配对象除了提供匹配整体的相关信息外,还提供访问模式中每个子表达式的能力。子匹配时按位置来访问的。第一个子匹配位置为0,表示整个模式对应的匹配,随后是每个子表达式对应的匹配。
- 定义在头文件random中的随机数库具有一组协议:随机数引擎类和随机数分布类。一个引擎类可以生成unsigned随机数序列,一个分布类使用一个引擎类生成指定类型的、在给定范围内的、服从特定概率分布的随机数。
- C++程序不应该使用库函数rand,而应使用default_random_engine类和恰当的分布类对象。
default_random_engine e; //生成随机无符号数
for(size_t i = 0 ; i < 10 ; ++i)
{
//e()“调用”对象来生成下一个随机数
cout<< e() << “ ”;
}
- 一个给定的随机数发生器一直会生成相同的随机数序列。一个函数如果定义了局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static的。否则,每次调用函数都会生成相同的序列。
- 随机数发生器会生成相同的随机数序列这一特性在调试中很有用。但是,一旦我们的程序调试完毕,我们通常希望每次运行程序都会生成不同的随机结果,可以通过提供一个种子来达到这一目的。种子就是一个数值,引擎可以利用它从序列中一个新位置重新开始生成随机数。
- uniform_real_distribution类型让标准库来处理从随机整数到随机浮点数的映射。
uniform_int_distribution类型让标准库来处理从随机整数到范围内整数的映射。
default_random_engine e; //生成无符号随机整数
//0到1(包含)的均匀分布
uniform_real_distribution<double> u(0 , 1);
- 由于分布类型一个模板参数,因此当我们希望使用默认随机数类型时要记得在模板名之后使用空尖括号。
- 由于引擎返回相同的随机数序列,所以我们必须在循环外声明引擎对象。否则,每步循环都会创建一个新引擎,从而每步循环都会生成相同的值。类似的,分布对象也要保持状态,因此也应该在循环外定义。
- 操纵符用于两大类输出控制:控制数值的输出形式以及控制补白的数量和位置。
- 大多数改变格式状态的操纵符都是设置/复原成对的:一个操纵符用来将格式状态设置为一个新值,而另一个用来将其复原,恢复为正常的默认格式。
- 当操纵符改变流的格式状态时,通常改变后的状态对所有后续IO都生效。
- 在默认情况下,整型值的输入输出使用十进制。我们可以使用操纵符hex、oct和dec将其改为十六进制、八进制或是改回十进制。
操纵符hex、oct和dex只影响整形运算对象,浮点值的表示形式不受影响。
- 当对流应用showbase操纵符时,会在输出结果中显示进制。
- 默认情况下,浮点值按六位数字精度打印;如果浮点值没有小数部分,则不打印小数点;根据浮点数的值选择打印成定点十进制或科学计数法形式。标准库会选择一种可读性更好的格式:非常大和非常小的值打印为科学计数法形式,其他值打印为定点十进制形式。
- 我们通过调用IO对象的precision成员或使用setprecision操纵符来改变精度。都定义在头文件iomanip中。
- 一般情况下,在读取下一个值之前,标准库保证我们可以退回最多一个值。
- 头文件cstdio定义了一个名为EOF的const,我们可以用它来检测从get返回的值是否是文件尾,而不必记忆表示文件尾的实际数值。
- 标准库提供了一对函数,来定位(seek)到流中给定位置,以及告诉(tell)我们当前位置。
虽然标准库为所有流类型都定义了seek和tell函数,但它们是否会做有意义的事情依赖于流绑定到哪个设备。大多数系统中,绑定到cin、cout、cerr和clog的流不支持随机访问。对这些流我们可以调用seek和tell函数,但在运行时会出错,将流置于一个无效状态。
fstream和sstream就适用。
- 标准库实际定义了两对seek和tell函数,一对用于输入流,另一对用于输出流。g版本表示我们正在“获得”(读取)数据。而p版本表示我们正在“放置”(写入)数据。
- 即使标准库进行了区分,但它在一个流中只维护单一的标记——并不存在独立的读标记和写标记。
标记只有一个,表示缓冲区中的当前位置。标准库将g和p版本的读写位置都映射到这个单一的标记。
由于只有单一的标记,因此只要我们在读写操作间切换,就必须进行seek操作来重定位标记。
- tell函数通常用来记住一个位置,以便稍后再定位回来。
- 一个异常如果没有被捕获,则它将终止当前的程序。
如果找到一个匹配的catch字句,则程序进入到该字句并执行其中的代码。当执行完这个catch字句后,找到与try块关联的最后一个catch字句之后的点,并从这里继续执行。
- 当我们抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。
- 如果catch的参数是基类类型,则我们可以使用其派生类类型的异常对象对其进行初始化。此时,如果catch的参数是非引用类型,则异常对象将被切掉一部分,这与将派生类对象以值传递的方式传给一个普通函数差不多。另一方面,如果catch的参数是基类的引用,则该参数将以常规方式绑定到异常对象上。
异常声明的静态类型将决定catch语句所能执行的操作。如果catch的参数是基类类型,则catch无法使用派生类特有的任何成员。
- 为了一次性捕获所有异常,我们使用省略号作为异常声明,这样的处理代码称为捕获所有异常的处理代码,形如catch(...)。一条捕获所有异常的语句可以与任意类型的异常匹配。
catch(...)通常与重新抛出语句一起使用,其中catch执行当前局部能完成的工作,随后重新抛出异常。
- 通过提供noexcept说明指定某个函数不会抛出异常。
void recoup(int) noexcept; //不会抛出异常
void alloc(int); //可能抛出异常
对于一个函数来说,noexcept说明要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现。
在成员函数中,noexcept说明符需要跟在const及引用限定符之后,而在final、override或虚函数的=0之前。
- noexcept说明符接受一个可选的实参,该实参必须能转换为bool类型:如果实参是true,则函数不会抛出异常;如果实参是false,则函数可能抛出异常:
void recoup(int) noexcept(true); //recoup不会抛出异常
void alloc(int) noexcept(false); //alloc可能抛出异常
- 如果我们为某个指针做了不抛出异常的声明,则该指针将只能指向不抛出异常的函数。
如果我们显示或隐式地说明了指针可能抛出异常,则该指针可以指向任何函数,即使是承诺了不抛出异常的函数也可以。
- 模板特例化必须定义在原始模板所属的命名空间中。和其他命名空间名字类似,只要我们在命名空间中声明了特例化,就能在命名空间外部定义它了。
- ::member_name 表示全局命名空间中的一个成员。
- 内联命名空间中的名字可以被外层命名空间直接使用。定义内联命名空间的方式是在关键字namespace前添加关键字inline。
关键字inline必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候可以写inline,也可以不写。
- 未命名的命名空间是指关键字namespace后紧跟花括号括起来的一系列声明语句。未命名的命名空间中定义的变量拥有静态生命周期:他们在第一次使用前创建,并且直到程序结束才销毁。
定义在未命名的命名空间中的名字可以直接使用。同样的我们不能对未命名的命名空间的成员使用作用域运算符。
未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同。如果未命名的命名空间定义在文件的最外层作用域中,则该命名空间中的名字一定要与全局作用域中的名字有所区别。
- 在C语言中,声明为static的全局实体在其所在的文件外不可见。
在文件中进行静态声明的做法已经被C++标准取消了,现在的做法是使用未命名的命名空间。
- 不能在命名空间还没有定义前就声明别名,否则将产生错误。
一个命名空间可以有好几个同义词或别名,所有别名都与命名空间原来的名字等价。
- using声明语句一次只引入命名空间的一个成员。它的有效范围从using声明的地方开始,一直到using声明所在的作用域结束为止。在此过程中,外层作用域的同名实体将被隐藏。
using namespace_name::name
- 对于using声明来说,我们只是简单地令名字在局部作用域内有效。相反,using指示是令整个命名空间的所有内容变得有效。通常情况下,命名空间中会含有一些不能出现在局部作用域中的定义,因此,using指示一般被看做是出现在最近的外层作用域中。
- 头文件如果在其顶层作用域中含有using指示或using声明,则会将名字注入到所有包含了该头文件的文件中。
- 当我们给函数传递一个类类型的对象时,除了在常规的作用域查找外还会查找实参类所属的命名空间。对传递类的引用或指针的 调用同样有效。
对于接受类类型实参的函数来说,其名字查找将在实参类所属的命名空间中进行。
- using声明语句声明的是一个名字,而非一个特定的函数。当我们为函数书写using声明时,该函数的所有版本都被引入到当前作用域中。
- 与using声明不同的是,对于using指示来说,引入一个与已有函数形参列表完全相同的函数并不会产生错误。此时,只要我们指明调用的是命名空间中的函数版本还是当前作用域的版本即可。
- 派生类的构造函数初始值列表将实参分别传递给每个直接基类。其中基类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始值列表中积累的顺序无关。
- 允许允许派生类从它的一个或几个基类中继承构造函数,但是如果从多个基类中继承了相同的构造函数(即形参列表完全相同),则程序将产生错误。
- 与只有一个基类的继承一样,多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行拷贝、移动或赋值操作。只有当派生类使用的是合成版本的拷贝、移动或赋值成员时,才会自动对其基类部分执行这些操作。在合成的拷贝控制成员中,每个基类分别使用自己的对应成员隐式地完成构造、赋值或销毁等工作。
- 在多重继承的情况下,相同的查找过程在所有直接基类中同时进行。如果名字在多个基类中都被找到,则对改名字的使用将具有二义性。
对于一个派生类来说,从它的几个基类中分别继承名字相同的成员时完全合法的,只不过在使用这个名字时必须明确指出它的版本。
- 虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
- 必须在虚派生的真实需求出现前就已经完成虚派生的操作。
虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。
- 指定虚基类的方式是在派生列表中添加关键字virtual。public和virtual的顺序随意。
- 因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,并且不会产生二义性。
- 含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最底层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序一次对其进行初始化。
虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。
一个类可以有多个虚基类。此时,这些虚的子对象按照它们在派生列表中出现的顺序从左向右依次构造。
- 当我们使用一条new表达式时,实际上执行了三步操作。第一步,new表达式调用一个名为operator new(或者operator new[])的标准库函数,该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象的数组)。第二步,编译器运行相应的构造函数以构造这些对象,并为其传入初始值。第三步,对象被分配了空间并构造完成,返回一个指向该对象的指针。
- 当我们使用一条delete表达式删除一个动态分配的对象时,实际执行了两步操作。第一步,对sp所指的对象或者arr所指的数组中的元素执行对应的析构函数。第二步,编译器调用名为operator delete(或者operator delete[])标准库函数释放内存空间。
- 我们可以使用作用域运算符令new表达式或delete表达式忽略定义在类中的函数,直接执行全局作用域中的版本。例如,::new只在全局作用域中查找匹配的operator new函数,::delete与之类似。
- 因为operator new用在对象构造之前,而operator delete用在对象销毁之后,所以这两个成员(new和delete)必须是静态的,而且它们不能操纵类的任何数据成员。当我们将上述运算符函数定义成类的成员时,他们是隐式静态的。我们无须显示地声明static,当然这么做也不会引发错误。
- 调用析构函数会销毁对象,但是不会释放内存。
- typeid运算符用于返回表达式的类型。
dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。
- dynamic_cast运算符使用形式:
dynamic_cast<type*>(e) //e必须是一个有效的指针
dynamic_cast<type&>(e) //e必须是一个左值
dynamic_cast<type&&>(e) //e不能是左值
type必须是一个类类型,并且通常情况下该类型应该含有虚函数。
e的类型必须符合以下三个条件中的任意一个:e的类型是目标type的公有派生类、e的类型时目标type的公有基类或者e的类型就是目标type的类型。
- 我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。
- 当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型。而当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才会求得。
- 注意:typeid应该作用于对象,因此使用*bp而不是bp。*bp获得动态绑定的对象,bp是指向静态对象的指针。
- C++包含两种枚举:限定作用域的和不限定作用域的。
限定作用域的枚举类型:enum class open_modes{input , output , append};
enum struct open_modes{input , output , append};
不限定作用域的枚举类型:enum color{red , yellow , green};
enum {red=6 , yellow=10 , green=20};
- 在限定作用域的枚举类型中,枚举成员的名字遵序常规的作用域准则,并且在枚举类型的作用域外是不可访问的。
在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同。
- 默认情况下,枚举值从0开始,依次加1。不过我们也能为一个或几个枚举成员指定专门的值。枚举值不一定唯一。如果我们没有显示地提供初始值,则当前枚举成员的值等于之前枚举成员的值加一。
- 枚举成员是const,因此在初始化枚举成员时提供的初始值必须是常量表达式。也就是说,每个枚举成员本身就是一条常量表达式。
- 只要enum有名字,我们就能定义并初始化该类型的成员。要想初始化enum对象或者为enum对象赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象。
- 一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型。因此,我们可以在任何需要整型值的地方使用它们。
- 实际上enum是由某种整数类型表示的。我们可以在enum的名字后面加上冒号以及我们想在该enum中使用的类型。
如果我们没有指定enum的潜在类型,则默认情况下限定作用域的enum成员类型是int。对于不限定作用域的枚举类型来说,其枚举成员不存在默认类型,我们只知道成员的潜在类型足够大,肯定能够容纳枚举值。
- 可以将一个不限定作用域的枚举类型的对象或枚举成员传给整型形参。
- 一般情况下,指针指向一个对象,但是成员指针指示的是类的成员,而非类的对象。类的静态成员不属于任何对象,因此无须特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有什么区别。
- 成员指针的类型囊括了类的类型以及成员的类型。当初始化一个这样的指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象。直到使用成员指针时,才提供成员所属的对象。
//pdata可以指向一个常量(非常量)Screen对象的string成员
const string Screen::*pdata;
当我们初始化一个成员指针(或向它赋值)时,需指定它所指的成员:
pdata = &Screen :: contents;
声明成员指针最简单的方法是使用auto或decltype:
auto pdata = &Screen :: contents;
- 当我们初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据。成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时我们才提供对象的信息。
- 指针访问运算符:.*和->*,这两个运算符使得我们可以解引用指针并获得该对象的成员。
- 想要通过一个指向成员函数的指针进行函数调用,必须首先利用.*运算符或->*运算符将该指针绑定到特定的对象上。因此与普通的函数指针不同,成员指针不是一个可调用对象,这样的指针不支持函数调用运算符。
- 嵌套类的名字在外层作用域中是可见的,在外层作用域之外不可见。
- union是一种特殊的类。一个union可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当我们给union的某个成员赋值以后,该union的其他成员就变成未定义的状态了。
- union不能含有引用类型的成员。含有构造函数或析构函数的类类型也可以作为union的成员类型。
- union可以为其成员指定public、protected和private等保护标记。默认情况下,union的成员都是公有的,这一点与struct相同。
- union既不能继承自其他类,也不能作为基类使用,所以union中不能含有虚函数。
- 定义一个未命名的对象,我们可以直接访问它的成员。
匿名union不能包含受保护的成员或私有成员,也不能定义成员函数。
- 类可以定义在某个函数的内部,我们称这样的类为局部类。局部类定义的类型只在定义它的作用域内可见。
局部类的所有成员(包括函数在内)都必须完整定义在类的内部。因此,局部类的作用与嵌套类相比相差很远。
在局部类中也不允许声明静态数据成员
- 如果局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类使用。
- 可以在局部类的内部再嵌套一个类。此时,嵌套类的定义可以出现在局部类之外。不过,嵌套类必须定义在与局部类相同的作用域中。
- 取地址符不能作用于位域,因此任何指针都无法指向类的位域。
- 每个函数的声明揭示其签名式,也就是参数和返回类型。
- const vector<int>::iterator iter = vec.begin(); //iter的作用像个T* const
vector<int>::const_iterator cIter = vec.begin(); //cIter的作用像个const T*