MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++表达式与运算符的奥秘

2021-07-281.7k 阅读

C++表达式与运算符的基础概念

表达式的定义

在C++ 中,表达式是由运算符和操作数组合而成的代码单元,它能够产生一个值。操作数可以是变量、常量或者其他表达式。例如,3 + 5就是一个简单的表达式,其中35是操作数,+是运算符,这个表达式的值为8

运算符的分类

C++ 拥有丰富的运算符类型,主要可分为以下几类:

  1. 算术运算符:用于执行基本的数学运算,如加法(+)、减法(-)、乘法(*)、除法(/)和取模(%)。
    int a = 10;
    int b = 3;
    int sum = a + b; // 加法运算,sum的值为13
    int diff = a - b; // 减法运算,diff的值为7
    int product = a * b; // 乘法运算,product的值为30
    int quotient = a / b; // 除法运算,quotient的值为3,因为是整数除法,小数部分被截断
    int remainder = a % b; // 取模运算,remainder的值为1
    
  2. 赋值运算符:用于给变量赋值,最常见的是=。此外,还有复合赋值运算符,如+=-=*=/=等。
    int num = 5; // 使用 = 进行赋值
    num += 3; // 等价于 num = num + 3,num的值变为8
    num *= 2; // 等价于 num = num * 2,num的值变为16
    
  3. 比较运算符:用于比较两个值,结果是一个布尔值(truefalse)。常见的比较运算符有==(等于)、!=(不等于)、<(小于)、>(大于)、<=(小于等于)和>=(大于等于)。
    int x = 10;
    int y = 15;
    bool result1 = x < y; // true
    bool result2 = x == y; // false
    bool result3 = x != y; // true
    
  4. 逻辑运算符:用于组合多个布尔表达式,包括逻辑与(&&)、逻辑或(||)和逻辑非(!)。
    bool a = true;
    bool b = false;
    bool and_result = a && b; // false,因为逻辑与要求两边都为true结果才为true
    bool or_result = a || b; // true,因为逻辑或只要有一边为true结果就为true
    bool not_result =!a; // false,逻辑非将true变为false
    
  5. 位运算符:用于对整数的二进制位进行操作,包括按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移(<<)和右移(>>)。
    int num1 = 5; // 二进制为 00000101
    int num2 = 3; // 二进制为 00000011
    int and_bitwise = num1 & num2; // 按位与,结果为 00000001,即1
    int or_bitwise = num1 | num2; // 按位或,结果为 00000111,即7
    int xor_bitwise = num1 ^ num2; // 按位异或,结果为 00000110,即6
    int not_bitwise = ~num1; // 按位取反,结果为 11111010,在有符号整数中是 -6
    int left_shift = num1 << 1; // 左移1位,结果为 00001010,即10
    int right_shift = num1 >> 1; // 右移1位,结果为 00000010,即2
    
  6. 自增和自减运算符++用于将变量的值增加1,--用于将变量的值减少1。它们有前置和后置两种形式。
    int count = 5;
    int preIncrement = ++count; // 前置自增,count先加1,然后赋值给preIncrement,preIncrement和count的值都为6
    int postIncrement = count++; // 后置自增,先将count的值赋给postIncrement,然后count再加1,postIncrement的值为6,count的值为7
    
  7. 条件运算符(三元运算符)? :,它是C++ 中唯一的三元运算符,形式为condition? expression1 : expression2。如果conditiontrue,则返回expression1的值,否则返回expression2的值。
    int num = 10;
    int result = num > 5? num * 2 : num / 2; // 因为num > 5为true,所以result的值为num * 2,即20
    
  8. 逗号运算符,,用于将多个表达式连接起来,从左到右依次计算每个表达式,并返回最后一个表达式的值。
    int a1 = 1;
    int b1 = 2;
    int result1 = (a1++, b1++, a1 + b1); // 先a1自增为2,b1自增为3,最后返回a1 + b1,即5
    
  9. sizeof 运算符:用于获取数据类型或变量所占用的字节数。
    int intSize = sizeof(int); // 获取int类型的字节数,在32位系统上通常为4
    double doubleSize = sizeof(double); // 获取double类型的字节数,通常为8
    
  10. 成员访问运算符:用于访问类或结构体的成员,包括点运算符(.)和箭头运算符(->)。
    struct Point {
        int x;
        int y;
    };
    Point p;
    p.x = 10; // 使用点运算符访问结构体成员
    Point* ptr = &p;
    ptr->y = 20; // 使用箭头运算符通过指针访问结构体成员
    
  11. 函数调用运算符(),用于调用函数,传递参数。
    int add(int a, int b) {
        return a + b;
    }
    int sum1 = add(3, 5); // 使用函数调用运算符调用add函数,返回8
    
  12. 指针运算符:包括取地址运算符(&)和间接寻址运算符(*)。&用于获取变量的地址,*用于通过指针访问其所指向的值。
    int num2 = 10;
    int* ptr2 = &num2; // 取num2的地址并赋给ptr2
    int value = *ptr2; // 通过指针ptr2访问其所指向的值,value为10
    

