本文为《C++ Primer》的读书笔记
目录
表达式
左值 (lvalue) 和 右值 (rvalue)
C++的表达式要不然是右值, 要不然就是左值
- 当一个对象被用作右值的时候, 用的是对象的值(内容)。右值只能被用在表达式右边
- 当对象被用作左值的时候, 用的是对象的身份(在内存中的位置)
理解的关键是:
- 只有左值才能放在表达式左边 (被赋值),因此,一些临时量,例如
1+1
,"abc"
这种不能被赋值的就统统是右值 ;而引用、变量这些能被赋值的就统统是左值
但这样的表述其实并不严谨 (例如为了维持向后兼容性,类类型允许向右值赋值;
const int
类型的变量是左值但不能被赋值),但基本正确
例如:
- 返回左值引用的函数, 连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子
- 返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值
求值顺序
- 优先级规定了运算对象的组合方式, 但是没有说明运算对象按照什么顺序求值。在大多数情况下, 不会明确指定求值的顺序
// 无法知道到底f1在f2之前调用还是f2在f1之前调用
int i = f1() * f2();
// 该循环的行为是未定义的!
while (beg!= s.end() && !isspace(*beg))
*beg = toupper(*beg++) ; //错误: 该赋值语句未定义
- 问题在于:赋值运算符左右两端的运算对象都用到了
beg
, 并且右侧的运算对象还改变了beg
的值, 所以该赋值语句是未定义的。编译器可能按照下面的任意一种思路处理该表达式,也可能采取别的什么方式处理它:
*beg = toupper(*beg);
*(beg + 1) = toupper(*beg);
算术运算符
- 一元负号运算符对运算对象值取负后, 返回其(提升后的)副本:
/*
* `b`的值为真, 参与运算时将被提升成整数值`1`, 对它求负后的结果是`-1`。
* 再转换回布尔值并将其作为`b2`的初始值,显然这个初始值转换成布尔值后应该为`1`。
* 所以,`b2`的值是真
*/
bool b2 = -b; // b = true, b2 也是 true!
- 在除法运算中,C++11新标准规定商一律向 0 取整(即直接切除小数部分)
- 取余运算的运算对象必须是整数类型。
m%(-n)
等于m%n
,(-m)%n
等于-(m%n)
逻辑和关系运算符
- 进行比较运算时除非比较的对象是布尔类型, 否则不要使用布尔字面值
true
和false
作为运算对象
if(val == true) { /* ... */) //只有当val等于1 时条件才为真!
如果val
不是布尔值, 那么进行比较之前会首先把true
转换成val
的类型。也就是说, 如果val
不是布尔值, 则代码等价于如下形式:
if (val == 1) { / * ... * /)
赋值运算符
- 赋值运算满足右结合律,返回左侧的运算对象
- 例如,在下面的例子中,因为赋值运算符满足右结合律,所以靠右的赋值运算
jval=0
作为靠左的赋值运算符的右侧运算对象。又因为赋值运算返回的是其左侧运算对象, 所以靠右的赋值运算的结果(即jval
)被赋给了ival
- 例如,在下面的例子中,因为赋值运算符满足右结合律,所以靠右的赋值运算
int ival, jval;
ival = jval = 0; //正确:都被赋值为0
递增和递减运算符
*p++
:输出当前值并将p
向后移动一个元素,等价于*(p++)
- 自增、自减运算符操作数不能为常量或算术表达式:
++(++a); //正确,++a返回a
++(a++); //错误,a++返回值
- 后置运算符优先级高于前置运算符
- 后置:左结合;前置:右结合
// 这种代码...只可能考试的时候见到吧
a+++b // (a++)+b
a+ ++b // a+(++b)
+++b // +(++b)
- 除非必须,否则不用递增递减运算符的后置版本:
前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。对于整数和指针类型来说,编译器可能对这种额外的工作进行一定的优化;但是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了。建议养成使用前览版本的习惯,这样不仅不需要担心性能的问题,而且更重要的是写出的代码会更符合编程的初衷- 下面是迭代器类型的递增运算符的前置和后置版本对应的反汇编,可以看出前置版本确实更简洁,效率更高:
c++;
00326541 push 0
00326543 lea eax,[ebp-124h]
00326549 push eax
0032654A lea ecx,[c]
0032654D call std::_String_iterator<std::_String_val<std::_Simple_types<char> > >::operator++ (0321190h)
00326552 lea ecx,[ebp-124h]
00326558 call std::_String_iterator<std::_String_val<std::_Simple_types<char> > >::~_String_iterator<std::_String_val<std::_Simple_types<char> > > (032136Bh)
++c;
0032655D lea ecx,[c]
00326560 call std::_String_iterator<std::_String_val<std::_Simple_types<char> > >::operator++ (0321587h)
条件运算符
- 嵌套条件运算符
finalgrade = (grade > 90) ? "high pass"
: (grade < 60) ? "fail" : "pass";
随着条件运算嵌套层数的增加,代码的可读性急剧下降。因此, 条件运算的嵌套最好别超过两到三层
在输出表达式中使用条件运算符
- 条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号:
cout << ((grade < 60) ? "fail": "pass"); //输出pass 或者fail
cout << (grade < 60) ? "fail": "pass"; //输出 1 或者 0!
cout << grade < 60 ? "fail" : "pass"; //错误:试图比较cout 和60
在第二条表达式中,grade
和60
的比较结果是<<
运算符的运算对象,因此如果grade<60
为真输出1, 否则输出0。<<
运算符的返回值是cout
, 接下来cout
作为条件运算符的条件。也就是说,第二条表达式等价于
cout << (grade < 60) ; // 输出1 或者 0
cout? "fail" : "pass"; //根据cout的值是true还是false产生对应的宇面值
因为第三条表达式等价于下面的语句,所以它是错误的:
cout << grade; // 小于运算符的优先级低于移位运算符,所以先输出grade
cout < 60 ? "fail": "pass"; //然后比较cout和60!
位运算符
^
位异或- 一般来说, 如果运算对象是“ 小整型”, 则它的值会被自动提升成较大的整数类型。例如
char
进行移位运算时会自动提升为int
- 运算对象可以是带符号的, 也可以是无符号的。如果运算对象是带符号的且它的值为负, 那么位运算符如何处理运算对象的" 符号位“ 依赖于机器。而且,此时的左移操作可能会改变符号位的值, 因此是一种未定义的行为
关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型
sizeof
运算符
sizeof
运算符返回一条表达式或一个类型名字所占的字节数, 其所得的值是一个size_t
类型
- 运算符的运算对象有两种形式:
sizeof (type)
sizeof expr
sizeof
并不实际计算其运算对象的值:
sizeof *p; //p所指类型的空间大小
即使p
是一个无效的指针也不会有什么影响
- 对数组执行
sizeof
运算得到整个数组所占空间的大小。注意,sizeof
运算不会把数组转换成指针来处理 - 对
string
对象或vector
对象执行sizeof
运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间
constexpr size_t sz = sizeof(ia) / sizeof(*ia); // 返回ia的元素数量
int arr2[sz]; //正确: sizeof 返回一个常量表达式
逗号运算符 (comma operator)
- 逗号运算符首先对左侧的表达式求值, 然后将求值结果丢弃掉,真正的结果是右侧表达式的值
- 从本质上讲,逗号的作用是将一系列运算按顺序执行
int a = 1, b = 2, c = 3, sum;
sum = (a + b, b + c, c + a); // sum = 4
sum = a + b, b + c, c + a; // sum = 3,表达式值为4
运算符优先级表
语句
switch
- 根据整数变量或表达式的值,从一组动作中选择一个执行,可用
swich
语句构成多分支选择结构
switch (表达式) // 判断表达式括号应具有整型值
{
case 标号 1:
语句 1;
// break;
case 标号 2:
语句 2;
// break;
// ...
case 标号 n:
语句 n;
// break;
default:
语句 n+1
}
case
关键字和它对应的值一起被称为case
标签(case label)case
标签必须是整型常量表达式- 任何两个
case
标签的值不能相同
char ch = getVal();
int ival = 42;
switch(ch) {
case 3.14: //错误: case标签不是一个整数
case ival: //错误: case标签不是一个常量
// ...
default:
// ...
}
- 即使不准备在
default
标签下做任何工作, 定义一个default
标签也是有用的。其目的在于告诉程序的读者, 我们已经考虑到了默认的情况, 只是目前什么也没做 - 标签不应该孤零零地出现, 它后面必须跟上一条语句或者另外一个
case
标签。如果switch
结构以一个空的default
标签作为结束,则该default
标签后面必须跟上一条空语句或一个空块 switch
内部的变量定义switch
的执行流程有可能会跨过某些case
标签。如果程序跳转到了某个特定的case
, 则switch
结构中该case
标签之前的部分会被忽略掉。这种忽略掉一部分代码的行为引出了一个有趣的问题:如果被略过的代码中含有变量的定义该怎么办?
答案是:如果在某处一个带有初值的变量位于作用域之外, 在另一处该变量位于作用域之内, 则从前一处跳转到后一处的行为是非法行为
case true:
//因为程序的执行流程可能绕开下面的初始化语句, 所以该switch 语句不合法
string file_name; //错误:控制流绕过一个隐式初始化的变量
int ival = 0; //错误:控制流绕过一个显式初始化的变量
int jval; //正确:因为jval 没有初始化
break;
case false:
//正确: jval 虽然在作用域内, 但是它没有初始化
jval = next_num(); //正确:给jval 赋一个值
if(file_name.empty()) // file_name 在作用域内, 但是没有被初始化
//...
假设上述代码合法,则一旦控制流直接跳到false
分支,也就同时略过了变量file_name
和ival
的初始化过程。此时这两个变量位于作用域之内, 跟在false
之后的代码试图在尚未初始化的情况下使用它们, 这显然是行不通的。因此C++规定, 不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置。
如果需要为某个case
分支定义并初始化一个变量, 我们应该把变量定义在块内, 从而确保后面的所有case
标签都在变量的作用域之外
case true:
{
//正确:声明语句位于语句块内部
string file_name = get_file_name();
//...
}
break;
case false:
if (file_name.empty()) //错误: file_name 不在作用域之内
传统for
语句
for
语句头中的多重定义for
语句头中可以定义多个对象。但是只能有一条声明语句,因此, 所有变量的基础类型必须相同
- 如果缺省条件判断部分,则相当于是无限循环
范围for
语句
- 可以遍历容器或其他序列的所有元素
for (declaration : expression)
statement
expression
表示的必须是一个序列, 比如用花括号括起来的初始值列表、数组或者vector
或string
等类型的对象, 这些类型的共同特点是拥有能返回迭代器的begin
和end
成员 (内置数组也可以使用)declaration
定义一个变量, 序列中的每个元素都得能转换成该变量的类型。确保类型相容最简单的办法是使用auto
类型说明符。如果需要对序列中的元素执行写操作, 循环变量必须声明成引用类型
vector<int> v = {0,1,2,3,4,5,6,7,8,9};
for (auto &r : v)
{
r *= 2;
}
- 范围
for
语句的定义来源于与之等价的传统for
语句:
for (auto beg = v.begin(), end = v.end(); beg != end; ++beg)
{
auto &r = *beg;
r *= 2;
}
goto
语句
看得懂即可,别用!!!
goto label;
- 其中,
label
是标识一条语句的标示符。带标签语句是一种特殊的语句, 在它之前有一个标示符以及一个冒号:
end: return; //带标签语句, 可以作为goto的目标
- 标签标示符独立于变量或其他标示符的名字, 因此, 标签标示符可以和程序中其他实体的标示符使用同一个名字而不会相互干扰
goto
语句和控制权转向的那条带标签的语句必须位于同一个函数之内- 和
switch
语句类似,goto
语句也不能将程序的控制权从变量的作用域之外转移到作用域之内:
//...
goto end;
int ix = 10; //错误:goto语句绕过了一个带初始化的变量定义
end:
//错误:此处的代码需要使用ix, 但是goto语句绕过了它的声明
ix = 42;
- 向后跳过一个已经执行的定义是合法的。跳回到变量定义之前意味着系统将销毁该变量, 然后重新创建它:
//向后跳过一个带初始化的变量定义是合法的
begin:
int sz = get_size();
if (sz <= 0) {
goto begin;
}