======表达式====== //第 4 章笔记// ---- ====表达式基础==== * 由操作数组成 * 可求值,并且(通常)返回结果(求值广义上是对系统产生影响的操作) * 最基本的表达式: * 变量 / 常量(字面值) * 通常表达式会包含**运算符**,可复合 ===操作符=== * 表达式的重要组成部分 ==操作符的特性== * 可以接收多少操作数:元数 * 可以接收什么类型的操作数:不满足类型要求时,可能存在类型转换 * 操作数是左值还是右值 * 结果的类型(考虑结果主要是在复合的情况下) * 结果是左值还是右值 * 操作符的优先级和结合性: * 参考 C++ Operator precedene * 括号可以改变优先级 * 相同优先级具有相同的结合性 * 操作符重载:不改变操作数的**个数,优先级和结合性** ==操作符求值顺序的不确定性== int x = 0; // C++ 没有规定此类的表达式的顺序 // complier depended func(x = x + 1; x = x + 1) ; ===左值与右值=== * 左右之分是基于**表达式**来判定的 ==glvalue (generalized) 泛左值== * 表达式 * 求值结果可以确定一个对象 / 内存位置 / 函数 * 起到了一个**标识**的作用 // 对 x 的评估结果是获得 x 关联的内存位置 x = 3; ==prvalue(pure) 纯右值== * 只能作为操作数来使用(不能作为赋值的对象) * 可以被用于初始化某个对象 * 构造临时对象时,也是右值 x = 3; // used for init 3 + 2; // used as operands // temp objects is pvalue int{}; ==xvalue 亡值== * 资源可以被重新使用的值 std::vector x; // 调用右值引用 void fun(std::vector&& par) // x 被转变为了亡值, x 对应的 vector 资源已经被转交 fun(std::move(x)); ==左值右值的判断== * 左值: * 是 ''gvalue'':就是标识了一个一个对象,位或者函数 * 不是 ''xvalue'':并不是即将消除的值 * 右值: * 是一个即将消亡的值 * 如果用于初始化或者 operand,则是纯右值 ''prvalue'' * 与 C 不同,左值和右值与 ''='' 的左右没有特定的关系 // x 是左值 const int x = 3; // error, x 不能放到 = 左边 // x 是 immutiable value x = 3; // 右值可以放到 = 左边 struct Str {}; int main() { // Str() 是临时变量,是右值 Str() = str(); } ==左值与右值的转换== //x, y 是左值 int x = 3; int y = 3; // + 需要右值作为 operand,此时传入的是左值 // 左值在这里就转变为了右值 // l to r conversion x + y ==Temporary materialization== struct Str { int x; } int main() { // prvalue temp object Str(); // 从 Str() 临时对象中取出 x 代表的数据 // 这个过程中,取值过程将 Str() 代表的临时对象转变了有标识的值 // 所以 Str() 标识了一个位置,但又即将消亡 // 也就是 prvalue 到 gvalue 转变 Str().x; } void fun(const int& par) {} int main() { //3 从 rvalue 转换为 xvalue fun(3); } ==decltype 与左右值== * 接收表达式 * ''xvalue'' 返回 T&& * ''lvalue'' 返回 T& * ''prvalue'' 返回 T int main() { int x; // y 是 int 类型,x 是实体 decltype(x) y; // z 是 int& 类型, (x) 是表达式 decltype((x)) z; // w 是 int&& decltype(std::move(x)) w = std::move(x); } Entity: **unparenthesized id-expression** or an **unparenthesized class member access expression**. ===类型转换=== ==隐式转换== * 隐式转换由下列的**有限**转换序列组成: * 0 或 1 个标准转换序列(//standard conversion//) * 0 或 1 个用户定义的转换序列(//user-defined conversion//) * 一个标准转换序列包括: - 左值到右值的转换,数组到指针的转换,以及函数到指针的转换 - 数值的提升与转换(//numeric promotion & conversion//) [[https://en.cppreference.com/w/cpp/language/implicit_conversion|Reference: Implicit conversions]] * 数值提升(无损): * 整型提升 * 浮点型提升 * 数值转换(可能有损): * 可能会更改值,因为两种类型的数据可能存在对方不能表示的范围 // 数值提升:低精度自动到高精度 // 整型提升:e.g. int to float 3 + 0.5; // 浮点型提升:e.g. float to double // 数值转换 // 不是所有的类型都能进行隐式转换 // error, 字符串不能转换为 double "abc" + 0.5; ,==显式转换== 显式转换的意义:处理隐式转换无法处理的场景: int x = 3; int y = 4; // 使用显式转换得到浮点数结果 // x 被隐式转换(左值->右值->double),y 被显式转换 std::cout << (x / static_cast(y)); * ''static_cast<>()'':() 中是被转换的值,<> 中是需要被转换的类型 // 显式的将数值从 int 转化为 double static+cast(3) + 0.5; 类型转换有局限性,不是所有类型都可以互转,无论是显性还是隐性 * 可以将基类的引用转换为派生类的引用 * 可以将 ''void'' 类型的指针转换为任意类型的指针(这种转换不被隐式转换支持) * 编译器完成 * 无法去除常量性(使用 const_cast) * ''const_cast<>()'':转换当前表达式的 const int x = 3; const int& ref = x; // ref2 是 non-const 的引用 int& ref2 = const_cast(ref); 注意:如果是绑定常量的引用,请不要使用 const_cast 改变其常量性。编译器对常量经常会进行编译优化,这往往是基于编译器来进行的。这种行为是 undefined 的,非常危险。 * ''reinterpret_cast<>()'':将当前类型重新解释为另外一种类型(将当前内存空间以另外一种形式来解释) * 主要用于将指针类型与其对应的类型相互转换 int x = 3; int *ptr = &x; // 强行解释 int 到 double, 通过指针 // 指针转换后,会以 double 的方式解引用当前的 int // 由于 double 需要 8 位, int 4 位,则解引用会将后 4 位的内存内容与前四位合并,并解引用 double * ptr2 = reinterpret_cast(ptr); * C-style 转换 c-style 转换会以特定的执行顺序执行 C++ cast 来进行转换。该转换过程是由编译器通过尝试得出的,并不完全可控。最好的方式是避免在 C++ 中使用诸如此类风格的转换。 * [[https://en.cppreference.com/w/cpp/language/explicit_cast|C-style cast 的顺序,见 section 1]] // 不推荐在 C++ 中使用 int x = 3; (double)x; C++ 希望用户尽量少使用显性的类型转换(名字又臭又长)。 ====表达式详述==== ===算术运算符=== * 分为三个优先级 * 正(''+''),负(''-'')(一元) * 乘除 / 末除(二元) * 加减(二元) * 左结合 * 通常 operand 和 结果都是右值 // rvalue 3 + 5 // lvalue->rvalue->rvalue int x = 3; int y = 5; x + y * 加减法可以引用于进行**指针移位**的运算 * 正负操作符会进行**整型提升** // y 是 int 类型 short x = 3; auto y = +x; * 整数的除法会向 0取整 // 1 4 / 3 // - 1 -4 / 3 * 求余:只能接收整数 operand * ''m%n'' 时,''m'' 与 ''m%n'' 的结果同号 ===逻辑与关系操作符=== * 关系操作符:判断大小相等 * 不能串联多个多个关系运算符(比如 ''a>b>c'') * 比较时,会将 bool 转换为其他类型,因此不要写 if(a == true) 的写法。 * 三路比较 ''<=>'' (C++ 20) * 可以直接先求出 a 和 b 的关系,然后使用该关系的结果用于 if-else,效率高 * 返回值类型: * ''std::strong_ordering'':包含了一些关系相关的常量(比如 ''std::strong_odering::less'',包含相等和等价) * ''std::weak_ordering'':不包含相等关系(用于只能说等价,不能说相等的数据结构,) * ''std::partial_ordering'':还包含了一种 unordered 的关系。 * 比如两个浮点数比较时,无法区分正负的 ''0'',因此是一种等价但无法区别的关系 * 另外一个例子是 ''NaN''(not a number),任意数与其比较都会返回 partial ordering 的关系。 * 逻辑操作符:与或非 * 只要 operand 可以转换为 bool 即可运算 * operand 和 结果都是右值 * **除了逻辑非**,其他的结合方式都是左结合 * 具有短路逻辑(short-circual):左边为真后右边不会执行 * 优先级:与高于或 ===位操作符=== * ''~'': 按位取反,''|'':按位或,''&'':按位与,''^'' 按位异或 * 接受右值,返回右值 * 除取反,都是左结合 * 计算过程中可能会存在整型提升 * 不存在短路逻辑 char x = 3; // 00000011 ~x; // -4, 11111100 char y = 5; // 00000101 x & y; // 00000001 x | y; // 000000111 x ^ y; // 00000110 ==左移右移== * 缺出来的位置用 0 补全 * 一定条件下可以替代乘/除 2,并且速度更快 char x = 3; //00000011 x >> 1; // 00000001 char y = -4; // 11111100 // 与输出操作符合并使用时,需要使用括号进行重载 std::cout << (y << 1); // 11111000 ==整型提升会根据符号位来填充== unsigned char x = 3; // char 到 int 的提升 // unsigned 会按位进行 0 的补全 unsigned char z = 0xff // 11111111 // 0000...00011111111 总共32位 auto y = ~x; // 结果为 256 //signed 的提升会按照符号位来进行补全的提升,这里是 1 signed char z = 3; // 提升过后的值为 11111......111111 // 求反后的结果是 00000....000000 y = ~z; // 结果是 0 ===赋值操作符=== * 赋值操作符左边为**可修改的**左值,右边为可转换为左边类型的右值。 * 赋值操作符为右结合(先评估等号右边) * 求值结果为左算子 * 可以引入大括号防止 narrowing converstion short x; // error, can't store a unsigned int to short x = {0x80000003}; // 无精度损失的转换不会被阻止 x = {3}; // 只要存在 norrowing conversion 的可能,编译器就不会通过 // y 可能会被修改导致 norrowing conversion int y = 3; x = {y}; // 使用编译器期 const 确保 y 不会被修改 constexpr int y = 3; x = {y}; * 赋值操作符的优先级非常低 ==交换两个数== // bitwise xor int x = 2; int y = 3; // x = 2^3 | y = 3 x^=y; // 任何数与 0 xor 结果都是其本身 // x = 2^3 | y = 3^2^3 = 2^3^3 = 2^0 = 2 y^=x; // x =2^3^2 = 3 | y = 2 x^=y; // 最后结果 x = 3, y =2 ===自增 / 自减操作符=== * 后缀 ''i++'':返回 ''i'' (的原始值),再自增 * 前缀 ''++i'':自增,再返回 ''i'' (运算之后的)值 * 前缀返回**左值**,后缀返回**右值** * 后缀在返回的时候当前变量已经更新,因此只能返回一个历史内容,属于临时变量,因此是右值 * 前缀可以视作 x = x + 1; 得到是左值。该左值还可以接着放到等号左边,因此 ++(++x) 也是合法的。 * 推荐使用前缀:后缀会创造**临时变量**并返回,效率较低。 * 后缀一般用于需要利用返回值的时候,比如运算符重载时(类)可能会用到 ===其他操作符=== ==成员访问操作符== * ''.'' 成员访问操作符 * 实质是 ''->'' 通过(this)指针访问成员 struct Str { int x }; int main() { Str a; // a 是左值,返回左值 a.x; // Str() 是 右值, 返回值为右值引用 Str().x; //(*ptr).x,返回左值 ptr->x; } ==三元条件操作符== // 只会求值一个分支 true ? 3:5; // 条件表达式返回的类型必须相同 ture ? 1: "hello"; // error // 都是左值,则返回左值,否则返回右值 int x = 0; false ? 1 : x; // 返回右值 // 右结合 // 先判断 score == 0 int score = 100; int res = (score > 0) ? 1: (score == 0) ? 0:-1; ==逗号操作符== * 典型应用: * for 循环中可以写出较为复杂的语句 * 元编程:折叠表达式,包展开 * 函数的参数表达式不是逗号操作符,参数列表求值顺序不定 // 确保操作数从左向右求值 // 求值结果为右算子 2, 3; // result is 3 // 左结合 // (2, 3) , 4 2, 3, 4; ==sizeof== * 返回类型 / 对象 / 表达式返回值占用的字节数 int x; // 推荐统一使用带括号的形式 sizeof(int); sizeof(x); // 对表达式评估时,不会真正执行求值 int* ptr = nullptr; // 等价 sizeof(int) sizeof(*ptr); ==域操作符== 用于访问域内的变量 int = x; namspace ABC { int x; } int main() { int x; int y = x; // local int y = ::x; // global int y = ABC::x // ABC } ===C++17表达式求值顺序=== * 之前的限定求值:逗号,三元条件,逻辑与 / 或(短路) * C++17 新引入的限定 // 先求 e1,再求 e2 e1[e2]; e1.e2; e1.*e2; e1->*e2; e1<>e2; e2=e1 / e2+=e1/ e2*=e1; * newType(e) 会先分配内存再求值