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)

用于比较两个值,结果为布尔值 truefalse

运算符名称示例描述
==等于a == b如果 ab 相等,返回 true
!=不等于a != b如果 ab 不相等,返回 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 << na 的所有位向左移动 n 位,右边补 0。相当于 a * 2^n
>>右移a >> na 的所有位向右移动 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 = ba = b
+=a += ba = a + b
-=a -= ba = a - b
*=a *= ba = a * b
/=a /= ba = a / b
%=a %= ba = a % b
&=a &= ba = a & b
|=a |= ba = a | b
^=a ^= ba = a ^ b
<<=a <<= ba = a << b
>>=a >>= ba = 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如果 condtrue,表达式的值为 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)

简化的优先级表 (从高到低):

  1. () [] . -> ::
  2. ++ -- ! ~ * & sizeof (一元运算符)
  3. * / % (乘除模)
  4. + - (加减)
  5. << >> (位移)
  6. < <= > >= (关系)
  7. == != (相等)
  8. & (按位与)
  9. ^ (按位异或)
  10. | (按位或)
  11. && (逻辑与)
  12. || (逻辑或)
  13. ?: (条件)
  14. = += -= 等赋值运算符 (右结合)
  15. , (逗号)

最佳实践: 不要去死记硬背完整的优先级表!如果一个表达式的求值顺序不明显,请使用括号 () 来明确指定顺序。这能极大地提高代码的可读性并避免错误。


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;
  1. 优先级分析: * / > + - > > == > &&
  2. 求值步骤:
    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)
  3. result 的最终值为 true

最佳实践总结

  1. 括号是你的好朋友:在不确定优先级或为了代码清晰时,毫不犹豫地使用括号。
  2. 避免未定义行为:不要在单个表达式中对同一个变量进行多次无序的修改。
  3. 理解短路求值:利用 &&|| 的短路特性来编写更安全、更高效的代码。
  4. 优先使用前置递增/递减:对于非内置类型,++it 通常优于 it++
  5. 区分位运算符和逻辑运算符& vs &&| vs || 是初学者常犯的错误,它们的功能完全不同。
  6. 了解整数除法:记住 int / int 的结果仍然是 int,小数部分被截断。