《C++ Primer中文版》第四章:表达式

目录

第四章:表达式

4.1基础

4.1.1基本概念

4.1.2优先级与结合律

4.1.3求值顺序​

4.2算术运算符

​4.3逻辑和关系运算

4.4赋值运算符

4.5递增和递减运算符​​

4.6成员访问运算符

4.7条件运算符

4.8位运算符

4.9sizeof运算符

4.10逗号运算符

4.11类型转换

4.11.1算术转换

4.11.2其他隐式类型转换

4.11.3显式转换

命名的强制类型转换

dynamic_cast

支持运行时类型识别static_cast

const_cast

reinterpret_cast

4.12运算符优先级

​​​​​


 

第四章:表达式

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

4.1基础

4.1.1基本概念

一元运算符( unary operator) 和二元运算符(binary operator)
1.组合运算符和运算对象
对于含有多个运算符的复杂表达式来说,要想理解它的含义首先要理解运算符的优先级(precedence) 结合律(associativity) 以及运算对象的求值顺序(order of evaluation)


2.运算对象转换

在表达式求值的过程中,运算对象常常由一种类型转换成另外种类型。 小整数类型(如bool、char、short等)通常会被提升、

( promoted)成较大的整数类型。

3.重载运算符
事实上是为已存在的运算符赋予了另外一层含义,所以称之为重载运算符(overloaded operator)
4.左值和右值  c++11

C++的表达式要不然是右值(rvalue, 读作“are-value"),要不然就是左值(lvalue,读作“ell-value")。这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能

归纳:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)
●赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。
●取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值
●内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符 的求值结果都是左值
●内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值

4.1.2优先级与结合律

复合表达式(compound expression)是指含有两个或多个运算符的表达式。
括号无视优先级与结合律

//不同的括号组合导致不同的组合结果
cout << ( 6 + 3 ) *(4/2+2)<< endl;//输出36
cout << (( 6 + 3) * 4)/2+2<< endl;//输出20
cout << 6 + 3 * 4 / ( 2 + 2) << endl;//输出9

优先级与结合律有何影响

int ia[] = {0,2,4,6,8}; // 含有5个整数的数组
int last = *(ia+4) ;//把last初始化成8,也就是ia[4]的值
last = *ia + 4;//last=4,等价于ia[0]+4
#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
#include <cstring>

using namespace std;
int main()
{
    vector<int> vec;
    srand( (unsigned) time (NULL) ) ;
    cout <<"系统自动为向量生成一-组元素......" << endl;
    for(int i = 0; i!= 10; i++)
        vec.push_back (rand()%100) ;
    cout << "生成的向量数据是:"<< endl;
    for (auto c : vec)
      cout<<c<<" ";
    cout << endl ;
    cout << "验证添加的括号是否正确:"<< endl;
    cout << "*vec.begin()的值是: "<< *vec.begin() <<endl;
    cout <<"* (vec. begin())的值是:"<<*(vec.begin()) << endl;
    cout << "*vec.begin()+1的值是:"<< *vec.begin() + 1 <<endl;
    cout << "(* (vec. begin()))+1的值是: "<< (* (vec.begin())) + 1 <<endl;

   return 0;

}

4.1.3求值顺序

求值顺序、优先级、结合律

int i=f1 () * f2() ;
优先级规定,g()的返回值和h()的返回值相乘。
结合律规定,f() 的返回值先与g()和h()的乘积相加,所得结果再与j ()的返回
值相加。
对于这些函数的调用顺序没有明确规定。
如果f、g、h和j是无关函数,它们既不会改变同对象的状态也不执行IO任务,
那么函数的调用顺序不受限制。反之,如果其中某几个函数影响同一对象,
则它是条错误的表达式,将产生未定义的行为。

4.2算术运算符

元负号运算符对运算对象值取负后,返回其(提升后的) 副本:
int i = 1024;
intk=-i;// k是-1024
bool b = true;
bool b2 = -b;// b2是true!

C++语言的早期版本允许结果为负值的商向上或向下取整,C++11新标准则规定商律向0取整(即直接切除小数部分)。
除了-m导致溢出的特殊情况,其他时候(-m)/n和m/(-n)都等于- (m/n),m%(-n)等于m%n, (-m)%n等于-(m%n)。具体示例如下:


4.3逻辑和关系运算符

逻辑与和逻辑或运算符

短路求值( short-circuit evaluation )
●对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。
●对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
 

==相等性运算符的两个运算对象都需要求值,C++没有规定其求值顺序。


