Advertisement

《C++ Primer》笔记——第四章 表达式

阅读量:

一、基础

1.左值右值(没有弄明白)

C++中的表达式要么是左值(lvalue),要么是右值(rvalue)。例如,在赋值语句中左操作数是否位于左边?而右操作数无法在赋值语句左侧出现。

C++语言中,在涉及右指针的情况下应依据目标内容进行引用;而作为左指针使用的对象则需基于其内存地址进行引用。在需要引用内容的位置上可用左指针替代右指针;但必须明确区分二者,在处理涉及内存地址的情况时要格外谨慎。

在使用decltype关键字时,左值与右值表现出显著差异。当所涉表达式的求值结果为左值时,默认选择将返回与其相对应的引用类型。由于取地址运算符生成的是右值,在这种情况下,默认选择的作用域指向器将具有int*类型的指针。即此指向器实际上是指向了一个整型变量的间接指针。

2.优先级与结合律

括号无视优先级与结合律。

3.求值顺序

运算对象的优先级决定了它们的组合方式,并未指出运算对象如何进行求值,在一般情况下,默认不会有明确规定的计算顺序。当遇到未指定操作顺序的运算符时,在某些情况下若操作表达式指向并修改了同一个对象,则会导致错误并产生未定义的行为。

比如:

复制代码
 int i=0;

    
  
    
 cout<<i<<” ”<<++i<<endl;

该编译器可能在计算顺序上存在差异,在处理自增运算时会根据具体情况选择不同的执行路径;这种行为会导致程序运行时出现不一致的结果。例如,在某些情况下该编译器可能会在计算完变量当前值后再执行自增操作;然而,在另一些情况下则会直接对变量进行自增后再获取其新值。因此这种代码无法保证预期的功能表现

运算对象的求值顺序不受优先级及结合方式的影响;对于形如f()+g()*h()+j()这类表达式而言,在此结构下尽管运算次序及其结合规则已被明确指定;但具体到各子函数(即f、g、h、j)之间的调用次序则并未作出明确规定;若这些子函数彼此之间互不相关,并且既不修改同一个对象的状态也不执行输入输出操作,则它们可以以任意顺序被调用;然而若其中任意两个或多个子函数存在对同一对象的操作,则该整个表达式的构造将导致运行时错误并引发不可预测的行为

在书写复合表达式时, 应用以下两条经验和规则能起到显著作用: 第一条经验是在不确定的情况下适当使用括号以确保组合关系符合程序逻辑; 第二条规则是如果修改了某个运算对象的值, 则避免在其他地方再次使用该运算对象.

二、算数运算符

1.优先级

一元运算符具有最高的 precedence level,在其后依次是乘除法;加减法则拥有最低 precedence level。所有运算符均遵循左结合规则,在 precedence levels相同的情况下,则按从左往右的方式进行结合。

多数布尔类型的运算对象将被升级为其对应的int类别,在这种情况下,当该运算对象等于True时赋值1,在其等于False的情况下赋值0;任何非零数值会被视为True,在数值等于零的情况下被视为False。

如下:

复制代码
 bool b1=true;

    
  
    
 bool b2=-b1;//b2的值任然是true

在代码中使用到的变量b₁属于布尔类型,在运算过程中会被转换为其对应的整数类型中的数值1。接着对数值取反得到-1。其数值结果仍不等于零。因此变量b₂仍然保持逻辑真值。

2溢出

要注意的是不同类型值的范围,不能超出范围,超出范围就会产生溢出。

3.除法运算(/)

在C++中,在进行integer type division运算时所得的结果仍然是一个integer value;然而,在早期版本中规定了当商为负值时会采取上取整或下取整的方式计算。

C++11新标准规定商一律向0取整(不管正负)。

4.运算符(%)

运算符%俗称取余,参与取余运算的两个对象必须是整数类型。

新标准对于m%n的运算,正负号只匹配m。假设m、n都是正整数,如下为例:

m%(-n);等价于m%n;

(-m)%n;等价于-(m%n);

(-m)%(-n);等价于-(m%n);

三、逻辑和关系运算符

该关系运算符可应用于算术类型或指针类型的表达式;而逻辑运算符则用于处理任何可转换为布尔类型的变量。无论是哪种类型的布尔操作(即逻辑或关系操作),它们都会产生一个布尔结果;当一个对象的值等于零时,则视为假;否则被视为真;参与这些操作的所有对象的结果均为右值对象

比如下面这个有问题的写法:

复制代码
    if(i<j<k)//由于i和j比较后返回的是布尔值,布尔值再参与运算时提升为int型0或1,后和k比较,这样由于不知道i、j、k和0或1的大小关系,会出现不可预知的结果。

正确写法是:

复制代码
    if(i<j&&j<k)

2.短路求值

逻辑与运算符和逻辑或运算符依次先计算左边的运算对象后再计算右边的运算对象,在左边的运算对象无法确定整个表达式的结果时才开始计算右边的对象,这种执行方式被称为短路求值。

3.相等性测试和布尔字面值

当执行相等性测试并进行比较运算时(即当执行比较操作时),除非被比较的对象是布尔类型(即只有当被比较的变量或表达式都是布尔类型的),否则应避免将布尔字面值true和false用作操作对象(即作为操作数)。

四、赋值运算符

赋值运算的结果是由其左侧参与运算的对象决定,并且这个结果作为一个左值变量存储起来。当赋值算子连接两个不同类型的变量时,则右边的那个变量会被转换为与左边相同的变量类型以保证数据类型的统一性。

2.赋值运算满足右结合律

如下:

复制代码
    ival=jval=0;

3.赋值运算优先级较低

考虑到赋值操作的优先级低于比较运算符,在处理条件判断时,为了确保计算的正确性与逻辑顺序完整性, 建议在赋值部分适当加入括号

五、递增递减运算符

1.递增递减两种写法

存在两种类型的递增与递减运算符,在编程语言中通常将这些操作分为两类:一类作用于当前对象后再进行操作(称为前置运算),另一类则先计算再更新对象(称为后置运算)。它们的作用均是对目标进行一次增量或降量;其关键区别在于,在执行后置运算时会先读取当前的状态而不立即修改该状态,在执行前置运算时则会先修改再读取。

比如:

复制代码
 int i=0,j;

    
  
    
 j=++i;//i=1,j=1前置版本得到递增之后的值
    
  
    
 j=i++;//i=2,j=1后置版本得到递增之前的值

一般推荐使用前置写法,将前置写法变成一种习惯。

2.一条语句中混用解引用和递增运算符

递增运算符高于解引用运算符!!!

如下依次输出vector对象中元素的例子:

复制代码
 vector<int> a = { 1,2,3,4,5,6,7,8,9 };

    
  
    
 auto ptr = a.begin();
    
  
    
 while (ptr != a.end())
    
  
    
    cout << *ptr++<< endl;

由于递增运算符高于解引用运算符,所以上面代码中ptr++相当于(ptr++)

推荐*ptr++这种简洁的写法,简洁既是美德!

3.运算对象可按任意顺序求值

通常情况下,并未对操作数的求值顺序作出明确的规定。然而,在一般情况下并没有问题出现。但若一个子表达式修改了某个操作数的值,则当另一个子表达式需要用到该数值时,此时操作数的求值顺序变得至关重要。

仍然采用这一例子:f() + g() \times k() + j();尽管运算符优先级与结合律已经非常明确,在这种情况下也没有任何问题;但如果四个操作子所对应的函数之间存在某种关联性,则可能会导致意想不到的结果。

再看例子,假如beg是string对象的指针:

复制代码
    *beg=toupper(*beg++);//错误,该赋值语句未定义

根本原因是由于尽管在程序设计中通常遵循从左至右执行赋值操作的习惯这一约定而导致的;然而,在涉及变量beg与beg++这两个运算符时其求值运算的优先级或次序并未有统一规定因而会导致整个赋值操作产生歧义进而使得整个表达式无法得到明确确定的结果

