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、容器初探
    3、二叉树:先序,中序,后序循环遍历详解
    Hebbian Learning Rule
    论文笔记 Weakly-Supervised Spatial Context Networks
    在Caffe添加Python layer详细步骤
    论文笔记 Learning to Compare Image Patches via Convolutional Neural Networks
    Deconvolution 反卷积理解
    论文笔记 Feature Pyramid Networks for Object Detection
    Caffe2 初识
    论文笔记 Densely Connected Convolutional Networks
  • 原文地址:https://www.cnblogs.com/pluse/p/5106696.html
Copyright © 2011-2022 走看看