#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
#include <cstring>
#include <string>

using namespace std;
int main()
{
    vector<string> text {"ddd" ,"dnakjd.","dada"};
    // s是对常量的引用;元素既没有被拷贝也不会被改变
    for (const auto &s : text) { // 对于text的每个元素
        cout << s; //输出当前元素
        //遇到空字符串或者以句号结束的字符串进行换行
        if (s.empty() || s[s.size() - 1] =='.')
            cout << endl;
        else
            cout <<" "; //否则用空格隔开
    }

   return 0;

}

逻辑非运算符

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

关系运算符

vs

相等性测试与布尔字面值

 

4.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 sl, s2;
s1=s2="OK"; //字符串字面值"OK"转换成string对象

赋值运算优先级较低

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

如果不加括号的话含义会有很大变化,比较运算符!=的运算对象将是get_value函
数的返回值及42,比较的结果不论真假将以布尔值的形式赋值给i,这显然不是我们期
望的结果。

切勿混淆相等运算符和赋值运算符
复合赋值运算符

int sum = 0;
//计算从1到10(包含10在内)的和
for (int val = 1; val <= 10; ++val)
sum += val;   //等价于sum=sum+val

4.5递增和递减运算符

在一条语句中混用解引用和递增运算符

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)
cout << * pbeg++ << endl; // 输出当前值并将pbeg向前移动一个元素

运算对象可按任意顺序求值

for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
    *it = toupper (*it) ;//将当前字符改成大写形式
//该循环的行为是未定义的!
while (beg != s.end () && !isspace (*beg) )
*beg = toupper (*beg++); // 错误:该赋值语句未定义


*beg = toupper (*beg) ;//如果先求左侧的值
*(beg + 1)= toupper (*beg) ;//如果先求右侧的值

4.6成员访问运算符

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的成员

箭头运算符作用于一个指针类型的运算对象,结果是一一个左值。

点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。
vector<string> :: iterator     
      

iter++->empty() ;等价于(*iter++) .empty();含义是解引用迭代器当前位置的对象内容,得到一个字符串,判断该字符串是否为空,然后把迭代器向后移动一位。
 

4.7条件运算符

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 ? "fail" : "pass"; //错误:试图比较cout和60

等价于

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

4.8位运算符

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

char类型的运算对象首先提升成int类型,提升时运算对象原来的位保持不变,往高位(high order position)添加0即可。因此在本例中,首先将bits提升成int类型,增加24个高位0,随后将提升后的值逐位求反。

unsigned long quiz1= 0;//我们把这个值当成是位的集合来使用
1UL << 27//生成一个值,该值只有第27位为1
quiz1 |= 1UL<< 27; //表示学生27通过了测验
//quiz1 = quiz1 | 1UL << 27; //等价于quiz1  |=1UL<<27;
quizl &=~(1UL << 27) ;//学生27没有通过测验
bool status = quiz1&(1UL<<27);//学生27是否通过了测验?

移位运算符(又叫I0运算符)满足左结合律

cout << "hi"<<"there" << endl ;

的执行过程实际上等同于

