【C++笔记】4. 表达式和运算符

4. 表达式和运算符

4.1 基础

  1. 一元运算符:作用于1个运算对象。
    类似的有:二元运算符、三元运算符。
    函数调用也是一种特殊的运算符。

  2. 运算对象转换:从一种类型转换成另一种类型。

  3. 重载运算符:可以对运算符自定义含义。
    比如cout和cin的<<>>运算符。
    但运算对象的个数、运算符的优先级、结合律无法改变。

  4. 左值和右值:C++的表达式要么是左值,要么是右值。
    当一个对象被用作右值的时候,用的是对象的值;
    当一个对象被用作左值的时候,用的是对象的身份。
    一个重要的原则:需要右值的地方可以用左值代替,但不能把右值当成左值。

  5. 使用关键字decltype的时候,左值和右值也不同。
    如果表达式的求值结果是左值,decltype作用于该表达式得到一个引用类型。
    举例:假定p类型是int*
    解引用得到左值,所以decltype(*p)的结果是int&
    取地址得到右值,所以decltype(&p)的结果是int**

  6. 括号无视优先级。

  7. 结合律的典型示例:输入输出运算。
    比如cin >> v1 >> v2,先进行cin >> v1
    表达式返回的是cin,故继续进行cin >> v2

  8. 优先级只规定了运算对象的组合方式,但未说明运算对象按什么顺序求值。
    举例:int i = 0; cout << i << " " << ++i << endl;
    结果可能是“1 1”,也可能是“0 1”。
    由于表达式的行为不可预知,因此不论编译器生成什么都是错误的。

  9. 当然,有4种运算符是明确规定了运算对象求值顺序的。
    分别是&&||?:,
    例如:当&&的左侧表达式值为0时,右侧表达式会被屏蔽掉。
    &&的左侧表达式值为1时,右侧表达式才会执行运算。

  10. 尽量避免求值顺序、优先级、结合律杂糅的语句。
    f()+g()*h()+j();的表达式中:
    f、g、h、j互相无关时,先g*h,再与f、j相加。
    f、g、h、j若影响同一个对象,则是一条错误语句,产生未定义的行为。

  11. 综上,写代码时应采取以下两个建议:
    (1)拿不准时,使用括号强制规范运算顺序。
    (2)一条语句中,一个对象尽量只出现一次。
    比如:*beg = toupper(*beg++);,赋值号左右都出现了beg,此时会因为编译器的不同产生不同的结果。
    (3)*++p*++iter等:这种写法很常见,且很简洁。

4.2 算术运算符

  1. 算数运算符的运算对象和求值结果都是右值。

  2. 所有运算对象最终会转换成同一类型。

  3. 一元负号运算符对运算对象值取负后,返回其提升后的副本。

  4. 布尔值不应参与运算,否则会出现下面的情况:
    b是一个布尔值,若进行一元符号运算,会将其提升为int,因此-b的值就是int型的-1。
    此时赋给b2,由于-1不为0,因此b2会被赋值为1,即b2是true。

    bool b = true;
    bool b2 = -b;     // 此时b2是true
    
  5. 计算机在处理算术表达式时,可能会导致溢出。
    比如short类型最大可表示32767,此时若令该变量加1,则会产生溢出,此时输出的max值为-32768。
    short max = 32767; ++max; cout << max;

  6. 要求表达式返回int类型的加、减、乘、除、取余运算,会丢弃小数部分。

  7. 取余运算要求运算对象必须是整数类型。

  8. 表达式(m/n)*n+m%n的结果是m

  9. 当除法运算和取余运算的运算对象有负数时,运算规则如下:

    表达式运算结果表达式运算结果
    21 % 6321 / 63
    21 % 7021 / 73
    -21 % -8-5-21 / -82
    21 % -5121 / -54