运算符的优先级与结合性

优先级规则

运算符的优先级决定了在一个复杂表达式中各个运算符执行的先后顺序。优先级高的运算符先执行,优先级低的运算符后执行。例如,在表达式3 + 5 * 2中,乘法运算符*的优先级高于加法运算符+,所以先计算5 * 210,然后再计算3 + 1013

以下是C++ 运算符优先级的大致顺序(从高到低):

  1. 作用域解析运算符::
  2. 后缀运算符()(函数调用)、[](数组下标)、.(成员访问)、->(通过指针成员访问)、++(后置自增)、--(后置自减)
  3. 单目运算符++(前置自增)、--(前置自减)、+(正号)、-(负号)、~(按位取反)、!(逻辑非)、*(间接寻址)、&(取地址)、sizeoftypeid
  4. 乘除运算符*(乘法)、/(除法)、%(取模)
  5. 加减运算符+(加法)、-(减法)
  6. 移位运算符<<(左移)、>>(右移)
  7. 关系运算符<(小于)、>(大于)、<=(小于等于)、>=(大于等于)
  8. 相等运算符==(等于)、!=(不等于)
  9. 按位与运算符&
  10. 按位异或运算符^
  11. 按位或运算符|
  12. 逻辑与运算符&&
  13. 逻辑或运算符||
  14. 条件运算符? :
  15. 赋值运算符=+=-=*=/=%=&=^=|=<<=>>=
  16. 逗号运算符,

结合性

当一个表达式中出现多个优先级相同的运算符时,结合性决定了运算的顺序。结合性分为左结合和右结合。大多数二元运算符是左结合的,即从左到右进行计算。例如,在表达式10 - 5 - 3中,由于减法运算符是左结合的,先计算10 - 55,然后再计算5 - 32

而一些运算符是右结合的,如赋值运算符。例如,在表达式a = b = c中,由于赋值运算符是右结合的,先将c的值赋给b,然后再将b的值赋给a

表达式求值与类型转换

隐式类型转换(自动类型转换)

在表达式求值过程中,如果操作数的类型不同,C++ 会进行隐式类型转换,将它们转换为相同的类型。通常,这种转换是从低精度类型向高精度类型转换,以避免数据丢失。例如:

int num3 = 5;
double result2 = num3 + 2.5; // num3会被隐式转换为double类型,然后进行加法运算,result2的值为7.5

常见的隐式类型转换规则如下:

  1. 整型提升:对于小于int类型的整型(如charshort),在进行运算时会被提升为int类型。如果short的大小与int相同,或者操作数为unsigned short且其值无法用int表示时,则提升为unsigned int
    char ch = 'A';
    int result3 = ch + 10; // ch被提升为int类型,然后进行加法运算
    
  2. 算术转换:如果一个操作数是float,另一个是doublefloat会被转换为double。如果一个操作数是整型,另一个是浮点型,整型会被转换为浮点型。
    float f = 3.5f;
    double d = 2.0;
    double result4 = f + d; // f被转换为double类型,然后进行加法运算
    

显式类型转换(强制类型转换)

有时,程序员需要明确地将一种类型转换为另一种类型,这就需要使用显式类型转换。C++ 提供了四种类型转换运算符:static_castdynamic_castconst_castreinterpret_cast

  1. static_cast:用于进行较为“安全”的类型转换,例如将int转换为double,或者将派生类指针转换为基类指针等。
    int num4 = 10;
    double d1 = static_cast<double>(num4); // 将int转换为double
    
  2. dynamic_cast:主要用于在继承体系中进行安全的向下转型(从基类指针或引用转换为派生类指针或引用)。它在运行时进行类型检查,如果转换失败,返回nullptr(对于指针)或抛出std::bad_cast异常(对于引用)。
    class Base { virtual void func() {} };
    class Derived : public Base {};
    Base* basePtr = new Derived();
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 安全的向下转型
    
  3. const_cast:用于去除对象的constvolatile属性。
    const int num5 = 10;
    int* ptr3 = const_cast<int*>(&num5); // 去除num5的const属性,不推荐这样做,可能导致未定义行为
    
  4. reinterpret_cast:用于进行危险的、低层次的类型转换,通常用于转换指针类型,不改变实际的二进制数据,只是重新解释其类型。
    int num6 = 10;
    char* ptr4 = reinterpret_cast<char*>(&num6); // 将int指针转换为char指针,可能导致未定义行为
    

