表达式基础

学习目标:

表达式:由一个或多个的运算对象和运算符组成。


学习内容:

一、基础

1. 基础概念

  • 运算符
    i. 一元运算符:作用于一个运算对象,如*、&等;
    ii. 二元运算符:作用于两个运算对象,如+、==等;
    iii. 三元运算符:作用于三个运算对象,如a>b ? a:b;
  • 表达式求值的结果依赖于:运算符的优先级、结合律以及运算对象的求值顺序。
  • 运算符重载:当运算符作用于类对象时,用户可以自定义其含义。
  • 左值和右值
    i. 左值:可以取到地址的表达式;
    ii. 右值:取不到地址的表达式;

2. 优先级和结合率

  • 字面意思就是数学上的优先级和结合率。提要提一下的就是对指针的影响。

    int a = {0, 2, 4, 6, 8};
    int b = *(a + 4);   // 8
    int c = *a + 4;     // 0 + 4
    
  • IO相关的运算符满足左结合率;

3. 求值顺序

  • 有四种运算符明确规定了对象的求值顺序
    i. &&,与运算符,规定先求左侧运算对象的值,只有当左侧对象为真时才会执行右侧对象;
    ii. ||,或运算符,规定先求左侧运算对象的值,只有当左侧对象为假时才会执行右侧对象;
    iii. ?a:b,条件运算符,条件成立,执行a,条件不成立,执行b;
    iv. a,b 逗号运算符,从左到右执行,先执行a,再执行b,最后返回b的结果;

二、算术运算符

  • 所有的算术运算符
    在这里插入图片描述
    优先级:一元 > 乘除余 > 加减 所有都满足左结合律:优先级相同时,从左往右计算

  • 需要注意的一些点
    i. 布尔类型在做一元算术运算符时,会先将布尔类型提升为int类型,再运算,运算完再降为布尔类型(如果是0,bool=0,只要不是0,bool=1);

    bool a = true;
    // a -> 1, -a = -1, -1 -> true
    bool b = -a;  // b = true
    

    ii. 整数除法,一律向下取整,只保留整数部分;小数除法还是保留小数部分;

    int a = 7;
    int b = 2;
    cout << a / b << endl;  // 3
    
    double c = 3.14;
    int d = 2;
    cout << c / d << endl;  // 1.57
    

    iii. 求余,运算对象必须是两个整数,否则会报错;

    double a = 3.14;
    int b = 2;
    cout << a % b << endl;  // 报错
    

    iv. 计算溢出问题(当前计算的结果超出该类型的范围),导致程序结果崩溃;

    short a = 32767;
    a += 1;
    cout << a << endl;  // -32768
    

    v. 除法:(-m)/ n = m / (-n) = -(m/n);
    vi. 求余:m % (-n) = m % n,(-m)% n = - (m%n)

三、逻辑和关系运算符

  • 所有的逻辑和关系运算符
    在这里插入图片描述

  • 一些需要注意的点
    i. 上述运算符优先级从高到低排列,如果搞不清优先级,最好是写的时候自己打上括号;
    ii. 逻辑与:当且仅当左侧运算对象为真时才会对右侧运算对象求值;
    iii. 逻辑非:当且仅当左侧运算对象为假时才会对右侧运算对象求值;
    iv. 关系运算符会比较左右运算对象的大小关系后再返回bool值;

    if(a < b < c)  // 错  a<b返回bool值  bool再和c比较 明显不是我们的意思
    if((a<b) && (b<c))  // 对
    if(a<b && b<c)  // 对
    

    v. bool值转换为其他算术类型时:true=1,false=0

