What & How & Why

差别

这里会显示出您选择的修订版和当前版本之间的差别。

到此差别页面的链接

两侧同时换到之前的修订记录前一修订版
zh:cpp:cpp_note:cpp_primer:4_expressions [2017/02/20 05:25] – [9.1.隐式转换] haregyzh:cpp:cpp_note:cpp_primer:4_expressions [2017/02/27 12:03] (当前版本) – 移除 haregy
行 1: 行 1:
-====表达式==== 
-C++ Primer 笔记 第四章\\ 
-<wrap em>我的笔记均包含大量个人理解内容,存在一定偏差。如果您发现错误,请留言提出,谢谢!</wrap> 
-===== ===== 
-====1.表达式基础==== 
  
-运算符分为一元运算符(unary operator )和二元运算符 (Binary operator)。“元”代表有几个operands。每个运算符的运算优先级(precedence) 和结合律(associativity)由运算符自身决定。 
-我们在对包含多个运算符的表达式进行计算的时候,常常会遇到一些需要考虑的问题:比如运算优先级,operands的转化,运算符的重载等等。要讨论这些规则之前,我们必须先明白两个概念:左值(Lvalues)和右值(Rvalues) 
-===1.1.左值和右值=== 
-每个表达式都有至少一个左值或者右值来充当自己的operand。 那什么叫左值?我们可以顾名思义,左值当然是能放到左边的了值了。放到什么左边?放到“=”的左边。不过左值不能这么来定义,因为这么定义会有一些例外。比如 const 对象,这是一个左值,但是不能放到“=”左边。书中讲了一个简单的判定规则:**当使用左值的时候,我们是使用的对象在内存里的位置;当使用右值的时候,我们使用的是对象的值。**看看“=”左边的值,是不是都能取地址,都能赋值?\\ 
-判断左值的具体方法可以参考:\\ 
-[[https://www.zhihu.com/question/39846131|关于C++左值和右值区别有没有什么简单明了的规则可以一眼辨别?]] **看轮子哥的解答**。\\ 
-\\ 
-了解了定义以后,有几个表达式的规则就根据左右值的区分来制订了: 
-  - 在表达式中,左值可以取代右值,反过来不行。 
-  - 赋值运算中,“=”号左边如果是一个Nonconst的左值,那么表达式得到的结果类型也是左值。(很好想象,比如 a = 10; 得到的是a) 
-  - 取地址操作需要一个左值,而操作结果返回一个指针,是右值。(当然了,左值代表位置,取地址当然是对左值操作。取回的是地址的具体数值,当然是右值。) 
-  - 解引用(*)运算,下标运算和迭代器的解引用,都产生左值。 
-  - 迭代器的自增 / 自减 也需要左值进行运算,<wrap em>但只有prefix版本返回左值</wrap>(++ / --i)。 
-  - 用decltype对表达式进行操作,如果表达式返回左值,那么decltype的返回值是一个引用。 
-===1.2.运算符重载 / 转化=== 
-关于运算符对operand的转化后面会详细说到详细的规则。\\ 
-对于运算符的重载,要注意的是运算符不管怎么重载,operand的类型都是由重载的定义来决定的。但是,**重载并不能改变运算符本身的运算优先级和结合律。** 
-===1.3.表达式判定顺序=== 
-优先级和结合律并不能保证运算对象的求值顺序,operand的运算顺序在大多数情况下是未知的;而有时候这种未知的操作顺序会带来很严重的后果,比如: 
-<code:cpp linenums:1> 
-int i = 0; 
-cout << i << ++i << endl; 
-</code> 
-在这里我们根本就不知道i是多少。如果前面的i先运算,那么我们打印的是0, 1;但如果后面的++i先运算,我们打印的就是1,1了,这显然是不合理的。\\ 
-<wrap em>对于没有指定operand运算顺序的表达式,如果operands的运算牵涉并改变了同一个值,结果将导致undefined.</wrap>\\ 
-那么问题来了,既然这么危险,为啥C++不直接禁止这么用?\\ 
-我们再来看一个例子: 
-<code:cpp linenums:1> 
-f() + g() * h() + j(); 
-</code> 
-我们知道这几个function我们不知道谁先进行运算,因此如果他们改变同一个对象,我们又要得到undefined的值了。但是如果他们操作的是不同的对象呢?\\ 
-试想一下,如果C++强行规定了运算顺序,那么不管怎么样,我们同时只能进行两个functions的运算。但如果我们不指定顺序,那么这4个functions是可以同时进行运算的。这样的话,不指定顺序比制定顺序快了一倍。当我们明白这些function在操作什么时,我们也不需要害怕上述的危险了,反而能得到性能的提升。这也很符合C++的哲学:让程序员来决定程序的好坏。\\ 
- \\ 
-当然如果把握不准,我们还有几个方法来处理这个问题: 
-  - 用括号保证优先级。 
-  - 如果改变了operand的值,就不要把这个operand再放到同一个表达式的其他任何地方。(有例外,如实际上已经指定了优先级的*iter++,详见[[zh:cpp:cpp_note:cpp_primer:4_expressions#4.1.自增运算符和解引用|section4.1]]) 
-==1.3.1.四种确定了operand运算顺序的运算符== 
-对于一些运算符,operand运算的顺序是指定了的。比如逻辑运算符(&& / || ),条件运算符(? :),逗号运算符(,)。 
- 
-====2.算术运算符==== 
-算术运算符的优先级可以同样可以参考P166。有几点需要注意的是: 
-  - 一元运算符的优先级 > 乘除 > 加减。 
-  - 运算的时候都是从左到右(左结合律) 
-  - 算术运算符得到的结果都是右值。 
-  - 一元“+”运算符可以应用到指针和算术类型上,返回的值是operand的拷贝。 
-  - 整数之间的除法结果是整数。 
-===2.1.算术运算异常=== 
-算术运算的异常通常有几种情况: 
-  * 如果我们进行了了一些数学意义上无意义的运算,那么很可能导致算术运算的异常。比如:除0操作 
-  * 超出了类型范围算术运算(overflow),比如: 
-     <code:cpp linenums:1> 
-     short si = 32767; 
-     si += 1; //error, overflows.</code> 
-注意:这样的错误编译器很可能不会给出异常,因为在有些系统里是有值得出的,比如上述计算得出si的值为-32768(我的系统)。 
-===2.2.余数/商=== 
-对于整数的除法,现行的新标准已经禁止商像负无穷方向取整了。\\ 
-对于余数的计算 M%N,有: 
-  - M % (-N) = M % N 
-  - -(M) % N = - (M % N) 
- 
-====3.逻辑运算符==== 
-逻辑运算符都是先求左边operand的值,根据左边结果来判定是不是要求右边的值,这种求值方式我们称为short-circuit evaluation. 
-&& / || / !都是比较常见的运算符,主要用于条件判断。对于默认的条件判断来说,括号里的值只要非负,就为真。比如: 
-<code:cpp> 
-while(a); // true if a is any non-zero value. 
-</code> 
-这里牵涉到类型的隐式转换,后面会cover相关内容。 
-===3.1.关系运算符=== 
-关系运算符用于鉴别operand的大小,所以也是返回bool类型的值。\\ 
-因为关系运算是有结合律的,所以不能进行连续的关系运算,会导致结果错误。一般都与逻辑运算符混用,达到不同的条件判断。\\ 
-注意:不进行关系运算的时候,不要用true / false来进行运算。(进行比较后会有隐式转换) 
-===3.2.赋值运算符=== 
-赋值运算符有几个要点: 
-  - 赋值运算符的左边必须是可以操作的左值。 
-  - 赋值结果的类型与左边的operand相同。 
-  - 赋值运算中如果左右两个operand的类型不同,右operand会转化成和左operand相同的类型。 
-  - C++11中,List初始化不能进行narrow conversion(老版本是可以的,反过来也是可以的,比如用double去装int),list可以为空(进行默认初始化)。 
-==3.2.1.赋值运算的结合律== 
-赋值运算的结合律是从右到左的。比如: 
-<code:cpp linenums:1> 
-int ival, jval; 
-ival = jval = 0; // expression will do jval = 0 first; then do ival = jval 
-</code> 
-**<wrap em>注意:在多重赋值中,所有的对象类型必须一致,或者可以通过类型转换达到一致。</wrap>** 
-====4.自增自减运算符==== 
-假设用自增运算符(自减也是相同的)对变量 i 进行操作,那么我们会得到两种情况:++i 和 i++.\\  
-简单的说来: 
- 
-  * <wrap em> ++i 是先自增再运算 </wrap> 
-  *<wrap em> i++ 是先运算再自增</wrap> 
-  
-从结果来说: 
-  * **++i 返回的是 i+1的值,返回的值是<wrap em>左值</wrap>**。 
-  * i++ 返回的是 i 的值,返回的是一个右值。 
-从程序效率上来说:  
-  * ++i 做了两步:取值,增加。 
-  * i++ 做了三步:取值,增加,再取原来的值。 
-对于一些迭代器运算,<wrap em>++i的效率要高于i++的</wrap>。详情可以参考:\\ 
-[[https://www.zhihu.com/question/19811087|在程序开发中,++i 与 i++的区别在哪里?]] 叶王的回答。 
-==4.1.自增运算符和解引用== 
-有时候我们会希望使用变量的值,然后再对其加1,这时候我们可以用*iter++这样的表达式。比如我们要遍历打印一个vector:\\ 
-如果我们用一般的方法,我们需要做两部: 
-<code:cpp linenums:1> 
-auto pbeg = v.begin(); 
-while (pbeg != v.end()) { 
-    cout << *pbeg << endl; // print current value; 
-    ++pbeg; // iterator move to the next position; 
-} 
-</code> 
-但现在有了 *iter++这种形式,语句就可以更简洁了: 
-<code:cpp linenums:1> 
-auto pbeg = v.begin(); 
-while (pbeg != v.end()) 
-    *pbeg++; // print current value then move to the next position. </code> 
-而对于 *pbeg++ 来说,解引用运算的优先级高于自增。所以这个表达式也等价于*(pbeg++)。\\ 
-注意:除非operand的运算顺序明显(如上例),避免用自增 / 自减 运算对同一个数值进行运算,修改(原因见[[zh:cpp:cpp_note:cpp_primer:4_expressions#1.3.表达式判定顺序|section1.3]]) 
-====5.成员访问运算符==== 
-成员运算符有 "." 和 "->"两种。 "."(dot)运算符比较常用,一般用于访问成员,比如访问对象a的function(),我们就可以写成:a.function()。\\ 
-有时候我们知道对象a的指针p,用p来访问function()就要先解引用,再访问,比如:(*p).function()。注意:因为解引用的优先级低于dot成员访问,所以必须用括号强制先行解引用运算。\\ 
-这种方法比较繁琐,我们可以用"->"运算符来代替,比如如下两种方法都是等价的: 
-<code:cpp linenums:1> 
-(*p).function(); 
-p->function(); 
-</code> 
-**对于dot运算,运算结果与参与运算对象的类型相同**(这里指左值/右值)。在(*p).function()里,*p是左值,所以我们可以得知"->"**运算符返回的是左值。 
-** 
-====6.条件运算符==== 
-if-else语句可以用条件运算符的形式表达出来: 
-<code:cpp > 
-condition ? expr1(if_true) : expr2(if_false); 
-</code> 
-如果两个条件表达式都是左值,那么运算的结果是左值,否则结果是右值。\\ 
-有几点需要注意的是: 
-  * 用条件运算符进行嵌套会大大影响程序的可读性。 
-  * 条件运算符的优先级非常低,如果在复杂的表达式中运用,必须要用括号保证优先级,比如: 
-    <code:cpp > 
-cout << (condition ? expr1(if_true) : expr2(if_false)) << endl;  
-</code> 
-====7.位运算符==== 
-位运算符作用于整数运算,并且把运算对象都当作是二进制运算。位运算有几种比较重要的运算符: 
-<code:cpp linenums:1 > 
-~expr; // bitwase NOT, 按位求反 
-expr1 << expr2; //left shift, 左位移  
-expr1 >> expr2; //right shift, 右位移  
-expr1 & expr2 ;//bitwise AND, 按位与运算,只有位都为1的之后为真 
-expr1 ^ expr2; //bitwise XOR, 按位异或运算,位异号的时候为真 
-expr1 | expr2; //bitwise OR, 按位或运算,只要位中有一个为1,就为真 
-</code> 
-对于位移运算符">>, <<" ,我们需要注意的是:  
-  - 位移运算符<wrap em>右边的operand必须非负</wrap>,而且<wrap em>值必须小于结果的位数</wrap>,否则会导致undefined。 
-  - 对于位运算符,我们最好采用unsigned类型来进行计算。如果位运算符的operand带有符号,那么运算符对operand符号位的处理是根据机器来指定的。换句话说,符号位的处理是没有明确定义的;如果运算导致符号位被修改(比如位移操作),那么这个运算就会导致undefined. 
-  - 位移运算的结合律是从左到右的。 
-  - 位移运算的优先级高于关系运算符,复合使用的时候必须要用括号确定优先级。 
-\\ 
-位运算因为只进行二进制的条件和位移运算,所以效率是非常高的。我们通常可以把位运算引用到一些比较简单的条件判断上,比如判断奇偶: 
-<code:cpp linenums:1> 
-int a; 
-a % 2; //the way we used to evaluate odd / even 
-a & 0x1; // using AND. if the last bit of a is 0, the expression will return 0; then we know a is a even. If the expression returns 1, then a is a add。 
-</code> 
-还有其他更多高效率的简单位运算应用,详情可以参考Matrix 67的位运算系列:[[http://www.matrix67.com/blog/archives/263|位运算简介及实用技巧]] 
-====8.sizeof / 逗号运算符==== 
-===8.1.sizeof运算符=== 
-sizeof会按字节返回一个类型所占的空间。sizeof 满足右结合律,返回的值是一个"size_t"的类型。\\ 
-有几点要注意的是: 
-  - sizeof并不会对类型/表达式进行运算,所以即使表达式是无效的(比如无效的指针),sizeof也会计算出值。 
-  - sieeof对引用的求值返回的是引用绑定的对象所占空间的大小。 
-  - sizeof对指针的求值返回的是指针所占空间的大小。 
-  - sizeof对数组的求值返回的是element_space * element_number;因为sizeof不会执行表达式,所以sizeof不会将数组转化成指针处理。 
-  - sizeof对vector / string 求值只返回fixed size,比如:<code:cpp linenums:1> 
-string s1 ="a"; 
-string s2 ="abcdef"; 
-vector<int>vi(10,1); 
-vector<char>vc(20,'c'); 
-cout << sizeof(s1) << " " << sizeof(s2) << " " << sizeof(vi) << " " << sizeof(vc) << endl;</code>返回的值是:32 32 24 24。 
-  - 以前用sizeof访问类成员,必须要通过类对象来访问;但是新标准我们可以使用scope运算符来访问,比如: <code:cpp linenums:1> 
-sizeof object.member; // the old way 
-sizeof class::member; // the new standard</code> 
-===8.2.逗号运算符=== 
-逗号运算符会先对左边的表达式求值,然后丢掉结果;接着会对右边的表达式求值。整个逗号运算的结果是右边表达式的值。 
-====9.类型转换==== 
-在运算中,两种可以互相转换的类型称之为**有关联的**。 
-===9.1.隐式转换=== 
-隐式转换的进行是由计算机根据表达式中operand的类型来判断的。可能发生隐式转换的情况有: 
-  - 小整型类型(short / char)的提升(转化成较大的整数类型)。 
-  - 条件语句中,非bool表达式转化成bool表达式。 
-  - 初始化中,初始值转化为变量的类型;赋值中,右边的operand的类型会自动转化为左边operand的类型。 
-  - 算术 / 关系运算中,operands需要转化为相同类型。 
-  - 函数调用的类型转换。 
-==9.1.1.算术类型的隐式转换== 
-算术类型的转换分为好几种:   
-  * 算术转换:在运算中,operand将被转换为最宽的类型(比如int + double, int会转化成double)。 
-  * 整型的提升。 
-  * 无符号类型的转化;规则分为好几个步骤: 
-      - 首先执行小整形类型提升。 
-      - 如果operand的类型一致,那么小类型转化为大类型。 
-      - 如果operand的类型不一致,转换的结果依赖机器。总的说来, 占用空间少的类型将转换为占用空间多的类型;如果operand的大小相同,那么signed会转化为unsigned. 
-==9.1.2.数组对指针的隐式转换== 
-在绝大部分应用数组的表达式里,<wrap em>当我们使用数组的时候,数组会自动的转化成指向数组第一个元素的指针。</wrap>这个转换非常重要;但同时也有几个不会进行转化的例外: 
-  * 使用sizeof运算数组 
-  * 使用decltype运算数组 
-  * 使用"&"对数组取地址 
-  * 用引用初始化数组 
-==9.1.3.指针的隐式转换== 
-  * 常数0 / nullptr keyi 可以转化成任意指针类型 
-  * 指向任意 nonconst 类型的指针可以转化成 void* 
-  * 指向任意类型的指针可以转化成 const void* 
-  * 如果指针/算术类型的值为0,放到条件中就可以转化成false,否则为true. 
-  * **指向nonconst对象的指针可以转化为指向const对象的指针**,也就是说我们可以将指向T类型的指针和引用转向指向const T类型的指针和引用。(意味着这里不能通过指针和引用修改 T代表的内容) 
-  * 类的自定义转换 
-===9.2.显式转换=== 
-显示转换的使用方式为: 
-<code:cpp> 
-cast-name<type> (expression) 
-</code> 
-其中type是转换的目标类型,expression是要转换的值。**如果type是引用类型,那么转化的结果是左值。** 
-显式转换分为4种类型: 
-  * static_cast:任何不包含low_level const的表达式都可以用static_cast来做类型转换。(一般用于显式的type narrowing) 
-  * const_cast:只能将low_level const 转化为 nonconst。实际上是改变了对指向/引用对象的写操作许可。\\ 
-          注意:尽管const_cast可以改变写权限,但是我们不能在改变写权限的时候同是做初始化:<code:cpp>char *p = const_cast<char*> (pc);//writing through p is undefined.</code>同理,const_cast只能改变表达式的常量性质,不能改变表达式的类型。 
-  * reinterpret_cast:重新解释类型。(注:此处不是很理解这个概念,回头遇到再补上) 
-~~DUOSHUO~~