C++ Primer 学习笔记——第四章

发布时间 2023-06-05 15:21:12作者: 木木亚伦

第四章 表达式

前言

本章主要介绍:语言本身定义、并用于内置类型运算对象的运算符。简单介绍:几种标准库定义的运算符。

表达式本身由一个或多个运算对象组成,其目的是得到一个结果:

value operator n*(operation object)

通过运算符将一个或多个运算对象结合起来便组成了表达式!


4.1 基础

基本概念

一元运算符(unary operator)、二元运算符(binary operator)和三元运算符(ternary operator)

解释:

  • 一元运算符:作用于一个运算对象的运算符,例如:取地址符(&)和解引用符(*)
  • 二元运算符:作用于两个运算对象的运算符,例如:相等运算符(==)和乘法运算符(*)
  • 三元运算符:作用于三个运算对象的运算符,格式为:condition ? expression1 : expression2

补充

函数调用也是一种特殊的运算符,只不过它对运算对象数量没有限制。

对于能够充当多种元运算符的运算符,根据上下文判断其属于那种元运算符,例如:*。

重载运算符

在这里简单介绍一下重载运算符,重载运算符其实就是对已存在的运算符赋予另一层含义,例如:IO库中的“<<”和“>>”运算符。

重载运算符会改变运算对象的类型和返回值的类型,但是无法改变运算对象的个数、运算符的优先级和结合律。

左值和右值

具体查看代码

优先级和结合律

关键点:

  • 优先级和结合律决定了运算对象组合的方式
  • 括号无视该规则
  • 表达式依赖于子表达式的结合方式
  • 高优先级的运算对象比低优先级的运算对象更为紧密的组合在一起
  • 算术运算对象满足左结合律

示例:

int a=1+2*4/2+6;    /* 11 */
int b=(1+2)*4/2+6;  /* 12 */

求值顺序

对于函数的调用来说,求值顺序的不同很有可能会产生不同的结果,甚至是错误!

例如:


int test_number = 1;

int test_evaluationOrder_test_1() { return test_number++; }
int test_evaluationOrder_test_2() { return test_number += 3; }

/**
 * 求值顺序的探讨
 */
void test_evaluationOrder() {
    int i = 0;
    /**
     * <<运算符并未明确规定何时以及如何对运算对象进行求值,没有明确的执行顺序,那么该表达式就是错误的。因为您无法确定是i先执行还是++i先执行,得到的答案不论如何都是错误的
     * */
    std::cout << i << " " << ++i << "\n";

    /**
     * 同理:这下面一句也是错误的。
     * 你无法判断是test_1()先执行还是test_2()先执行。
     */
    int number = test_evaluationOrder_test_1() * test_evaluationOrder_test_2();
    std::cout << number << "\n";
}

事实上,除了以下四种运算符明确规定运算对象的求值顺序,其他运算符并没有明确说明:

  • 逻辑与(&&)运算符
  • 逻辑或(||)运算符
  • 条件(?:)运算符
  • 逗号(,)运算符

这就导致一个问题:这些函数的调用顺序没有明确规定,那么如果某几个函数会影响到同一个对象,那么必然导致结果的唯一性。

建议

对于复合表达式而言,有两条经验之谈:

  1. 拿不准最好用括号括起来。
  2. 如果已经改变某个对象的值,那么表达式中就不能再调用能够影响该值的函数。

4.2 算术运算符

算术运算符分别有:

  • + - ,一元正号(负号)
  • * / % ,乘法、除法、取余
  • + -,加号、减号

算术运算有时候也会产生未定义的结果,例如除数是0、数据溢出等。

在取余这里,C++ 11定义有:

  • m%(-n)=m%n
  • (-m)%n=-(m%n)

具体示例:

21%6=3;         21/6=3;
21%7=0;         21/7=3;
-21%-8=-5;      -21/-8=2;
21%-5=1;        21/-5=-4;    

4.3 逻辑和关系运算符

关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。二者返回值皆为布尔类型。

其含有:

  • !,逻辑非
  • < <= > >=,小于、小于等于、大于、大于等于
  • == !=,等于、不等于
  • &&,逻辑与
  • ||,逻辑或

注意

在相等性测试和布尔字面值上,如果想要测试一个算术对象或指针对象的真值,最后的方式是用if语句:if(value),在这种条件下,编译器会自动将算术对象转换为布尔值并进行判断。

注意:不要使用if(value==true)的方式进行判断,原因:①、该写法不直观;②、如果value不是布尔值则失去比较意义:如果value不是布尔值,那么首先将true转换为value的类型,如果value不是布尔值,则转换为if(value==1),这样的比较是没有意义的。

综述,如果进行比较运算,如果比较对象不是布尔类型,请不要使用布尔字面值作为运算对象参与比较。

4.4 赋值运算符

赋值运算符的左侧运算对象必须是一个可修改的左值。

赋值运算满足右结合律,例如:

int rval,lval;
rval=lval=0;    /* rval和lval的值均为0 */

lval作为靠左赋值运算符的右侧运算对象,赋值运算返回其左侧运算对象,所以靠右的赋值运算的结果被赋值给rval。

4.5 递增和递减运算符

递增和递减运算符有前置和后置两种方式,其原理:

前置:将对象本身加/减1后作为左值返回;

后置:将对象原始值的副本作为右值返回,后再进行对象本身的加/减1;

建议

除非必要,在使用递增和递减运算符时一律使用前置方式,使用后置方式会导致不必要的资源浪费,同时前置方式更符合编程初衷。

4.6 成员访问运算符