四、赋值运算符

  • 赋值运算符 =;

  • 初始化不是赋值;

    int a = 0, b = 0, c = 0;  // 初始化  错
    const int d = a;  // 初始化 错
    
  • 赋值运算符的左侧运算对象必须是一个可修改的左值;

    1024 = c;  // 左侧字面量一个右值  错
    a + b = c;  // 左侧算法表达式是一个右值  错
    d = c;  // 左侧是一个不可修改的左值  错
    
  • 赋值是允许左右两侧类型不一致的,因为可以类型转换;

    c = 0;     // 对 类型一致 直接赋值
    c = 3.14;  // 对 类型不一致 先类型转->3 再赋值
    
  • c++11可以使用初始化列表进行赋值,但是这种赋值方式更加严格,类型必须一致;

    c = {3.14};  // 类型不一致 不能用列表初始化
    c = {3};     // 对
    vector<int> v;
    v = {0,1,2,3}; // 对 初始化列表赋值用在vector中比较多
    
  • 赋值运算满足右结合律,所以允许多重赋值a=b=0,相当于先把0赋值给b,返回b,再把b赋值给a,返回a;

    a = b = 0;  // 对
    

    但是多重赋值要注意类型,很容易出错:

    int a, *b;
    a = b = 0;  // 错 不能把指针类型b赋值给a
    
  • 赋值运算符的优先级低于关系运算符,所以一般在条件语句中使用赋值运算符都要用括号括起来;

    int i;
    if((i=get_value()) != 42)  // 对
    if(i=get_value() != 42)  // 错 先执行关系运算符 会将判断结果bool值赋给int类型i
    
  • 不要把赋值运算符=和相等运算符==搞混了;

    int a = 0;
    int b = 0;
    if(a = 2){  // 把2赋值给a,并返回a,if(a) 不等于0 所以是true  执行cout
        cout << "a = 2" << endl;  
    }
    if(b == 2){ // b==2 不成立 返回false 不执行cout
        cout << "b == 2" << endl;
    }
    // 输出a = 2
    
  • 复合赋值运算符:+=、-=、*=、/=、%=、<<==、>>=、&=、^=、|=; 几乎等价于 a = a op b; 唯一区别是复合赋值运算符的左值只求一次值,而a = a op b需要对左值a求两次值,但是这点区别对程序性能几乎忽略不计,所以一半都用复合运算符,增加程序的可读性。

五、递增和递减运算符

  • 递增运算符++、递减运算符–

  • 前置递增和后置递增
    i. 前置递增: ++a,首先将a自增,再返回自增后的结果;
    ii. 后置递增: a++,首先创建一个a的副本,保留原始值,再将a自增,最后返回a的副本,即返回原始值;

    int a = 0, b;
    b = ++a;  // a=1 b=1
    b = a++;  // a=2 b=1
    

    iii. 一般情况都使用前置递增,因为后置递增需要保留原始值的副本,浪费资源;

  • 混用解引用和后置递增符(后置递增重点使用方法):后置递增运算符优先级高于解引用 输出vector中所有大于0的元素,直到遇到负数为止

    vector<int> v = {0, 1, -2, 3};
    auto beg = v.begin();
    while(beg != v.end() && *beg >= 0){
        // beg++: beg先自增 向后移动 再返回beg 
        // *beg++: 返回*(原始位置副本)  
        cout << *beg++ << endl;
    }
    

    注意:*beg++ 等价于下面两种形式,但是一半都使用 *beg++,更简洁,这种后置递增方式很常见,需要熟悉这种写法;

    *beg++;      // <==>
    *(beg++);    // <==>
    *beg;beg++;  
    
  • 递增运算符可以改变运算对象的值,所以要注意一条语句前后都使用对象且会改变的情况: 运算对象的求值顺序没有规定,所以不知道*beg = toupper(*beg++);的求值顺序。 如果先求左侧的值 ==> *beg = toupper(*beg); 如果先求右侧的值 ==> *(beg+1) = toupper(*beg); 所以,程序也不知道怎么执行,崩溃了,此时我们说这条赋值语句是未定义的。

    vector<char> v = {'a', 'b', 'c', 'd'};
    auto beg = v.begin();
    while(beg != v.end() && !isspace(*beg)){
        *beg = toupper(*beg++);  // 错误  程序崩溃
    }
    

六、成员访问运算符

  • 类型:点运算符.和箭头运算符->,用于访问类对象的一个成员; p->mem 等价于 (*p).mem

    string a = "Hello World";
    string *p = &a;
    cout << a.size() << endl;     // string对象a的size成员 点运算符
    cout << (*p).size() << endl;  // p指针所指向的对象的size成员
    cout << *p.size() << endl;   // 错 解引用优先级低于点运算符 先执行p.size() 报错 
    cout << p->size() << endl;    // 箭头运算符  
    
  • 箭头运算符作用于一个指针类型的运算对象,结果是左值(对象是左值,成员是变量也是左值,那么p->mem的结果也是左值);点运算符如果成员所属对象是左值,那么结果是左值,如果成员所属对象是右值,那么结果是右值;

