精读《C++ primer》学习笔记(第四至六章)

第四章
重要知识点:
4.1 基础
函数调用是一种特殊的运算符,它对运算对象的数量没有限制。
重载运算符时可以定义运算对象的类型,返回值类型,但运算对象的个数,运算符的优先级,结合律无法改变。
当一个对象被用作左值时,使用的是对象在内存中的位置,当用作右值时,使用的是对象的值。左值有时可以当做右值使用,但右值不能当做左值使用。
decltype()函数中,如果表达式的求值结果是左值,则函数得到一个引用类型。
以下由于求值顺序问题造成的结果未定义错误:

int i = 0;
cout << i << " " << ++i << endl; // 结果输出不确定

&&||?:,运算符均确定了求值顺序。大多数二元运算符没有规定求值顺序是为了给编译器留下优化余地。
求值顺序与优先级和结合律无关。
Notes: 如果改变了某个运算对象的值,在表达式的其他地方就不要再使用这个运算对象。
4.2 算术运算符

bool b = true;
bool b2 = -b; // b2也是true
// 所以bool类型不应参与算术运算,以免误解

在上例中bool类型被提升为int,而后再转换为bool
当一元正号作用于指针时,返回的运算对象值是类型提升后的副本。

m % (-n) == m % n

4.4 赋值运算符
赋值运算符的结果是它的左值运算对象,并且是一个左值。
vector模板重载了赋值运算符并且可以接收初始值列表,发生赋值时用右侧运算对象替换左侧运算对象的元素。

vector<int> vi;
vi = {0, 1, 2, 3, 4, 5};

赋值满足右结合律,从右向左运算。
4.5 递增和递减运算符
有些迭代器不支持算术运算符,所以此时递增和递减运算符除了书写简洁外还是必须的。前置版本得到左值,后置版本得到右值。
Note: 不要使用后置版本,后置版本需要将初始值存储下来以便于返回这个未修改的内容,如果不需要修改前的值的,会造成多余的性能开销。
后置运算符的优先级高于解引用运算符:

cout << *iter++ << endl; // 记住这种写法

输出开始时指向的元素,并将指针前移。
如下的代码是未定义的,因为求值顺序的问题:

*beg = toupper(*beg++);

4.6 成员访问运算符
ptr->mem(*ptr).mem等效,解引用运算符的优先级低于点运算符,所以要加括号。
4.7 条件运算符
条件运算符满足右结合律,所以可以在右端嵌套,但不要超过两到三层。

cout << ((grade < 60) ? "fail" : "pass");

条件运算符优先级很低,所以上面式中的两层括号都不能省略。
4.8 位运算符
如果运算对象是小整型,将被提升为较大的整数类型。运算对象如果是带符号的且为负值, 则如何处理符号位依赖于机器。如果是右移运算符且为有符号数的话,在左侧插入符号位的副本。
移位运算符右侧的对象不能为负,而且要严格小于结果的位数。
移位运算符优先级高于算术运算符,但低于关系运算符,赋值运算符,条件运算符。
4.9 sizeof运算符
sizeof运算符返回size_t类型值。
两种形式:

sizeof (type)
sizeof expr

sizeof并不去计算表达式的值,decltype也是如此。在sizeof中解引用一个无效指针仍然是一种安全的行为,而且对于类成员的大小计算无须获取类对象。

Sales_data data, *p;
sizeof(Sales_data);
sizeof data; // 效果同上行
sizeof p;
sizeof *p;
sizeof data.revenue;
sizeof Sales_data::revenue; // 可以用作用域符来访问

对数组执行该运算将得到整个数组的大小,并不会把数组转换成指针来处理。对stringvector对象执行该运算只会返回固定部分的大小,不会计算对象中的元素占用了多少空间。
4.11 类型转换
如果无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的,如果带符号类型大于无符号的,则结果依赖于具体的机器。比如longunsigned int,如果intlong大小相同,则转换成无符号型,如果long大于int,则转换成long
数组常自动转换成指针,当用作decltype参数,或者&,sizeof,typeid等运算符的运算对象时,上述转换不会发生。
指针的转换:常量整数值0或者字面值nullptr能转换成任意指针类型,指向任意非常量的指针能转换成void*,指向任意对象的指针能转换成const void*
static_cast具有明确定义的类型转换,只要不包括底层const,都可以使用static_cast。如果是转换void*指针的话,必须保证转换后的类型与原先转向void*的指针所指的类型一致。
const_cast常用于有函数重载的上下文中,不能用来改变表达式的类型,只能用来去掉const性质,或者相反的转换。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为,反之不然。
reinterpret为位模式提供较低层次的重新解释,关键在于类型改变了,但在语法上又是正确的,所以存在潜在的问题。