表达式中的副作用与序列点

副作用的概念

表达式在求值过程中,除了产生一个值之外,还可能对程序的状态产生其他影响,这些影响被称为副作用。例如,自增和自减运算符不仅会返回一个值,还会改变变量本身的值,这就是一种副作用。

int count1 = 5;
int result5 = count1++; // 这里count1++有副作用,会改变count1的值,同时返回count1原来的值

序列点

序列点是程序执行过程中的一个点,在该点之前的所有副作用都必须完成,之后的副作用才会开始。C++ 中有几个明确的序列点:

  1. 完整表达式的结束处:一个完整表达式是指不包含其他完整表达式的表达式,例如语句中的表达式、return 语句中的表达式等。在完整表达式结束时,所有在该表达式中的副作用都必须完成。
    int a2 = 3;
    int b2 = 4;
    int c2 = a2++ + b2++; // 这里在分号处是一个序列点,在该点之前,a2++和b2++的副作用(自增操作)必须完成
    
  2. 逻辑与(&&)、逻辑或(||)和条件运算符(? :)的第一个操作数求值完成后:逻辑与和逻辑或运算符具有短路特性,即如果第一个操作数已经能够确定整个表达式的结果,第二个操作数将不会被求值。
    int x1 = 5;
    int y1 = 3;
    bool result6 = (x1 > 10) && (y1++ > 2); // 由于x1 > 10为false,y1++不会被求值,y1的值仍为3
    
  3. 逗号运算符(,)的第一个操作数求值完成后:逗号运算符从左到右依次求值,在第一个操作数求值完成后有一个序列点。
    int a3 = 1;
    int b3 = 2;
    int result7 = (a3++, b3++); // 先a3自增,然后b3自增,最后返回b3++的值,在a3++和b3++之间有一个序列点
    

理解序列点对于编写正确、可预测的代码非常重要,因为在没有序列点分隔的情况下,多个副作用的执行顺序是未定义的,可能导致难以调试的错误。

复杂表达式的分析与优化

复杂表达式的构建与分析

在实际编程中,常常会遇到复杂的表达式,这些表达式可能包含多个运算符和不同类型的操作数。例如:

int a4 = 3;
int b4 = 5;
int c4 = 2;
int result8 = (a4 + b4) * (c4++ - 1) / (++a4) % 3;

分析这样的复杂表达式,需要按照运算符的优先级和结合性逐步拆解。首先,根据优先级,括号内的表达式先计算,即a4 + b48c4++ - 1中先使用c4的值2计算2 - 11,然后c4自增为3。接着,++a4使a4先自增为4。然后按照顺序计算乘法8 * 18,除法8 / 42,最后取模2 % 32,所以result8的值为2

表达式优化

在编写代码时,对于复杂表达式进行优化可以提高程序的性能和可读性。

  1. 减少重复计算:如果一个子表达式在复杂表达式中多次出现,将其结果存储在一个临时变量中,避免重复计算。
    // 未优化
    int x2 = 5;
    int y2 = 3;
    int result9 = (x2 * y2) + (x2 * y2) + (x2 * y2);
    // 优化后
    int temp = x2 * y2;
    int result10 = temp + temp + temp;
    
  2. 合理使用括号:虽然按照运算符优先级可以确定计算顺序,但使用括号可以使表达式的逻辑更加清晰,避免误解。
    int a5 = 2;
    int b5 = 3;
    int c5 = 4;
    // 未使用括号,较难理解
    int result11 = a5 + b5 * c5 - a5 / b5;
    // 使用括号,逻辑更清晰
    int result12 = (a5 + (b5 * c5)) - (a5 / b5);
    
  3. 避免过度复杂的表达式:过于复杂的表达式会降低代码的可读性和可维护性。将复杂表达式拆分成多个简单的步骤和中间变量,使代码逻辑更清晰。
    // 复杂表达式
    int result13 = (a5 * b5 + c5) / (a5 - b5) * (a5 + c5);
    // 拆分后的代码
    int temp1 = a5 * b5 + c5;
    int temp2 = a5 - b5;
    int temp3 = a5 + c5;
    int result14 = temp1 / temp2 * temp3;
    

通过对C++ 表达式与运算符的深入理解,包括其基础概念、优先级、类型转换、副作用以及复杂表达式的分析与优化,程序员能够编写出更加高效、准确和可读的代码。在实际编程中,要根据具体的需求和场景,合理运用各种运算符和表达式,以实现程序的预期功能。