您的位置:首页 > 编程语言 > C语言/C++

《C++ Primer》学习笔记(四):优先级和关系运算符

2020-03-02 04:54 1426 查看

欢迎关注WX公众号:【程序员管小亮】

专栏C++学习笔记

《C++ Primer》学习笔记/习题答案 总目录

——————————————————————————————————————————————————————

  • 《C++ Primer》习题参考答案:第4章 - 优先级和关系运算符

文章目录

  • 2、算术运算符
  • 3、逻辑和关系运算符
  • 4、赋值运算符
  • 5、递增和递减运算符
  • 6、成员访问运算符
  • 7、条件运算符
  • 8、位运算符
  • 9、sizeof运算符
  • 10、逗号运算符
  • 11、类型转换
  • 12、运算符优先级表
  • 参考文章
  • 📚💻 Cpp-Prime5 + Cpp-Primer-Plus6 源代码和课后题

    写在前面

    哼哼,勤劳的日更博主又回来了,我的小粉丝们期待一下吧 😃

    优先级和关系运算符

    1、基础

    表达式由一个或多个 运算对象(operand) 组成,对表达式求值将得到一个 结果(result)。字面值和变量是最简单的 表达式(expression),其结果就是字面值和变量的值。把一个 运算符(operator) 和一个或多个运算对象组合起来可以生成较复杂的表达式。

    1)基础概念

    C++定义了 一元运算符(unary operator)二元运算符(binary operator),作用于一个运算对象的运算符是二元运算符,如取

    地址符( & )
    解引用符( * )
    ;作用于两个运算对象的运算符是二元运算符,如
    相等运算符( == )
    乘法运算符( * )
    。除此之外,还有一个作用于三个运算对象的 三元运算符(Ternary Operator)。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。

    可以看到

    *
    既能作为一元运算符也能作为二元运算符。。。

    表达式求值过程中,类型转换的规则有点复杂,如:整数能转换成浮点数,浮点数也能转换成整数,但是指针不能转换成浮点数。而小整数类型(如

    bool
    char
    short
    等)通常会被 提升(promoted) 为较大的整数类型,主要是
    int

    C++定义了运算符作用于 内置类型复合类型 的运算对象时所执行的操作。当运算符作用于 类类型 的运算对象时,用户可以自定义其含义,这被称作 运算符重载(overloaded operator),如:

    IO
    库的
    >>
    <<
    运算符以及
    string
    对象、
    vector
    对象和迭代器使用的运算符等。

    C++的表达式分为 右值(rvalue)左值(lvalue)

    • 当一个对象被用作右值的时候,用的是 对象的值(内容)
    • 当一个对象被用作左值的时候,用的是 对象的身份(在内存中的位置)

    其中,需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。

    • 赋值运算符需要一个非常量左值作为其左侧运算对象,得到的结果也仍然是一个左值;
    • 取地址符作用于一个左值运算对象,返回指向该运算对象的指针,该指针是一个右值;
    • 内置解引用运算符、下标运算符、迭代器解引用运算符、
      string
      vector
      的下标运算符都返回左值;
    • 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本返回左值,后置版本返回右值。

    如果

    decltype
    作用于一个求值结果是左值的表达式,会得到一个引用类型。

    2)优先级与结合律

    复合表达式(compound expression) 指含有两个或多个运算符的表达式。运算符和运算对象合理地组合在一起,优先级与结合律决定了运算对象的组合方式,高优先级运算符先运行(乘法和除法,然后加法和减法),如果优先级相同,则其组合规则由结合律确定,从左向右顺序运行。

    python 版本的数据结构与算法也上线了,欢迎查看——用Python解决数据结构与算法问题(三):线性数据结构之栈

    括号无视优先级与结合律,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。

    举一个稍稍复杂的例子理解一下:

    6 + 3 * 4 / 2 + 2 = 14
    
    <=> // 等价于
    
    // 这条表达式中的括号符合默认的优先级和结合律
    (( 6 + (( 3 * 4) / 2 )) + 2 ) = 14

    3)求值顺序

    对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。

    int i = 0;
    cout << i << " " << ++i << endl;    // undefined

    有4 种运算符明确规定了运算对象的求值顺序:

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

    处理复合表达式时建议遵循以下两点是有益的:

    • 不确定求值顺序时最好使用括号来强制让表达式的组合关系符合程序逻辑的要求;
    • 如果表达式改变了某个运算对象的值,则在表达式的其他位置不要再使用这个运算对象。

    不过,第2条规则有一个重要例外,当改变运算对象的子表达式本身就是另一个子表达式的运算对象时,规则无效。例如表达式

    *++iter
    ,递增运算符改变了 iter 的值,而改变后的 iter 的值又是解引用运算符的运算对象。这种或者类似情况下,求值的顺序不会成为问题,因为递增运算(即改变运算对象的子表达式)必须先求值,然后才轮到解引用运算。

    2、算术运算符

    算术运算符(按照运算符的优先级排序):


    上面的所有运算符都满足左结合律, 意味着当优先级相同时按照从左向右的顺序进行组合。

    除法运算:

    • 整数相除(
      /
      )结果还是整数,即直接弃除商的小数部分;
    • 取余取模 运算符(
      %
      )计算整数相除的余数。

    在除法运算中,C++语言的早期版本允许结果为负数的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接去除小数部分)。

    21 % 6; 	/* 结果是3  */ 	21 / 6; 	/* 结果是3  */
    21 % 7; 	/* 结果是0  */ 	21 / 7; 	/* 给果是3  */
    -21 % -8; 	/* 结果是-5 */ 	-21 / -8; 	/* 结果是2  */
    21 % -5; 	/* 结果是1  */ 	21 / -5; 	/* 结果是-4 */

    3、逻辑和关系运算符

    关系运算符作用于算术类型和指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。

    逻辑与 运算符

    &&
    逻辑或 运算符
    ||
    都是先计算左侧运算对象的值再计算右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会去计算右侧运算对象的值,这种策略称为 短路求值(short-circuit evaluation)

    • 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。
    • 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
    // s 是对常量的引用;元素既没有被拷贝也不会被改变
    for (const auto &s : text){	// 对于text 的每个元素
    cout << s; 				// 输出当前元素
    // 遇到空字符亭或者以句号结束的字符串进行换行
    if (s.empty() || s[s.size() - 1] == '.')
    cout << endl;
    else
    cout << " "; 		// 否则用空格隔开
    }

    if
    语句的条件部分首先检查
    s
    是否是一个空
    string
    ,如果是,则不论右侧如何都换行;只有当
    string
    对象非空时才求第二个运算对象的值,即是否是以句号结束。

    逻辑非 运算符

    !
    将运算对象的值取反后返回。

    // 输出vec的首元素(如果有的话)
    if (!vec.empty())
    cout << vec[O];

    关系运算符比较运算对象的大小关系并返回布尔值,关系运算符都满足左结合律。

    // 哎哟!这个条件居然拿i<j的布尔值结果和k比较
    if ( i < j < k) 	// 若k大于1则为真
    // 正确:当i小于j并且j小于k时条件为真
    if (i < j && j < k)
    {
    /* ... */
    }

    测试真值,最直接的方法就是将其作为

    if
    语句的条件。

    if (val)
    {
    /* ... */
    } // 如果val是任意的非0值,条件为真
    
    if (!val)
    {
    /* ... */
    } // 如果val是0,条件为真
    
    if (val == true)
    {
    /* ... */
    } // 只有当val等于true时条件才为真!
    
    if (val == 1)
    {
    /* ... */
    } // 只有当val等于1时条件才为真!

    进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值

    true
    false
    作为运算对象。

    4、赋值运算符

    赋值运算符

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

    int i = 0, j = 0, k = 0;	// 初始化而非赋值
    const int ci = i;			// 初始化而非赋值
    
    1024 = k ;					// 错误:字面值是右值
    i + j = k ;					// 错误:算术表达式是右值
    ci = k;						// 错误:ci是常量(不可修改的)左值

    C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。

    k = (3.14);						// 错误:窄化转换
    vector<int> vi;     			// 初始为空
    vi = {0,1,2,3,4,5,6,7,8,9}; 	// vi现在含有10个元素了,值从0到9

    赋值运算符满足右结合律。

    int ival, jval;
    ival = jval = 0;    // 正确:都被赋值为0
    
    int ival, *pval;	// ival的类型是int;pval是指向int的指针
    ival = pval = 0;	// 错误: 不能把指针的值赋给int
    
    string s1, s2;
    s1 = s2 = "OK";		// 字符串字面值"OK"转换成string对象

    因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。

    // 这是一种形式烦琐、容易出错的写法
    int i = get_value(); // 得到第一个值
    while (i != 42)
    {
    // 其他处理......
    i = get_value(); // 得到剩下的值
    }
    
    // 更好的写法:条件部分表达得更加清晰
    int i;
    while ((i = get_value()) != 42)
    {
    // 其他处理......
    }

    不要混淆相等运算符

    ==
    和赋值运算符
    =

    if (i = j)
    if (i == j)

    复合赋值运算符包括

    +=
    -=
    *=
    /=
    %=
    (算术运算符);
    <<=
    >>=
    &=
    ^=
    |=
    (位运算符)。任意一种复合运算都完全等价于 a = a op b。

    5、递增和递减运算符

    递增(

    ++
    )和递减(
    --
    )运算符是为对象加1或减1的简洁书写形式。这两个运算符还可应用于迭代器,因为很多迭代器本身不支持算术运算。

    递增和递减运算符分为前置版本和后置版本:

    • 前置版本:首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。
    • 后置版本:也会将运算对象加1(或减1),但求值结果是运算对象改变前的值的副本。
    int i = 0, j;
    j = ++i;    // j = 1, i = 1: 前置版本得到递增之后的值
    j = i++;    // j = 1, i = 2: 后置版本得到递增之前的值

    除非必须,否则不应该使用递增或递减运算符的后置版本。因为后置版本需要将原始值存储下来以便于返回修改前的内容,如果我们不需要这个值,那么后置版本的操作就是一种浪费。

    对于整数和指针类型来说,编译器可能对这种额外的工作进行一定的优化;但是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了。建议养成使用前置版本的习惯,这样不仅不需要担心性能的问题, 而且更重要的是写出的代码会更符合编程的初衷。

    auto pbeg = v.begin();
    // 输出元素直至遇到第一个负值为止
    while (pbeg != v.end() && *beg >= 0)
    // 输出当前值并将pbeg向前移动一个元素
    cout << *pbeg++ << endl;

    对于刚接触C++和C的程序员来说,

    *pbeg++
    不太容易理解,但其实这种写法非常普遍,所以一定要理解其含义。后置递增运算符的优先级高于解引用运算符,因此
    *pbeg++
    等价于
    *(pbeg++)
    pbeg++
    pbeg
    的值加1, 然后返回
    pbeg
    的初始值的副本作为其求值结果,此时解引用运算符的运算对象是
    pbeg
    未增加之前的值。最终,这条语句输出
    pbeg
    开始时指向的那个元素,并将指针向前移动一个位置。

    如果返回的是加1之后的值,解引用该值将产生错误的结果。不但无法输出第一个元素,而且更糟糕的是如果序列中没有负值,程序将可能试图解引用一个根本不存在的元素。

    在某些语句中混用解引用和递增运算符可以使程序更简洁!!!

    cout << *iter++ << endl;
    
    // cout << *iter << endl;
    // ++iter;

    6、成员访问运算符

    点运算符

    .
    和箭头运算符
    ->
    都可以用来访问成员,其中,点运算符获取类对象的一个成员;箭头运算符与点运算符有关,表达式
    ptr->mem
    等价于
    (*ptr).mem

    string s1 = "a string", *p = &s1;
    auto n = s1.size(); // 运行string对象s1的size成员
    n = (*p).size();    // 运行p所指对象的size成员
    n = p->size();      // 等价于(*p).size()

    因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。如果没如括号,代码的含义就大不相同了:

    //运行p的size成员,然后解引用size的结果
    *p.size (); // 错误:p是一个指针,它没有名为size的成员

    7、条件运算符

    条件运算符(? :)的使用形式如下:

    cond ? expr1 : expr2;

    其中 cond 是判断条件的表达式,而 expr1 和 expr2 是两个类型相同或可能转换为某个公共类型的表达式。先求 cond 的值,如果 cond 为真则对 expr1 求值并返回该值,否则对 expr2 求值并返回该值。

    string finalgrade = (grade < 60) ? "fail" : "pass";

    只有当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果才是左值,否则运算的结果就是右值。

    条件运算符可以嵌套,但是考虑到代码的可读性,运算的嵌套层数最好不要超过两到三层。

    finalgrade = (grade > 90) ? "high pass"
    : (grade < 60) ? "fail" : "pass";

    条件运算符的优先级非常低,因此当一个长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。

    cout << ((grade < 60) ? "fail" : "pass"); 	// 输出pass或者fail
    
    cout << (grade < 60) ? "fail" : "pass"; 	// 输出1或者0
    // 等价于<=>
    // cout << (grade < 60); 					// 输出1或者0
    // cout ? "fail" : "pass"; 					// 根据cout的值是true还是false产生对应的字面值
    
    cout << grade < 60 ? "fail" : "pass"; 		// 错误:试图比较cout和60
    // 等价于<=>
    // cout << grade; 							// 小于运算符的优先级低于移位运算符,所以先输出grade
    // cout < 60 ? "fail" : "pass"; 			// 然后比较cout和60

    8、位运算符

    位运算符(左结合律):

    在位运算中符号位如何处理并没有明确的规定,所以强烈建议仅将位运算符用于无符号类型的处理。

    左移运算符

    <<
    在运算对象右侧插入值为0的二进制位,右移运算符
    >>
    的行为依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在其左侧插入值为0的二进制位;如果是带符号类型,在其左侧插入符号位的副本或者值为0的二进制位,如何选择视具体环境而定。

    位求反运算符(

    ~
    )将运算对象逐位求反而生成一个新值,将1置为0、将0置为1。

    char 类型的运算对象首先提升成 int 类型,提升时运算对象原来的位保持不变, 往 高位(high order position) 添0即可。

    与(

    &
    )、或(
    |
    )、异或(
    ^
    )运算符在两个运算对象上逐位执行相应的逻辑操作。

    与和或都是比较常见的了,就不说了,对于 位异或运算符(
    ^
    来说,如果两个运算对象的对应位置元素不同则运算结果中该位为1,否则为0。

    移位运算符的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。

    cout << 42 + 10; 	// 正确:+的优先级更高,因此输出求和结果
    cout << (10 < 42); 	// 正确:括号使运算对象按照我们的期望组合在一起,输出1
    cout << 10 < 42; 	// 错误:试图比较cout和42!
    // 等价于<=>
    // (cout << 10) < 42;

    9、sizeof运算符

    sizeof
    运算符返回一个表达式或一个类型名字所占的字节数,返回值是
    size_t
    类型。

    Sales_data data, *p;
    sizeof(Sales_data); 			// 存储Sales_data类型的对象所占的空间大小
    sizeof data;					// data的类型的大小,即sizeof(Sales_data)
    sizeof p;						// 指针所占的空间大小
    sizeof *p;						// p所指类型的空间大小,即sizeof(Sales_data)
    sizeof data.revenue; 			// Sales_data的revenue成员对应类型的大小
    sizeof Sales_data::revenue; 	// 另一种获取revenue大小的方式

    sizeof
    的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。

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

    • char
      或者类型为
      char
      的表达式执行
      sizeof
      运算,返回值为1。
    • 对引用类型执行
      sizeof
      运算得到被引用对象所占空间的大小。
    • 对指针执行
      sizeof
      运算得到指针本身所占空间的大小。
    • 对解引用指针执行
      sizeof
      运算得到指针指向的对象所占空间的大小,指针不需要有效。
    • 对数组执行
      sizeof
      运算得到整个数组所占空间的大小。
    • string
      vector
      对象执行
      sizeof
      运算只返回该类型固定部分的大小,不会计算对象中元素所占空间的大小。

    10、逗号运算符

    逗号运算符

    ,
    含有两个运算对象,按照从左向右的顺序依次求值,最后返回右侧表达式的值。逗号运算符经常用在
    for
    循环中。

    vector<int>::size_type cnt = ivec.size();
    // 将把从size到1的值赋给ivec的元素
    for(vector<int>::size_type ix = 0;
    ix != ivec.size(); ++ix, --cnt)
    ivec[ix] = cnt;

    11、类型转换

    在C++语言中,某些类型之间有关联。如果两种类型有关联, 那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。换句话说,如果两种类型可以 相互转换(conversion),那么它们就是关联的。

    无须程序员介入,会自动执行的类型转换叫做 隐式转换(implicit conversions),可以尽可能地避免损失精度。

    在下面这些情况下, 编译器会自动地转换运算对象的类型:

    • 在大多数表达式中,比
      int
      类型小的整型值首先提升为较大的整数类型。
    • 在条件中,非布尔值转换成布尔类型。
    • 在初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
    • 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
    • 函数调用时也会发生类型转换。

    1)算术转换

    把一种算术类型转换成另一种算术类型叫做 算术转换(arithmetic conversion),其中运算符的运算对象将被转换成最宽的类型。

    整型提升(integral promotions) 负责把小整数类型转换成较大的整数类型。

    如果某个运算符的运算对象类型不一致, 这些运算对象将转换成同一种类型。但是如果某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。

    要想理解算术转换, 办法之一就是研究大量的例子:

    bool flag; 		char cval;
    short sval; 	unsignedshort usval;
    int ival; 		unsigned int uival;
    long lval; 		unsigned long ulval;
    float fval; 	double dval;
    
    3.14159L + 'a';	// 'a'提升成int,然后该int值转换成long double
    dval + ival; 	// ival转换成double
    dval + fval;	// fval转换成double
    ival = dval;	// dval转换成(切除小数部分后)int
    flag = dval;	// 如果dval是0,则flag是false,否则flag是true
    cval + fval;	// cval提升成int,然后该int值转换成float
    sval + cval;	// sval和cval都提升成int
    cval + lval;	// cval转换成long
    ival + ulval;	// ival转换成unsigned long
    usval + ival;	// 根据unsigned short和int所占空间的大小进行提升
    uival + lval;	// 根据unsigned int和long所占空间的大小进行转换

    2)其他隐式类型转换

    • 数组转换成指针:在大多数表达式中,数组名字自动转换成指向数组首元素的指针。
    int ia[10]; 	// 含有10个整数的数组
    int* ip = ia;	// ia转换成指向放组首元素的指针
    • 指针的转换:常量整数值0或字面值

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

    • 转换成布尔类型:任意一种算术类型或指针类型都能转换成布尔类型。如果指针或算术类型的值为0,转换结果是

      false
      ,否则是
      true

    char *cp = get_string();
    if (cp) /* ... */ 		// 如果指针cp不是0,条件为真
    while (*cp) /* ... */ 	// 如果*cp不是空字符,条件为真
    • 转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。
    int i;
    const int &j = i;	// 非常量转换成const int的引用
    const int *p = &i;	// 非常量的地址转换成const的地址
    int &r = j, *q = p;	// 错误:不允许const转换成非常量
    • 类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。
    string s, t = "a value";	// 字符串字而位转换成string类型
    while (cin >> s)			// while的条件部分把cin转换成布尔值

    3)显式转换

    显式类型转换 也叫做 强制类型转换(cast)

    int i, j;
    double slope = i/j;

    虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。建议尽量避免强制类型转换。

    命名的强制类型转换(named cast) 形式如下:

    cast-name<type>(expression);

    其中 type 是转换的目标类型,expression 是要转换的值。如果 type 是引用类型,则转换结果是左值。cast-name 是

    static_cast
    dynamic_cast
    const_cast
    reinterpret_cast
    中的一种,用来指定转换的方式。

    • dynamic_cast
      支持运行时类型识别。
    • 任何具有明确定义的类型转换,只要不包含底层
      const
      ,都能使用
      static_cast
      。当需要把一个较大的算术类型赋值给较小的类型时,
      static cast
      非常有用。
      static cast
      对于编译器无法自动执行的类型转换也非常有用。
    • const_cast
      只能改变运算对象的底层
      const
      ,同时也只有
      const_cast
      能改变表达式的常量属性。
      const_cast
      常常用于函数重载。
    • reinterpret_cast
      通常为运算对象的位模式提供底层上的重新解释。
      reinterpret_cast
      本质上依赖于机器。要想安全地使用
      reinterpret_cast
      必须对涉及的类型和编译器实现转换的过程都非常了解。

    早期版本的C++语言中,显式类型转换包含两种形式:

    type (expression);    // 函数形式的强制类型转换
    (type) expression;    // C语言风格的强制类型转换

    与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了了,容易被看漏,所以一旦转换过程出现问题,追踪起来也更加困难。

    12、运算符优先级表



    参考文章

    • 《C++ Primer》
    • 点赞 5
    • 收藏
    • 分享
    • 文章举报
    我是管小亮 博客专家 发布了216 篇原创文章 · 获赞 4537 · 访问量 66万+ 他的留言板 关注
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: