Advertisement

C++Primer第4章 表达式

阅读量:

4.1 基础

4.1.1 基本概念

C++定义了运算符:

  • 一元操作员:执行针对单一操作数的操作员类型。
    • 二元操作员:执行针对两个操作数的操作员类型。
    • 三元操作员:涉及三个操作数的操作员。
    • 函数调用也属于一种特殊的操作员类型。

C++语言通过运算符定义了对内置类型和复合类型运算对象所执行的具体操作。当对类类型的运算对象进行运算时,则可以实现运算符的重载功能。在进行重载操作时,我们所关注的重点在于确定运算对象的类型以及返回值的类型这两个要素,并且这些要素都是由对应的运算符本身所决定的;然而,在这种情况下需要注意的是,在重载过程中必须保持不变的是参与运算的对象数目、运算法则的优先级以及结合顺序等关键属性。

C++的表达式要不然是右值,要不然就是左值

  • 在C语言中:左侧操作数可位于赋值语句左侧端点处;而右侧操作数则不可
    • 在C++语言中:
      • 当一个类实例类型标识符后面跟有一个表达式时,则该表达式即成为该实例类型的右式
      • 当一个类实例类型标识符后面跟有一个变量或常量时,则该变量或常量即成为该实例类型的左式引用

各类不同的运算符对操作数的要求各有差异,在某些情况下则要求操作数具有左值属性,在另一些情况下则会要求操作数具有右值属性。在处理结果时也会出现类型上的区别,在某些情况下会返回具有左值的结果,在另一些情况下则会返回具有右值的结果

左侧表达式可以作为右侧表达式的使用场景,在这种情况下是指向其所代表的内容;然而,在这种情况下将右侧表达式当作左侧表达式是不合适的。

需要用的左值的常见的运算符:

  • 赋值操作必须附有一个非恒定左操作数
  • 取地址操作应用于具有左值特性的运算对象
  • 内置解引用、下标运算符、迭代器解引用运算符以及string和vector类型的下标运算符所得的结果均为左值
  • 内置类型与迭代器配合使用的递增递减操作用于处理具有左值特性的操作数后所得的结果均为左值

当使用关键字decltype时,在处理过程中左右两边的处理存在差异;具体而言,在该操作中若该表达式的求值结果为左值,则decltype作用于该表达式(而非变量)将返回相应的引用类型:

  • 当p为指向整型的指针时(或如果p是int *),decltype§的作用域类型定义为其类型定义为指向整型的指针。
    • 取地址运算符用于获取变量p的内存地址时(或当取地址运算符生成右值时),ReturnType类型的定义式为\texttt{sizeof}(\texttt{intptr_t})。(注:此处修改了原文表述方式,并添加了一些细节以增加自然流畅度)

4.1.2 优先级与结合律

复合型数学公式 通常被定义为包含两个或更多运算符的复杂方程组。一般情况下,公式的计算结果取决于其排列结构。

  • 高优先级运算符的运算对象具有更高的结合程度,并且比低优先级运算符的运算对象更紧密地结合在一起
    • 当优先级相同时,则其结合规则由结合律决定

    • 算术运算符遵循左结合性,即从左至右依次进行相应的运算操作

    • 括号无视优先级和结合律

优先级与结合律的影响:

  • 这种关系将直接影响程序的准确性。
  • 这种结合律在处理输入输出运算时起到重要作用。

4.1.3 求值顺序

优先级决定了运算对象如何组合在一起进行运算,在常规情况下未做具体说明指出计算时的具体执行顺序;通常情况下也不会对运算对象的计算顺序做出详细规定。

对于那些没有明确的操作优先级的情况而言,在程序中如果一个表达式试图指向并修改同一个对象时,则可能会导致错误行为的发生

复制代码
    int i = 0;
    cout << i << " " << ++i << endl;	//未定义的,表达式行为不可预知
    
    
      
      
    
    代码解释

有四种运算符明确规定了运算对象的求值顺序:

  1. 逻辑与(&&)运算符:首先确定前一操作数的值;仅在前一操作数结果为真时才会进一步计算后一操作数。
  2. 逻辑或(||)运算符也被称为条件判断中的"或者"关系。
  3. 条件判断中的"?:"运算法则是一种用于基于条件执行不同路径的操作机制。
  4. 逗号运算是程序中用于分隔多个独立表达式的一种功能特性。

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

建议:处理复合表达式:

  1. 在不确定的情况下最好使用括号以确保表达式组合关系符合程序逻辑。
  2. 当某个运算对象被修改其值时,在该操作影响到的地方不再引用该运算对象;若被修改的对象本身是另一个子表达式的运算对象,则此规定不再适用:*++iter.

