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; // 二进制:00111111
unsigned 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, // 0001
WRITE = 1 << 1, // 0010
EXECUTE = 1 << 2, // 0100
HIDDEN = 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 - 表达式与语句
查看完整系列目录了解更多内容。