( (cout << "hi") <<”there" ) << endl;

移位运算符的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。因此在一次使用多个运算符时,有必要在适当的地方加上括号使其满足我们的要求。

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


(cout << 10) < 42;也就是“把数字10写到cout,然后将结果(即cout)与42进行比较”。

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

4.9sizeof运算符

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(ia) /sizeof(*ia)返回ia的元素数量
constexpr size t_sz = sizeof (ia) /sizeof (*ia) ;
int arr2[sz];
//正确: sizeof返回一个常量表达式,

4.10逗号运算符

逗号运算符(comma operator)

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;

4.11类型转换

如果两种类型可以相互转换(conversion),那么它们就是关联的。

int ival = 3.541 + 3; //编译器可能会警告该运算损失了精度

隐式转换( implicit conversion)

4.11.1算术转换

( arithmetic conversion)的含义是把一种算 术类型转换成另外一种算术类型

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

 

无符号类型的运算对象

bool     flag;
char     cval ;
short    sval ;
unsigned short 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所占空间的大小进行转换

4.11.2其他隐式类型转换

1.数组转换成指针:

int ia[10] ;
//含有10个整数的数组
int* ip = ia;  //ia转换成指向数组首元素的指针

当数组被用作decltype 关键字的参数,或者作为取地址符(&)、 sizeof 及typeid等运算符的运算对象时,上述转换不会发生。 同样的,如果用一个引用来初始化数组,上述转换也不会发生。当在表达式中使用函数类型时会发生类似的指针转换。
 

2.指针的转换:

C++还规定 了几种其他的指针转换方式,包括常量整数值0或者字面值nullptr能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*。在有继承关系的类型间还有另外一种指针转换的方式。

3.转换成布尔类型:

存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果是false;否则转换结果是true:

char *cp = get_string() ;
if (cp) /* ...*/ //如果指针cp不是0,条件为真
while (*cp) /* ...*/ //如果*cp不是空字符,条件为真

4转换成常量:

允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果T是一种类型,我们就能将指向T的指针或引用分别转换成指向const T 的指针或引用:

int i;
const int &j = i; // 非常量转换成 const int的引用
const int *p = &i; //非常量的地址转换成const的地址
int &r=j, *q= p; //错误:不允许const转换成非常量

相反的转换并不存在,因为它试图删除掉底层const。
5.类类型定义的转换:

类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。如果同时提出多个转换请求,这些请求将被拒绝。
我们之前的程序已经使用过类类型转换:一处是在需要标准库string类型的地方使用C风格字符串;另一处是在条件部分读入istream: 

string s,t = "a value"; //字符串字面值转换成string类型
while (cin >> s) //while的条件部分把cin转换成布尔值

条件(cin>>s)读入cin的内容并将cin作为其求值结果。条件部分本来需要一个布尔类型的值,但是这里实际检查的是istream类型的值。幸好,I0 库定义了从istream向布尔值转换的规则,根据这一规则,cin自动地转换成布尔值。所得的布尔值到底是什么由输入流的状态决定,如果最后一次读入成功,转换得到的布尔值是true;相反,如果最后一次读入不成功,转换得到的布尔值是false。

4.11.3显式转换

int i, j;
double slope = i/j;
就要使用某种方法将i和/或j显式地转换成double,这种方法称作: 强制类型转换( cast)。

命名的强制类型转换


dynamic_cast

支持运行时类型识别

static_cast

任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_ cast。例如,通过将一个运算对象强制转换成double类型就能使表达式执行浮点数除法:

//进行强制类型转换以便执行浮点数除法
double slope = static_cast<double> (j) / i;

当需要把一个较大的算术类型赋值给较小的类型时,static_cast 非常有用。此时,强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息;但是当我们执行了显式的类型转换后,警告信息就会被关闭了。

void* p = &d;
//正确:任何非常量对象的地址都能存入void*
//正确:将void*转换回初始的指针类型
double *dp = static_cast<double*>(p) ;

当我们把指针存放在void*中,并且使用static_ cast 将其强制转换回原来的类型时,应该确保指针的值保持不变。也就是说,强制转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义的后果。

const_cast

const_cast 只能改变运算对象的底层const
const char *pc;
char *p = const_cast<char*>(pc); //正确:但是通过p写值是未定义的行为

对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质(cast away the const)”。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。然而如果对象是一个常量, 再使用const_cast 执行写操作就会产生未定义的后果。

const char *cp;//错误: static_cast不能转换掉const性质
char *q = static_cast<char*> (cp) ;
static_cast<string> (cp) ;//正确:字符串字面值转换成string类型
const_cast<string>(cp) ;//错误: const_cast 只改变常量属性

const_cast常常用于有函数重载的上下文中

reinterpret_cast

reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。

int *ip;
char *pc = reinterpret_cast<char*>(ip) ;

我们必须牢记pc所指的真实对象是一个int而非字符,如果把pc当成普通的字符指针使用就可能在运行时发生错误。例如:
string str (pc) ;
可能导致异常的运行时行为。
使用reinterpret cast 是非常危险的,用pc初始化str的例子很好地证明了这一点。其中的关键问题是类型改变了,但编译器没有给出任何警告或者错误的提示信息。当我们用一个int的地址初始化pc时,由于显式地声称这种转换合法,所以编译器不会
发出任何警告或错误信息。接下来再使用pc时就会认定它的值是char*类型,编译器没法知道它实际存放的是指向int的指针。最终的结果就是,在上面的例子中虽然用pc初始化str没什么实际意义,甚至还可能引发更糟糕的后果,但仅从语法上而言这种操作
无可指摘。查找这类问题的原因非常困难,如果将ip强制转换成pc的语句和用pc初始化string对象的语句分属不同文件就更是如此。

旧式的强制类型转换

 

4.12运算符优先级

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值