C++Primer学习笔记:第4章 表达式

  • 表达式由一个或多个运算对象组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。把一个运算符和一个或多个运算对象组合起来可以生成较复杂的表达式。

  • 重载运算符包括运算对象的类型和返回值的类型,都是由该运算符定义的;但是运算对象的个数、运算符的优先级和结合律都是无法改变的

  • 当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。需要右值的地方可以用左值来代替,但是不能把右值当成左值使用。

    • 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值
    • 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值
    • 内置解引用运算符下标运算符迭代器解引用运算符stringvector的下标运算符的求值结果都是左值
    • 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得到的结果也是左值
  • 如果表达式的求值结果是左值,decltype作用于该表达式得到一个引用类型

    int a = 1;
    int *p = nullptr;
    decltype(*p) b = a;	//b是int &类型
    decltype(&p) c = nullptr;	//c是int **类型 	
    
  • 优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值,在大多数情况下,不会明确指定求值的顺序。对于表达式int i = f1() * f2(),我们知道f1()f2()一定在执行乘法之前被调用,但是我们无法直到到底f1f2的执行先后顺序。对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象将会引发错误并产生未定义行为(UB)。因为<<运算符没有明确规定何时以及如何对运算对象求值,因此cout << i << " " << ++i << endl是未定义的。

  • 有四种运算符明确规定了运算对象的求值顺序:逻辑与&&、逻辑或||、条件(三元)运算符?:、逗号运算符,

  • 运算对象的求值顺序与优先级和结合律无关,对每个运算对象的运算结果的计算顺序是通过优先级和结合律决定,但是对运算对象的运算顺序是不确定的。如果在一个表达式中有多个运算对象涉及对同一个对象的运算,那么很容易产生未定义的行为。例如f() + g() * h() + j()中,对这些函数的返回值的运算顺序是确定的,但是对这些函数的运算顺序是不确定的。

  • 书写复合表达式的准则:

    • 拿不准的时候最好使用括号来强制让表达式的组合关系复合程序逻辑的要求
    • 如果改变了某个对象的值,在表达式的其他地方不要再使用这个运算对象。这个其他地方是不包括当改变运算对象的子表达式本身就是另外一个子表达式的运算对象。例如:*++iter
  • 算数运算符的运算对象和求值结果都是右值。

  • 一元正号运算符、加法运算符和减法运算符都能作用于指针。当一元正号运算符作用于一个指针或者算术值时,返回运算对象的一个(提升后的)副本

    bool b = true;
    bool b2 = -b;	//相当于b2 = -1,所以b2为真	
    
  • 整数相除结果还是整数,参与取余运算的运算对象必须是整数类型

  • C++11新标准规定商一律向0取整(即直接切除小数部分)

  • 根据取余运算的定义,如果mn是整数且n非0,则表达式(m/n)*n+m%n的值和m相同。在C++11新标准中,除了-m导致溢出的特殊情况,其他时候(-m)/nm/(-n)都等于-(m/n)m%(-n)等于m%n(-m)%n等于-(m%n)

  • 逻辑运算符作用于任何能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。运算对象和求值结果都是右值

  • 逻辑与与逻辑或都采用短路求值。可以用左侧运算对象来保证右侧运算对象求值过程的正确性和安全性

  • 使用范围for循环时尽可能声明成引用类型,能够避免对元素的拷贝

  • 进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值truefalse作为运算对象

  • char *cp; if (cp && *cp) {}表示判断指针cp所指向的数组是否为空,如果C字符串数组为空,则数组中只有一个空字符\0

  • 复制运算的结果是它左侧的运算对象,并且是一个左值。如果赋值运算符左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。

  • C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。如果左侧运算对象是内置类型,那么初始值列表最多只能包含一个值,而且该值不能有丢失信息的风险(所占用的空间不应该大于目标类型的空间)。对于类类型来说,赋值运算的细节由类本身决定。vector模板重载了赋值运算符而且可以接收初始值列表,当赋值发生时用右侧运算对象的元素替换左侧运算对象的元素。无论左侧运算对象的类型是什么,初始值列表都可以为空,此时编译器创建一个值初始化的临时量并将其赋给左侧运算对象(我认为没有默认构造函数的类可能不行)

    vector<int> a;
    a = {1, 2, 3, 4};			
    a = {1};
    a = {1, 2, 3, 4, 5, 6, 7};
    
  • 赋值运算满足右结合律。因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号

    int i;
    while(1 == (i = getValue())) {
    	//
    }
    
  • 使用复合运算符只求值一次,使用普通的运算符则求值两次(一次计算一次赋值)。因此尽量使用赋值运算符。

  • ++--可以用于迭代器,很多迭代器本身不支持算术运算。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。如果不需要保存未修改版本的值,尽量使用前置版本不使用后置版本cout << *iter++ << endl;是一种被广泛使用的有效的写法。C++程序追求简洁、摒弃冗长,因此C++程序员应该习惯于这种写法。

  • 因为大多数的运算符都没有规定求值顺序,因此同一条表达式中最好在一个地方修改对象的值(且在改变这个对象以后就不要再进行使用),否则很容易产生未定义的行为(&& || , ?:除外)

  • ptr->item等价于(*ptr).item.运算符的优先级更高,因此括号必不可少。箭头运算符作用于一个指针, 结果是一个左值。点运算符作用于左值则结果是左值,作用于右值则结果是右值

  • 条件运算符cond ? expr1 : expr2,其中cond是判断条件的表达式,而expr1expr2是两个类型相同或者可能转换为某个公共类型的表达式。条件运算符是有求值顺序的,而且条件运算符只会对expr1expr2中的一个求值。当条件运算符的两个表达式都是左值或者能够转换成同一种左值类型时,运算的结果是左值,否则运算结果是右值

  • 条件运算符满足右结合律,意味着运算对象一般按照从右往左的顺序结合。条件运算的嵌套最好别超过二到三层。

    sting final_grade	= (grade > 90) ? "high pass"
    									: (grade < 60) ? "fail" : "pass";
    

    条件运算符的优先级比较低,因此最好在复合表达式中使用条件运算符的时候加上括号

  • 位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合(bitset可以表示任意大小的二进制位集合,也可以使用位运算符)

    ~ 		位求反		~expr
    <<		左移			expr1 << epxr2
    >>		右移
    &		位与
    ^		位异或
    |		位或
    

    如果运算对象是“小整型”,则它的值会被自动提升成较大的整数类型,如果运算对象是带符号的,有可能产生未定义的行为(如果有可能为负数),因此建议将位运算用于处理无符号类型

  • 移位运算符将经过移动的(可能进行了提升)左侧运算对象的拷贝作为球直接过。其中右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则就会产生未定义的行为。二进制位或者向左移(<<)或者向右移(>>),移出边界外的位就被舍弃掉了。<<在右侧插入值为0的二进制位,>>对无符号数来讲是 在左侧添加0,但是对带符号类型依赖环境

  • unsigned char在位运算中会被提升为unsigned intunsigned long在任何机器上都至少拥有32位。1UL << x制造一个第x为1,其他位都为0的数字,对这个数字取反可以得到第x位为0,其他位都为1的数字。然后通过|&进行操作

  • sizeof运算符返回一条表达式或一个类型名字所占的字节数。sizeof运算符满足又结合律,得到的是一个size_t类型的常量表达式。运算符的运算对象有两种形式:

    sizeof (type)
    sizeof expr	
    

    在第二种类型中,sizeof返回的是表达式结果类型的大小,与众不同的一点是sizeof并不实际计算其运算对象的值。因此解引一个无效指针仍然是一种安全的行为,因为指针实际上没有被真正使用。sizeof不需要真的解引用指针也能知道它所指向对象的类型。

    Sales_data data, *p;
    sizeof *p;		//返回Sales_data的空间大小
    sizeof data.revenue
    sizeof Sales_data::revenue	//同上	
    

    在C++11新标准匀速我们使用作用域运算符来获取类成员的大小。通常情况下只有通过类的对象才能访问到类的成员,但是sizeof运算符无需我们提供一个具体的对象

  • 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有元素执行一次sizeof运算并将所得的结果求和。注意,sizeof运算符不会把数组转换成指针来处理。因此可以用数组的大小除以单个元素的大小得到数组中元素的个数

    constexpr size_t sz = sizeof(ia)/sizeof(*ia);
    int arr2[sz];		//arr2的大小和ia一样	
    
  • string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。实测了一下发现对vectorstring应该也一样)使用sizeof返回固定的大小,对于vector<int>返回24

  • 逗号运算符有两个运算对象,按照从左往右的顺序依次求值(同&& || ?:一样规定了运算顺序)。先对左侧的运算对象求值,然后将求值结果抛弃,真正的运算结果是右侧表达式的值,如果右侧运算对象是左值,则最终的求值结果也是左值。

  • 隐式类型转换:

    • 大多数表达式中,比int类型小的整型值首先提升为较大的整数类型
    • 在条件中,非布尔值转换为布尔值
    • 初始化和赋值语句中,右侧运算对象转换成左侧运算对象的类型
    • 如果算术运算或关系运算的运算对象有多种类型,需要转换为同一种类型
  • 整型提升负责把小整数类型转换成较大的整数类型,整数类型中比int小的类型如果参与运算,如果可以放到int里就会提升成int,否则提升成unsigned int

  • 隐式类型转换:

    • 数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针。当数组被用作decltype关键字的参数,或者作为取地址符&sizeof以及typeid等运算符的运算对象时,以及用一个引用来初始化数组时,上述转换不会发生
    • 常量整数值0或者字面值nullptr能够转换为任意指针类型,指向任意非常量的指针能够转化成void*,指向任意对象的指针能够转化为const void*
  • 如果有的转换不会隐式自动转换就需要强制类型转换,虽然有时候不得不使用强制类型转换,但是这种方法本质上是非常危险的。

  • 一个命名的强制类型转换具有如下形式:

    cast-name<type>(expression);
    

    type是要转换的类型,expression是要转换的值,如果type是引用类型,则结果是左值。

    cast-namestatic_castdynamic_castconst_castreinterpret_cast中的一种

    • static_cast:任何具有明确定义的类型转换,只要不包含底层const,就可以使用static_cast

      • 把较大的算数类型赋给较小的类型

        double slope = static_cast<double>(j)
        
      • 无法自动执行的类型转换

        void *p = &d;
        double *dp = static_cast<double*>(p);
        
    • const_cast只能改变运算对象的底层const,用于将常量对象转换为非常量对象

      const char *pc;
      char *p = const_cast<char*>(pc);
      

      只有const_cast可以改变表达式的常量属性

      const_cast常用于有函数重载的上下文中

    • reinterpret_cast:通常为运算对象的位模式提供较低层次上的重新解释,不太好用,不建议使用

  • 应该尽量避免使用强制类型转换,且不推荐使用旧式类型转换:与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了,容易被看漏。

    int x;
    char y = (char)x;
    
上一篇:c数据结构 顺序表和链表 相关操作


下一篇:C++Primer学习笔记:第5章 语句