【笔记】《C++Primer》—— 第4章

2020-07-29 15:56:08 浏览数 (2)

第四章的标题是表达式,主要讲的内容是平时在用的表达式中的运算符和类型转换等概念,内容不复杂但是却很基础很有用,很多平时习以为常的写法在这章才被系统解释了一次。不过这篇概念比较多代码倒是没怎么写进来。(因为很多概念要解释的时候写成代码在编译器会疯狂报错233)

4.1 基础

  1. 第二章结尾提到的decltype关键字其实有很多需要注意的地方之前没有写出来,那就是decltype会根据其括号内的值的类型不同做出很多不同的反应。
  2. 首先表达式的值(value)通常来说分为两类,左值(lvalue)和右值(rvalue)。简单的分辨方法就是:可以利用&取到地址的值就是左值,也就是我们修改这个值是会连接到指定的栈上的内存的值,我们平时用的变量就是左值;其余的不是左值的值都是右值,例如很多的直接运算结果(1 1)之类的临时值。
  3. 然后在C11的时候标准又引入了一个要后很多章才遇到的操作:使用&&来得到右值引用。这个操作使得右值产生了分裂:没法被取到地址的临时值称为纯右值(prvalue),右值引用出现的值称为将亡值/临终值(xvalue)。然后xvalue和lvaue合称泛左值(gvalue)。
  4. 之所以提到这几个概念是因为之前没把decltype搞得很清楚(其实现在也还是有点懵,如果有错麻烦指出): 首先decltype的括号里面分两种大情况——无子括号和有子括号的情况: · 对于无子括号的情况,前面说到了会按照里面的内容推断的原来声明的类型进行返回。这里详细地说的话其实是分为内容是变量还是表达式。当这个是内容是单纯的变量时,返回的是这个变量的类型。当这个内容是表达式时,如果表达式是左值,返回的是这个左值的引用,例子如一个int* p指针,decltype(*p)的结果会是int&;如果表达式是右值返回的是普通的右值类型。 · 对于有子括号的情况,前面说到返回的都是引用,实际上更详细的是:当括号内是左值时,返回的是T&;当括号内是临终值时,返回的是右值引用T&&,当括号内是纯右值时,返回的是T。 这几个复杂的关系用的时候需要小心。
  5. 一般来说表达式最终的值依赖于值组合方式,按照运算符高优先级>低优先级,相同时按照结合律顺序,再相同时从左向右组合对象的值。
  6. 括号可以无视优先级和结合律,括号内的内容都会当作一个新的单独的表达式进行求值。
  7. 但是运算符本身的结合顺序又分为左结合和右结合,这个后面会看到,大多数都是左结合的。
  8. 表达式求值有个非常关键的地方,就是求值顺序只是规定了值的组合方式,大多数运算符并没有规定它们的求值顺序,也就是一条表达式里的函数是以什么顺序运行的其实是不确定的。int i=f1() f2();这里如果f1和f2都修改了同一个对象的话,结果输出将会是"未定义"的。
  9. 求值顺序有四个例外,下面会说到。
  10. 有关表达式求值的就是两点:不清楚组合优先级时一定要强制使用括号来指定优先级,在一条表达式内不要对一个对象进行多次操作。当然第二点还有一点点例外,就是递增运算的优先级高于解引用符,这是很常见的操作按照平时的习惯去写就好:* p。
4.2-4.3 算术与逻辑运算符
  1. 在表达式被求之前,小类型的对象会被转换为大类型,最终所有对象都是一个类型
  2. C11规定商一律向零取整(切除所有小数部分)
  3. 给小类型的值赋值了大数的话会溢出,溢出后具体是卷绕还是其他操作都是未定义的,非常危险。
  4. 逻辑运算符有“短路求值”的特性,也就是从左到右计算,只有当无法确认表达式结果时才会继续往右计算。这就是我们平时总是说的&&运算符要把高错误率的写在前面,||运算符要把高正确率的写在前面。
  5. 利用短路求值的特性,可以用if(s.empty()||s[s.size()-1]=='.')这样的写法,不用担心后半部分是否可以被取值因为前半部分会进行校验。
  6. 逻辑非运算符会返回bool值。
  7. 在比较运算中除非比较的对象是bool值否则绝对不要用bool值进行比较,因为bool会被转换为0和1
  8. 逻辑与&&的优先级高与逻辑或||。

4.4 赋值运算符

  1. 赋值运算符的左边必须是一个可修改的左值(不是const),且右边必须和左边类型相同或者可以被转换。
  2. C11支持使用花括号来初始化对象(类似数组的显式初始化),称为列表初始化。这种操作可以给单个类型进行初始化,但是要注意一方面列表里的对象就只能有一个了,另一方面这个对象转换后所占空间一定不能大于目标对象(小类型)。也就是int a={3};是对的,int a={3.14};是错误的。
  3. 无论左侧是什么,右侧都可以用空列表来初始化,此时会按照默认值来初始化。
  4. 赋值运算符和别的二元运算符不同,它是右结合的,也就是说靠右的对象会作用在左边的对象上,这也和我们平时写的一致。例如a=b=c=0;时所有对象都会被赋值0,这样的语句被称为多重赋值语句。
  5. 对于多重赋值语句要注意每个赋值号都要符合第一点的规定,例如指针不能自动转换为int,即便指针的值为0,即使0可以赋值给任何对象也不行,写的时候要注意。
  6. 赋值运算符的优先级是很低的,可以利用这一点要增加括号才能简化循环的操作。while((i=getvalue())!=42){;},这样又完成了赋值又完成了检验还增强了可读性。
  7. 复合赋值运算符,也就是 =,-=之类的符号,它们的优先级比赋值运算符还要低。
  8. 复合赋值运算符可以被两个赋值运算符替代,但是还是有个小小的优点,复合版本只会进行一次赋值求值,效率比两行赋值符高一点点点。