4.3 逻辑和各系运算符

  1. 逻辑运算符和关系运算符的返回值类型都是布尔类型。
    运算对象和求值结果都是右值。

  2. 短路求值:本节“1.基础”中的第9条。
    应用:左侧运算对象可以确保右侧运算对象求值过程的正确性和安全性。

  3. 课本上的一个例子:
    s被声明成了引用,因为text的元素是string对象,可能非常大,所以将s声明为引用类型可以避免对元素的拷贝。
    而被声明为常量,也是因为不需要对string做写操作。

    for (const auto &s : text){
        cout << s;
        if (s.empty() || s[s.size()-1] == '.')
            cout << endl;
        else
            cout << " ";
    }
    
  4. 不建议将int和bool进行比较!

    int val = 2;
    if (val == true) ;
    // 先把true转变为int类型,也就是1
    // 然后执行语句 `if (val == 1)`
    // 会执行 `if (0)`
    if (val);
    // 直接把val转变为bool类型,也就是1;
    // 会执行 `if(1)`
    

4.4 赋值运算符

  1. C++允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。
    无论右侧运算对象类型是什么,初始值列表的都可以为空。

    vector <int> vi = {};
    vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    
  2. 赋值运算满足右结合律。类型相同的对象才能进行赋值运算。

  3. 赋值运算符比关系运算符优先级低,因此在条件语句中,赋值语句应加上括号。

  4. 复合赋值运算,比普通运算效率更高。
    a+=1a=a+1,后者比前者多做一次求值。

4.5 递增和递减运算符

  1. ++ii++效率更高。
    因为前置版本直接返回+1后的值,而后置版本要先暂存一下原有的i。
  2. 后置递增运算符的优先级高于解引用运算符。
    *p++是一种更简洁、更不易出错的写法。
    其功能是,先自加1,再使用加1之前的值。
  3. 但应避免同一条语句中前后同时出现p的情况。
    *p = toupper(*p++),此时该赋值语句是未定义的。
    根据编译器的不同,会导致不同的运算顺序。

4.6 成员访问运算符

  1. 点运算符和箭头运算符
    ptr->mem等价于(*ptr).mem
  2. 解引用运算符优先级低于点运算符。

4.7 条件运算符

  1. 条件运算符的优先级非常低。因此使用时须加圆括号。

