4. 表达式和运算符
4.1 基础
-
一元运算符:作用于1个运算对象。
类似的有:二元运算符、三元运算符。
函数调用也是一种特殊的运算符。 -
运算对象转换:从一种类型转换成另一种类型。
-
重载运算符:可以对运算符自定义含义。
比如cout和cin的<<
和>>
运算符。
但运算对象的个数、运算符的优先级、结合律无法改变。 -
左值和右值:C++的表达式要么是左值,要么是右值。
当一个对象被用作右值的时候,用的是对象的值;
当一个对象被用作左值的时候,用的是对象的身份。
一个重要的原则:需要右值的地方可以用左值代替,但不能把右值当成左值。 -
使用关键字
decltype
的时候,左值和右值也不同。
如果表达式的求值结果是左值,decltype作用于该表达式得到一个引用类型。
举例:假定p类型是int*
,
解引用得到左值,所以decltype(*p)
的结果是int&
。
取地址得到右值,所以decltype(&p)
的结果是int**
。 -
括号无视优先级。
-
结合律的典型示例:输入输出运算。
比如cin >> v1 >> v2
,先进行cin >> v1
。
表达式返回的是cin
,故继续进行cin >> v2
。 -
优先级只规定了运算对象的组合方式,但未说明运算对象按什么顺序求值。
举例:int i = 0; cout << i << " " << ++i << endl;
结果可能是“1 1”,也可能是“0 1”。
由于表达式的行为不可预知,因此不论编译器生成什么都是错误的。 -
当然,有4种运算符是明确规定了运算对象求值顺序的。
分别是&&
、||
、?:
、,
。
例如:当&&
的左侧表达式值为0时,右侧表达式会被屏蔽掉。
当&&
的左侧表达式值为1时,右侧表达式才会执行运算。 -
尽量避免求值顺序、优先级、结合律杂糅的语句。
f()+g()*h()+j();
的表达式中:
f、g、h、j互相无关时,先g*h,再与f、j相加。
f、g、h、j若影响同一个对象,则是一条错误语句,产生未定义的行为。 -
综上,写代码时应采取以下两个建议:
(1)拿不准时,使用括号强制规范运算顺序。
(2)一条语句中,一个对象尽量只出现一次。
比如:*beg = toupper(*beg++);
,赋值号左右都出现了beg,此时会因为编译器的不同产生不同的结果。
(3)*++p
、*++iter
等:这种写法很常见,且很简洁。
4.2 算术运算符
-
算数运算符的运算对象和求值结果都是右值。
-
所有运算对象最终会转换成同一类型。
-
一元负号运算符对运算对象值取负后,返回其提升后的副本。
-
布尔值不应参与运算,否则会出现下面的情况:
b是一个布尔值,若进行一元符号运算,会将其提升为int,因此-b的值就是int型的-1。
此时赋给b2,由于-1不为0,因此b2会被赋值为1,即b2是true。bool b = true; bool b2 = -b; // 此时b2是true
-
计算机在处理算术表达式时,可能会导致溢出。
比如short类型最大可表示32767,此时若令该变量加1,则会产生溢出,此时输出的max值为-32768。
short max = 32767; ++max; cout << max;
-
要求表达式返回int类型的加、减、乘、除、取余运算,会丢弃小数部分。
-
取余运算要求运算对象必须是整数类型。
-
表达式
(m/n)*n+m%n
的结果是m
。 -
当除法运算和取余运算的运算对象有负数时,运算规则如下:
表达式 运算结果 表达式 运算结果 21 % 6 3 21 / 6 3 21 % 7 0 21 / 7 3 -21 % -8 -5 -21 / -8 2 21 % -5 1 21 / -5 4
4.3 逻辑和各系运算符
-
逻辑运算符和关系运算符的返回值类型都是布尔类型。
运算对象和求值结果都是右值。 -
短路求值:本节“1.基础”中的第9条。
应用:左侧运算对象可以确保右侧运算对象求值过程的正确性和安全性。 -
课本上的一个例子:
s被声明成了引用,因为text的元素是string对象,可能非常大,所以将s声明为引用类型可以避免对元素的拷贝。
而被声明为常量,也是因为不需要对string做写操作。for (const auto &s : text){ cout << s; if (s.empty() || s[s.size()-1] == '.') cout << endl; else cout << " "; }
-
不建议将int和bool进行比较!
int val = 2; if (val == true) ; // 先把true转变为int类型,也就是1 // 然后执行语句 `if (val == 1)` // 会执行 `if (0)` if (val); // 直接把val转变为bool类型,也就是1; // 会执行 `if(1)`
4.4 赋值运算符
-
C++允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。
无论右侧运算对象类型是什么,初始值列表的都可以为空。vector <int> vi = {}; vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
-
赋值运算满足右结合律。类型相同的对象才能进行赋值运算。
-
赋值运算符比关系运算符优先级低,因此在条件语句中,赋值语句应加上括号。
-
复合赋值运算,比普通运算效率更高。
a+=1
,a=a+1
,后者比前者多做一次求值。
4.5 递增和递减运算符
++i
比i++
效率更高。
因为前置版本直接返回+1后的值,而后置版本要先暂存一下原有的i。- 后置递增运算符的优先级高于解引用运算符。
*p++
是一种更简洁、更不易出错的写法。
其功能是,先自加1,再使用加1之前的值。 - 但应避免同一条语句中前后同时出现p的情况。
*p = toupper(*p++)
,此时该赋值语句是未定义的。
根据编译器的不同,会导致不同的运算顺序。
4.6 成员访问运算符
- 点运算符和箭头运算符
ptr->mem
等价于(*ptr).mem
。 - 解引用运算符优先级低于点运算符。
4.7 条件运算符
- 条件运算符的优先级非常低。因此使用时须加圆括号。
4.8 位运算符
-
一般来说,如果运算对象是“小整型”,则它的值会被自动提升为较大的整数类型。
位运算符如何处理带符号数,依赖于不同的机器本身。
因此强烈建议仅将位运算符用于处理无符号类型。 -
位运算的应用:
通过下面这个例子可知,非运算符比左移运算符优先级高。unsigned long quiz = 0; quiz |= 1UL << 27; // 将第27位置为1 quiz &= `(1UL << 27); // 将第27位置为0 bool status = quiz & (1UL << 27); // 查看第27位的状态
-
移位运算符满足左结合律。
4.9 sizeof运算符
-
sizeof运算符返回的是一条表达式或一个类型名字所占的字节数。
sizeof (type)
或sizeof expr
。
注意哦,sizeof是个运算符! -
sizeof函数并不计算表达式的值。
比如sizeof *p
,由于sizeof和*优先级一样,所以表达式从右往左结合。
且因sizeof不会计算表达式的值,所以即使p是一个无效指针也没关系。
因为sizeof不需要解引用指针也能知道它所指对象的类型。 -
sizeof的作用规律:
表达式 运算结果 sizeof(char)
1字节 sizeof(引用)
原对象所占空间字节 sizeof(指针)
指针本身所占空间字节 sizeof(*指针)
所指对象所占空间字节,且指针不必有效 sizeof(数组)
整个数组所占空间字节 sizeof(string)
固定部分所占空间字节 sizeof(vector))
固定部分所占空间字节
4.10 逗号运算符
- 经常用在for语句中。
- 表达式的值,为逗号右边的表达式的值。
4.11 类型转换
-
隐式转换:C++语言先将类型统一,再将两个不同类型的值相加。
-
算术转换:运算对象自动转换成最宽的类型。
整型提升:bool, char, signed char, unsigned char, short, unsigned short, 都会提升为int类型。 -
较大的char类型(如wchar_t, char16_t, char32_t)则会转换成较大的int类型(int, unsigned int, long, unsigned long, long long, unsigned long long)里能容纳对象的最小类型。
-
无符号类型的运算对象
一般来说,带符号的要转换成无符号的。
int和unsigned int:int先转换为unsigned int,再运算。如果int恰好为负值,则产生相应的副作用。 -
在不同的编译环境下,long和unsigned int的转换规则不同:
若int和long大小相同,则long转换为unsigned int;
若long比int多,则unsigned int转换为long。 -
理解算术转换
bool b; char c; short s; unsigned short us; int i; unsigned int ui; long l; unsigned long ul; float f; double d; 3.14159L + 'a'; // a → int → longdouble d + i; // i → double d + f; // f → double i = d; // d丢弃小数部分,赋给i b = d; // d为0时b为0,其他情况为1 c + f; // c → int → float // 此处s和c都转化为int s + c; // c → int, s → int // 此处c直接转换为long c + l; // c → long // 不需要分情况 i + ul; // i → ul // 分情况: // 若short和int空间一样大,则 i → unsigned short // 若short比int空间小,则 us → int us + i; // 分情况: // 若long和int空间一样大,则 l → unsigned int // 若long比int空间大,则 ui → long ui + l;
-
通常,数组名会转换成指针。
例外:decltype()的参数,取地址、sizeof、typeid等运算符的运算对象时,转换不会发生。 -
指针的转换:
nullptr能转换成任意指针类型、指向任意非常量的指针能转换成void*
、指向任意对象的指针能转换成const void *
。 -
转换成布尔类型:非0时全部转换成1。
-
转换成常量:允许指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是如此。反之不行。
int i; const int &r = i; // 非常量转换成const int的引用 const int *p = &i; // 非常量转换成const int的指针 int &s = r; // 错误,不允许const转换成非常量 int *q = p; // 错误,不允许const转换成非常量
-
类类型定义的转换举例:
C风格字符串初始化string对象:string s = "cpp_primer"
。
条件部分读入cin并转换为bool值:while (cin >>s)
。 -
显式转换(也叫强制类型转换)
命名的强制类型转换、旧版本的强制类型转换
形式:cast_name<type>(expression);
-
static_cast:只要不包含底层const,都可以使用。用途:
强制执行浮点数除法double d = static_cast<double> (j)/i;
,
找回存在于void*
的指针。 -
const_cast:只能改变底层const。
简单来说就是:原来通过指针不能改变对象,现在可以了。
如果对象本身不是常量,使用强制类型转换获得写权限是合法的行为。
如果对象是一个常量,在使用const_cast执行写操作会引起未定义行为。const char *pc; char *p = const_cast<char*>(pc);
-
reinterpret_cast:运算对象的位模式提供低层次上的解释。
使用是危险的,除非特别需要。int *ip; char *pc = reinterpret_cast<char*>(ip);
-
旧式的强制类型转换
char *pc = (char*) ip;
由于表现不清晰,不推荐使用。
4.12 运算符优先级
优先级 | 运算符 | 方向 | 功能 |
---|---|---|---|
1 | 左 | :: | 作用域 |
2 | 左 | . | 成员 |
2 | 左 | -> | 成员 |
2 | 左 | [] | 下标 |
2 | 左 | () | 函数调用 |
2 | 左 | () | 类型构造 |
3 | 右 | ++ | 后置递增 |
3 | 右 | – | 后置递减 |
3 | 右 | typeid() | 类型ID |
3 | 右 | cast_name<type>(expr) | 类型转换 |
4 | 右 | ++ | 前置递增 |
4 | 右 | – | 前置递减 |
4 | 右 | ~ | 按位取反 |
4 | 右 | ! | 逻辑非 |
4 | 右 | - | 一元负号 |
4 | 右 | + | 一元正号 |
4 | 右 | * | 解引用 |
4 | 右 | & | 取地址 |
4 | 右 | () | 类型转换 |
4 | 右 | sizeof | 对象大小 |
4 | 右 | sizeof() | 类型大小 |
4 | 右 | new | 创建对象 |
4 | 右 | new[] | 创建数组 |
4 | 右 | delete | 释放对象 |
4 | 右 | delete[] | 释放数组 |
4 | 右 | noexcept | 能否抛出异常 |
5 | 左 | ->* | 指向成员选择的指针 |
5 | 左 | .* | 指向成员选择的指针 |
6 | 左 | * | 乘法 |
6 | 左 | / | 除法 |
6 | 左 | % | 取余 |
7 | 左 | + | 加法 |
7 | 左 | - | 减法 |
8 | 左 | << | 向左移位 |
8 | 左 | >> | 向右移位 |
9 | 左 | < | 小于 |
9 | 左 | <= | 小于等于 |
9 | 左 | > | 大于 |
9 | 左 | >= | 大于等于 |
10 | 左 | == | 相等 |
10 | 左 | != | 不等 |
11 | 左 | & | 位与 |
12 | 左 | ^ | 位异或 |
13 | 左 | | | 位或 |
14 | 左 | && | 逻辑与 |
15 | 左 | || | 逻辑或 |
16 | 右 | ? : | 条件 |
17 | 右 | = | 赋值 |
18 | 右 | 复合赋值 | 复合赋值 |
19 | 右 | throw | 抛出异常 |
20 | 左 | , | 逗号 |