同样的例子还有上面的一条代码:

复制代码
 int i=0;

    
  
    
 cout<<i<<” ”<<++i<<endl;//错误,将产生不确定结果

在流运算中,在使用cout输出时,默认会按照从左到右的方式进行处理。然而,在处理i和++i的情况时,默认没有规定它们之间的求值顺序;因此上述代码存在错误。

六、成员访问运算符

点运算符和箭头运算符。ptr->mem等价于(*ptr).mem

在操作过程中需要注意的是,在处理指针变量时应当先明确各操作之间的优先顺序:在表达式中若出现解引用操作,则其优先级需低于取值操作(即点操作)。具体而言,在对表达式(ptr).mem执行赋值操作时必须使用括号以确保正确的计算顺序(否则会导致结果出错)。如果直接执行ptr.mem的操作,则意味着先对该变量进行取值操作(即获取其指向内存单元的内容),随后再进行解引用处理(即将得到的内容转换为对应的物理地址)。但是需要注意的是,在这种情况下变量应为指针类型而非普通变量类型;否则将导致错误的发生

七、条件运算符

条件运算符(?:)可以嵌套使用。

八、位运算符

1.位运算符

整数类型的对象可以通过位运算符进行操作,并将运算对象视为二进制集合的一种形式。Bitset是一种能够存储任意数量的二进制位的数据结构。

该运算符遵循左结合规则。若操作数类型为‘short’(小整型),则其数值会自动提升至较大的整数类型。若操作数为有符号类型且数值为负,则其在执行位操作时如何处理符号位取决于具体的机器架构,并可能导致未定义的行为发生。

2.移位运算符(<<和>>)

然后让左侧运算对象根据右侧的要求进行位移。接着用移动后的左侧运算对象的一个副本来计算结果。需要注意的是,在此操作中右边的对象必须是非负数,并且其数值必须严格小于操作的结果长度。在二进制移位时,默认超出范围的部分会被截断。

左移操作符(<<)会在右侧填充零值的二进制位;右移操作符(>>)的行为主要取决于其操作对象的数据类型。若涉及无符号数据类型的运算,则该操作会在其左侧附加零值;而对于带符号的数据类型,在进行右移操作时,在其左侧会附加复制符号位或填入零值。具体情况则需参考具体的编程环境设定。

3.按位求反(~)

按位求反运算符(~)将运算对象逐位求反得到一个新值,将1置0,将0置1

小整形将升级为较大的整数类型,在此过程中其他位保持不变;向高位添加零即可完成转换。

4.位与(&)、位或(|)、位异或(^)运算符

位与(&):两个运算对象的对应位置都是1则运算结果为1,否则为0

位或(|):两个运算对象的对应位置至少有一个1则运算结果为1,都是0时结果为0

位异或(^):两个运算对象的对应位置有且只有一个为1,则运算结果为1,否则为0

5.移位运算符满足左结合律

移位运算符又叫IO运算符。

移位运算符的其优先级低于算数运算符,并处以上述关系运算符、赋值运算符以及条件运算符之中

如:

复制代码
    cout<<(10<24)<<endl;

运算符<<优先级高于关系运算符<,因此要加括号。

九、sizeof运算符

1.sizeof

该运算符用于表示一条表达式或一个类型名称所占用的空间数量,并将其表示为一个size_t类型的常量表达式。
该运算符遵循右结合规则,并不会真正计算其操作数的值。

2.C++11标准sizeof

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

对char或者类型为char的表达式执行sizeof运算,结果为1

对引用类型执行sizeof运算,结果是被引用对象所占空间的大小

对指针执行sizeof运算,结果是指针本身所占空间的大小

在解引用操作中对指针执行sizeof运算,则其结果等于被指向对象所占用的空间大小;无需判断其有效性。

