时隔半年,最近开始第二遍看C++ Primer,发现自己之前有很多理解不透彻的语法细节或STL库使用,故写下此博客以做总结。(只会写易错、生疏的知识点)
第二章
—占坑–
C++Primer二刷
第四章 表达式
-
C++的表达式要不然是右值(rvalue),要不然是左值(lvalue)。
- 可具名的对象是左值,临时的不可具名的对象是右值。除了常量左值对象外,左值都可以位于赋值语句左侧;而右值只能位于赋值语句右侧。左值可以用取地址符
&
来取地址,右值不可。 - 使用关键字decltype时,如果表达式的结果是左值,那么将得到一个引用类型。
- 可具名的对象是左值,临时的不可具名的对象是右值。除了常量左值对象外,左值都可以位于赋值语句左侧;而右值只能位于赋值语句右侧。左值可以用取地址符
-
C++规定了运算对象的组合方式和顺序,但没有规定运算表达式中运算对象本身的求值顺序。
-
如果多个运算对象表达式涉及到同一个对象,那么可能会引起未定义行为:
// 注意,f1() 和 f2()的调用顺序不保证 int i = f1() * f2(); // 错误,赋值运算符左右两侧的求值顺序不保证,最终的大写字母可能被赋给*beg或*(beg + 1) while (beg != s.end() && !isspace(*beg)) *beg = toupper(*beg++);
-
只有四种运算符规定了运算对象的求值顺序。它们是
&&
、||
、?:
、,
。
-
-
C++规定商一律向0取整。取余运算则要满足(m/n) * + m % n == m。
-
取余运算满足:如果m%n不等于0,则它的符号和m相同。
-
除了
-m
导致溢出的特殊情况,(-m)/n
和m/(-n)
等于-(m/n)
;m%(-n)
等于m%n
,(-m)%n
等于-(m%n)
。cout << 21 / 5 << " " << 21 / (-5) << " " << (-21) / 5 << " " << 21 % 5 << " " << 21 % (-5) << " " << (-21) % 5 << endl; // 输出为: 4 -4 -4 1 1 -1
-
-
C++运算符的优先级(部分):成员选择符 > 后置递增递减运算 > 前置递增递减运算、解引用、取地址 > 算术运算 > 移位运算 > 大小比较运算 > 相等性测试符 > 位运算 > 赋值运算
-
可以混用解引用和递增运算符,将先执行递增运算符再进行解引用运算。
*pebg++ // 输出当前值并将pebg前移一个元素 *++pebg // 前移pebg并输出前移后位置的元素
-
赋值运算符优先级较低,在条件语句中出现时要用括号围起。
while ((i = get_value()) != 42) // 不使用括号的话,那么i将为get_value() != 42的布尔值结果。 ...
-
条件运算符优先级很低,在ostream的输出表达式中使用条件运算符时,也要加上括号。
cout << (flag ? "fail" : "pass"); // 不使用括号的话,cout将输出flag的值。
-
-
C++中的移位运算符。
- 对于有符号类型负数的左移和右移运算是未定义行为。
- 移位运算符右侧的运算对象(即移位的数量)必须不为负,而且值必须严格小于结果的位数。
- 左移运算符向右侧插入二进制位0,右移运算符的行为则依赖与运算对象的类型:对于无符号类型,在左侧插入二进制位0,对于带符号类型,在左侧插入符号位的副本或二进制0,这取决于具体环境。
-
同decltype一样,sizeof关键字不会实际计算其对象的值:
int *p = nullptr; sizeof(*p); // 可以在sizeof中解引用无效指针,此语句将返回int类型的大小
-
如果算术运算的两个算术对象类型不同,那么它们将按照一定规则进行转换。
-
对于bool, char, signed char, unsigned char, short, unsigned short等类型来说,只要它们所有可能的值都能存在int里,它们就会提升成int类型;否则,提升成unsigned int类型。而较大的char类型(wchar_t等)提升成int, unsigned int, long, unsigned long, long long, unsigned long long 中最小的一种类型。
总的来说,就是将算术对象提升到最小的可容纳它们大小的类型(最低也要提升到int类型)。
-
如果一个是无符号类型,另一个是有符号类型,那么如果无符号类型不小于有符号类型,那么带符号类型将转换成无符号类型。如果有符号类型大于无符号类型,那么将依赖于机器:若无符号类型对象的所有制都能存储在带符号类型中,则无符号类型对象转换为有符号类型,如果不能,带符号类型对象将转换成无符号类型。
第一次比较的是类型本身的大小(sizeof),而当有符号类型大于无符号类型时,第二次比较将比较运算对象实际值的大小。
-
第五章 语句
-
空语句只含有一个单独的分号。
-
如果程序的某个地方,语法上需要一条语句但是逻辑上不需要,那么就可以使用空语句。
-
多余的空语句
;;
是被允许的。 -
如果循环的工作在条件部分就可以完成时,那么我们会使用空语句,但是此时应该加上注释以向他人声明这是有意省略循环体的:
while (cin >> s && s != sought) ; // 空语句
-
-
所谓空块,指的是内部没有任何语句的一对花括号。
- 空块的作用等价于空语句。
-
每个else与离它最近的尚未匹配的if匹配。
-
最近的if语句必须与else处于同一层中,如果if在一个块中,那么它不会被块外的else匹配:
if (grade % 10 >= 3) { // 被else匹配 if (grade % 10 > 7) lettergrade += '+'; } else // 与第一个if匹配 lettergrade += '-';
-
-
switch语句的case标签必须是整形常量表达式(const int或者枚举类型)。
-
如果在case标签代码中有对变量的定义时要小心,因为:如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处是非法行为。
switch (flag) { case true: // forbidden : crosses initialization of str // string str; 隐式初始化 int jval; // 未初始化 // forbidden : crosses initialization of ival // int ival = 1; 显式初始化 break; case false: jval = 1; break; }
-
多个case标签可以合并,因为case执行结束仅当遇到break或者到达switch结尾:
while (cin >> ch) { switch (ch) { case 'a': case 'e': case 'i': case 'o': case 'u': ++tCnt; break; } }
-
-
break语句的作用范围仅限于最近的循环或者switch,而continue仅作用于最近的循环。
-
goto语句可以跳转到同一函数内的标签位置:
- 标签名独立于变量或其他标识符的名字,所以它可以与变量同名而不受干扰。
- 和switch语句一样,goto语句也不能绕过变量的带有初始化的声明。
-
使用范围for循环遍历容器时要小心。
-
范围for语句
(auto &r : v)...
等价于:for (auto beg = v.begin(), end = v.end(); beg != end; ++beg) { auto &r = *beg; ... // 对r执行操作 }
-
以上,如果在for循环过程中修改了容器的值,比如当v为vector类型时调用了
v.push_back()
,那么预存的end迭代器将指向原来的容器结尾处,如果此push_back()引起了vector内数组对象的移动,那么end迭代器将指向无效的值,引起未定义行为。
-
-
C++定义了一组标准异常类。
- 我们只能以默认初始化的方式初始化exception、bad_alloc、bad_cast对象。
- 对于其他异常类型如runtime_error、logic_error等,我们必须使用string对象或C风格字符串为它提供一个初值。
- 所有异常类都有一个重载的what()方法,对于有字符串初始值的异常来说,此what()返回该字符串;对于其他无初始值的异常类型来说,what()返回的内容由编译器决定。
第六章 函数
-
向一个函数传参有两种方式:
- 当形参是引用类型时,形参称为实参的别名,我们说这是按引用传参。
- 当形参是值类型(包括指针)时,实参会被拷贝给形参,我们说这是按值传参。
- 对于大的类类型对象或者容器对象,它们的拷贝十分低效,甚至有的类类型(如ostream)不支持拷贝,那么我们应该尽可能使用按引用传递来避免对拷贝构造函数的调用。
-
const类型参数 + 引用、指针参数。
-
当用实参初始化形参时会忽略掉顶层const(除了
const T*
和const T&
都是顶层const)。// error: 对同名函数的选择存在二义性 void fcn(const int i) {} void fcn(int i) {}
-
如果存在函数
void reset(int &i)
,那么以下调用都非法:const int i = 0; double d = 2.0; reset(i); reset(10); reset(i + 1); reset(d); // 指针同理
如果函数参数为非常量的引用或指针,那么实参不能是字面值、求值结果为右值的表达式、需要转换的对象、带有底层const类型的对象。
-
-
数组作为形参、作为返回值。
-
不能以值的方式传递数组,传递的数组会被转换为指针(自然也无法靠此指针参数获得数组的大小 )所以以下声明等价:
void print(const int*); void print(const int[]); void print(const int[10]); // 这个10并没有什么用...
-
可以将形参定义成数组的引用,这时传入的参数视为数组类型而非指针,所以它携带了数组的大小信息。
void print(int (&arr)[10]) { // 支持sizeof获取数组大小,自然也支持范围for循环 cout << sizeof(arr) << endl; for (auto elem : arr) cout << elem << endl; }
-
传递多维数组时,第二维(以及后面所有维度)的大小都不能省略:
void print(int matrix[][20], int rowSize) {}
这是因为如果不知道有多少列,又因为多维数组本质是线性排布的一位数组,那么形如
matrix[1][0]
这样的写法将无法寻址(因为不知道matrix指针要偏移多少,事实上应该偏移20 * sizeof(int)
)。 -
返回值为数组类型时,。。。
-
-
含有可变形参的函数