zoukankan      html  css  js  c++  java
  • C++ Primer 5th 第4章 表达式

    表达式是运算对象及运算符组成的式子,表达式求值将得到一个结果,单独的变量或者字面值也算表达式,结果是其本身。

    运算符分为:一元运算符、二元运算符、三元运算符。一元即一个作用对象,二元两个作用对象,以此类推。

    函数调用是一种特殊的运算符(此运算符无作用对象数量限制)

    它的运算形式为:

    primary-expression ( expression-list )

    对于expression-list来说,其数量不限,因此说函数调用运算符对运算对象的数量没有限制。

    
    

    左值和右值:这两个名词是从C语言中继承得到的,在C++中,当一个对象被用作右值时,使用的是它的值(内容),当对象被用作左值的时候,用的是它的身份(在内存中的位置)。通俗来理解的话,例如

    int a=5;//这句代码声明定义了一个int变量,变量值为5,其执行的动作是在内存中划出地址为0x7fff0444—0x7fff0447这4个字节来存储int变量a,并且存储了一个二进制的5(101)

    当a被用作右值时,例如

    int b=a;//这里a放在右侧,使用了a的右值,即a的值5,因此b=5。

    而当给变量a赋值时,形如

    a=7;//这时a放在左侧,被用作左值,使用的不再是它的值了,使用的是它的身份,即使用这个地址0x7fff0444—0x7fff0447。

    以上int大小和内存地址都是为说明而假定的。

    不同的运算符在表达式中的行为表现和返回值各不相同,有的要求左侧运算对象,有的要求右侧运算对象,例如取地址符&,要求右侧运算对象,即对象在右边:&a;(取变量a的地址),而后置递增++则要求运算对象在左边:p++。返回值也有差异,有的返回左值,有的返回右值。哪些返回左值,哪些返回右值,需要自己留心一下,大部分返回的都是右值。

    左值可以当左值用,也可以当右值用,但是右值只能用作右值。

    运算符不仅要求左侧或右侧运算对象,也会要求左值对象或者右值对象,比如赋值运算符“=”,它的要求是左侧必须是左值,右侧左值和右值皆可,运算完毕后返回一个左值。

    复合表达式则是含有多个运算符的组合,在含有多个运算符和运算对象时,具体要视结合情况、优先级以及先后顺序综合而定。

    这三个名词之间并没有关系,优先级不影响结合,结合律也与求值顺序无关。

    优先级是在复合表达式中存在多个运算符时,哪个部分先组合。

    而结合律是该运算符是将左侧还是右侧视为一个整体,注意是视为一个整体,不是决定求值顺序。

     优先级和结合律共同决定运算对象的组合方式,也即它们决定表达式中每个运算符对应的运算对象怎么组合。

    而求值顺序则是同一个运算符作用的2个或多个对象到底哪个先求值。例如g() * f(),g()和f()的返回值相乘,但是先调用g()还是先调用f()这是求值顺序的问题,大多数运算符对这个并没有规定。

    只有4个运算符规定了求值的次序:

    1. 逻辑与&&,先求左侧对象,左侧为真,再求右侧

    2. 逻辑或 || ,先求左侧对象,左侧为假,再求右侧

    3. 条件运算符 ? :

    4. 逗号运算符 ,

    逗号运算符具有从左向右的关联性。 由逗号分隔的两个表达式将从左向右进行计算。 
    在某些上下文(如函数参数列表)中,逗号可用作分隔符。 不要将该逗号用作分隔符与将其用作运算符的情况混淆;这两种用法完全不同。

    额外说明一点:当优先级相同时,有些表达式求值顺序不一定,此时产生的结果也就不确定了,对于这种情况一定要予以避免。

    优先级并没有规定运算对象的求值顺序,对于表达式行为不可预知的程序,无论编译器生成什么代码,程序都是错误的。

    例如对几个函数进行运算:g( )*f( )/g( ),当f( )和g( )同时对一个全局变量作出改变时,就有可能导致不确定的结果。

    算术运算符的运算对象和求值结果都是右值;

    赋值运算符的结果是左侧对象,即返回左侧对象,且是个左值,另外,赋值运算符是右结合,从右往左运算。

    递增和递减运算符有前置和后置两个版本,后置版本优先级较高。

    前置版本对对象作出改变后,返回改变后的对象本身,是一个左值。而后置版本则是改变对象后,返回对象改变前的副本,是一个右值。

    在实际使用当中,除非有明确的需求,否则尽量使用前置。例如for循环的头部,for(int i=0;i<1000;++i),这里一定不要用后置版本的递增运算。因为这里后置版本需要将原始值存储下来以便返回修改之前的内容,但在循环这里并没有使用,造成浪费,在循环特别大的时候,或者某些复杂的迭代器类型,这种额外的消耗是非常大的。使用后置版本是很糟糕的代码,因此能使用前置时一定要用前置。

    位运算符是一种专门用于整型的运算符,即只能用于带符号或无符号的char,short,int与long类型。不能用于指针或者double类型,另外当位运算符用于“小整型”时,会进行提升操作,即char,short类型会被提升成int类型。

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

    该运算符的运算形式有2种

    sizeof (type)

    sizeof expr

    sizeof并不会对其后的运算对象进行计算,例如,其后如果是一个不合法的指针,不会对该指针进行运算(解引用),因为sizeof运算符不解引用也能获取所指对象的类型。

    对数组执行sizeof运算,得到的是整个数组的大小,不会像大多数情况那样将数组名转化为指针。

    对string类型或vector类型的对象直接执行sizeof运算,则返回的是固定的大小,而不是所有元素所占空间总和的大小,要计算string类型或vector类型的对象大小需要使用其自带的size方法与元素类型的乘积来计算。如下:

    #include <iostream>
    #include <vector>
    
    int main()
    {
        std::vector<int> v = {1, 2, 3, 4, 5};
        auto n1 = sizeof (* v.begin());        //使用迭代器获取单个元素大小
        auto n2 = v.size();                    //获取总元素个数
        std::cout << n1 * n2;                  //计算实际大小
        return 0;
    }

    最后,sizeof返回的值是一个常量表达式。

    C++11的类型转换有显式和隐式之分,也有旧式和新式之分。

    显式转换就是明确指出将某个类型转换到另一种类型,转换的前提条件是两种类型之间可以相互转换,比如int转换为double,而指针和int就无法相互转换

    隐式转换则是一个表达式中含有不同类型的对象,计算时,程序将自动的去转换。

    新式转换是使用命名的强制类型转换,目前有static_cast、const_cast和reinterpret_cast。

    static_cast可以在不同类型之间转换,也即改变表达式的类型。const_cast只能用于const和非const转换,即对表达式添加或者去除const属性,但是不能改变表达式的类型。

    旧式转换则是从C语言继承而来的。形如int(n)或者(int)n 

    练习4.1:表达式 5 + 10 * 20 / 2 的求值结果是多少?

    105

    练习4.2:根据4.12节中的表,在下述表达式的合理位置添加括号,使得添加括号后运算对象的组合顺序与添加括号前一致。

    (a) *vec.begin()        //*(vec.begin())
    (b) *vec.begin() + 1      //(*(vec.begin())) + 1

    练习4.3:C++语言没有明确规定大多数二元运算符的求值顺序,给编译器优化留下了余地。这种策略实际上是在代码生成效率和程序潜在缺陷之间进行了权衡,你认为这可以接受吗?请说出你的理由。

    不可以接受,过度散漫。

     

    练习4.4:在下面的表达式中添加括号,说明其求值的过程及最终结果。编写程序编译该(不加括号的)表达式并输出结果验证之前的推断。

    12 / 3 * 4 + 5 * 15 + 24 % 4 / 2        //91

    练习4.5:写出下列表达式的求值结果。

    (a) -30 * 3 + 21 / 5    //-86
    (b) -30 + 3 * 21 / 5    //-18
    (c) 30 / 3 * 21 % 5     //0
    (d) -30 / 3 * 21 % 4    //-2


    练习4.6:写出一条表达式用于确定一个整数是奇数还是偶数。

    -1 % 2 == 0

    练习4.7:溢出是何含义?写出三条将导致溢出的表达式

    溢出是某个对象的大小超过某种类型的最大值或者最小值,导致无法存储。

    char c = 999;
    short s = 655360;
    unsigned ui = -1;

    练习4.8:说明在逻辑与、逻辑或及相等性运算符中运算对象求值的顺序。

    逻辑与先求左侧值,左侧为真,则继续求右侧值。

    逻辑或先求左侧值,左侧为假,则继续求右侧值

    相等性运算符没有规定求值顺序。

    练习4.9:解释在下面的if语句中条件部分的判断过程。

    const char *cp = "Hello World";
    if (cp && *cp)

    首先判断cp是否为空指针,如果不是,再解引用cp,看是否为空字符串,不为空则执行if的语句

    练习4.10:为while 循环写一个条件,使其从标准输入中读取整数,遇到 42 时停止。

    int i;
    while (cin >> i && i != 42)

      

    练习4.11:书写一条表达式用于测试4个值a、b、c、d的关系,确保a大于b、b大于c、c大于d。

    a > b && b > c && c > d

    练习4.12:假设i、j 和k 是三个整数,说明表达式 i != j < k 的含义。

     先判断j是否小于k,若小于返回true,否则返回false,然后将返回的bool值提升为0或1,再与i的值进行比较,比较后返回一个布尔值。

    练习4.13:在下述语句中,当赋值完成后 i 和 d 的值分别是多少?

    int i; double d;
    (a) d = i = 3.5; // i = 3, d = 3.0
    (b) i = d = 3.5; // d = 3.5, i = 3

    练习4.14执行下述 if 语句后将发生什么情况?

    if (42 = i) // 编译错误,赋值运算符左侧要求是非const左值,字面值是右值。
    if (i = 42) // 编译成功,若i是非const的,则恒成立

    练习4.15下面的赋值是非法的,为什么?应该如何修改?

    double dval; int ival; int *pi;
    dval = ival = pi = 0;

    赋值运算符是右结合的,且返回赋值运算符左侧的对象,因此pi优先运算,赋值完毕后返回一个空指针,再去对int类型的ival进行赋值,由于指针类型无法与整型进行互相转换,因此赋值非法,应该修改为 pi=0;dval=ival=0;

    练习4.16:尽管下面的语句合法,但它们实际执行的行为可能和预期并不一样,为什么?应该如何修改?

    (a) if (p = getPtr() != 0)
    (b) if (i = 1024)

    若p和i不是const变量,则if语句的条件恒成立,应该修改为

    (a) if ((p=getPtr()) != 0)
    (b) if (i == 1024)

    练习4.17:说明前置递增运算符和后置递增运算符的区别。

    前置版本对对象作出改变后,返回改变后的对象本身,是一个左值。而后置版本则是改变对象后,返回对象改变前的副本,是一个右值。

    练习4.18:如果第132页那个输出vector对象元素的while循环使用前置递增运算符,将得到什么结果?

    开头从第二个元素开始解引用,末尾解引用vector最后一个元素之后的尾后迭代器,将会导致溢出。

    练习4.19假设 ptr 的类型是指向 int 的指针、vec的类型是vector、ival的类型是int,说明下面的表达式是何含义?如果有表达式不正确,为什么?应该如何修改?

    (a) ptr != 0 && *ptr++     
    (b) ival++ && ival
    (c) vec[ival++] <= vec[ival]

    (a) 先判断ptr是否为空指针,若不为空,先将ptr递增使其指向下一个int元素,然后对递增前的ptr解引用,并判断解引用后的值是否为0

    (b) 先将ival的值自增加1,然后判断自增前的值是否为0,不为0则再判断自增后的值是否为0

    (c) C++没有规定<=运算符的求值顺序,应修改为vec[ival]<=vec[ival+1]

    练习4.20:假设iter的类型是vector<string>::iterator, 说明下面的表达式是否合法。如果合法,表达式的含义是什么?如果不合法,错在何处?

    (a) *iter++;      //合法,将iter指向下一个元素,解引用移动前的迭代器
    (b) (*iter)++;     //不合法,字符串没有自增运算
    (c) *iter.empty();  //不合法,访问iter的成员empty,但iter是个迭代器,没有empty成员
    (d) iter->empty();  //合法,检查元素是否为空字符串
    (e) ++*iter;      //不合法,字符串没有自增运算
    (f) iter++->empty(); //合法,将iter指向下一个元素,检查移动前的元素是否为空字符串

    练习4.21:编写一段程序,使用条件运算符从 vector 中找到哪些元素的值是奇数,然后将这些奇数值翻倍。

    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    int main()
    {
        vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};
        for (auto &i : v)
        {
            cout << ( (i % 2 == 0) ? i : i * 2  ) << '	';
    
        }
        return 0;
    }

    练习4.22:本节的示例程序将成绩划分为high pass、pass和fail三种,扩展该程序使其进一步将 60 分到 75 分之间的成绩设定为low pass。要求程序包含两个版本:一个版本只使用条件运算符;另一个版本使用1个或多个if语句。哪个版本的程序更容易理解呢?为什么?

    #include <iostream>
    
    using namespace std;
    
    int main()
    {
       int grade; (grade
    > 90) ? "high pass" : (grade > 60 && grade < 75) ? "low pass" : (grade > 60) ? "pass" : "fail"; if (grade > 90) cout << "high pass "; if (grade > 75) cout << "pass "; if (grade > 60 ) cout << "low pass "; else cout << "fail "; return 0; }

    if语句版本容易理解。条件运算符嵌套层数过多,代码难以阅读,if语句条理清晰,容易理解。

     

    练习4.23:因为运算符的优先级问题,下面这条表达式无法通过编译。根据4.12节中的表(第147页)指出它的问题在哪里?应该如何修改?

    string s = "word";
    string pl = s + s[s.size() - 1] == 's' ? "" : "s" ;

    加运算符优先级比相等运算符优先级高,导致无法编译通过。

    应修改为

    string pl = s + (s[s.size() - 1] == 's' ? "" : "s") ;

    练习4.24:本节的示例程序将成绩划分为 high pass、pass和fail三种,它的依据是条件运算符满足右结合律。假如条件运算符满足的是左结合律,求值的过程将是怎样的?

    如果条件运算符满足的是左结合律。
    示例程序将会被解读成 finalgrade = ((grade > 90) ? "high pass" : (grade < 60)) ? "fail" : "pass"; 先判断grade > 90是否成立,根据返回值来执行"high pass"或者grade < 60, 然后将"high pass"或grade  < 60中的一个作为最后的条件来判断。

    练习4.25:如果一台机器上int占32位、char占8位,用的是Latin-1字符集,其中字符'q'的二进制形式是01110001,那么表达式'q' << 6的值是什么?

    首先提升为整型,然后对提升后整型左移6为,得到:00000000 00000000 00011100 01000000

    练习4.26:在本节关于测验成绩的例子中,如果使用unsigned int作为quiz1的类型会发生什么情况?

    因为unsigned int标准规定最小为16位,因此有可能导致无法存储,产生未定义的行为。

    练习4.27:下列表达式的结果是什么?

    unsigned long ul1 = 3, ul2 = 7;
    (a) ul1 & ul2   //返回3
    (b) ul1 | ul2    //返回7
    (c) ul1 && ul2  //返回bool值true
    (d) ul1 || ul2   //返回bool值true

    练习4.28:编写一段程序,输出每一种内置类型所占空间的大小。

    #include <iostream>
    
    using namespace std;
    
    int main()
    {
        cout << sizeof(bool) << '
    ';
        cout << sizeof(char) << '
    ';
        cout << sizeof(wchar_t) << '
    ';
        cout << sizeof(char16_t) << '
    ';
        cout << sizeof(char32_t) << '
    ';
        cout << sizeof(short) << '
    ';
        cout << sizeof(int) << '
    ';
        cout << sizeof(long) << '
    ';
        cout << sizeof(long long) << '
    ';
        cout << sizeof(float) << '
    ';
        cout << sizeof(double) << '
    ';
        cout << sizeof(long double) << '
    ';
        return 0;
    }

    练习4.29:推断下面代码的输出结果并说明理由。实际运行这段程序,结果和你想象的一样吗?如不一样,为什么?

    int x[10];   int *p = x;
    cout << sizeof(x)/sizeof(*x) << endl;  //输出10,sizeof计算数组名时不转换为指针,40/4=10
    cout << sizeof(p)/sizeof(*p) << endl;  //输出2,64位系统指针是8字节,int是4字节,8/4=2

    练习4.30:根据4.12节中的表(第147页),在下述表达式的适当位置加上括号,使得加上括号之后表达式的含义与原来的含义相同。

    (a) sizeof x + y         //(sizeof x) + y
    (b) sizeof p->mem[i]     //sizeof ((p->men)[i])
    (c) sizeof a < b         //(sizeof a) < b
    (d) sizeof f()           //sizeof (f())

    练习4.31:本节的程序使用了前置版本的递增运算符和递减运算符,解释为什么要用前置版本而不用后置版本。要想使用后置版本的递增递减运算符需要做哪些改动?使用后置版本重写本节的程序。

    本节的程序中使用前置版本或者后置版本的效果是一样的,但是后置版本还需要保留改变前的变量并进行返回,这是额外的无任何意义的操作,因此无需使用后置版本。这里前置版本和后置版本的效果是一样的,无需任何改写。

    练习4.32:解释下面这个循环的含义。

    constexpr int size = 5;
    int ia[size] = {1, 2, 3, 4, 5};
    for (int *ptr = ia, ix = 0; ix != size && ptr != ia + size; ++ix, ++ptr)
    {
        /* ... */
    }

    这个循环在遍历数组 ia,ix和ptr都是用于循环是否结束的判断依据。

    练习4.33:根据4.12节中的表(第147页)说明下面这条表达式的含义。

    someValue ? ++x, ++y : --x, --y  //someValue为真,则分别递增x和y,然后返回y,否则递减x,然后返回x,再x,--y,先丢弃x,然后递减y,返回y

    练习4.34:根据本节给出的变量定义,说明在下面的表达式中将发生什么样的类型转换:

    (a) if (fval)
    (b) dval = fval + ival;
    (c) dval + ival * cval;

    需要注意每种运算符遵循的是左结合律还是右结合律。

     (a) bool转换

     (b) ival转换为float,计算完的结果转换为double

     (c) cval转换为int,计算完毕后转换为double

    练习4.35:假设有如下的定义,

    char cval;
    int ival;
    unsigned int ui;
    float fval;
    double dval;

    请回答在下面的表达式中发生了隐式类型转换吗?如果有,指出来。

    (a) cval = 'a' + 3;
    (b) fval = ui - ival * 1.0;
    (c) dval = ui * fval;
    (d) cval = ival + fval + dval;

    (a)'a'转换为int,计算完后int转换为char

    (b)ival转换为double,ui转换为double,计算完后double转换为float

    (c)ui转换为float,计算完后float转换为double

    (d)ival转换为float,计算完后float转换为double,最后计算完后double转换为char

    练习4.36:假设i是int类型,d是double类型,书写表达式 i*=d 使其执行整数类型的乘法而非浮点类型的乘法。

    i *= int(d);  //或者 i*= static_cast<int>(d);

    练习4.37:用命名的强制类型转换改写下列旧式的转换语句。

    int i; double d; const string *ps; char *pc; void *pv;
    (a) pv = (void*)ps;  // pv = static_cast<void*>(const_cast<string*>(ps));
    (b) i = int(*pc);   // i = static_cast<int>(*pc)
    (c) pv = &d;      // pv = static_cast<void*>(&d)
    (d) pc = (char*)pv;  // pc = static_cast<char*>(pv)

    练习4.38:说明下面这条表达式的含义。

    double slope = static_cast<double>(j/i);

    将 j/i 的结果转换为 double,然后赋给slope。

  • 相关阅读:
    新内核2.6.30编译完之后在目标板上看不到ttyS1
    使用memset、memcpy等函数需要包含string.h而不是strings.h
    软件模式之原则设计
    由编译错误看L. lxxxx的正确位置
    设计模式之策略模式
    make menuconfig提示'make menuconfig' requires the ncurses libraries.
    抽取界面用 XML 和 XSL 构建有良好适应性的 Web 应用前端
    .Net框架下的XSLT转换技术简介
    派生和继承
    UML 类图介绍
  • 原文地址:https://www.cnblogs.com/pluse/p/5106696.html
Copyright © 2011-2022 走看看