4-1 C++运算符基本概念

4.1.1 基本概念

函数观点

操作符可视为一个函数,用参数,副作用,返回值来分析它

cout<<i<<j<<endl>>操作符

  • 参数:ostream对象和值(value)
  • 副作用:将值输入到ostream中,这里是输入到标准输出(屏幕)中
  • 返回值:返回左边的ostream对象,继续与右边的值结合,这是cout可以链式使用的原因

左值和右值

操作符的返回值可以分为左值(lvalue)和右值(rvalue)

  • 左值:用的是对象的身份(内存中的位置)
  • 右值:用的是对象的值(内容)
  • 基本原则:需要右值时可以用左值代替,反之不可以。

运算符重载

对指定的类重新定义某些操作符的操作,但无法改变其运算对象的个数,优先级和结合律

4.1.2 优先级、结合律与求值顺序

优先级和结合律

  • 优先级:每个操作符都有优先级,高级的先运算
  • 结合律:同级运算符遵循结合律,从左到右运算
  • 可用小括号强制改变运算顺序

求值顺序

书中表述

《C++ Primer(英)》137,对应中文版123:

优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。在大多数情况下,不会明确指定求值的顺序。对于如下的表达式
int i = f1()* f2();
我们知道f1和f2一定会在执行乘法之前被调用,因为毕竟相乘的是这两个函数的返回值。但是我们无法知道到底f1在f2之前调用还是f2在f1之前调用。

因此,当在一个表达式的某处改变了一个变量的值,而在该表达式的另一处又使用到它,可能会产生一个未定义的结果,这是一个隐藏错误。

image-20220115110101384

实践表明(猜想)

但是,在g++ 8.1版本下,编译器可以处理上述情况,输出0 1

个人推测:编译器在执行cout<<i<<" "<<++i<<endl;时,会先从左到右执行指令,直到把所有子表达式为字面量。再从左到右输出,所以可以认为编译器执行了两步:(i=0为例)

  1. 子表达式i++i从左到右转为字面量:cout<<0<<" "<<1<<endl;
  2. 输出0 1

实践验证

#include<iostream>
using namespace std;
int m = 0;   //全局变量,f,g,j都可以改变m的值

 //三个函数都改变了m的值
int f();  //m++
int g();  //m+=2
int j();  //m*=2
int main(){
    //如果f,g,j求值顺序不定的话,该式的输出结果会不同,结果未定义。
    //但实际上程序输出19
    cout<<f()+g()*j()<<endl;
    return 0;
}

int f(){
    m++;
    return m;
}
int g(){
    m+=2;
    return m;
}
int j(){
    m*=2;
    return m;
}

按照上述猜想可以解释19这一结果

  • 将子表达式fgj从左到右执行,子表达式转为字面量
    • 执行f后,m = 1
    • 执行g后,m = 3
    • 执行j后,m = 6
    • 表达式变为cout<<1 + 2*3<<endl;
  • 输出19

可能的解释:编译器的优化行为

《C++ Primer(英)》138对应中文版124

image-20220115111449575

所以或许可以认为,在遇到某一表达式的子表达式操作了同一对象且改变了对象的值时,编译器会遵循结合律,对每个字表达式从左到右求值,这一动作是编译器的优化动作,来提高执行效率。

但是否每种编译器都执行此类优化呢?不知道。

所以,如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。

一些运算符指定了求值顺序

一般运算符不指定求值顺序,但编译器会对其进行优化。而有些运算符明确指定了求值顺序,前三者都是从左到右。

  • &&:左边的子表达式为真时,才会继续求右边的子表达式的值
  • ||:左边的子表达式为假时,才会继续求右边的子表达式的值
  • ,(逗号)
  • ?:(三目条件判断运算符)

再述求值顺序

如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象,来避免编译器未定义行为

//编写程序把字符串的所有字符大写
#include<iostream>
#include<string>
using namespace std;
int main(){
    string s = "hello, world";
    auto it = s.begin();
    while(it != s.end()){
        *it = toupper(*it++);  //右值改变了it,左值又用到了it,该行为结果是未定义的
    }
    cout<<s<<endl;
    return 0;
}

*it = toupper(*it++);可能被解释为

  • *it = toupper(*it);
  • *(it+1) = toupper(*it);

image-20220117115110086

修改后:

#include<iostream>
#include<string>
using namespace std;
int main(){
    string s = "hello, world";
    auto it = s.begin();
    while(it != s.end()){
        *it = toupper(*it); //把两个语句分开,避免被修改的对象被重用
        it++;
    }
    cout<<s<<endl;
    return 0;
}

image-20220117115241980

原文链接: https://www.cnblogs.com/timothy020/p/15817160.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    4-1 C++运算符基本概念

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/186404

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 上午11:06
下一篇 2023年2月12日 上午11:07

相关推荐