关于对数组进行sizeof运算的结果表明,该运算的结果等于该数组占用的空间总量.特别指出,在本实现中,默认情况下不会将数组转换为指向其元素的指针.

对string对象实例或vector对象实例执行sizeof操作,其返回值表示该类型固定内存占用量,并不计算其动态内存分配的空间。

十、逗号运算符

该运算符能够处理两个操作数,并遵循从左到右的顺序进行计算。其结果等于表达式右边的部分,并且该运算符具有最低优先级。

如:

复制代码
 i = 20, j = 2 * i; // i=20,j=40,表达式的值是40

    
  
    
 int a = 20, 40;//相当于int (a=20),40;因此40不起作用
    
  
    
 int a=(20,40);//最终值是逗号后面的40,a=40

十一、类型转换

1.隐式转换

隐式转换即为一种无需程序员干预即能完成类型转换的方式。只有当两种类型之间存在关联时才能发生隐式转换。

隐式转换即为一种无需程序员干预即可完成类型的自动转变方式。只有当两种数据类型的关联性足够强时才能实现这种转变。

常见隐式转换:

大多数情况下,比int类型小的整数值首先提升为较大的整数类型

在条件中,非布尔型转换成布尔型

初始化步骤中对初始值进行数据类型的适应性处理;在赋值操作时会自动将右边的操作数转化为与左边变量匹配的数据类型

当涉及算术运算或关系运算时,若运算对象具有不同类型的属性,则需执行归一化处理。

函数调用时也会发生类型转换

2.算术转换(隐式转换)

算术转换的目的在于将某类算术类型转换为另一类算术类型。遵循的是运算符作用的对象被转化为最宽类型的这一原则。

要注意的是有无符号运算对象的类型转换规则!

3.其他隐式转换

将数组转化为指向其元素的指针;整数值0或空值nullptr可以被隐式地转为任何类型的指针变量;非静态指针始终可以被隐式地转为void类型;对象指向型变量始终可被显式地赋值一个布尔类型的值;当目标是不带const修饰符时,默认情况下应指定const_cast<某种基类>的方式进行转化;非静态且非const的对象引用均可转为const void

4.显示转换

强制类型转换本质上是十分危险的。

命名的强制类型转换:

cast-name(expression)

cast-name属于这些成员类型之一:static_cast, dynamic_cast等; target_type为目标转换类型; 如果为引用对象, 则返回左值; 目标要进行转换的对象是expression.

5.static_cast

所有明确定义过的类型转换操作(除底层const外),都可通过static_cast来实现;当将较大数值类型的变量赋值给较小区分类型的变量时;那些编译器无法自动处理的特殊情况(例如,在需要恢复void*指针类型的场景中),仍需手动进行相应的处理以确保兼容性

比如:

复制代码
 int i = 10, j = 25;

    
 double slope = static_cast<double>(j) / i;
    
  
    
 //再看一个例子
    
  
    
 double j = 25;
    
  
    
 void *p = &j;
    
  
    
 double *dp = static_cast<double*>(p);

6.const_cast

const_cast 仅能作用于操作对象的底层常量属性,并被称为"去除常量属性"(cast away const)。此外,在只有当使用 const_cast 时才能修改表达式的常量属性时,请特别注意这种情况通常出现在涉及函数重载的情境中。

比如:

复制代码
 const char* cp;

    
  
    
 char *p = const_cast<char *>(cp);

7.reinterpret_cast

reinterpret_cast一般用于将运算对象的位模式进行低层次重新解读。这种操作本质上依赖于机器架构,在实际应用中需要对涉及的数据类型及其编译器的具体实现有深入理解,否则可能会导致不可预见的结果。

比如:

复制代码
 int *ip;

    
  
    
 char *pc = reinterpret_cast<char*>(ip);

ip所表示的对象本质上是一个整数类型而不是一个字符。如果误将pc当作普通字符指针使用,在运行时可能会导致问题。

复制代码

全部评论 (0)

还没有任何评论哟~