4 表达式 | Expression

表达式

REF

An expression is a sequence of operators and their operands, that specifies a computation.

Expression evaluation may produce a result (e.g., evaluation of 2+2 produces the result 4) and may generate side-effects (e.g. evaluation of std::printf("%d",4) prints the character '4' on the standard output).

基本概念

分类

根据运算对象数量可分为一元运算符 unary operator二元运算符binary operator等,函数调用也是一种特殊的运算符。

类型转换

在不同类型对象进行运算时,常常会发生类型转换。如小整数类型一般会被 提升 promote 成较大整数类型。

重载运算符 overloaded operator

当用户对类类型的对象进行运算时,可以自定运算符及其含义,称为 重载运算符 overloaded operator 。IO库zhongde'>>'和'<<',以及vector和string对象中的许多运算符都是重载的运算符。

左右值

C++表达式可以分为:

  • lvalue and rvalue
  • glvalue and prvalue

其中:
lvalue+xvalue=glvalueprvalue+xvlaue=rvalue

简单来说,左值用对象的身份,右值用对象的数值。

详见 https://en.cppreference.com/w/cpp/language/value_category

优先级 precedence、结合律 associativity、求值顺序 order of evaluation

优先级和结合律

https://en.cppreference.com/w/cpp/language/operator_precedence

求值顺序

参见:https://en.cppreference.com/w/cpp/language/eval_order

若某个语句没有指定执行顺序,但表达式指向并修改了同一个对象,则无法明确何时、如何对该对象求值,会引发错误产生未定义的行为 undefined behavior

  1. If a side effect on a scalar object is unsequenced relative to another side effect on the same scalar object, the behavior is undefined.
     i = ++i + 2;       // undefined behavior until C++11
     i = i++ + 2;       // undefined behavior until C++17
     f(i = -2, i = -2); // undefined behavior until C++17
     f(++i, ++i);       // undefined behavior until C++17, unspecified after C++17
     i = ++i + i++;     // undefined behavior
    
  2. If a side effect on a scalar object is unsequenced relative to a value computation using the value of the same scalar object, the behavior is undefined.
    cout << i << i++; // undefined behavior until C++17
    a[i] = i++;       // undefined behavior until C++17
    n = ++i + i;      // undefined behavior
    

算术运算符 arithmetic operators

+a
-a
a + b
a - b
a * b
a / b
a % b
~a
a & b
a | b
a ^ b
a << b
a >> b

逻辑logical 和 比较comparison 运算符

//logical operator
!a
a && b
a || b

//comparison operator
a == b
a != b
a < b
a > b
a <= b
a >= b
a <=> b

短路求值 short-circuit evaluation

逻辑运算符都是从左往右求值,直到无法确定后再从右开始。

  • || 当且仅当左侧为假才向右计算
  • && 当且仅当左侧为真才向右计算
  • 使用以上规则可以规避某些溢出,如:
    index != s.size() && !isspace(s[index])
    
    该式若改变 && 两端顺序,则isspace首先因下标越界而出错。

赋值运算符 assignment operator

赋值运算符左侧对象必须是一个可修改的左值。结果是左侧运算对象,且是一个左值。

a = b
a += b
a -= b
a *= b
a /= b
a %= b
a &= b
a |= b
a ^= b
a <<= b
a >>= b

右结合律

int a1, a2;
a1 = a2 = 0;  //正确,从右往左

int a3, *a4;
a3 = a4 = 0;  //错误,类型不同且无法转换

复合赋值运算符

假设算术运算符形如 X ,则:

a = a X b; 等价于 a X= b;,而左边求值两次,右边一次。

递增/递减运算符 increment/decrement operator

  • 前置版本++i:首先将运算对象加1,然后将改变后的对象作为结果继续运算。更广泛使用,能避免不必要的语句,如for循环中会少进行一次判断。
  • 后置版本i++: 将运算对象加1,但求值结果是运算对象改变之前的值的副本。较少使用,需要先储存副本,增加了不必要的运算。

后置自增和解引用混用

*p++ 是一种常用的避免无法输出首字母的表达式。由于递增递减的优先级高于解引用,该式等价于*(p++)

auto* p = v.begin();
while (p != v.end()) 
    cout<< *p++ <

用前置自增改写会增加代码而变得不简洁:

auto* p = v.begin();
while (p != v.end()) {
    cout<< *p <

使用递增递减应避免未定义行为

成员访问运算符 member access operator

a[b]
*a
&a
a->b
a.b
a->*b
a.*b

https://en.cppreference.com/w/cpp/language/operator_precedence

a->b等价于(*a).b而不是*a.b。后者相当于a本身是一个指针,试图访问该指针的b成员,自然出错。

条件运算符

cond ? expr1 : expr2 :先求cond的值,若真则对expr1求值并返回,否则对expr2求值并返回。

条件运算符嵌套

const int A = 90;
const int B = 80;
const int C = 70; 
const int D = 60;

int main(){
    int grade = 0;
    cin>>grade;
    string str_grade = (grade>=A)? "A" : 
                       (grade>=B)? "B" :
                       (grade>=C)? "C" :
                       (grade>=D)? "D" : "F";
    cout<

相较于switch和if等语句,由于会逐渐向后计算,使用该方式可减少判断条件的长度;但由于前式都需要计算,可能会略微减慢。

位运算符

用于整数类型的运算对象并将其视为二进制位的集合,并提供检查和设置二进制位的功能。位运算符同样可用于bitset的标准库类型。

如果运算对象类型是小整型,会被自动提升promote成较大整型。此外,由于不同编译器位运算对最前面符号位的处理方式不同,可能出现未定义undefined结果,因此避免为有符号数进行位运算。

~a
a & b
a | b
a ^ b
a << b
a >> b
  • 若是短类型,先进行提升:短类型前补满0转为长类型

    • 无论是否带符号,提升后默认是有符号值。
  • 若超限,则:

    • 超上限:减去容量(char: 256=0400);
    • 超下限:加上容量
  • 无符号数按位取反:容量减去当前值

  • 有符号数按位取反(提升并使容量合法时):

    • 所有位按位取反;
    • 若取反后符号位是1,显示的值是再(对绝对值)按位取反并+1。相当于 ~正->负:绝对值 +1 变符号
    • 若取反后符号位是0,显示的值是再(对绝对值)按位取反并-1。相当于 ~负->正:绝对值 -1 变符号
//char: int8_t;
//sizeof(unsigned char): 1byte=8bits; 3bit/num;
//char-max = 0B11'111'111 = 0377 = 255 = 0xFF = unsigned char -1;
unsigned char c1o = 0227;
//unsigned char 0b10'010;111 = 151
//to unsigned int 0b00000000'00000000'00000000'10010111
unsigned int i1o = 0227;
//unsigned int 0b00000000'00000000'00000000'10010111
auto c1o_(~c1o);
//int 0b11111111'11111111'11111111'01101000  =>
//int -(0b00000000'00000000'00000000'10010111+1) => int -152
auto i1o_(~i1o);
//unsigned int 0b11111111'11111111'11111111'01101000 
//to unsigned int 4294967144
cout<

不超限的短类型负数:

signed char c1o = -027; 
//注意char可能为unsigned也可能为signed

int x = -0b00000000'00000000'00000000'00010111;
int xx = 0b11111111'11111111'11111111'11101001;

cout<<~c1o<<'\t'<<~xx<<'\t'<<~x<

负号和按位取反的区别:

int main(){
    int a = ~0b00000000'00000000'00000000'00000100;
    //int a = ~5;
    int b = 0b11111111'11111111'11111111'11111011;
    //int b = -5;
    int c = -0b00000000'00000000'00000000'00000101;
    //int c = -5;
    cout<

负号返回值增加了一个取补码的过程,在表达式中的结果同样也是提符号-取补码作为绝对值-进行输出。而按位操作单纯对位操作,不取补码。二者单独使用均可逆,但混用时注意流向。

移位运算符(io运算符)满足左结合律。

sizeof运算符

返回的是某表达式或类型名所占的字节数。该运算符满足右结合律,所得的值是一个size_t类型的常量表达式。sizeof并不实际计算表达式的值而是直接返回字节数。运算符的运算对象形式如下:

sizeof (type);
sizeof expr;

sizeof *p比较特殊。首先,由于sizeof()满足右结合律并且优先级等于*运算符,按照从右向左的顺序组合,等价于sizeof(*p)。此外,由于不会实际求运算对象的值,即使是无效指针也可以安全获取结果。

一些规则:

  • sizeof(char): 1
  • 对string或vector执行sizeof只返回该类型固定部分的大小而不是对象中元素占了多少空间。
  • ia是个数组,可以使用 sizeof(ia)/sizeof(ia[0]) 或者 sizeof(ia)/sizeof(*ia) 得到数组大小。
    • 注意不要使用指向数组的指针:sizeof(p)/sizeof(*p)
  • sizeof的返回值是一个常量表达式,因此可以用该结果声明数组维度。

逗号运算符 comma operator

  • 用在for循环中,实现多个变量的自增/自减
  • 用在同一基本类型的连续赋值中

类型转换

如果两种类型有关联,则当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代:若两种类型可以相互转换conversion,则二者就是关联的。

隐式转换 implicit conversion:自动执行,无需人为介入。如下列情况:

  • 比int小的整型提升时;
  • 非布尔转为布尔;
  • 初始化的初始值转为变量的类型;赋值中右侧类型转为左侧类型
  • 多种类型进行混合算术运算;
  • 函数调用时的类型转换。

算数转换 arithmetic conversion

把一种算术类型转换成另一种算术类型。运算符的运算对象将转换成最宽的类型。例如有整型和浮点就都换为浮点;含有long double,则都换为long double。

整型提升integral promotion

  • 对于 bool, char, signed char, unsigned char, short, unsigned short,只要它们所有可能的值都能存在int里,它们就会提升成int类型,否则提升成unsigned int类型。
  • 对于 wchar_t, char16_t, char32_t,提升为能够容纳它们的 int, unsigned int, long, unsigned long, long long, unsigned long long 中最小的一种类型。

无符号类型

在进行整型提升后,再决定是否是带符号的类型。
具体规则如下:

  • 若只同时存在有符号或同时存在无符号,则转为容量较大的类型(无论正负)。
  • 若无符号类型 >= 有符号类型,均转为无符号类型:
    signed char a = -10;
    unsigned int b = 1;
    unsigned long c = 3.0;
    auto d = a+b+c; //unsigned long
    
  • 若有符号类型 > 无符号类型
    • 若无符号类型的所有值都可以存在有符号类型中,则转为带符号类型
    • 若不能,则转为无符号类型
    • 主要依赖于系统或编译器对类型大小的定义。

其他隐式类型转换

https://en.cppreference.com/w/cpp/language/implicit_conversion

特殊类型

某些特殊类型如size_t, size_type, ptrdiff_t, wchar_t都可以隐式转换为相应的合理类型(主要由操作系统和编译器决定)。为了程序的可移植性,应多使用这些类型。

数组转为指针

int ia[10];
int* ip = ia; //右边隐式转换为指向首元素的指针
int* ip = std::begin(ia);

上述转换不会在decltype, &, sizeof, typeid中发生。用引用初始化数组也不会发生。

指针的转换

  • 常量整数值的指针或者nullptr能转为任意指针类型;
  • 指向任意非常量的指针能转换为void*;
  • 指向任意对象的指针能转换成const void*。
  • 再有继承关系的类中还有其他转换方式。

指向常量的指针

允许将指向非常量类型的指针转换成指向相应常量类型的指针(增加底层const),反之由于试图删除底层const,无法实现。

int a = 1;
const int& ra = a;
const int* pa = &a;
int& rr = ra; //错误
int* pp = pa; //错误
int* const ppc = pa; //错误,没有底层,顶层自然不一致

类类型定义的转换

显式转换

强制类型转换 cast

具体参见:
https://en.cppreference.com/w/cpp/language/explicit_cast

命名的强制类型转换

形式如:
cast-name(expression)

  • type:目标类型。若是引用类型,则结果是左值。
  • expression:要转换的表达式
  • cast_name:包括 static_cast, dynamic_cast, const_cast, reinterpret_cast 指定了转换方式。

注意:不可用于左值,使用cast后的值进行初始化等操作时可与auto配合使用以减少类型名的重复编写。

static_cast

任何具有明确定义的类型转换,只要不包含底层const就可以使用。

static_cast不能转换掉expression的const、volatile、或者__unaligned属性。

int a = 1;

float b = static_cast(a);
//两种旧式转换方式
float c = float(a);
float d = (float)a;

int* const  pc = &a;
int* p = static_cast(pc); 
int* pp = pc;           //而且非底层const不必static_cast可以直接转换

const_cast

只能改变运算对象的底层const,这种行为称为 去掉const性质 | cast away the const。注意必须具有底层const。若对象是个常量(具有顶层const)则

int a = 1;

const int* pa = &a;
int* p = const_cast(pa);

const int& ra = a;
int& r = const_cast(ra);

reinterpret_cast

reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释,即以二进制存在形式的重新解释
可将int*转为char*等。使用此强制转换较为危险。

int* const pp = &b;
unsigned int* qq = reinterpret_cast(pp); 
unsigned int* qq = reinterpret_cast(pp); 

https://en.cppreference.com/w/cpp/language/reinterpret_cast

dynamic_cast

https://en.cppreference.com/w/cpp/language/dynamic_cast

pointer_cast

https://en.cppreference.com/w/cpp/memory/shared_ptr/pointer_cast

std::static_pointer_cast, std::dynamic_pointer_cast, std::const_pointer_cast, std::reinterpret_pointer_cast

旧型强制转换

type(expr)   //函数形式
(type)expr   //C语言风格

根据涉及类型不同,可以具有与 const_cast, static_cast, reinterpret_cast 任意一种相似的行为。若替换为const_caststatic_cast合法,则行为与命名一致,否则执行的是reinterpret_cast

运算符优先级

你可能感兴趣的:(4 表达式 | Expression)