Tips for C++ Primer Chapter 4 表达式

2018-06-17 21:52:00来源:未知 阅读 ()

新老客户大回馈,云服务器低至5折

第4章 表达式

左值与右值:不同运算符对运算对象的要求、及其返回值类型

赋值运算符:需要一个(非常量)左值作为其左侧运算对象,得到的结果仍然是一个左值

取地址符:作用于一个左值运算对象,返回一个右值(它是一个指向左值运算对象的指针)。

解引用运算符、下标运算符:求值结果是左值

内置类型和迭代器的递增递减运算符:作用于左值运算对象,其前置版本所得结果是左值

 

当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。

注意:使用关键字decltype时:

  如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)会得到一个引用类型。

  例如:

  假定p的类型是int*,因为解引用运算符生成左值,所以decltype(*p)的结果是int&。

  另一方面,因为取地址运算符生成右值,所以decltype(&p)的结果是int**(指向整型指针的指针)。

 

整数的除法与取余运算

(假设m>=0,n>0)

m/n 两整数相除,结果为负值的情况

C++11之前:允许该负值(小数)向上或向下取整。

C++11:一律向0取整(即直接切除小数部分)。(-m)/n和m/(-n)都等于-(m/n)。

 

m%n 取余运算

C++11之前:允许m%n的符号匹配n的符号。

C++11:m%n的符号匹配m的符号。(-m)%n等于-(m%n),(-m)%(-n)等于-(m%n);m%(-n)等于m%n。

 

逻辑运算符与关系运算符

短路求值

&&:当左侧运算对象为时才会对右侧运算对象求值。

||:当左侧运算对象为时才会对右侧运算对象求值。

 

关系运算符满足左结合律,返回bool值

if(i<j<k) //首先运算(i<j)结果是一个bool值(0或1),再判断该bool值是否小于k

if(i<j && j<k) //这才是我们的目的

 

相等性测试与布尔值字面值

int val = 2;

if(val) cout<<"yes"; //将会输出"yes";因为val被转换成bool类型,转换结果是true

if(val == true) cout<<"yes"; //不会输出"yes";因为bool型字面值true被转换成int,转换结果是1,而val!=1

注:除非比较对象都是布尔类型,否则不要使用布尔值字面值(true/false)作为运算对象。

 

运算符优先级

算术运算>关系运算>逻辑运算

例如:i!=j<k+l 相当于 i!=(j<(k+l))

总结:括号 > 后置递增递减运算符 > 前置递增递减运算符 > 其它单目运算符 > 算数运算 > 移位运算 > 关系运算 > 位运算(& > ^ > |) > 逻辑运算(&& > ||) > 条件运算(? :) > 赋值运算 > 复合赋值运算

总结:复合赋值运算、赋值运算、条件运算、单目运算是右结合,其余运算符一般是左结合。

 

赋值运算符

赋值运算的结果是它的左侧运算对象,并且是一个(可修改的)左值。

与其它二元运算符不同的是:赋值运算满足右结合律。

int a = 0, b = 1;
a=b=2; //a的值为2,b的值为2(先把2赋给b,再把b的值赋给a)
(a=b)=2; //a的值为2,b的值为1(先把b的值赋给a,再把2赋给a)

注:那什么是不可修改的左值呢?例如:

const int ci = 0; //ci是一个左值,但它是个常量,所以它是个常量左值。

 

复合赋值运算符

a operator= b 相当于 a = a operator b

注意:二者在程序性能上有区别:

使用复合赋值运算时,只对a求值一次;

使用赋值运算时,会对a求值两次(一次是作为右侧表达式的一部分求值,另一次是作为赋值运算符的左侧运算对象求值)。

 

递增和递减运算符

前置版本:首先将运算对象加1(或减1),然后将改变后的对象作为求值结果(返回左值)

后置版本:首先将运算对象加1(或减1),但求值结果是运算对象改变之前的那个值的副本(返回右值)

注:除非需要,尽量使用前置版本。

前置版本把值加1或减1后就直接返回了该运算对象。而后置版本因为要返回对象未修改之前的值,所以要将原始值拷贝存储一个副本,因而时间和空间上都有额外的开销。

 

混用解引用符与递增递减运算符

vector<int> v{4,5,6};

auto it = v.begin(); //开始时迭代器指向v的首元素

 

*(++it) 与 *++it 与 *(it+1) 此三者等价(都输出5),这容易理解。

注解:前两者先将迭代器自身加1(返回左值,值改变后的it本身),然后对迭代器解引用;

后者对“值为迭代器加1的值的内存中的未命名迭代器对象”(返回右值,一个新的迭代器)解引用;

 

但是 *it++ 与 *(it++) 二者等价(都输出4),这似乎难以理解。

原因是:后置递增运算符的优先级高于解引用运算符,并且后置版本的递增运算符返回的是运算对象改变之前的那个值

因此,在先后顺序上来看,的确是先让迭代器加1了,但是解引用符的运算对象却是迭代器未加1之前的值(的副本)。

注:你可能会试着这样理解:*it++相当于先*it,取得了it的值,再令it++;而 *(it++)相当于先it++(假设记其为it2),再*(it2);事实上,这两种理解均是错误的,原因已经解释。

 

