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。

原文地址:https://www.cnblogs.com/pluse/p/5106696.html