1. 运算符的分类
我们可以从两个维度对运算符进行分类:
A. 按操作数数量分类:
- 一元运算符 (Unary Operator):只对一个操作数进行操作。例如
!a
(逻辑非),-b
(取负),++c
(自增)。 - 二元运算符 (Binary Operator):对两个操作数进行操作。这是最常见的类型,例如
a + b
,c > d
。 - 三元运算符 (Ternary Operator):对三个操作数进行操作。C++ 中只有一个三元运算符,即条件运算符
? :
。
B. 按功能分类(我们将主要按此分类进行讲解):
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 成员/指针运算符
- 其他运算符 (如
sizeof
, 条件运算符等)
2. 各类运算符详解
a) 算术运算符 (Arithmetic Operators)
用于执行基本的数学运算。
运算符 | 名称 | 示例 | 描述 |
+ | 加法 | a + b | 计算两者之和 |
- | 减法 | a - b | 计算两者之差 |
* | 乘法 | a * b | 计算两者之积 |
/ | 除法 | a / b | 计算两者之商。注意:整数除法会舍弃小数部分 |
% | 取模 (取余) | a % b | 计算 a 除以 b 的余数。通常只用于整数。 |
++ | 自增 | a++ 或 ++a | 将操作数的值加 1 |
-- | 自减 | a-- 或 --a | 将操作数的值减 1 |
代码示例:
#include <iostream>
int main() {
int a = 10, b = 4;
std::cout << "a + b = " << (a + b) << std::endl; // 14
std::cout << "a - b = " << (a - b) << std::endl; // 6
std::cout << "a * b = " << (a * b) << std::endl; // 40
std::cout << "a / b = " << (a / b) << std::endl; // 2 (整数除法,2.5 被截断)
std::cout << "a % b = " << (a % b) << std::endl; // 2 (10 = 4*2 + 2)
// ++ 和 -- 的区别 (前缀与后缀)
int c = 5;
// 后缀 a++: 先使用 c 的值 (5),再将 c 加 1
std::cout << "c++ is " << c++ << std::endl; // 输出 5
std::cout << "Now c is " << c << std::endl; // 输出 6
int d = 5;
// 前缀 ++d: 先将 d 加 1,再使用 d 的新值 (6)
std::cout << "++d is " << ++d << std::endl; // 输出 6
std::cout << "Now d is " << d << std::endl; // 输出 6
}
b) 关系运算符 (Relational Operators)
用于比较两个值,结果为布尔值 true
或 false
。
运算符 | 名称 | 示例 | 描述 |
== | 等于 | a == b | 如果 a 和 b 相等,返回 true |
!= | 不等于 | a != b | 如果 a 和 b 不相等,返回 true |
> | 大于 | a > b | |
< | 小于 | a < b | |
>= | 大于等于 | a >= b | |
<= | 小于等于 | a <= b |
代码示例:
#include <iostream>
int main() {
int x = 5, y = 5, z = 10;
std::cout << std::boolalpha; // 让 cout 输出 "true"/"false" 而不是 1/0
std::cout << "x == y: " << (x == y) << std::endl; // true
std::cout << "x != z: " << (x != z) << std::endl; // true
std::cout << "x < z: " << (x < z) << std::endl; // true
}
常见错误: 把 ==
(比较) 写成 =
(赋值)。if (x = 5)
是合法的,但它的意思是把 5
赋给 x
,然后判断 5
的布尔值(非零为 true
),这几乎总是逻辑错误。
c) 逻辑运算符 (Logical Operators)
用于组合多个布尔表达式。
运算符 | 名称 | 示例 | 描述 |
&& | 逻辑与 (AND) | expr1 && expr2 | 只有当两个表达式都为 true 时,结果才为 true |
|| | 逻辑或 (OR) | expr1 || expr2 | 只要任一表达式为 true ,结果就为 true |
! | 逻辑非 (NOT) | !expr | 如果表达式为 true ,结果为 false ,反之亦然 |
短路求值 (Short-circuit Evaluation):
- 对于
&&
,如果第一个表达式为false
,则不再计算第二个表达式。 - 对于
||
,如果第一个表达式为true
,则不再计算第二个表达式。
这在代码中非常重要,例如if (ptr != nullptr && ptr->value > 10)
可以安全地防止对空指针解引用。
代码示例:
#include <iostream>
int main() {
int age = 25;
bool has_license = true;
std::cout << std::boolalpha;
if (age >= 18 && has_license) {
std::cout << "Can drive." << std::endl; // Can drive.
}
bool is_weekend = true;
bool is_holiday = false;
if (is_weekend || is_holiday) {
std::cout << "Can rest." << std::endl; // Can rest.
}
if (!is_holiday) {
std::cout << "Need to work." << std::endl; // Need to work.
}
}
d) 位运算符 (Bitwise Operators)
直接对整数的二进制位进行操作。常用于低级编程、设备驱动和性能优化。
运算符 | 名称 | 示例 | 描述 |
& | 按位与 (AND) | a & b | 对应位都为 1 时,结果位才为 1 |
| | 按位或 (OR) | a | b | 对应位只要有一个为 1,结果位为 1 |
^ | 按位异或 (XOR) | a ^ b | 对应位不同时,结果位为 1 |
~ | 按位取反 (NOT) | ~a | 翻转所有位 (0变1,1变0) |
<< | 左移 | a << n | 将 a 的所有位向左移动 n 位,右边补 0。相当于 a * 2^n |
>> | 右移 | a >> n | 将 a 的所有位向右移动 n 位。对于无符号数,左边补 0;对于有符号数,行为可能取决于实现(通常是补符号位)。相当于 a / 2^n |
代码示例:
#include <iostream>
#include <bitset>
int main() {
unsigned char a = 5; // 00000101
unsigned char b = 12; // 00001100
std::cout << "a & b = " << (a & b) << " (" << std::bitset<8>(a & b) << ")" << std::endl; // 4 (00000100)
std::cout << "a | b = " << (a | b) << " (" << std::bitset<8>(a | b) << ")" << std::endl; // 13 (00001101)
std::cout << "a ^ b = " << (a ^ b) << " (" << std::bitset<8>(a ^ b) << ")" << std::endl; // 9 (00001001)
std::cout << "~a = " << int(~a) << " (" << std::bitset<8>(~a) << ")" << std::endl; // 250 (11111010)
std::cout << "a << 2 = " << (a << 2) << " (" << std::bitset<8>(a << 2) << ")" << std::endl; // 20 (00010100)
std::cout << "b >> 1 = " << (b >> 1) << " (" << std::bitset<8>(b >> 1) << ")" << std::endl; // 6 (00000110)
}
e) 赋值运算符 (Assignment Operators)
用于给变量赋值。
运算符 | 示例 | 等价于 |
= | a = b | a = b |
+= | a += b | a = a + b |
-= | a -= b | a = a - b |
*= | a *= b | a = a * b |
/= | a /= b | a = a / b |
%= | a %= b | a = a % b |
&= | a &= b | a = a & b |
|= | a |= b | a = a | b |
^= | a ^= b | a = a ^ b |
<<= | a <<= b | a = a << b |
>>= | a >>= b | a = a >> b |
代码示例:
int x = 10;
x += 5; // x 现在是 15
x *= 2; // x 现在是 30
复合赋值运算符 op=
不仅是简写,有时也更高效。
f) 成员/指针运算符 (Member/Pointer Operators)
运算符 | 名称 | 示例 | 描述 |
. | 成员访问 | obj.member | 访问对象或结构体的成员 |
-> | 指针成员访问 | ptr->member | 通过指向对象的指针访问其成员。等价于 (*ptr).member |
& | 取地址 | &var | 获取变量的内存地址,返回一个指针 |
* | 解引用 | *ptr | 获取指针所指向地址处的值 |
g) 其他运算符
运算符 | 名称 | 示例 | 描述 |
sizeof | 尺寸 | sizeof(type) 或 sizeof(expr) | 在编译时计算类型或表达式结果所占的字节数 |
?: | 条件 (三元) | cond ? expr1 : expr2 | 如果 cond 为 true ,表达式的值为 expr1 ;否则为 expr2 |
, | 逗号 | expr1, expr2 | 先计算 expr1 ,然后丢弃其结果,再计算 expr2 ,整个表达式的值是 expr2 的值。不常用,主要用在 for 循环中。 |
(type) | 类型转换 (C-style) | (int)3.14 | 将一个表达式强制转换为另一种类型。C++ 推荐使用 static_cast , dynamic_cast 等 |
:: | 范围解析 | std::cout | 用于指定命名空间或类的范围 |
<=> | 三路比较 (C++20) | a <=> b | ”宇宙飞船运算符”,同时进行 a<b , a==b , a>b 的比较,返回一个对象表示小于、等于或大于零。 |
3. 运算符的优先级和结合性
当一个表达式中有多个运算符时,由优先级 (Precedence) 和 结合性 (Associativity) 决定计算顺序。
-
优先级:哪个运算符先执行。例如,
*
和/
的优先级高于+
和-
。所以a + b * c
等价于a + (b * c)
。 -
结合性:当多个优先级相同的运算符在一起时,决定计算方向。
-
左结合 (Left-to-Right):大多数运算符是左结合。例如
a - b - c
等价于(a - b) - c
。 -
右结合 (Right-to-Left):赋值运算符、一元运算符和三元运算符是右结合。例如
a = b = c
等价于a = (b = c)
。
简化的优先级表 (从高到低):
()
[]
.
->
::
++
--
!
~
*
&
sizeof
(一元运算符)*
/
%
(乘除模)+
-
(加减)<<
>>
(位移)<
<=
>
>=
(关系)==
!=
(相等)&
(按位与)^
(按位异或)|
(按位或)&&
(逻辑与)||
(逻辑或)?:
(条件)=
+=
-=
等赋值运算符 (右结合),
(逗号)
最佳实践: 不要去死记硬背完整的优先级表!如果一个表达式的求值顺序不明显,请使用括号 ()
来明确指定顺序。这能极大地提高代码的可读性并避免错误。
4. 运算符重载 (Operator Overloading)
C++ 允许我们为自定义的类型(类或结构体)重新定义大多数运算符的行为。这使得我们可以写出更直观、更像数学表达的代码。
例如,你可以定义一个 Vector2D
类,并重载 +
运算符来实现两个向量的相加。
#include <iostream>
struct Vector2D {
double x, y;
// 重载 + 运算符
Vector2D operator+(const Vector2D& other) const {
return {x + other.x, y + other.y};
}
};
// 重载 << 运算符,使其可以打印 Vector2D 对象
std::ostream& operator<<(std::ostream& os, const Vector2D& vec) {
os << "(" << vec.x << ", " << vec.y << ")";
return os;
}
int main() {
Vector2D v1 = {1.0, 2.0};
Vector2D v2 = {3.0, 4.0};
Vector2D sum = v1 + v2; // 直观地调用了重载的 operator+
std::cout << "v1: " << v1 << std::endl;
std::cout << "v2: " << v2 << std::endl;
std::cout << "Sum: " << sum << std::endl; // 输出: Sum: (4, 6)
}
5. 运算符总结与最佳实践
- 明确意图:使用括号
()
避免任何关于优先级和结合性的混淆。 - 注意副作用:
++
和--
会修改变量的值,在同一个表达式中对一个变量多次使用它们可能会导致未定义行为(例如i = i++
)。 - 区分运算符:清楚地区分
&
(按位与) 和&&
(逻辑与),|
和||
,=
和==
。 - 短路求值:善用
&&
和||
的短路特性来编写更安全、高效的代码。 - 运算符重载:谨慎使用,只在行为符合直觉时重载运算符(例如用
+
做加法,而不是减法),以保持代码的可读性。
4. 表达式 (Expressions)
A. 表达式的求值顺序和副作用
- 副作用 (Side Effect):指表达式在求值过程中,除了返回一个值之外,还修改了某些状态(如修改变量的值、进行I/O操作)。例如,
i++
的副作用就是使i
的值增加。 - 求值顺序 (Order of Evaluation):C++ 标准并未规定大多数二元运算符(如
+
,*
)的操作数的求值顺序。
cout << f1() + f2();
我们无法确定是f1()
先被调用还是f2()
先被调用。
这会导致一个非常危险的问题:未定义行为 (Undefined Behavior, UB)。
如果你在一个表达式中多次修改同一个变量,且没有明确的顺序规定,就会产生 UB。
错误的例子 (UB!):
int i = 5;
i = ++i; // UB! i被读取和修改,顺序不确定
cout << i << i++; // UB! 对 i 的使用和修改顺序不确定
有顺序保证的运算符:
逻辑与(&&
)、逻辑或(||
)、逗号(,
)和条件(?:
)运算符,它们的操作数求值顺序是确定的(从左到右)。
B. 表达式的值类别 (Value Categories)
在现代 C++ 中,表达式的值可以分为几类,最重要的是左值 (lvalue) 和右值 (rvalue)。
-
左值 (lvalue):指代一个内存位置,可以被取地址(用
&
),并且通常可以被赋值。你可以把它想象成一个有名字、有固定地址的“容器”。 -
变量名 (
int x; x
是左值) -
解引用的指针 (
*p
) -
数组元素 (
arr[0]
) -
右值 (rvalue):通常指一个临时的、即将销毁的值,不能被取地址。你可以把它想象成一个“即用即弃”的临时数据。
-
字面量 (
10
,true
) -
算术表达式的结果 (
a + b
) -
函数返回的非引用临时值
简单的判断方法:能对它取地址 (&
) 的,基本上就是左值。
int x = 10;
int* p = &x; // 正确, x 是左值
int* q = &(x+1); // 错误! x+1 的结果是一个临时值(右值),没有固定地址
这个概念对于理解 C++11 引入的移动语义 (Move Semantics) 和右值引用 (Rvalue Reference, &&
) 至关重要。
5. 综合示例与最佳实践
综合示例
分析下面这个复杂的表达式:
int a = 5, b = 10, c = 2;
bool result = a * b / c > 8 && b - a == 5;
- 优先级分析:
*
/
>+
-
>>
==
>&&
- 求值步骤:
a.a * b
(5 * 10 = 50)
b.(结果a) / c
(50 / 2 = 25)
c.(结果b) > 8
(25 > 8 =true
)
d.b - a
(10 - 5 = 5)
e.(结果d) == 5
(5 == 5 =true
)
f.(结果c) && (结果e)
(true && true
=true
) result
的最终值为true
。
最佳实践总结
- 括号是你的好朋友:在不确定优先级或为了代码清晰时,毫不犹豫地使用括号。
- 避免未定义行为:不要在单个表达式中对同一个变量进行多次无序的修改。
- 理解短路求值:利用
&&
和||
的短路特性来编写更安全、更高效的代码。 - 优先使用前置递增/递减:对于非内置类型,
++it
通常优于it++
。 - 区分位运算符和逻辑运算符:
&
vs&&
,|
vs||
是初学者常犯的错误,它们的功能完全不同。 - 了解整数除法:记住
int / int
的结果仍然是int
,小数部分被截断。