zoukankan      html  css  js  c++  java
  • Tips for C++ Primer Chapter 4 表达式

    第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')

  • 相关阅读:
    BootStrap 之 CSS全局样式中的表格
    BootStrap 之 CSS全局样式中的图片
    BootStrap 之 CSS全局样式中的按钮
    Objective-C中的消息发送总结
    Android Studio精彩案例(七)《ToolBar使用详解<一>》
    How to Change Default Web ADI Upload Parameters for FlexField Import / Validation
    iOS中的颜色
    基于OGG的Oracle与Hadoop集群准实时同步介绍
    Android简易实战教程--第五十一话《使用Handler实现增加、减少、暂停计数》
    Android Studio精彩案例(六)《使用一个Demo涵盖补间动画所有知识》
  • 原文地址:https://www.cnblogs.com/junjie_x/p/7590346.html
Copyright © 2011-2022 走看看