C++_表达式
表达式
表达式由运算对象和运算符组成。对表达式求值会得到一个结果。
最简单的表达式有:
- 字面值
- 变量
例如:
1 | 42 |
表达式基础
运算符的种类
按运算对象个数分类:
- 一元运算符:作用于一个运算对象
如&、*、!、++ - 二元运算符:作用于两个运算对象
如+、-、== - 三元运算符:作用于三个运算对象
如条件运算符?:
此外,函数调用也可以看作一种特殊运算形式。
左值和右值
C++ 表达式要么是左值,要么是右值。
- 左值:表示对象的身份,可理解为“有位置的对象”
- 右值:表示对象的值,通常不能出现在赋值号左侧
例如:
1 | int i = 10; |
可以简单记为:
- 用作左值时,关注的是对象的“位置”
- 用作右值时,关注的是对象的“内容”
优先级与结合律
复杂表达式中,运算符如何组合由:
- 优先级
- 结合律
共同决定。
优先级
优先级高的先结合。
例如:
1 | 3 + 4 * 5 |
先算 4 * 5,再算 3 + ...
结合律
当优先级相同时,看结合方向。
左结合
从左到右组合:
1 | a - b - c |
等价于:
1 | (a - b) - c |
大多数二元运算符都左结合。
右结合
从右到左组合:
1 | a = b = c |
等价于:
1 | a = (b = c) |
常见右结合运算符:
- 赋值运算符
- 条件运算符
?: - 一元运算符
括号
括号优先级最高,可强制改变组合方式。
1 | (a + b) * c |
求值顺序
优先级和结合律只决定“怎么组合”,不决定“先算谁”。
例如:
1 | f() + g() |
即使加号是二元运算符,也不表示一定先算 f() 还是先算 g()
未指定求值顺序的风险
如果一个表达式中:
- 既修改同一个对象
- 又以其他方式访问该对象
- 且没有明确的求值顺序
就可能产生未定义行为
例如:
1 | int i = 0; |
明确规定求值顺序的运算符
下面这些运算符明确规定了运算对象的求值顺序:
&&||?:,
算术运算符
算术运算符用于数值计算。
| 运算符 | 含义 |
|---|---|
+ |
一元正号 / 加法 |
- |
一元负号 / 减法 |
* |
乘法 |
/ |
除法 |
% |
求余 |
说明:
- 大多数算术运算符是左结合
- 一元
+、-的优先级高于二元+、-
整数除法
若两个运算对象都是整数,则结果仍是整数,小数部分被舍弃:
1 | 5 / 2 // 结果是 2 |
求余运算
% 只能用于整数类型:
1 | 7 % 3 // 结果是 1 |
逻辑运算符和关系运算符
关系运算符
用于比较大小或相等关系,结果是 bool。
| 运算符 | 含义 |
|---|---|
< |
小于 |
<= |
小于等于 |
> |
大于 |
>= |
大于等于 |
== |
相等 |
!= |
不相等 |
关系运算符通常用于:
- 算术类型
- 指针类型(部分比较合法)
逻辑运算符
逻辑运算符作用于能转换成 bool 的类型,结果也是 bool。
| 运算符 | 含义 |
|---|---|
! |
逻辑非 |
&& |
逻辑与 |
| ` |
说明:
!是一元运算符,右结合&&和||是左结合&&和||具有短路求值特性
例如:
1 | if (p && *p == 10) |
若 p 为空指针,*p == 10 不会执行。
赋值运算符
赋值运算符要求:
- 左侧必须是可修改的左值
1 | int i; |
特点
- 赋值表达式的结果是左侧对象
- 结果本身还是左值
- 赋值运算符是右结合
例如:
1 | int a, b, c; |
等价于:
1 | a = (b = (c = 0)); |
类型转换
如果左右类型不同,右侧会转换成左侧类型:
1 | int i; |
列表赋值
C++11 允许使用花括号赋值:
1 | int i; |
但会禁止某些可能丢失信息的窄化转换。
递增和递减运算符
递增 ++ 和递减 -- 用于加 1、减 1。
分为:
- 前置:先修改,再返回
- 后置:先返回旧值,再修改
前置与后置
1 | int i = 0, j; |
区别:
++i:返回修改后的对象i++:返回修改前的副本
返回值性质
- 前置版本返回左值
- 后置版本返回右值
因此一般来说:
- 需要结果时再用后置
- 不需要旧值时优先使用前置
尤其对迭代器,前置通常更高效。
优先级注意
后置 ++ 的优先级高于解引用 *
1 | *pbeg++ |
等价于:
1 | *(pbeg++) |
含义:
- 先取当前指针位置的值
- 再让指针向后移动一位
成员访问运算符
成员访问有两种形式:
| 运算符 | 用途 |
|---|---|
. |
对象访问成员 |
-> |
指针访问成员 |
例如:
1 | string s = "hello"; |
其中:
1 | p->size() |
等价于:
1 | (*p).size() |
优先级注意
. 的优先级高于 *,所以:
1 | *p.size() |
会被解释成:
1 | *(p.size()) |
这是错误的,因为 p 是指针,没有 size() 成员。
正确写法:
1 | (*p).size() |
条件运算符
条件运算符形式:
1 | cond ? expr1 : expr2 |
含义:
- 若
cond为真,结果是expr1 - 否则结果是
expr2
例如:
1 | int a = 10, b = 20; |
特点
- 条件运算符是右结合
- 只会计算
expr1和expr2中被选中的那一个
可以嵌套:
1 | grade = score > 90 ? 'A' : |
条件运算符适合简单逻辑,复杂情况建议写成
if-else。
位运算符
位运算符对整数类型进行逐位操作。
| 运算符 | 含义 |
|---|---|
~ |
按位取反 |
<< |
左移 |
>> |
右移 |
& |
按位与 |
^ |
按位异或 |
| ` | ` |
说明
按位取反 ~
把每一位:
1变00变1
左移 <<
右边补 0
右移 >>
- 对无符号数:左边补
0 - 对有符号数:结果依赖实现,需谨慎
按位与 &
两位都为 1,结果才为 1
按位异或 ^
两位不同,结果为 1
按位或 |
两位中有一个为 1,结果就为 1
位运算常用于标志位、权限控制、底层优化等场景。
sizeof 运算符
sizeof 返回某个类型或表达式所占的字节数。
形式:
1 | sizeof(type) |
例如:
1 | sizeof(int) |
特点
- 结果是一个常量表达式
- 返回类型是
size_t - 不会真正计算表达式的值
例如:
1 | int *p = nullptr; |
示例
1 | Sales_data data, *p; |
逗号运算符
逗号运算符形式:
1 | expr1, expr2 |
执行过程:
- 先计算左侧表达式
- 丢弃左侧结果
- 再计算右侧表达式
- 整个表达式结果为右侧结果
例如:
1 | int i = 0, j = 1; |
结果:
i变为1j变为2k的值是1
类型转换
当表达式需要某种类型,而给出的运算对象是另一种相关类型时,编译器可能自动进行转换。
这种转换叫:
- 隐式类型转换
例如:
1 | int ival = 3.14; // ival 得到 3 |
算术转换
算术类型之间会自动转换,一般转换到“更宽”的类型。
例如:
1 | double d = 3 + 4.5; // 3 先转成 double |
整型提升
较小整数类型通常先提升为 int 或 unsigned int,如:
boolcharsigned charunsigned charshortunsigned short
例如:
1 | char c = 'a'; |
布尔值提升规则:
false -> 0true -> 1
其他常见隐式转换
数组转指针
多数情况下,数组名会转换成首元素指针:
1 | int arr[3]; |
指针转换
0或nullptr可转换为空指针- 任意对象指针可转换为
const void* - 非常量对象指针可转换为
void*
转换为 bool
0、空指针 ->false- 非
0、非空指针 ->true
转换为常量类型
允许:
1 | int *p; |
不允许反向自动转换,因为会去掉底层 const
类类型转换
类类型可以自定义转换规则,但编译器一次最多只会自动应用一种类类型转换。
显式类型转换
显式转换由程序员主动指定。
C++ 推荐使用命名的强制类型转换:
1 | cast-name<type>(expression) |
其中 cast-name 是以下四种之一:
static_castconst_castreinterpret_castdynamic_cast
static_cast
用于一般性、定义明确的转换。
例如:
1 | double d = 3.14; |
适合:
- 算术类型转换
void*与具体类型指针间某些转换- 明确合法的转换
不能去掉底层 const。
const_cast
只能改变对象的底层 const 属性。
1 | const char *pc; |
说明:
- 语法上合法
- 但如果原对象本来就是常量,通过
p修改它会产生未定义行为
换句话说:
- 原对象非常量:可能安全
- 原对象本身是常量:修改就是未定义行为
reinterpret_cast
按底层位模式重新解释对象。
1 | int *ip; |
特点:
- 非常底层
- 与机器和实现强相关
- 风险大,不建议随意使用
dynamic_cast
主要用于继承体系中做运行时类型转换,依赖运行时类型识别(RTTI)。
常用于基类指针/引用转换为派生类指针/引用。
旧式强制类型转换
C++ 仍保留 C 风格写法,但不推荐使用:
1 | type(expr) // 函数式 |
例如:
1 | int i = (int)3.14; |
问题:
- 不够明确
- 不容易看出转换目的
- 可能混合多种转换行为
现代 C++ 更推荐使用命名转换,如
static_cast<int>(3.14)。
易错点总结
优先级不等于求值顺序
1 | f() + g() |
谁先算不一定。
不要在一个表达式里同时修改并读取同一对象
1 | cout << i << ++i; // 危险/未定义 |
前置和后置 ++ 不同
1 | ++i // 先改后用 |
*p++ 不是 (*p)++
1 | *p++ // 等价于 *(p++) |
. 优先级高于 *
1 | (*p).size(); // 正确 |
sizeof 不会真正求值
1 | sizeof(*p); // 不会真的访问 *p |
慎用 reinterpret_cast
除非非常清楚底层实现,否则不要用。




