C++ primer笔记
文章目录
chapter1
在大多数系统中,main
的返回值被用来指示状态。返回值0表示成功,非0的返回值的含义由系统定义,通常用来指出错误类型。
main
函数的返回值类型必须为int。
所谓语句块(block),就是用花括号包围的零个或者多条语句的序列。语句块也是语句的一种,在任何要求使用语句的地方都可以使用语句块。
当我们使用一个istream
对象作为条件时,其效果是检测流的状态、如果流是有效的,即流未遇到错误,那么检测成功。当遇到文件结束符(EOF)或遇到一个无效输入时,istream
对象会变成无效。处于无效状态的istream
对象会使条件为假。
windows中:EOF——ctrl+z
Unix/Linux中:EOF——ctrl+d
一些常见的编译器可以检查出的错误:
- 语法错误(syntax error)
- 类型错误(type error)
- 声明错误(declaration error)
在c++中,我们通常通过定义一个类(class)来定义自己的数据结构。一个类定义了一个类型,以及与其关联的一组操作。
标准头文件一般不带后缀。
重定向:prog < infile >outfile
:从一个名为infile的文件中读取输入,输出到outfile。
成员函数(member function)是定义为类的一部分的函数,有时也被称为方法(method)。
endl
是一个被称为操作符(manipulator)的特殊值。写入endl
的效果是结束当前行,并将设备关联的缓冲区(buffer)中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都会真正写入输出流中,而不是仅仅停留在内存中等待写入流。
while (std::cin >> value)
:当遇到文件结束符(EOF)或遇到一个无效输入时,istream
对象的状态会变成无效。处于无效状态的istream
对象会使条件变成假。
一个类定义了一个类型以及与其关联的一组操作。
chapter 2
C++基本数据类型:算术类型(arithmetic type):字符、整型数、布尔值和浮点数;空类型(void)。
符号 | 名称 | 最小尺寸(具体尺寸因编译器和平台而异) |
---|---|---|
bool | 布尔类型 | 未定义 |
char | 字符类型 | 8bit |
wchar_t | 宽字符 | 16bit |
char16_t | Unicode 字符 | 16bit |
char32_t | Unicode 字符 | 32bit |
short | 短类型 | 16bit |
int | 整型 | 16bit |
long | 长整型 | 32bit |
long long | 长整型 | 64bit |
float | 单精度浮点型 | 6位有效数字 |
double | 双精度 | 10位有效数字 |
long double | 扩展精度 | 10位有效数字 |
C++规定:一个int
至少和一个short
一样大,一个long
至少和一个int
一样大,一个long long
至少和一个long
一样大。通常,float
以一个字(32位)来表示,double
以2个字(64位)来表示,long double
以3或4个字(96位或128位)来表示。
尽管整型字面值可以存储在带符号数据类型中,但严格来说,十进制字面值不会是负数。如果我们定义了一个形如-42的负十进制字面值,那个负号并不在字面值之内,他的作用仅仅是对字面值取负值而已。
true
和false
是布尔类型的字面值,nullptr
是指针字面值。
在C++语言中,初始化和赋值是两个完全不同的操作。初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
列表初始化
int var{0};
作为C++11标准的一部分,用花括号来初始化变量得到了全面应用——列表初始化(list initialization)。当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。
为了支持分离式编译,C++语言将声明和定义区分开来。
声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字,则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量。extern语句如果包含初始值就不再是声明了,而变成定义了。
任何包含了显示初始化的声明即成为定义。
在函数体内部如果试图初始化一个由extern关键字标记的变量将引发错误。
大多数计算机以2的整数次幂个比特作为块来处理内存,可寻址的最小内存块称为字节(byte),存储的基本单元称为字(word),它通常由几个字节组成。
类型int
,short
,long
和long long
都是带符号的,通过在这些类型名前添加unsigned
就可以得到无符号类型。
与其他类型不同,字符类型被分成三种:char
,unsigned char
和signed char
。类型signed char
和char
并不一样。尽管字符类型有三种,但是字符的表现形式却只有两种:带符号和无符号的。类型char
实际上会表现为上述两种形式中的一种,具体是哪一种由编译器决定。
当一个算术表达式中既有无符号数又有int
值时,那个int
值会转换成无符号数。
当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不是一个负值。
字面值常量(literal),每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。
转义序列(escape sequence)
变量提供一个具名的,可够程序操作的存储空间。C++中的每个变量都有其数据类型。数据类型决定着变量所占内存空间的大小和布局方式,该空间能存储的值的范围,以及变量能参与的运算。
对C++程序员来说,变量(variable)和对象(object)一般可以互换使用。
如果要在多个文件中使用同一变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝不能对其重新定义。
复合类型(compound type)是指基于其他类型定义的类型。
引用:引用必须初始化。定义引用时,程序把他和它的初始值绑定在一起,而不是将初始值拷贝给引用。
引用本身不是一个对象,不能定义引用的引用。
引用只能绑定到对象上,而不能与字面值或某个表达式的计算结果绑定在一起。一旦定义了引用,就无法令其再绑定到其他的对象。
指针:指针就是一个对象,指针无需在定义时赋值。
C++11引入nullptr
字面值。
NULL预处理变量是在cstdlib
中定义的,预处理变量不属于命名空间std
。
**顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)**表示指针所值的对象是一个常量。
更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都是适用的。底层const则与指针和引用等复合类型的基本类型部分有关。
当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能狗转换。
常量表达式是指值不会改变且在编译过程中就能得到计算结果的表达式。
字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定。
chapter 3
using namespace::name
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去。
string
标准库的string
表示可变长的字符序列,使用string
类型要先包含string
头文件。string
定义在命名空间std
中。
如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化。
标准库允许把字符字面值和字符串字面值转换成string
对象。
在名为cname
的头文件中定义的名字从属于命名空间std
,而定义在名为.h
的文件中的则不然。
vector
标准库类型vector
表示对象的集合,其中所有的对象的类型都相同。
vector
定义在命名空间std
中。
vector
是一个类模板。模板本身不是类或函数,编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器应该把类或函数实例化成何种类型。
vector
能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector
。
- 在使用拷贝初始化时(即使用=时),只能提供一个初始值
- 如果提供的时一个类内初始值,则只能使用拷贝初始化或者使用花括号的形式初始化
- 如果提供的是初始化元素的列表,则只能把初始值都放在花括号里进行列表初始化,而不是放在圆括号里。
在某些情况下,初始值的真实含义依赖于传递初始值时用的是花括号韩式圆括号。如果使用的是圆括号,可以说提供的值是用来构造(construct)vector对象的。如果使用的是花括号,可以表述成我们想列表初始化(list initialize)该vector对象。只有在无法执行列表初始化时才会考虑其他初始化方式(要想列表初始化vector对象,花括号里的值必须与元素类型相同)。
迭代器
除了vector外,标准库还定义了其他几种容器。所有的标准容器都可以使用迭代器,但其中只有少数几种才同时支持下标运算符。
和指针类似,也能通过解引用迭代器来获取它所指示的元素,执行解引用的迭代器必须合法并确实指示着某个元素。
数组
与vector一样,数组的元素应为对象,因此不存在引用的数组。
如果表达式的内容是解引用操作,则decltype
将得到引用类型。
decltype((var))
的结果永远是引用,而decltype(var)
的结果只有当var
本身就是一个引用时才是引用。
当使用数组作为一个auto变量的初始值时,推断得到的类型时指针而非数组。
当使用decltype
关键字时,得到的类型为对应的数组类型。
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(在内存中的位置)。
赋值运算符需要一个(非常量)左值作为其左值运算对象,得到的结果也仍然是一个左值。
取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。
chapter 4
表达式由一个或多个运算对象(operand
)组成,对表达式求值将得到一个结果(result
)。字面值和变量是最简单的表达式(expression
)。其结果就是字面值和变量的值。
运算符优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。
递增(++
)和递减(--
)运算符有两种形式,前置版本和后置版本。前置版本将对象本身作为左值返回,右值版本则将对象原始值的副本作为右值返回。
左移运算符(<<
)在右侧插入值为0的二进制位。右移运算符(>>
)的行为则依赖于其左侧运算对象的类型:如果运算对象为无符号类型,则在左侧插入值为0的二进制位;如果为带符号型,则在左侧插入符号位的副本或者值为0的二进制位,如何选择视具体环境而定。
根据取余运算的定义,如果m和n是整数且n非0,则表达式(m/n)*n+m%n
的求值结果与m相等。隐含的意思是,如果m%n
不等于0,则它的符号和m相同。
除了-m导致溢出的特殊情况,其他时候(-m)/n
和m/(-n)
都等于-(m/n)
,m%(-n)
等于m%n
,(-m)%n
等于-(m%n)
。
位求反运算符,char类型的运算对象首先提升成int类型,提升时运算对象原来的位保持不变,往高位添加0即可。
sizeof运算符
sizeof
并不会计算运算对象的值。
sizeof*p=>sizeof(*p)
,因为sizeof并不计算运算的值,所以即使p是一个无效的指针也不会有什么影响。在sizeof的运算对象中解引用一个无效指针仍是一种安全的行为,因为指针实际上并没有被真正使用。sizeof不需要真的解引用指针也能知道它所指的对象的类型。
强制类型转换
cast-name <type> (expression)
- cast-name: static_cast,dynamic_cast,const_cast,reinterpret_cast
- type:目标类型,如果type是引用类型,则结果为左值
- expression:要转换的值
static_cast
任何具有明确定义的类型转换,只要不包含底层const
,都可以使用static_cast
。
const_cast
const_cast
只能改变运算对象的底层const
。
对于常量对象转换成非常量对象的行为,一般称为“去掉const
性质”。一旦去掉某个对象的const
属性,编译器就不再阻止我们对该对象进行写操作了。如果对象本身是一个常量,再使用const_cast
执行写操作就会产生未定义的后果。
只有const_cast
能改变表达式的常量属性,其他形式的命令强制类型转换改变表达式的常量属性都会引发编译器错误。
也不能用const_cast
改变表达式类型。
reinterpret_cast
reinterpret_cast
通常为运算对象的位模式提供较低层次上的重新解释。
chapter 6
initializer_list类型
std::initializer_list<T>
类型对象是一个访问 const T
类型对象数组的轻量代理对象。
与vector
不同的是,initializer_list
对象中的元素永远是常量值,我们无法改变initializer_list
对象中元素的值。
我们通过调用运算符(call operator)来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针。圆括号之内是一个用逗号隔开的实参列表,我们用实参初始化函数的形参。
尽管实参和形参存在对应关系,但是并没有规定实参的求值顺序。
- 名字的作用域是程序文本的一部分,名字在其中可见
- 对象的生命周期是程序执行过程中该对象存在的一段时间
大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj(windows)
或者.o(Unix)
的文件,后缀名的含义是该文件包含对象代码(object code)
顶层const作用于对象本身,当用实参初始化形参时,会忽略定策const。当形参有顶层const时,传给他常量对象或者非常量对象都是可以的。
我们可以使用非常量初始化一个底层const对象,但是反过来不可以,同时一个普通的引用必须用同类型的对象初始化。
我们不能表达const对象,字面值或者需要类型转换的对象传递给普通的引用形参。
int main(int argc, char *argv[]){...}
argv是一个数组,它的元素是指向C风格字符串的指针。也可以定义为
int main(int argc, char **argv){...}
其中argv指向char*
返回数组指针的函数:
Type (*function(parameter_list))[dimension]
return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数地返回类型。
返回一个值地方式和初始化一个变量或形参地方式完全一样:返回地值用于初始化调用点地一个临时量,该临时量就是函数调用地结果。
不要返回局部变量对象的引用或者指针。
调用一个返回引用的函数返回左值,其他返回类型得到右值。
我们允许main函数没有return 语句直接结束。如果控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0地return语句。
尾至返回类型
C++11中增加了尾至返回类型,任何函数地定义都能使用尾置返回。尾置返回类型跟在形参列表后面并以一个->
符号开头。为了表示函数真正地返回类型跟在形参列表之后,我们在本该出现返回类型地地方放置auto:
auto func(int i)->int(*)[10]
函数重载
如同一作用域内地几个函数名字相同但形参列表不同,我们称之为重载函数。
当调用这些函数时,编译器会根据传递地实参类型推断出想要地是哪个函数。
不允许两个函数除了返回类型外其他所有地要素相同。
顶层const不影响传入函数地对象,一个拥有顶层const地形参无法和另一个没有顶层const的形参区分开来。
另一方面,如果形参是某种类型的指针或者引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时const是底层的。
当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
当调用重载函数时,有三种可能的结果:
- 编译器找到一个与实参最佳匹配的函数,并生成调用函数的代码
- 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误。
- 有多于一个函数可匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用。
重载对于作用域的一般性质并没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名。
在C++中,名字查找在类型检查之前。
既可以在类的声明中,也可以在函数定义中声明缺省参数,但不能既在类声明中又在函数定义中同时声明缺省参数。
和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。对于某个给定的内联函数或者constexpr函数来说,它的定义必须完全一致。所以,内联函数和constexpr函数通常定义在头文件中。
constexpr函数不一定返回常量表达式。
编译器定义的几个局部静态变量。
__FILE__
存放文件名的字符串字面值__LINE__
存放当前行号的整型字面值__TIME__
存放文件编译时间的字符串字面值__DATA__
存放文件编译日期的字符串字面值__FUNC__
存放函数的名字
函数匹配
第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数,候选函数具备两个特征:一是与被调用函数重名;二是其声明在调用点可见。
第二步是考察本次调用提供的实参,然后从候选集中选出能被这组实参调用的函数,这些新选出函数称为可行函数。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能够转换为形参的类型。
第三步是从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数调用提供的实参,寻找形参与实参类型最匹配的那个可行函数。最匹配的基本思想是:实参类型与形参类型越接近,它们匹配越好。
如果在检查了所有实参后没有任何一个函数脱颖而出,则该调用是错误的。编译器将报告二义性调用的信息。
当我们把函数名作为一个值使用时,该函数自动地转换为指针。
pf = func
与pf=&func
等价
还能直接使用指向函数地指针调用该函数,无须提前解引用。
pf(a,b)
、(*pf)(a,b)
和func(a,b)
等价。
为了确定最佳匹配,编译器将实参类型到形参类型地转换分成几个等级:
- 精确匹配包括:
- 实参类型和形参类型相同
- 实参从数组类型或函数类型转换成对应地指针类型
- 向实参添加顶层const或者从实参中删除顶层const
- 通过const转换实现的匹配
- 通过类型提升实现的匹配
- 通过算术类型转换或指针转换实现的匹配
- 通过类类型转换实现的匹配
chapter 7
this
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。
在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为this所指的正是这个对象。任何对类成员的直接访问都被看成是this的隐式引用。
任何自定义为this的参数或变量的行为都是非法的。我们可以在成员函数内部使用this,因为this的目的总是指向“这个”对象,所以this是一个常量指针,不允许改变this中保存的地址
const成员函数
void func() const;
成员函数在紧随参数列表之后加上const关键字,这里const的作用是修改隐式this指针的类型。
默认情况下,this的类型是指向类类型非常量版本的常量指针。尽管this是隐式的,但它仍需要遵循初始化规则,意味着(在默认情况下)我们不能把this绑定到一个常量对象上,这使得不能在一个常量对象上调用普通的成员函数。
C++的做法是允许把const关键字放在成员函数的参数列表之后,此时,紧随在参数列表后面的const表示this是一个指向常量的指针。
编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体,因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。
IO类属于不能被拷贝的类型,因此只能通过引用来传递它们。
构造函数
构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数没有返回类型。类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或参数类型上有所区别。
构造函数不能被声明成const的,当我创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此构造函数在const对象的构造过程中可以向其写值。
如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数。
编译器创建地构造函数又被称为合成地默认构造函数。对大多数类来说,合成的默认构造函数将按照如下规则初始化类的数据成员:
- 如果存在类内初始值,用它来初始化成员。
- 否则,默认初始化该成员。
构造函数初始值列表
class A
{
public:
A():var1(0){}
private:
int var1;
}
其中花括号定义了(空的)函数体,冒号和花括号之前的代码称为构造桉树初始值列表(constructor initialize list),它负责为新创建的对象的一个或几个数据成员赋初值。
对于一个普通的类来说,必须定义它自己的默认构造函数,原因有三:
- 编译器只有在发现类不包含任何构造函数的情况下,才会替我们生成一个默认的构造函数。一旦我们定义了一些其他的构造函数,那么除非我们再定义一个默认的构造函数,否则类将没有默认构造函数。这条规则的依据是:如果一个类再某种情况下需要控制对象的初始化,那么该类很可能在所有的情况下都需要控制。
- 对于某些类来说,合成的默认构造函数可能执行错误的操作,含有内置类型或复合类型成员的类应该在类的内部初始化它的这些成员,或者定义一个自己的默认构造函数。否则,用户在创建类的对象时可能得到未定义的值。
- 有的时候,编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员,而且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。
在c++11标准中,如果需要默认的行为,那么可以通过在参数列表后面写上=default
来要求编译器生成构造函数。其中=default
既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果=default
在类内部,则默认构造函数是内联的;如果它在类外部,则该成员函数默认情况下不是内联的。
当成员属于某种类型且该类型没有定义默认构造函数时,也必须将这个成员初始化。
构造函数初始值列表只说明用于初始化成员地值,而不限定储时化地具体执行顺序。
成员初始化地顺序与他们在类定义中地出现顺序一致。
如果一个构造函数为所有地参数都提供了默认实参,则它实际上也定义了默认构造函数。
访问控制与封装:
在c++中,使用访问说明符(access specifiers)加强类的封装性
- 定义在public说明符之后的成员在整个程序内可以被访问,public成员定义类的接口
- 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部封装了类的实现细节。
友元:
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。友元声明只能出现在类定义的内部,但是类内出现的具体位置不限。友元不是类的成员,也不受它所在区域访问控制级别的约束。
友元声明的作用是影响访问权限,它本身并非普通意义上的声明。
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
友元关系不存在传递性。
定义在类内部的成员函数是自动inline的。
mutable变量:
可以在类的内部把inline作为声明的一部分显式地声明成员函数。同样的,也能在类的外部用inline关键字修饰函数地定义。
一个可变数据成员永远不会是const,即使他是const对象地成员。因此,一个const成员函数可以改变一个可变成员地值。
名字查找(寻找所用名字最匹配地声明地过程)地过程:
- 首先,在名字所在地块中寻找其声明语句,只考虑在名字地使用之前出现地声明
- 如果没有找到,继续查找外层作用域
- 如果最终没有找到匹配地声明,则程序报错
对于定义在类内部地成员函数来说,解析其中名字地方法与上述地查找规则有所区别。类的定义分成两步处理:
- 首先,编译成员地声明
- 直到类全部可见后才编译函数体
编译器处理完类中地全部声明后才会处理成员函数地定义。
注意:声明中使用地名字,包括返回类型或者参数列表中使用地名字,都必须在使用前确定可见。
一旦遇到类名,那么定义地剩余部分就在类地作用域之内了,这里地剩余部分包括参数列表和函数体。结果就是,我们可以直接使用类的其他成员而无需再次授权了。
另一方面,函数地返回值类型通常出现在函数名之前。因此当成员函数定义在类地外部时,返回类型中使用地名字都位于类地作用域之外。此时返回类型必须指明它是哪个类地成员。
如果成员是const、引用或者某种未提供默认构造函数地类类型,我们必须通过构造函数地初始值列表为这些成员提供初始值。
在很多类中,初始化和赋值地区别事关底层效率问题:前者直接初始化数据成员,后者则先初始化再赋值。
当对象被默认初始化时自动执行默认构造函数。默认构造函数在以下情况下发生:
- 当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时
- 当一个类本身含有类类型地成员且使用合成地默认构造函数时
- 当类类型地成员没有在构造函数初始值列表中显式地初始化时
值初始化在以下情况下发生:
- 在数组初始化地过程中如果我们提供地初始值数量少于数组地大小时
- 当我们不使用初始值定义一个局部静态变量时
- 当我们通过书写形式入
T()
地表达式显式地请求值初始化时,其中T
是类型名
如果想定义一个使用默认构造函数进行初始化的对象,正确的方法是去掉对象名之后的空的括号对:(MyClass obj)
当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体一次被执行。
类的静态成员存在与任何对象之外,对象中不包含任何与静态数据成员有关的数据。
静态成员函数也不与任何对象绑定在一起,他们不包含this指针。作为结果,静态成员函数不能声明成const的,而且也不能在static函数体内使用this指针。
在要求隐式转换的程序上下文中,我们可以通过将构造函数声明成explicit
加以阻止。
关键字explicit
只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit
。只能在类内声明构造函数时使用explicit
关键字,在类外,定义时不重复。
explicit
构造函数只能用于直接初始化。
发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=)。此时,我们只能使用直接初始化而不能使用explicit
构造函数。
虽然静态成员不属于类的某个对象,但是我们仍然能够使用类的对象、引用或者指针来访问静态成员。
成员函数不用通过域运算符就可以直接使用静态变量。
当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句。
chapter 9
顺序容器
顺序容器类型:
vector | 可变大小数组。支持快速随机访问 |
---|---|
deque | 双端队列。支持快速随机访问 |
list | 双向链表 |
forward_list | 单向链表 |
array | 固定大小数组。支持快速随机访问,不能添加或删除元素 |
string | 与vector类似的容器,专门用于保存字符 |
新标准库容器的性能几乎肯定与最精心优化过的同类型数据结构一样好(通常会更好)。现代c++程序应该使用标准库容器,而不是更原始的数据结构,比如内置数组。
顺序容器的添加/删除操作(会改变容器大小,array不支持这些操作)
emplace
操作构造函数而不是拷贝元素。
在调用emplace_back时,会在容器管理的内存空间中直接创建对象,而调用push_back则会创建一个局部临时对象,并将其压入容器中。
包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有一个back成员函数。这两个操作分别返回首元素和尾元素的引用。若容器为空,函数行为未定义。
在容器中访问元素的成员函数(即,front,back,下标和at)返回的都是引用。
在一个forward_list中添加或者删除元素的操作是通过改变给定元素之后的元素来完成的。forward_list定义了名为insert_after
、emplace_after
和erase_after
的操作。
容器大小管理操作:
C.shrink_to_fit() | 将capacity()减少为与size()相同大小,只是用于vector,string,deque |
---|---|
C.capacity() | 不重新分配内存空间的话,C可以保存多少元素,只适用于vector和string |
C.reserve(n) | 分配至少能够容纳n个元素的内存空间 |
调用reserve不会减少容器占用的内存空间,类似的,resize成员函数只改变容器中元素的个数,而不是容量。
容器适配器:
- stack
- queue
- priority_queue
本质上,一个适配器是一种机制,能是某种事物的行为看起来像另外一个事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。我们可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
chapter 12
在c++中,动态内存的管理时通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象并释放与之关联的内存。
新的标准库提供两种智能指针类型来管理动态对象。
智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。
程序使用动态内存出于以下三种原因之一:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象之间共享数据
用delete释放一块并非new分配的内存,或者将相同的指针释放两次,其行为是未定义的。
虽然一个const对象的值是不能被改变,但它本身是可以被销毁的。
chapter 13
当定义一个类时,我们显式地或隐式地指定在此类型地对象拷贝、移动赋值和销毁时做什么。
一个类通过定义五种特例地成员函数来控制这些操作,包括:拷贝构造函数(copy constructor)拷贝赋值运算符(copy-assignment operator)、移动构造函数(move constructor)、移动赋值运算符(move-assignment operator)和析构函数(destructor)。
拷贝和移动构造函数定义了当用同类型地另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型地另一个对象时做什么。
析构函数定义了当此类型对象销毁时做什么。
我们称这些操作为拷贝控制操作(copy control)
如果一个类没有定义所有这些拷贝控制函数,编译器会自动为它定义缺失的操作。因此,很多类会忽略这些拷贝控制操作。但是,对一些类来说,依赖这些操作地默认定义会导致灾难。
通常,实现拷贝控制操作最困难地地方是首先认识到什么时候需要定义这些操作。
拷贝初始化通常使用拷贝构造函数来完成。但是,如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。
拷贝初始化不仅在我们用=
定义变量时会发生,在下列情况下也会发生:
- 将一个对象做为实参传递给一个非引用类型地形参
- 从一个返回类型为非引用类型地函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
chapter 14
如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此,成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个。
不能被重载的运算符:
- 作用域解析运算符:
::
- 成员指针访问运算符:
.*
- 成员访问运算符:
.
- 三元运算符,条件运算符
?:
- sizeof长度运算符
选择作为成员或者非成员:
- 赋值(=),小标(
[]
),调用(()
)和成员访问箭头(->
)运算符必须是成员 - 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同
- 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增,递减和解引用运算符,通常应该是成员。
- 具有对称性质的运算符看了能转换任意一端的运算对象,例如算术、相等性,关系和位运算符等,因此他们通常应该是普通的非成员函数。
- 输入输出运算符必须是非成员函数
point->mem
的执行过程:
- 如果point是指针,则我们应用内置的箭头运算符,表达式等价于
(*point).mem
。首先,解引用该指针,然后从所得的对象中获取指定的成员。如果point所指的类型没有名为mem的成员,程序会发生错误。 - 如果point是定义了
operator->
的类的一个对象,则我们使用point.operator->()
的结果来获取mem。其中,如果该结果是一个指针,则执行第一步;如果该结果本身含有重载的operator->()
,则重复调用当前步骤。最终,当这一过程结束时程序或者返回了所需的内容,或者返回一些表示陈旭错误的信息。
当我们编写一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象。在lambda表达式产生的类中,含有一个重载的函数调用运算符。
lambda是函数对象。
标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命令操作的调用运算符。
c++语言中有几种可调用的对象:函数、函数指针、lambda表达式bind创建的对象以及重载了函数调用运算符的类。
调用形式(call signature),指明了调用返回的类型以及传递给调用的实参类型。
一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const的。
other
类 | class |
---|---|
对象 | object |
构造函数 | constructor |
析构函数 | destructor |
运算符 | operator |
改写 | override |
重载 | overloading |
封装 | encapsulation |
继承 | inheritance |
动态绑定 | dynamic binding |
虚函数 | virtual function |
多态 | polymorphism |
成员函数 | member function |
成员变量 | date member |
基类 | base class |
派生类 | derived class |