第五章:
重要知识点:
5.1 简单语句
表达式语句的作用是执行表达式并丢弃掉求值结果。
5.3 条件语句
如果switch语句中某几个case对应同一个操作,可以写到同一行中,强调是某个范围内的值。
case标签必须是常量表达式;
即使default标签下不打算做任何事,定义这个标签也是有用的,可以告诉读者,已经考虑了默认的情况,只是目前还什么都没有做。
标签之后如果没有标签,不该孤零零地出现,所以default之后应该加上一个空语句。
如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处调到后一处的行为是非法的。即不允许跳过变量的初始化语句直接转到该变量作用域的另一个位置。
可以把变量定义在块内,则之后的所有case便签都在变量的作用域之外了。
5.4 迭代语句
range for可以用来遍历初始值列表,数组,vector, string等类型的对象,这种类型的特点是拥有能返回迭代器的beginend成员。
每次迭代器都会重新定义循环控制变量,并将其初始化成序列中的下一个值。在range for语句中,预存了end()的值,一旦在序列中添加或者删除元素,则该函数的值将失效。
do-while循环中,循环的条件不能定义在do内部。
5.5 跳转语句
goto语句中,标签标示符可以和程序中其他实体的标示符使用同一个名字而不相互干扰。
5.6 try语句块和异常处理
try语句块内声明的变量在块外部无法访问,特别是在catch子句内也无法访问。
如果在抛出异常的函数中,没有找到能够处理异常的catch子句,则将继续搜索上层调用,直到terminate标准库函数,执行该函数将导致程序非正常退出。
对于那些需要处理异常并继续执行的程序,我们必须清楚异常何时发生,以及之后如何确保对象有效,资源无泄漏,程序处于合理状态。
只能以默认初始化的方式初始化exceptionbad_allocbad_cast对象,只能以string对象或者C风格的字符串初始化其他异常类型的对象。
第六章
重要知识点:
6.1 函数基础
通过调用运算符来执行函数,它作用于一个表达式,该表达式是函数或者指向函数的指针,调用表达式的类型是函数的返回类型。
没有规定实参的求值顺序。
有个别形参不会被用到,则此类形参通常不命名以表示在函数体内不会使用它。
自动对象: 只存在于块执行期间的对象称为自动对象,形参是一种自动对象。
局部静态变量定义成static类型,自创建开始到程序结束时销毁。
注意区分默认初始化值初始化,自动对象执行的是前者,局部静态变量执行的是后者。
函数原型:类似于变量,函数的名字必须在使用之前声明。只能定义一次,但可以声明多次,如果一个函数永远不会被用到,那么它可以只有声明没有定义。如果有必要,可以写上形参的名字,帮助读者理解函数的作用。
函数声明定义在头文件中可以确保同一函数的所有声明保持一致,而且如果需要改变函数的接口,只需要改变一条声明即可。
编译可以用-c参数。
6.2 参数传递
在C++语言中,建议使用引用类型的形参替代指针,因为引用执行的是引用传递,而指针是值传递,涉及到指针的拷贝。

void fcn(const int i) {}
void fcn(int i) {}

错误,函数重复定义了。因为在传入形参时,顶层const会被忽略。
应当尽量使用常量引用。
数组有两个特性:不允许拷贝数组和使用数组时会将其转换为指针。
一开始函数并不知道数组的确切尺寸,所以应该提供一些额外的信息。
有三种方法:
数组本身包含一个结束标记;
传递指向数组首元素和尾后元素的指针;
显式传递一个表示数组大小的形参。
形参可以使数组的引用,

f(int &arr[10]) // 错误,将其声明成了引用的数组
f(int (&arr)[10]) // 正确

这一用法中数组大小是数组类型的一部分,所以这也限制了函数的可用性,不可以传递大小不是10的数组。
传递多维数组时传递的是一个指向数组的指针。两种写法:

int (*matrix)[10];
int matrix[][10];

注意括号不能省略:

int *matrix[10];
int (*matrix)[10];

main函数的形参有两个:

int argc, char *argv[];
int argc, char **argv;

第二个形参是一个数组,它的元素是指向C风格字符串的指针,数组的第一个元素是程序名或者空字符串,最后一个指针之后的元素值保证为0。
有两种方法处理传递实参数量不定的函数,如果实参类型相同,用initializer_list,如果实参类型不同,用可变参数模板。
C++还有一种特殊的形参类型,即省略符,一般用于与C函数交互的接口程序。
initializer_list定义在同名的头文件中,这也是一种模板类型,但其中的元素永远是常数值。如果要向形参中传递一个值的序列,则必须把序列放到一对花括号内。