4.5-4.7 递增递减,成员访问,条件运算符
  1. 递增递减有前置和后置两个版本,其中后置版本会返回原来的值然后将值加/减1,这导致了性能比前置版本稍差一点。
  2. 虽然编译器会对这两种操作做出一定的优化,但是对一些复杂类型例如标准库的vector之类,优化这样的迭代耗费是巨大的,所以如无必要全部递增减都应该使用前置版本来提高性能表现。
  3. 自然也有有必要使用后置符的情况:在循环中cout<<*p <<endl;会先输出指针的值然后再移动指针,这是因为递增的优先级高于解引用符且后置递增会先返回原值再运算,在这个例子中如果使用前置符就很容易跳掉第一个值且容易不小心访问到越界内存。
  4. 箭头运算符->的结果是左值,点运算符的结果需要按照成员所属对象来判断。
  5. 条件运算符?:可以简化一些简单的if-else,但是要注意的是条件运算符的优先级非常低(比赋值高1而已),所以如果在IO符之类的地方使用条件运算符的话要记得使用括号来强制优先级。
  6. 条件运算符是允许嵌套的,效果和elseif差不多,在冒号:后面嵌套新的?:,但是这样的写法并不直观,强烈建议不要嵌套超过两到三层,嵌套时也要注意换行提高可读性,效率比if低。

4.8-4.10 位运算符,sizeof,逗号运算符

  1. 位运算符由于比较少用所以例如左移右移经常被重载成IO符。
  2. 移位操作如何处理符号位是未定义的,所以强烈建议只对无符号类型进行位运算。
  3. 在为运算时char会被提升为int
  4. 位运算一个用途是用每个位来表示bool值从而高密度地保存一组信息
  5. sizeof返回的是size_t类型的字节数,它也是右结合的运算符。实现的效果有点类似decltype,即不进行实际运算的类型推断。
  6. sizeof实际上对于表达式是可以不加括号的。即sizeof expr,但加上也无妨
  7. C11后sizeof可以对类成员进行推断大小了,但是返回的大小只会是那个类的默认的固定大小,所以要小心对string之类的变长对象进行推断
  8. 对数组进行sizeof推断可以返回整个数组的大小,但是当这个数组被作为参数传递后这个效果会消失,数组会被转为指针,只能返回指针本身大小了
  9. 一种常用操作是通过sizeof计算数组的元素个数,写法是sizeof(a)/size(*a);
  10. 对char进行sizeof得到的字节是1
  11. 逗号运算符比较少用到,它的优先级是所有运算符中最低的。逗号运算符会从左到右对表达式进行运算,最终返回最右边表达式的结果
  12. 逗号运算符最常用的地方就是在for循环中同时对多个计数器迭代和同时赋值多个值的时候。

4.11 类型转换

  1. 两个类型可以互相转换说明它们是相互关联的
  2. 隐式转换很常见,主要就是小类型会转为大类型,条件中非布尔值会转为布尔值。
  3. 如果另一个无符号类型不小于有符号类型,那有符号类型会转换为无符号的
  4. 如果无符号类型的所有值都能存入有符号类型中,此时的转换结果是依赖机器的,无符号类型会转换为有符号。
  5. 数组会在大多数表达式中转换为指针除了sizeof,decltype之类的运算符
  6. 非常量类型的指针可以转换为常量指针,但是不能反反向隐式转换
  7. cin的返回值是读入成功还是失败的bool值
  8. 在强制类型转换中,C 的推荐写法与C和早期C 不同,以前我们使用type (expr);或者(type) expr两种写法,但是实际上这两种写法的效果范围很广,并不合适。
  9. C 推荐使用的写法是命名的强制类型转换,形式为:cast-name<type>(expr); 这里cast-name是显示写出了需要进行的强制转换的类型,分为四种static_cast,const_cast,reinterpret_cast和dynamic_cast。dynamic_cast后面再提。
  10. static_cast是最常用最基础的转换,我们平时使用的强制类型转换都可以改成这个。它还可以将编译器无法自动执行的类型进行转换,例如将void*转为其他的指针类型。
  11. const_cast比较危险,可以强制去除对象的const,要注意的是const_cast只能改变const性质,无法改变表达式的类型。
  12. reinterpret_cast非常危险,它可以将仍和指针类型重新指向,例如将char*改为int*,这会很容易引发难以追踪的错误。
  13. 强制类型转换都是危险的,如果可以的话尽量避免使用它们。

4.12 运算符优先级表

0 人点赞