C++学习:六个月从基础到就业——C++基础语法回顾:运算符与表达式

C++学习:六个月从基础到就业——C++基础语法回顾:运算符与表达式

本文是我C++学习之旅系列的第二篇技术文章,主要回顾C++中的运算符和表达式,包括优先级规则、类型转换和常见陷阱。查看完整系列目录了解更多内容。

引言

在C++中,运算符是执行特定操作的符号,而表达式则是由运算符和操作数组成的序列,用于计算值。理解运算符的语义、优先级和结合性对于编写正确的C++代码至关重要。本文将详细介绍C++中的各类运算符、表达式求值规则以及一些常见的陷阱和最佳实践。

基本运算符类型

C++中的运算符可以按功能大致分为以下几类:

算术运算符

算术运算符用于执行基本的数学运算:

运算符 描述 示例
+ 加法 a + b
- 减法 a - b
* 乘法 a * b
/ 除法 a / b
% 取模(整数除法的余数) a % b
++ 自增 ++aa++
-- 自减 --aa--

需要注意的是,自增(++)和自减(--)运算符有前缀和后缀两种形式,它们的行为略有不同:

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

关系运算符的结果是布尔值(truefalse):

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++会执行隐式类型转换,遵循以下基本规则:

  1. 如果一个操作数是布尔型,另一个不是,则布尔型会被提升为整型
  2. 如果一个操作数是整型,另一个是浮点型,则整型会被转换为浮点型
  3. 较小的整型(如char、short)在计算前会被提升为至少int大小
  4. 在二元运算中,较低级别的类型会被提升为较高级别的类型
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

最佳实践与建议

  1. 使用括号明确优先级:即使你对优先级规则非常熟悉,使用括号也能让代码更易读。

  2. 避免在一行中多次修改同一变量:这可能导致未定义行为。

    // 不好的做法
    int i = 0;
    int j = ++i + ++i;  // 未定义行为
    
    // 好的做法
    int i = 0;
    ++i;
    int j = i + i;
    
  3. 小心隐式类型转换:尤其是在混合有符号和无符号类型时。

    unsigned int a = 10;
    int b = -5;
    if (b < a) {
          
            // b被转换为无符号类型,比较结果可能不符合预期
        // ...
    }
    
  4. 使用显式类型转换:优先使用现代C++的类型转换运算符,而不是C风格的类型转换。

    // 不推荐
    int i = (int)3.14;
    
    // 推荐
    int i = static_cast<int>(3.14);
    
  5. 理解自增/自减运算符的行为:尤其是前缀和后缀形式的区别。

    int i = 5;
    int j = ++i;  // i=6, j=6
    int k = i++;  // i=7, k=6
    
  6. 避免过度依赖运算符优先级:即使你确定优先级正确,过于复杂的表达式也会增加代码的维护难度。

  7. 使用const防止意外修改:尤其在表达式中使用变量时。

    const int MAX_SIZE = 100;
    int array[MAX_SIZE];  // 确保MAX_SIZE不会被意外修改
    
  8. 考虑使用命名函数代替复杂表达式:这能提高代码的可读性和可维护性。

    // 不易理解的表达式
    if ((x & (x-1)) == 0 && x != 0) {
          
          
        // ...
    }
    
    // 更易理解的命名函数
    bool isPowerOfTwo(int x) {
          
          
        return x != 0 && (x & (x-1)) == 0;
    }
    
    if (isPowerOfTwo(x)) {
          
          
        // ...
    }
    

总结

本文回顾了C++中的运算符和表达式,包括各类运算符的使用、优先级规则、类型转换和常见陷阱。正确理解和使用这些基本概念,对于编写高效、无错的C++代码至关重要。在后续的文章中,我们将继续探索C++的其他基础特性,包括控制流结构、函数和类的定义等。

希望这篇文章对你有所帮助。如果有任何问题或建议,欢迎在评论区留言讨论!

参考资料

  1. Bjarne Stroustrup. The C++ Programming Language (4th Edition)
  2. Scott Meyers. Effective Modern C++
  3. cppreference.com - 运算符优先级
  4. C++ Core Guidelines - 表达式与语句

查看完整系列目录了解更多内容。