4.8 位运算符

  1. 一般来说,如果运算对象是“小整型”,则它的值会被自动提升为较大的整数类型。
    位运算符如何处理带符号数,依赖于不同的机器本身。
    因此强烈建议仅将位运算符用于处理无符号类型。

  2. 位运算的应用:
    通过下面这个例子可知,非运算符比左移运算符优先级高。

    unsigned long quiz = 0;
    quiz |= 1UL << 27;                 // 将第27位置为1
    quiz &= `(1UL << 27);              // 将第27位置为0
    bool status = quiz & (1UL << 27);  // 查看第27位的状态
    
  3. 移位运算符满足左结合律。

4.9 sizeof运算符

  1. sizeof运算符返回的是一条表达式或一个类型名字所占的字节数。
    sizeof (type)sizeof expr
    注意哦,sizeof是个运算符!

  2. sizeof函数并不计算表达式的值。
    比如sizeof *p,由于sizeof和*优先级一样,所以表达式从右往左结合。
    且因sizeof不会计算表达式的值,所以即使p是一个无效指针也没关系。
    因为sizeof不需要解引用指针也能知道它所指对象的类型。

  3. sizeof的作用规律:

    表达式运算结果
    sizeof(char)1字节
    sizeof(引用)原对象所占空间字节
    sizeof(指针)指针本身所占空间字节
    sizeof(*指针)所指对象所占空间字节,且指针不必有效
    sizeof(数组)整个数组所占空间字节
    sizeof(string)固定部分所占空间字节
    sizeof(vector))固定部分所占空间字节

4.10 逗号运算符

  1. 经常用在for语句中。
  2. 表达式的值,为逗号右边的表达式的值。

4.11 类型转换

  1. 隐式转换:C++语言先将类型统一,再将两个不同类型的值相加。

  2. 算术转换:运算对象自动转换成最宽的类型。
    整型提升:bool, char, signed char, unsigned char, short, unsigned short, 都会提升为int类型。

  3. 较大的char类型(如wchar_t, char16_t, char32_t)则会转换成较大的int类型(int, unsigned int, long, unsigned long, long long, unsigned long long)里能容纳对象的最小类型。

  4. 无符号类型的运算对象
    一般来说,带符号的要转换成无符号的。
    int和unsigned int:int先转换为unsigned int,再运算。如果int恰好为负值,则产生相应的副作用。

  5. 在不同的编译环境下,long和unsigned int的转换规则不同:
    若int和long大小相同,则long转换为unsigned int;
    若long比int多,则unsigned int转换为long。

  6. 理解算术转换

    bool    b;   char            c;
    short   s;   unsigned short  us;
    int     i;   unsigned int    ui;
    long    l;   unsigned long   ul;
    float   f;   double          d;
    
    3.14159L + 'a';  // a → int → longdouble
    d + i;           // i → double
    d + f;           // f → double
    i = d;           // d丢弃小数部分,赋给i
    b = d;           // d为0时b为0,其他情况为1
    c + f;           // c → int → float
    
    // 此处s和c都转化为int
    s + c;           // c → int, s → int
    // 此处c直接转换为long
    c + l;           // c → long
    
    // 不需要分情况
    i + ul;          // i → ul
    
    // 分情况:
    // 若short和int空间一样大,则 i → unsigned short
    // 若short比int空间小,则 us → int
    us + i;
    
    // 分情况:
    // 若long和int空间一样大,则 l → unsigned int
    // 若long比int空间大,则 ui → long
    ui + l;
    
  7. 通常,数组名会转换成指针。
    例外:decltype()的参数,取地址、sizeof、typeid等运算符的运算对象时,转换不会发生。

  8. 指针的转换:
    nullptr能转换成任意指针类型、指向任意非常量的指针能转换成void*、指向任意对象的指针能转换成const void *

  9. 转换成布尔类型:非0时全部转换成1。

  10. 转换成常量:允许指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是如此。反之不行。

    int i;
    const int &r = i;    // 非常量转换成const int的引用
    const int *p = &i;   // 非常量转换成const int的指针
    int &s = r;          // 错误,不允许const转换成非常量
    int *q = p;          // 错误,不允许const转换成非常量
    
  11. 类类型定义的转换举例:
    C风格字符串初始化string对象:string s = "cpp_primer"
    条件部分读入cin并转换为bool值:while (cin >>s)

  12. 显式转换(也叫强制类型转换)
    命名的强制类型转换、旧版本的强制类型转换
    形式:cast_name<type>(expression);

  13. static_cast:只要不包含底层const,都可以使用。用途:
    强制执行浮点数除法double d = static_cast<double> (j)/i;
    找回存在于void*的指针。

  14. const_cast:只能改变底层const
    简单来说就是:原来通过指针不能改变对象,现在可以了。
    如果对象本身不是常量,使用强制类型转换获得写权限是合法的行为。
    如果对象是一个常量,在使用const_cast执行写操作会引起未定义行为。

    const char *pc;
    char *p = const_cast<char*>(pc);
    
  15. reinterpret_cast:运算对象的位模式提供低层次上的解释。
    使用是危险的,除非特别需要。

    int *ip;
    char *pc = reinterpret_cast<char*>(ip);
    
  16. 旧式的强制类型转换
    char *pc = (char*) ip;
    由于表现不清晰,不推荐使用。

4.12 运算符优先级

优先级运算符方向功能
1::作用域
2.成员
2->成员
2[]下标
2()函数调用
2()类型构造
3++后置递增
3后置递减
3typeid()类型ID
3cast_name<type>(expr)类型转换
4++前置递增
4前置递减
4~按位取反
4!逻辑非
4-一元负号
4+一元正号
4*解引用
4&取地址
4()类型转换
4sizeof对象大小
4sizeof()类型大小
4new创建对象
4new[]创建数组
4delete释放对象
4delete[]释放数组
4noexcept能否抛出异常
5->*指向成员选择的指针
5.*指向成员选择的指针
6*乘法
6/除法
6%取余
7+加法
7-减法
8<<向左移位
8>>向右移位
9<小于
9<=小于等于
9>大于
9>=大于等于
10==相等
10!=不等
11&位与
12^位异或
13|位或
14&&逻辑与
15||逻辑或
16? :条件
17=赋值
18复合赋值复合赋值
19throw抛出异常
20,逗号
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值