七、条件运算符

  • 条件运算符是唯一的三元运算符,形式:cond ? exp1 : exp2 执行顺序:先求出条件cond的值,如果是True执行exp1,并返回exp1的值,否则执行exp2,并返回exp2的值;

    // 先判断条件grade是否大于60,如果是返回pass 否则返回fail
    string final_grade = (grade > 60) ? "pass" : "fail";
    
  • 条件运算符满足右结合律,当嵌套条件运算符时,右边的条件运算可以作为左边条件运算的分支;但是嵌套条件运算符会降低程序的可读性,所以一般最多嵌套2-3层;

    // 先判断是否大于90分,如果大于输出high pass 否则再执行右边分支
    // 判断是否大于60分,如果是返回pass 否则返回fail
    string final_grade = (grade > 90) ? "high pass" : (grade > 60) ? "pass" : "fail";
    
  • 条件运算符的优先级比较低,所以在输入表达式时最好使用括号括起来;

    cout << ((grade > 60) ? "pass" : "fail");  // fail
    // 等价于 cout << (grade > 60); cout ? "pass" : "fail";
    cout << (grade > 60) ? "pass" : "fail";    // 0
    // 等价于 cout << grade; cout > 60 错
    cout << grade > 60 ? "pass" : "fail";      // 错
    

八、位运算符

  • 位运算符作用于整数类型的运算符对象,把运算对象看成二进制位的集合。位运算符提供检查和设置二进制为的功能;

  • 所有位运算符总结
    在这里插入图片描述

  • 位操作通常处理无符号类型整数;

  • 移位运算符(<<、>>),移之后填充0

    unsigned char bits = 0233; 
    // 2^3=8 所以二进制从右往左数每3位表示八进制的一位
    // 8进制 -> 2进制24个0+10011011 -> 16个0+10011011+8个0
    bits << 8;
    // 24个0+10011011 -> 27个0+10011
    bits >> 3; 
    // 24个0+10011011 -> 32个0
    bits << 31; 
    
  • 移位运算符都满足左结合律;

  • 优先级顺序:算法运算符 > 移位运算符 > 关系/赋值/条件运算符;

    cout << 42 + 10;    // 对 算术运算符 > 移位运算符
    cout << (42 + 10);  // 对
    cout << 42 > 10;    // 错 等价于 cout << 42; cout > 10;错误
    
  • 位求反运算符~,所有位置0->1,1->0

    unsigned char b1 = 0145;
    // 24个0+01101000 -> 24个1+1001011
    ~b1;
    
  • 位与&、位或|、异或^运算 位与&:两个运算对象对应位置都是1,则该位置返回1,否则返回0; 位或|:两个运算对象对应位置有一个为1,则该位置返回1,否则返回0; 异或^:两个运算对象对应位置必须一个为0一个为1,则该位置返回1,否则返回0;

    unsigned char b1 = 0145;  // 01100101
    unsigned char b2 = 0257;  // 10101111
    b1 & b2;  // 00100101
    b1 | b2;  // 11101111
    b1 ^ b2;  // 11001010
    

九、sizeof运算符

  • sizeof运算符返回一条表达式或者一个类型名字所占的字节数byte;

  • 形式:sizeof(type)或sizeof expr;第二种返回表达式结果类型的大小;sizeof运算符并不计算其运算对象的大小,只关注其类型;
    i. 对引用执行sizeof运算符返回被引用对象所占空间的大小;
    ii. 对指针执行sizeof运算返回指针本身所占空间的大小;
    iii. 对解引用指针执行sizeof运算符返回指针所指的对象所占空间的大小,指针可以无效;
    iv. 对数组执行sizeof运算符是算数组中所有对象所占空间的和,对解引用数组执行sizeof运算符是算数组第一个元素所占的空间;

    Sale_data data, *p;
    sizeof(Sale_data);  // 返回存储Sale_data类型的对象所占用的空间大小
    sizeof data;  // 类对象data的类型的大小
    sizeof p;     // 指针对象p所占的空间的大小
    // 等价于sizeof (*p) p可以是一个未初始化的指针,因为这里根本不需要解引用,只需要知道类型即可知道内存大小
    sizeof *p;    // 指针p所指向的内容的类型的大小
    sizeof data.revenue;   // 通过对象:Sale_data类的revenue成员对象的类型的大小
    sizeof Sale_data::revenue;  // 通过类名:Sale_data类的revenue成员对象的类型的大小
    
  • sizeof常用与求数组中元素的个数,string和vector中的元素个数通常不用sizeof求;

    int a[] = {1, 2, 3};
    constexpr size_t sz = sizeof(a) / sizeof(*a);  // 3
    