求值顺序与优先级、结合律

运算对象的求值顺序与优先级和结合律无关。

例如:对一条形如 f() + g()*h() + j() 的表达式,

优先级规定:先进行g()*h()

结合律规定:先将f()与g()*h()的结果相加,再与j()相加

但是,若这些函数是无关函数,则这些函数的实际调用顺序是不确定的;如果其中的几个函数影响同一对象,将产生未定义行为。

注:只有&&、||、条件运算符、逗号运算符4种运算符明确规定了求值顺序。

 

条件运算符

当条件运算符的两个表达式都是左值并且是同类型的左值,运算结果是左值

例如:

int a = 0, b = 0;
( 0<1 ? a : b ) = 10; //最后a的值是10,b的值是0

相当于:

if(0<1)
  a = 10;
else
  b = 10;

 

当条件运算符的两个表达式都是左值,但是不同类型的左值,则编译出错

例如:上例改成“int a = 0; double b = 0;”则编译错误。

 

当条件运算符的两个表达式其一是左值,另一是右值,则编译出错

 

当条件运算符的两个表达式都是右值,运算结果是右值

例如:

int c = 0<1 ? 2 : 3; //c的值是2

 

在输出表达式中使用条件运算符

条件运算符的优先级非常低(前面已讨论到),因此尽量对条件表达式加括号。

例如:

  cout << grade < 60 ? "fail" : "pass";

它的实际意义是:

  cout << (grade < 60); //输出0或1

  cout ? "fail" : "pass"; //根据cout的值产生对应的字面值

正确写法:

  cout << ( grade < 60 ? "fail" : "pass" );

 

位运算符

当位运算对象是“小整型”(char、short等),它的值会被自动提升。(通常是int)

当位运算对象是带符号的,且它的值为负,那么位运算符如何处理“符号位”依赖于机器。而且此时的左移操作可能会改变符号位的值,带符号整数的左移是一种未定义行为。

注:建议仅将位运算用于处理无符号类型。

 

移位运算符

<<:右侧插入0

>>:对无符号类型,左侧插入0;对带符号类型,左侧插入0,或者插入1(符号位的副本)

 

位求反运算符

~:每个二进制位0变成1,1变成0

 

位与、位或、位异或运算符

对二进制的每一位:

&:若对应位两个都是1,则结果为1;否则为0

|:若对应位至少有一个1,则结果为1;否则为0

^:若对应位有且只有一个1,则结果为1;否则为0

 

sizeof运算符

sizeof运算符返回一条表达式一个类型名字在当前机器所占的字节数。返回值类型是 constexpr size_t。

sizeof只是推断表达式的字节数,而不会实际执行表达式的计算。

 

sizeof(引用) 得到引用所指对象的字节数。

sizeof(指针) 得到指针本身所占字节数。

sizeof(数组名) 不会把数组名转换成指针,而是得到整个数组所占字节数。

 

逗号运算符

首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符的最后结果是右侧表达式的值。

如果右侧运算对象是左值,那么最终求值结果也是左值。

int a = 0, b = 0;

(++a, b) = 10;; //a的值是1,b的值是10

int c = (a, b); //c的值是10

 

显式转换

强制类型转换形式:

  cast-name<type>(expression);

type是转换的目标类型,如果type是引用类型,则结果是左值;

expression是待转换的表达式;

cast-name指定了转换规则,可以是static_cast、const_cast等。(此外还有dynamic_cast、reinterpret_cast这里不讨论)

 

static_cast

只要表达式不包含底层const,都可以使用static_cast。

例如:

int i = 1, j = 2;

double d = static_cast<double>(i) / j; //将int强制转换成double;d的值是0.5

void *p = &d;

double *dp = static_cast<double*>(p); //将void*强制转换成double*

 

const_cast

const_cast只能改变运算对象的底层const。

(PS:具有底层const特性:不能通过指针修改其所指对象的值;具有顶层const特性:指针本身的值不能被修改)

const char *pc;

char *p = const_cast<char*>(pc); //合法;但是通过p写值是未定义行为

常量对象pc转换成了非常量对象p,此类行为被称为“去掉const性质(cast away the const)”

一旦我们去掉某个对象的const性质,编译器就不在阻止我们对该对象进行写操作了。

  如果对象本身不是一个常量,那么使用const_cast后是合法的行为;

  然而如果对象是一个常量,再使用const_cast执行写操作会产生未定义的后果。

例如:

char c = 'a';
const char *pc = &c;
char *p = const_cast<char*>(pc);
*pc = 'b'; //非法;不能通过pc修改c的值
*p = 'b'; //合法;可以通过p修改c的值(*p的值和c的值都是'b'),因为c本身不是一个常量,使用const_cast获得对c的写权限是合法的

注意:若c是一个常量,即:第一行是 “const char c = 'a';” ,那么执行 “*p = 'b';” 会产生未定义的后果。

(在我的机器上的实验结果是:编译器不会发出error或warning,程序运行,最后*p的值是'b',c的值是'a')

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:9.22 (1)

下一篇:9.22作业