lst2(lst);
lst2 = lst;

vector不同的是,这种类型的赋值和拷贝不会拷贝列表中的元素,拷贝后,原始列表和副本将共享元素。
省略符形参只能出现在形参列表的最后一个位置,而且只用于C和C++的通用类型,这些代码使用了varargsC标准库的功能。
省略符对应的实参无须类型检查,形参声明后面的逗号是可选的。

void foo(parm_list, ...);
void foo(...);

6.3 返回类型和return语句
返回void的函数不要求一定有return语句,因为在函数的最后一句后面会隐式地执行return
函数返回的值用于初始化调用点的一个临时量。
返回局部对象的指针或者引用是错误的。

const string &manip()
{
    string ret;
    if (!ret.empty())
        return ret;
    else
        return "Empty"; // 错误,字符串字面值转为局部临时`string`对象
}

调用运算符的优先级和点运算符,箭头运算符一样,符合左结合律。
函数可以返回花括号包围的值的列表,表示对函数返回的临时量进行初始化。
cstdlib头文件中定义了两个预处理变量来表示程序执行的成功与失败。

int main()
{
    if (some_failure)
        return EXIT_FAILURE;
    else
        return EXIT_SUCCESS;
}

main函数不能调用自己。
返回数组的指针时可以使用类型别名,如果不使用别名,则必须记住数组的维数:

int (*p2)[10] = &arr; // 指向10个整数数组的指针
int (*func(int i))[10]; // func函数,返回指向数组的指针

使用尾置返回类型

auto func(int i) -> int(*)[10];

使用decltype

int odd[] = {1, 3, 5};
decltype(odd)* arrPtr(int i){
//return ...
}

decltype并不负责把数组类型转换成指针,所以其结果还是一个数组,要想表示返回指针,还需要加一个*符号。
6.4 重载函数
顶层const不影响传入函数的对象,所以不能用顶层const形参的不同区分函数,但底层const可以实现函数的重载。当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
最好只重载那些确实非常相似的操作。
一旦在当前的作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。
6.5 特殊用途语言特性
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
函数可以多次声明,后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧所有形参必须有默认值。
局部变量不能作为默认实参。
在返回类型前加上inline就可以定义成内联函数了。
constexpr函数的返回类型和所有形参都必须是字面值类型,而且有且只有一条return语句。constexpr函数被隐式指定为内联函数。当其实参是常量表达式时,返回值也是常量表达式,反之不然。当用在需要常量表达式的上下文中时,编译器会检查函数的结果是否符合要求。
一般把内联函数和constexpr函数定义在头文件中,便于展开。
C++程序员有时用到一种头文件保护的技术,写一些代码在调试时使用,准备发布时先屏蔽掉代码。
assert是一种预处理宏,如果表达式为假,输出信息并终止程序的执行,如果表达式为真,则什么都不做。

assert(expr);

该宏定义在cassert头文件中,常用于检测不能发生的条件。不能用来替代程序的运行时检查,以及程序本身包含的错误检查。
定义了NDEBYGassert什么也不做,否则将执行运行时检查。

#ifdef NDEBUG
cerr << __func__ << endl;
#endif

__FILE__:存放文件名的字符串字面值;
__LINE__:存放当前行号的整型字面值;
__TIME__:存放文件编译时间的字符串字面值;
__DATE__:存放文件编译日期的字符串字面值。
6.6 函数匹配
如果有一个函数的每个实参匹配都不劣于其他可行函数的匹配,而且至少有一个实参的匹配优于其他可行函数的匹配,那么就可以选择该函数,否则报告二义性错误。
调用重载函数时应避免强制类型转换,如果需要强转,说明我们设计的形参不够好。
所有算术类型转换的级别一样。
6.7 函数指针
函数指针指向函数而非对象,函数的类型由返回值和形参列表共同决定。所以要想声明一个函数指针,只需要用指针替换函数名即可。如:

bool (*pf)(const string&, const string&);

括号不能省。函数名可以自动转换为指针,取地址符是可选的:

pf = lengthCompare;
pf = &lengthCompare; // 和上行相同

指向不同函数类型的指针间不存在转换规则。
函数的形参可以是指向函数的指针。
返回指向函数的指针时,返回类型不会自动转为指针。
第四至六章课后练习答案
"实验楼"平台课程项目作业
本文原载于:实验楼

posted @ 2016-07-01 00:20  Yanqiang  阅读(601)  评论(3编辑  收藏  举报