十、逗号运算符

  • 逗号运算符,常用在for语句中,按照从左到右的顺序依次求值。

    vector<int> vec = {1, 2, 3, 4};
    int cnt = vec.size(); 
    for(int idx = 0; idx != vec.size(); ++idx, --cnt){
        vec[idx] = cnt;  
    }  // vec = {4, 3, 2, 1}
    
  • 逗号运算符两侧分别一个表达式,先计算左侧表达式,再将表达式结果丢弃掉,再计算右侧表达式,并将结果作为最终逗号运算符的结果,返回;

    // 如果x>y,x自增,y自增,且返回y;反之x自减,且返回x
    x > y ? ++x, ++y : --x
    

十一、类型转换
1. 隐式转换-算术转换

  • 整型提升:小整数类型转换为较大的整数类型: bool、char、short等小整型,转换为int、long等大整型;

  • int->float->double。

    bool flag;
    short sval;
    int ival;
    long lval;
    float fval;
    char cval;
    unsigned short usval;
    unsigned int uival;
    unsigned long ulval;
    double dval;
    
    3.14159L + 'a';  // 'a' -> int -> long double
    dval + ival;     // int -> double
    dval + fval;     // float -> double
    ival = dval;     // double -> int
    flag = dval;     // dval = 0 flag = false else flag = true
    cval + fval;     // char -> int -> float
    sval + cval;     // short -> int, char -> int
    cval + lval;     // char -> int -> long
    // 下面三种不用管,一般不会把无符号数和有符号数相加
    ival + ulval;
    usval + ival;
    uival + lval;
    

2. 其他隐式转换

  • 数组转换为指针:大多数用到数组的表达式中,数组自动会转换为数组首元素的指针;

    int a[10];
    int *p = a;  // 数组a转换为数组首元素的指针
    
  • 算术类型或指针可以向布尔类型自动转换;

    char *cp = get_string();
    if(cp)   // 如果指针cp是空指针,返回false,否则返回true
    if(*cp)  // 如果*cp是空字符,返回false,否则返回true 
    
  • 字面串字面值转换成string类型;

    string s, t = "hello world";
    
  • while(cin >> a) // 返回cin 再转换为bool类型

    while(cin >> a)  // 返回cin 再转换为bool类型
    

3. 显式转换
尽量慎用显示转换(强制类型转换)。

形式:const_type<type>(expression)

其中,expression是需要转换的值,type是需要转换的目标类型,const_type是static_cast、dynamic_cast、const_cast、reinterpret_cast四种。static_cast、dynamic_cast用的最多,const_cast在重载函数中用的多,reinterpret_cast不怎么用。

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

    int i, j;
    double c = static_cast<double>(j) / i;  // 把int型强制转换为double
    
    void* p = &d;   // 先把非常量对象的地址保存入void*中,再加那个void*转换为初始的指针类型;
    double *dp = static_cast<double*>(p);
    
    const char *cp;
    char *p = static_cast<char*>(cp);  // 错  static_cast无法转换底层const
    
  • dynamic_cast: 支持运行时动态类型识别

  • const_cast:只能改变运算对象的底层const,一般可用于去除const性质。

    const char *pc;  // 底层const
    char *p = cosnt_cast<char*>(pc);  // 把const char* -> char*
    
    string s = const_cast<string>(pc);  // 错 const_cast只能改变底层cast 
    
    void *pv;
    const string *ps;
    pv = static_cast<void*>(const_cast<string*>(ps));  // const string* -> string* -> void*
    
  • reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。用的少,也注意慎用。

  • 15
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值