C++学习:六个月从基础到就业——C++基础语法回顾:运算符与表达式
本文是我C++学习之旅系列的第二篇技术文章,主要回顾C++中的运算符和表达式,包括优先级规则、类型转换和常见陷阱。查看完整系列目录了解更多内容。
引言
在C++中,运算符是执行特定操作的符号,而表达式则是由运算符和操作数组成的序列,用于计算值。理解运算符的语义、优先级和结合性对于编写正确的C++代码至关重要。本文将详细介绍C++中的各类运算符、表达式求值规则以及一些常见的陷阱和最佳实践。
基本运算符类型
C++中的运算符可以按功能大致分为以下几类:
算术运算符
算术运算符用于执行基本的数学运算:
运算符 | 描述 | 示例 |
---|---|---|
+ | 加法 | a + b |
- | 减法 | a - b |
* | 乘法 | a * b |
/ | 除法 | a / b |
% | 取模(整数除法的余数) | a % b |
++ | 自增 | ++a 或a++ |
-- | 自减 | --a 或a-- |
需要注意的是,自增(++
)和自减(--
)运算符有前缀和后缀两种形式,它们的行为略有不同:
int a = 5;
int b = ++a; // 前缀形式:先增加a,再将a的值赋给b(结果:a=6, b=6)
int c = 5;
int d = c++; // 后缀形式:先将c的值赋给d,再增加c(结果:c=6, d=5)
整数除法会截断小数部分:
int result = 5 / 2; // 结果为2,而不是2.5
要获取浮点结果,至少一个操作数必须是浮点类型:
double result = 5.0 / 2; // 结果为2.5
关系运算符
关系运算符用于比较两个值:
运算符 | 描述 | 示例 |
---|---|---|
== | 等于 | a == b |
!= | 不等于 | a != b |
> | 大于 | a > b |
< | 小于 | a < b |
>= | 大于等于 | a >= b |
<= | 小于等于 | a <= b |
关系运算符的结果是布尔值(true
或false
):
int a = 5, b = 10;
bool result = a < b; // 结果为true
逻辑运算符
逻辑运算符用于组合布尔表达式:
运算符 | 描述 | 示例 |
---|---|---|
&& | 逻辑与 | a && b |
|| | 逻辑或 | a || b |
! | 逻辑非 | !a |
逻辑运算符遵循短路求值规则:
bool a = false, b = true;
bool result1 = a && b; // 结果为false
bool result2 = a || b; // 结果为true// 短路求值示例
int x = 5;
if (x != 0 && 10 / x > 1) { // 安全:如果x为0,第二部分不会被求值// ...
}
位运算符
位运算符对整数的二进制位进行操作:
运算符 | 描述 | 示例 |
---|---|---|
& | 按位与 | a & b |
| | 按位或 | a | b |
^ | 按位异或 | a ^ b |
~ | 按位取反 | ~a |
<< | 左移 | a << b |
>> | 右移 | a >> b |
位运算符的例子:
unsigned char a = 0x5A; // 二进制:01011010
unsigned char b = 0x3F; // 二进制:00111111unsigned char c = a & b; // 结果:00011010 (0x1A)
unsigned char d = a | b; // 结果:01111111 (0x7F)
unsigned char e = a ^ b; // 结果:01100101 (0x65)
unsigned char f = ~a; // 结果:10100101 (0xA5)
unsigned char g = a << 2; // 结果:01101000 (0x68)
unsigned char h = a >> 2; // 结果:00010110 (0x16)
位运算符常用于:
- 设置、清除或检查特定位
- 高效的乘除运算(左移右移)
- 位掩码操作
- 位域处理
赋值运算符
赋值运算符用于将值赋给变量:
运算符 | 描述 | 等价形式 |
---|---|---|
= | 简单赋值 | a = b |
+= | 加法赋值 | a = a + b |
-= | 减法赋值 | a = a - b |
*= | 乘法赋值 | a = a * b |
/= | 除法赋值 | a = a / b |
%= | 取模赋值 | a = a % b |
&= | 按位与赋值 | a = a & b |
|= | 按位或赋值 | a = a | b |
^= | 按位异或赋值 | a = a ^ b |
<<= | 左移赋值 | a = a << b |
>>= | 右移赋值 | a = a >> b |
复合赋值运算符的优势在于更简洁的代码和可能的性能优化:
int x = 10;
x += 5; // 等价于 x = x + 5;// 在复杂表达式中尤其有用
some_long_variable_name += another_long_variable_name;
成员访问运算符
成员访问运算符用于访问类、结构或联合的成员:
运算符 | 描述 | 示例 |
---|---|---|
. | 直接成员访问 | object.member |
-> | 通过指针的成员访问 | pointer->member |
.* | 通过对象访问成员指针 | object.*memberPtr |
->* | 通过指针访问成员指针 | pointer->*memberPtr |
示例:
struct Point {int x, y;void print() { std::cout << "(" << x << ", " << y << ")" << std::endl; }
};Point p{10, 20};
p.x = 15; // 直接访问成员变量
p.print(); // 调用成员函数Point* ptr = &p;
ptr->y = 25; // 通过指针访问成员
ptr->print(); // 通过指针调用成员函数// 成员指针示例
void (Point::*funcPtr)() = &Point::print;
(p.*funcPtr)(); // 通过对象调用成员函数指针
(ptr->*funcPtr)(); // 通过指针调用成员函数指针
其他重要运算符
运算符 | 描述 | 示例 |
---|---|---|
?: | 条件运算符(三元运算符) | condition ? expr1 : expr2 |
, | 逗号运算符 | expr1, expr2 |
sizeof | 获取类型或变量的大小 | sizeof(type) 或 sizeof expr |
new | 动态分配内存 | new Type |
new[] | 动态分配数组 | new Type[size] |
delete | 释放动态分配的内存 | delete ptr |
delete[] | 释放动态分配的数组 | delete[] array |
() | 函数调用 | func(args) |
[] | 数组下标访问 | array[index] |
typeid | 获取类型信息 | typeid(expr) |
static_cast | 静态类型转换 | static_cast<Type>(expr) |
dynamic_cast | 动态类型转换 | dynamic_cast<Type>(expr) |
const_cast | 常量转换 | const_cast<Type>(expr) |
reinterpret_cast | 重解释转换 | reinterpret_cast<Type>(expr) |
条件运算符(三元运算符)
条件运算符是唯一的三元运算符,常用于简单的条件赋值:
int max_value = (a > b) ? a : b; // 如果a大于b,返回a,否则返回b
逗号运算符
逗号运算符按顺序求值左侧和右侧的表达式,整个表达式的结果是右侧表达式的值:
int a = 1, b = 2;
int c = (a++, b++, a + b); // a=2, b=3, c=5
注意:不要将逗号运算符与其他上下文中的逗号分隔符混淆,如函数参数列表或变量声明中的逗号。
运算符优先级与结合性
运算符的优先级决定了复合表达式中的运算顺序,而结合性则决定了相同优先级运算符的求值顺序(从左到右或从右到左)。
以下是C++运算符按优先级从高到低的大致排序(简化版):
优先级 | 运算符 | 结合性 |
---|---|---|
1 | 作用域解析 :: | 左到右 |
2 | 后缀递增/递减 a++ a-- 函数调用 () 数组下标 [] 成员访问 . -> | 左到右 |
3 | 前缀递增/递减 ++a --a 一元运算符 + - ! ~ 类型转换 new delete sizeof 指针操作 * & | 右到左 |
4 | 乘除法 * / % | 左到右 |
5 | 加减法 + - | 左到右 |
6 | 移位 << >> | 左到右 |
7 | 关系 < <= > >= | 左到右 |
8 | 相等 == != | 左到右 |
9 | 按位与 & | 左到右 |
10 | 按位异或 ^ | 左到右 |
11 | 按位或 | | 左到右 |
12 | 逻辑与 && | 左到右 |
13 | 逻辑或 || | 左到右 |
14 | 条件 ?: | 右到左 |
15 | 赋值 = += -= 等 | 右到左 |
16 | 逗号 , | 左到右 |
复杂表达式示例:
int a = 5, b = 2, c = 3, d;
d = a + b * c; // 结果:d = 5 + (2 * 3) = 5 + 6 = 11
为了提高代码可读性,建议使用括号明确表达运算顺序,尤其是在涉及多种运算符的复杂表达式中:
d = a + (b * c); // 更加清晰
类型转换与表达式求值
隐式类型转换
当运算符作用于不同类型的操作数时,C++会执行隐式类型转换,遵循以下基本规则:
- 如果一个操作数是布尔型,另一个不是,则布尔型会被提升为整型
- 如果一个操作数是整型,另一个是浮点型,则整型会被转换为浮点型
- 较小的整型(如char、short)在计算前会被提升为至少int大小
- 在二元运算中,较低级别的类型会被提升为较高级别的类型
char a = 'A'; // ASCII值为65
int b = a + 1; // a被提升为int,结果为66
double c = b / 2; // 整型除法导致结果为33,然后被转换为33.0
算术类型提升规则
整数提升:小于int大小的整型(如bool、char、short)在表达式中会被提升为int或unsigned int。
一般算术转换:在涉及二元运算符的表达式中,如果操作数具有不同的类型,则会按照"转换为最宽类型"的原则进行转换。
转换等级从低到高:
bool → char → short → int → long → long long → float → double → long double
常见陷阱
整数除法
整数除法会截断小数部分,这是C++继承自C语言的行为:
int a = 5, b = 2;
int result = a / b; // 结果为2,小数部分被截断
正确处理方法:
double result = static_cast<double>(a) / b; // 结果为2.5
无符号数下溢
无符号整数减法导致结果小于零时,会发生环绕:
unsigned int a = 3;
unsigned int b = 5;
unsigned int result = a - b; // 结果不是-2,而是一个很大的正数(UINT_MAX - 1)
运算符优先级混淆
位运算符与逻辑运算符的优先级常常被混淆:
int a = 0x05; // 00000101
int b = 0x0A; // 00001010
bool result = a & b == 0; // 等同于 a & (b == 0),而不是 (a & b) == 0
正确做法应该使用括号明确意图:
bool result = (a & b) == 0; // 清楚表明先执行按位与操作,再比较结果
自增/自减运算符副作用
当在同一个表达式中多次修改同一个变量时,会产生未定义行为:
int i = 5;
int j = i++ + i++; // 未定义行为!
运算符重载
C++允许自定义类型重载大多数运算符,使其具有特殊的语义:
class Complex {
private:double real, imag;
public:Complex(double r = 0, double i = 0) : real(r), imag(i) {}// 重载+运算符(成员函数)Complex operator+(const Complex& rhs) const {return Complex(real + rhs.real, imag + rhs.imag);}// 重载输出流运算符(友元函数)friend std::ostream& operator<<(std::ostream& os, const Complex& c) {os << c.real;if (c.imag >= 0) os << "+";os << c.imag << "i";return os;}
};int main() {Complex a(1, 2), b(3, 4);Complex c = a + b; // 使用重载的+运算符std::cout << c; // 使用重载的<<运算符,输出"4+6i"return 0;
}
运算符重载的规则和限制:
- 不能改变运算符的优先级、结合性或操作数数量
- 不能重载不存在的运算符(如
**
等) - 某些运算符不能重载,如
.
、::
、.*
、?:
等 - 至少有一个操作数必须是用户定义类型
表达式的左值和右值
在C++中,表达式可以分为左值(lvalue)和右值(rvalue)。理解这个概念对于深入理解C++的语法和语义非常重要。
- 左值:可以出现在赋值运算符左侧的表达式,通常代表存储在内存中的对象。
- 右值:只能出现在赋值运算符右侧的表达式,通常是临时的、即将消亡的值。
例如:
int x = 5; // x是左值,5是右值
int& ref = x; // 可以绑定引用到左值
// int& ref2 = 5; // 错误:不能将非常量引用绑定到右值
const int& cref = 5; // 正确:可以将常量引用绑定到右值
C++11引入了右值引用(&&
),使我们能够更好地处理右值:
void process(int& x) { std::cout << "处理左值引用" << std::endl; }
void process(int&& x) { std::cout << "处理右值引用" << std::endl; }int main() {int a = 5;process(a); // 调用左值引用版本process(5); // 调用右值引用版本process(a+2); // 调用右值引用版本return 0;
}
位运算技巧
位运算是C++中的强大工具,尤其在需要性能优化的场景下。以下是一些常用技巧:
检查特定位
bool isBitSet(int value, int position) {return (value & (1 << position)) != 0;
}
设置特定位
int setBit(int value, int position) {return value | (1 << position);
}
清除特定位
int clearBit(int value, int position) {return value & ~(1 << position);
}
切换特定位
int toggleBit(int value, int position) {return value ^ (1 << position);
}
高效乘除
int multiplyByTwo(int value) {return value << 1; // 等同于value * 2
}int divideByTwo(int value) {return value >> 1; // 等同于value / 2(对于正数)
}
检查奇偶性
bool isEven(int value) {return (value & 1) == 0;
}
实际应用示例
位域与按位操作
下面的例子展示了如何使用位运算操作FLAG:
// 使用枚举定义标志位
enum Flags {READ = 1 << 0, // 0001WRITE = 1 << 1, // 0010EXECUTE = 1 << 2, // 0100HIDDEN = 1 << 3 // 1000
};// 设置权限
int permissions = 0;
permissions |= READ | WRITE; // 添加读写权限// 检查权限
bool canRead = (permissions & READ) != 0;
bool canExecute = (permissions & EXECUTE) != 0;// 删除权限
permissions &= ~WRITE; // 移除写权限// 切换权限
permissions ^= HIDDEN; // 如果隐藏,则显示;如果显示,则隐藏
优化条件语句
使用三元运算符可以简化一些条件语句:
// 传统if-else
int max;
if (a > b) {max = a;
} else {max = b;
}// 使用三元运算符
int max = (a > b) ? a : b;
复合赋值与运算符优先级
理解复合赋值和运算符优先级对于编写简洁、正确的代码至关重要:
int a = 5, b = 3, c = 2;// 错误的理解
a = b + c; // a = 5
a += b + c; // 很多人误以为等同于 a = a + b + c = 5 + 3 + 2 = 10// 正确的理解
// a += b + c 等同于 a = a + (b + c) = 5 + (3 + 2) = 5 + 5 = 10
最佳实践与建议
-
使用括号明确优先级:即使你对优先级规则非常熟悉,使用括号也能让代码更易读。
-
避免在一行中多次修改同一变量:这可能导致未定义行为。
// 不好的做法 int i = 0; int j = ++i + ++i; // 未定义行为// 好的做法 int i = 0; ++i; int j = i + i;
-
小心隐式类型转换:尤其是在混合有符号和无符号类型时。
unsigned int a = 10; int b = -5; if (b < a) { // b被转换为无符号类型,比较结果可能不符合预期// ... }
-
使用显式类型转换:优先使用现代C++的类型转换运算符,而不是C风格的类型转换。
// 不推荐 int i = (int)3.14;// 推荐 int i = static_cast<int>(3.14);
-
理解自增/自减运算符的行为:尤其是前缀和后缀形式的区别。
int i = 5; int j = ++i; // i=6, j=6 int k = i++; // i=7, k=6
-
避免过度依赖运算符优先级:即使你确定优先级正确,过于复杂的表达式也会增加代码的维护难度。
-
使用const防止意外修改:尤其在表达式中使用变量时。
const int MAX_SIZE = 100; int array[MAX_SIZE]; // 确保MAX_SIZE不会被意外修改
-
考虑使用命名函数代替复杂表达式:这能提高代码的可读性和可维护性。
// 不易理解的表达式 if ((x & (x-1)) == 0 && x != 0) {// ... }// 更易理解的命名函数 bool isPowerOfTwo(int x) {return x != 0 && (x & (x-1)) == 0; }if (isPowerOfTwo(x)) {// ... }
总结
本文回顾了C++中的运算符和表达式,包括各类运算符的使用、优先级规则、类型转换和常见陷阱。正确理解和使用这些基本概念,对于编写高效、无错的C++代码至关重要。在后续的文章中,我们将继续探索C++的其他基础特性,包括控制流结构、函数和类的定义等。
希望这篇文章对你有所帮助。如果有任何问题或建议,欢迎在评论区留言讨论!
参考资料
- Bjarne Stroustrup. The C++ Programming Language (4th Edition)
- Scott Meyers. Effective Modern C++
- cppreference.com - 运算符优先级
- C++ Core Guidelines - 表达式与语句
查看完整系列目录了解更多内容。