4.2 算术运算符

算术运算符(左结合律) 功能 用法
+ 一元正号 + expr
- 一元负号 - expr
* 乘法 expr * expr
/ 除法 expr / expr
% 求余 expr % expr
+ 加法 expr + expr
- 减法 expr - expr

除非另有特别说明,默认情况下所有算术运算符均可用于在任何支持其操作的数据类型的计算中进行操作,并且这些运算符也适用于能够转换为相应数据类型的其他数据类型。

算术运算符的运算对象和求值结果都是右值。

这些算子包括一元正号运算符、加法运算法则以及减法运算法则。
当施加一元正符号到一个指针或数值时,
它会生成该操作数的一个拷贝。
对于施加一元负符号的情况,
它会立即提供该操作数的相反数的增强版本。

复制代码
    int i = 1024;
    int k = -i;		//k是-1024
    bool b = true;
    bool b2 = -b;	//b2是true
    //布尔值不应该参与运算,-b就是一个很好的例子。
    
    
      
      
      
      
      
    
    代码解释

算术表达式可能产生未定义的结果:

  • 部分情况是由数学属性自身引起:例如除数为零的情形。
    • 另外一些情况则归因于计算机特性:例如溢出现象通常发生在计算结果超出该数据类型的表示范围时。

自C++11标准起始后的新规范明确要求将商向零方向取整(即直接去除小数位)

根据取余运算的定义:当m与n均为整数且n不为零时,则表达式(m/n)*n + m%n 的计算结果必然是与m相等。其意义在于,在这种情况下(即当m除以n余数不为零时),该余数值的符号将与被除数m保持一致。

C++新标准规定不允许m%n运算符进行符合运算n的操作符重载(除当m为负数时可能发生溢出的情况外),在其他情况下:

复制代码
    (-m)/n == m/(-n) == -(m/n);
    m%(-n) == m%n;
    (-m)%n == -(m%n);
    
    
      
      
      
    
    代码解释

4.3 逻辑和关系运算符

关系运算符应用于算术类型或指针类型的任何情况中;逻辑运算符应用于任何可以转换为布尔类型的类型;其操作数及计算结果均为右值。

其逻辑运算符与关系运算符的结果均为布尔类型。其中,值为零的运算对象(涉及算术类型或指针类型的)被判断为假;反之,则视为逻辑真。

结合律 运算符 功能 用法
! 逻辑非 !expr
< 小于 expr < expr
<= 小于等于 expr <= expr
> 大于 expr > expr
>= 大于等于 expr >= expr
== 相等 expr == expr
!= 不相等 expr != expr
&& 逻辑与 expr && expr
逻辑或 expr expr

逻辑与运算符与逻辑或运算符都遵循先计算左操作数再计算右操作数的顺序,并且只有在左操作数无法确定最终结果的情况下才会继续计算右操作数。这一策略被称为短路评估法。

复制代码
    if (val) {/*...*/}	//如果val是任意非0值,条件为真
    if (!val) {/*...*/}	//如果val为0,条件为真
    //试图将以上写法写出下面形式:
    if (val == true) {/*...*/}	//只有当val等于1时条件才为真
    
    
      
      
      
      
    
    代码解释

这种改写与之前代码相比:

较为复杂且不够直观的方式用于此操作。当val不是布尔值时,这种比较将不再具有原有的意义;true会被转换为与val相同的类型。

在执行比较运算时

4.4 赋值运算符

在赋值运算中,默认情况下左侧操作数必须是一个可变的左值。执行赋值后得到的结果与左侧操作数一致,并且结果本身也是一个左值。其数据类型与左侧操作符的操作数类型相同。当两侧操作数的数据类型不同时,默认情况下右侧会被转换为与左侧相同的类型。

C++11新标准支持使用花括号括起来的初始化列表表达式作为赋值操作的位置。

当左侧运算对象为内置类型时,则初始化列表的最大容量仅限于一个元素,并且这个值在进行转换后所占用的空间不得超过目标类型所能容纳的空间:

复制代码
    int k;
    k = {3.14};	//错误:窄化转换
    vector<int> vi;
    vi = {0,1,2,3,4,5,6,7,8,9};
    
    
      
      
      
      
    
    代码解释

无论左侧运算体的数据类型为任意,在初始值列表允许为空情况时,编译器在必要时会生成一个临时变量用于存储初始化值,并将该临时变量将被赋值给左侧运算体。

该类操作符遵循右结合律,在此规律下执行时,默认将右边的操作数视为左边操作数完成后所得到的结果。

由于赋值运算符在优先级上低于关系运算符,在条件语句中通常会将赋值部分包裹在括号内。

4.5 递增和递减运算符

递增和递减有两种形式:(作用于左值运算对象

  • 前置版本:随后会将运算对象加1(或减1),随后导致的结果是修改后的对象的副本,并将其作为左值返回。
    • 后置版本:然而,在这种情况下计算所得的结果将是修改前的那个数值副本,并将其存储为右值。

除非必须,否则不用后置版本的递增递减运算符。

4.6 成员访问运算符

在程序设计中, 点操作符以及箭头操作符均可用于访问对象的成员. 其中主要采用点操作符来获取类对象的一个成员, 而箭头操作符则与之相关联. 例如在C++语言中表达式ptr->mem等价于(*ptr).mem, 这表明两者在功能上具有对应关系. 此外需要注意的是解引用操作符号的优先级低于该操作符号

箭头运算符作用于一个指针类型的运算对象,结果是一个左值。

点运算符:

  • 当操作对象为LeftValue时,则运算结果为LeftValue
    • 当操作对象为RightValue时,则运算结果为RightValue

4.7 条件运算符

该运算符(? :)支持将基本的if-else逻辑整合到一个表达式中;该运算符采用的形式包括:

复制代码
    cond ? expr1 : expr2;
    //等价于
    if(cond)
    	return expr1;
    else
    	return expr2;
    
    
      
      
      
      
      
      
    
    代码解释

当两个条件表达式均为左操作数或能转换为同一类型的操作数时,在这种情况下运算结果为左操作数;否则,在这种情况下运算结果为右操作数。

该运算符遵循右结合律。该运算符能够进行嵌入式操作,并且可以在另一个运算符的 cond 或 expr 中作为参数参与计算。当嵌入层次增多时, 显著降低了代码的可读性, 此时建议限制在不超过两层

在包含多个嵌套条件运算符的情况下,由于其较低的优先级,在处理复杂的逻辑关系时可能会导致计算顺序错误。

复制代码
    cout << ((grade < 60) ? "fail" : "pass");	//输出pass或者fail
    
    cout << (grade < 60) ? "fail" : "pass";		//输出1或者0
    //等价于:
    cout << (grade < 60);
    cout ? "fail" : "pass";
    
    cout << grade < 60 ? "fail" : "pass";		//错误:试图比较cout和60
    //等价于:
    cout << grade;
    cout < 60 ? "fail" : "pass";
    
    
      
      
      
      
      
      
      
      
      
      
      
    
    代码解释

4.8 位运算符(左结合律)

位运算符应用于整数类型的操作数,并将其视为二进制形式中的每一位集合。这些运算符能够实现对特定二进制位的检查与设置功能。

该标准库中的bitset类型能够支持任意大小的二进制位集合;相应的位运算操作符也适配于该bitset数据结构。

运算符 功能 用法
~ 位求反 ~ expr
<< 左移 expr1 << expr2
>> 右移 expr1 >> expr2
& 位与 expr & expr
^ 位异或 expr ^ expr
位或 expr expr

当运算操作的对象为"small integer类型"时,则该数值会自动升级为更大的整型类型。该操作的对象既可以是有符号类型的变量或常量(即变量或常量具有正负号),也可以是没有符号类型的变量或常量(即变量或常量仅表示非负数值)。特别地,在有符号类型的变量或常量中存在负数值的情况下,则该数值经过位操作后其"sign bit"(即标志其正负性的那个比特)如何被处理将取决于具体的机器实现方式;此外,在这种情况下左移操作可能会导致该标志位发生变化而产生未定义的行为后果。

二进制位左移或右移时,在超出边界范围的位数会被丢失。左移操作符(<<)会在右侧填充0;而右移操作符的行为则由左侧操作对象的数据类型决定。

  • 对于无符号类型的运算对象,在左侧填充零值的二进制位。
  • 对于带符号类型的运算对象,在左方填充其符号位副本或零值的二进制位;选择方式则取决于具体环境。

位移算子(也被称为IO操作符)遵循左结合规则。然而,并非所有人都很少直接使用这些位移算子本身,并且几乎都很少直接使用其重载版本来进行IO操作。其重载版本的优先级和结合性与内置版本完全一致

移位运算符的优先级不高不低,介于中间:

  • 具有比算术运算符更低的优先级
    • 其在优先级上高于关系运算符、赋值运算符以及条件运算符

4.9 sizeof运算符

该运算符表示一条表达式或一个类型名称所占用的字节数。该运算符遵循右结合性规则,在计算时会生成属于size_t类型的常量结果。

运算符的运算对象有两种形式:

复制代码
    sizeof (type)
    sizeof expr		//返回表达式结果类型的大小
    
    
      
      
    
    代码解释

sizeof并非真正计算运算对象的实际值,在sizeof对其运算过程中即使对无效指针进行解引用操作仍被视为一种安全措施。这是因为该运算并未实际使用该指针指向的对象,在这种情况下typeof操作者同样能够确定其所指向的对象的具体类型。

C++11新标准提供了通过作用域运算符获取类成员大小的方法,在常规情况下仅当通过实例对象才能访问到其对应的数据项;然而,在这种情形下,默认情况下的sizeof运算符并不需要用户预先构造相应的对象实例就可以完成计算工作

sizeof运算符的结果部分地依赖于其作用的类型:

当计算一个字符型或为字符型的表达式时 所得结果即为其内存占用空间尺寸 其值固定在1倍字节范围内。
对于引用类型的变量 执行 sizeof 操作所得结果即为其内存地址字段所对应的实际存储空间尺寸。
当计算一个指针变量本身所占用内存空间尺寸时 所得结果即为其自身内存地址字段所对应的实际存储空间尺寸。
解引用后的指针变量进行 sizeof 操作所得结果即为其指向对象内存地址字段所对应的实际存储空间尺寸 其中被解引用后的指针变量无需满足有效性条件。
计算一个数组变量整体占用内存空间尺寸时 所得结果即为其整体内存地址字段范围内的所有元素共同占用的实际存储空间尺寸 此处的操作不会将数组转换为对应的指针形式 因此可以通过将整个数组长度除以每个元素占据内存地址字段所需的字节数来确定该数组中包含的具体元素数量。
对于 string 类型的对象或 vector 等序列容器类型进行 sizeof 操作只会返回其固定部分所需内存资源量 而不会计入这些对象或容器内部所有存储单元实际占据的总内存空间量。

由于sizeof函数返回的是一个固定数值表达式的结果值,在编程中我们可以利用这一特性来预先声明数组的空间维度

4.10 逗号运算符

该运算符涉及两个操作数,在程序中按自左至右的顺序依次进行计算。该运算符先计算左边的表达式并将其结果舍去,在此之后才会处理右边的操作数。其最终的结果即为右边表达式的计算结果;特别地,在当右边的操作数为lvalue时,则计算结果亦为lvalue。

4.11 类型转换

在C++语言中存在一些类型之间具有关联关系。如果两种类型具有关联关系,则当程序在操作其中一种类型的运算对象时可以替换为另一种相关联类型的对象或数值型实体以实现相同的功能。这种情况下认为它们是相关联的。

复制代码
    int ival = 3.541 + 3;	//存在隐式转换
    
    
      
    
    代码解释

算术类型之间的隐式转换被设计成尽可能避免损失精度

在数据处理过程中,在将数值3隐性地转换为双精度浮点数之后进行加法运算操作后得到的结果仍然是双精度浮点数值,在初始化阶段由于对象类型无法更改而导致后续计算中将该数值从双精度浮点形式转换为整数值,并且在此过程中舍弃了小数部分的信息

在数据处理过程中,在将数值3隐性地转换为双精度浮点数之后进行加法运算操作后得到的结果仍然是双精度浮点数值,在初始化阶段由于对象类型无法更改而导致后续计算中将该数值从双精度浮点形式转换为整数值,并且在此过程中舍弃了小数部分的信息

何时发生隐式类型转换:

  • 在多数表达式里,当一个较小的整数型变量与一个较大的整数型变量进行比较或参与计算时,较小的那个会被升级为更大的整数型.
  • 在条件判断里,非布尔类型的参数会被自动转换为布尔类型的逻辑.
  • 初始化阶段时,初始值会被转化为目标变量的数据类型的规则所决定.具体来说,在赋值语句中的右边会被左边变量的数据类型的规则所确定.
  • 当执行算术或关系操作时,如果操作对象涉及不同数据类型的变量,系统会自动将它们统一转化为相同的数据类型.
  • 函数调用的过程中也会经历各种数据类型的转换过程.

4.11.1 算术转换

整型提升 主要负责将小整数值升级为更大范围的整数值。该提升措施确保升级后的数据类型能够容纳原有数据的所有可能取值。

如果一个运算对象是无符号类型,另一个运算对象是带符号类型:

  • 当无符号类型的存储空间大小不低于带符号类型时(即sizeof(\text{unsigned type}) \geq sizeof(\text{signed type})),则对应的操作数会将带有标志位的操作数转为无标志位的形式:
    • 具体来说:
      • 如果所有由\text{unsigned type}表示的值都可以在\text{signed type}中找到对应的表示,则操作数会从\text{unsigned type}转为\text{signed type}
        • 例如:\text{long}\text{unsigned int}之间有这样的映射关系;并且当\text{int}\text{long}占用的空间相同时,则操作数会从\text{int}转为\text{unsigned int}
      • 否则,在无法将所有\text{unsigned type}的值映射到\text{signed type}时:
        • 操作数会从\text{signed type}转为相应的\text{unsigned type}
        • 例如:当\text{long}占用的空间比\text{int}更多时,则操作数会从\text{long}转为相应的\text{unsigned int}

4.11.2 其他隐式类型转换

数组映射为指针 :在大多数涉及数组的表达式中,默认情况下会将数组映射为指向其首元素的指针。然而,在以下特定情况下不会发生这种映射:当该数组作为typeof运算符的关键字参数、作为取地址操作符(&)、sizeof或typeid等运算符的操作对象时。

指针对应映射:常量整数值0或空值nullptr可映射至任一目标指针类型;非空指针将被映射至void类型;而指向的对象则会对应到const void类型。

转换成布尔类型:有一种从算术类型或指针类型向布尔类型自动转换的机制。当指针或算术类型的值为0时, 转换结果等于false; 否则, 转换结果等于true.

常量转换:具备将指向非静态类型指针转换为相应静态类型指针的能力;同样适用于引用操作。其相反的操作并不存在。

类类型定义的转换 :通过编译器自动完成的各种转换操作中的一种会被每次处理。

4.11.3 显示转换

强制类型转换(cast) :显示的将对象强制转换成另外一种类型。

命名的强制类型转换

第一种情况

第二种情况

第三种情况

第四种情况

所有具有明确定义的类型转换操作(即不涉及底层const的操作),都可以借助static_cast来实现相应的功能。通过将较大的算术类型赋值给较小的类型(例如int赋值给short),我们能够灵活地进行数据类型的转换操作。同样地,我们可以将void*指针强制转换回原来的数据类型的实例。

复制代码
    int j = 1;

    double slope = static_cast<double>(j);
    
    void *p = &d;
    double *dp = static_cast<double*>(p);
    
    
         
         
         
         
         
    代码解释

const_cast 仅能将运算对象的基础常量转换为 std::remove_t 类型的对象引用。只有当该对象不是一个常量时, 通过强转的方式获取修改权限的行为是被允许的.

复制代码
    const char *pc;

    char *p = const_cast<char*>(pc);
    
    
         
         
    代码解释

仅通过const_cast可以修改表达式的常量属性;而任何其他的命名强制类型转换操作均会导致编译器报错。同样地,在修改表达式类型时也不允许使用const_cast

复制代码
    const char *cp;

    char *q = static_cast<char*>(cp);	//错误,不能用static_cast转换掉const性质
    static_cast<string>(cp);		//正确:字符串字面值转换成string类型
    const_cast<string>(cp);			//错误:const_cast只改变常量属性
    
    
         
         
         
         
    代码解释

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

reinterpret_cast 一般用于对运算对象的bit pattern在较低层次上进行重新诠释。

复制代码
    int *ip;

    char *pc = reinterpret_cast<char*>(ip);
    string str(pc);	//可能导致异常的运行时行为
    
    
         
         
         
    代码解释

由于pc所指的真实对象实际为整型而非字符型,在不当作普通的字符指针处理时可能导致运行时错误。使用reinterpret_cast存在严重的风险,并且这种操作需要深入理解涉及的类型转换过程才能安全执行。

dynamic_cast

应该尽量避免强制类型转换!

传统的硬性数据类型的转换,在早期版本的C++语言中通过显式的方式进行展示,并包含两种不同的形式。

复制代码
    type(expr);			//函数形式的强制类型转换
    (type)expr;			//C语言风格的强制类型转换
    
    
      
      
    
    代码解释

基于所处理的不同数据类型,旧式的硬性类型转换机制其行为类似于const_cast、static_cast或reinterpret_cast。

与命名化的强制类型转换相比,在表现形式上来说旧式的强制类型转换并不那么清晰明了,并不容易被发现和忽视。因此,在转换过程中出现任何问题时追踪起来也会更加困难。

4.12 运算符优先级表

在这里插入图片描述
在这里插入图片描述

全部评论 (0)

还没有任何评论哟~