C++primer笔记——第四章【表达式】

【第四章】 表达式


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


4.1.1 基本概念
1、作用于n个运算对象的运算符是n元运算符。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。


运算对象转换:
2、很多时候即使运算对象的类型不同也没有关系,只要它们能被转换成同一种类型即可。

3、小整数类型(bool、char、short)通常会被提升成较大的整数类型,主要是int

重载运算符:
4、当运算符作用于类类型的运算对象时,用户可以自行定义其含义。因为这种自定义的过程实际上是为已存在的运算符赋予了另一层含义,所以称为重载运算符。

5、使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的。但是运算对象的个数,运算符的优先级和结合律是无法改变的。

左值和右值:
6、C++的表达式不是左值就是右值。可以这么记,左值可以位于赋值语句的左侧,右值则不能。

7、一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。

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

9、一个重要的原则:在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容。
已经有几种熟悉的运算符需要用到左值:
1)赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。
2)取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
3)内置解引用运算符,下标运算符,迭代器解引用运算符,string和vector的下标运算符的求值结果都是左值。
4)内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。

4.1.2 优先级和结合律
10、复合表达式是指含有两个或多个运算符的表达式

11、如果运算符的优先级相同,将按照从左向右的顺序组合运算对象。

4.1.3 求值顺序
int i = f1() * f2(); // 无法知道f1在f2之前调用还是f2在f1之前调用。

12、对于那些没有指定执行顺序的运算符,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
cout << i << " " << ++i <<endl; // 编译器可能先+在求i,也可能先求i在+

13、有四种运算符明确规定了运算对象的求值顺序。逻辑与&&,逻辑或||,条件运算符?:,逗号运算符。它们规定先求左侧运算对象的值,只有当左侧运算对象的值为真才继续求右侧运算对象的值


4.2 算术运算符
1、一元运算符的优先级最高,接下来是乘除法,最低的是加减法。

2、当一元正号运算符作用于一个指针或者算术值时,返回运算对象值的一个(提升后的)副本

3、运算符%称为取余或取模运算符,负责计算两个整数相除所得的余数,参与取余运算的运算对象必须是整数类型。
(-m)%n = -(m%n)    m%(-n) = m%n

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

逻辑与和逻辑或运算符:短路求值!!


逻辑非运算符:!将运算对象的值取反后返回


关系运算符:
2、因为关系运算符的求值结果是bool值,所以不能将几个关系运算符连在一起。

3、< <= > >= 的优先级高于 == !=  如i != j<k;

4.4 赋值运算符
1、赋值运算符的左侧运算对象必须是一个可修改的左值。
i + j = k; // 错误,算术表达式是右值
ci = k; // 错误,ci是常量(不可修改的)左值

2、赋值运算的结果是它的左侧运算对象,并且是一个左值。相应的,结果的类型就是左侧运算对象的类型。如果两侧运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型

复制运算满足右结合律:
3、对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同,或者可由右边对象的类型转换得到
int ival, *pval;
ival = pval = 0; // 错误,不能把指针的值赋给int

4.5 递增和递减运算符
1、递增和递减运算符有两种形式:前置版本和后置版本
1)前置版本首先将运算对象+1或-1,然后将改变后的对象作为求值结果
2)后置版本也将运算对象+1或-1,但是求值结果是运算对象改变之前那个值的副本
这两种运算符必须作用于左值运算对象。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。

在一条语句中混用解引用和递增运算符:
2、如果想在一条复合表达式中既将变量+1或-1又能使用它原来的值,这时就可以使用递增和递减运算符的后置版本
auto pbeg = v.begin();
while(pbeg != v.end() && *pbeg >=0)
cout<< *pbeg++ <<endl; // 输出当前值并将pbeg向前移动一个元素
后置递增运算符的优先级要高于解引用运算符,因此*pbeg++等价于*(pbeg++)。
pbeg++把pbeg的值+1,然后返回pbeg的初始值的副本作为其求值结果,此时解引用运算符的运算对象是pbeg未增加之前的值。
可以这么理解,这是基于一个事实的:如果返回的是加1之后的值,解引用该值将产生错误。
不但无法输出第一个元素,而且程序会试图解引用一个根本不存在的元素。

4.6 成员访问运算符
1、点运算符和箭头运算符都可用于访问成员。ptr->mem  等价于  (*ptr).mem

2、解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加括号。
*p.size(); // 错误,p是一个指针,没有名为size的成员
箭头运算符作用域一个指针类型的运算对象,结果是一个左值。
点运算符分两种情况:如果成员所属对象是左值,那么结果是左值。否则是右值

4.7 条件运算符
cond? expr1 : expr2;
两个表达式类型相同或可能转换为某个公共类型的表达式。条件为真就对1求值并返回该值,否则返回2

1、条件运算符满足右结合律,意味着运算对象一般按照从右向左的顺序组合

2、条件运算符的优先级很低,因此当一条长表达式中嵌套了条件运算符子表达式时,一般需要在两端加括号

4.8 位运算符
1、位运算符作用于整型的运算对象,并把运算对象看成是二进制位的集合

2、强烈建议将位运算符用于处理无符号类型

移位运算符:
首先令左侧运算对象的内容按照右侧运算对象的要求移动指定位数,然后将经过移动的(可能还进行了提升)左侧运算对象的拷贝作为求值结果。
其中,右侧运算对象一定不能为负,而且值必须严格小于结果的位数,否则就会产生未定义的行为。二进制位左移或右移,移出边界之外的位就被舍弃掉了
unsigned char bits = 0233;  // 10 011 011
bits << 8; // bits提升成int类型(32位),然后向左移动8位


位求反运算符~:将运算对象逐位求反后生成一个新值
位与&
位或|
位异或^运算符:相异为1,相同为0


使用位运算符:
unsigned long quiz1 = 0; // 定义quiz1的类型是unsigned long,这样该变量在任何机器上都将至少拥有32位,给它赋一个明确的初始值,使得它每一位在开始时都有统一固定的值
使用左移运算符和一个unsigned long类型的整数字面值1就能得到一个表示学生27通过了测验的数值
1UL << 27; // 生成一个值,该值只有第27位为1
quiz1 |= 1UL << 27; // 表示学生27通过了测验

4.9 sizeof运算符
1、sizeof运算符返回一条表达式或一个类型名字所占的字节数。

2、sizeof运算符满足右结合律,所得的值是一个size_t类型的常量表达式。运算符的运算对象有两种形式:
1)sizeof (type)
2)sizeof expr
在第二种形式中,sizeof返回的是表达式结果类型的大小。且不实际计算其运算对象的值
MyClass data, *p;
sizeof(MyClass); // 存储MyClass类型的对象所占空间大小
sizeof data; // data的类型的大小,即sizeof(MyClass)
sizeof p; // 指针所占空间的大小
sizeof *p; // p所指类型的空间大小,即sizeof(MyClass)
sizeof data.x; // MyClass的x成员对应类型的大小
sizeof MyClass::x; // 另一种获取x大小的方式
看*p这个,首先,因为sizeof满足右结合律而且与*运算符优先级一样,所以表达式从右向左顺序组合。即等价于sizeof(*p)
其次,因为sizeof不会实际求运算对象的值,所以即使p是一个无效指针,也不会有什么影响

3、sizeof运算符的结果部分地依赖于其作用的类型:
1)对char或者类型为char的表达式执行sizeof运算,结果为1
2)对引用类型执行sizeof运算得到被引用对象所占空间大小
3)对指针执行sizeof运算得到指针本身所占空间的大小!!!!
4)对解引用执行sizeof运算得到指针指向的对象所占空间的大小,指针不需要是有效的
5)对数组执行sizeof运算得到整个数组所占空间的大小,注意,sizeof运算不会把数组转换成指针来处理
6)对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。

4、因为执行sizeof运算能得到整个数组的大小,所以可以用数组的大小除以单个元素的大小得到数组中元素的个数
constexpr size_t sz = sizeof(ia) / sizeof(*ia);
int arr2[sz];

5、sizeof(short)=2;
sizeof(int)=4;
sizeof(float)=4;
sizeof(double)=8;

6、逗号运算符的真正结果是右侧表达式的值

4.11 类型转换
1、如果两种类型可以相互转换,那么它们就是关联的
int ival = 3.44 + 3;
上述类型转换是自动执行的,被称为隐式转换。

何时发生隐式转换:
1)比int类型小的整型值首先提升为较大的整数类型
2)在条件中,非bool转换成bool
3)初始化过程中,初始值转换成变量的类型
4)在赋值语句中,右侧运算对象转换成左侧运算对象的类型
5)如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型
6)函数调用时也会发生类型转换
例子见P143

4.11.2 其他隐式转换类型
除了算术转换之外还有几种隐式转换,包括以下几种:
1)数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针。
int ia[10];
int *ip = ia;
当数组被用作decltype关键字的参数,或者作为取地址&,sizeof即typeid等运算符的运算对象时,上述转换不会发生!!!
同样的,如果用一个引用来初始化数组,上述转换也不会发生。
2)指针的转换:常量整数值0或者字面值nullptr能转换成任意指针类型。指向任意非常量的指针能转换成void *。指向任意对象的指针能转换成const void*
3)转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样
也就是说,如果T是一种类型,就能将指向T的指针或引用分别转换成指向const T的指针或引用。
相反的转换不存在,因为它试图删除底层const

4.11.3 显示转换
命名的强制类型转换:
具有如下形式 cast-name<type>(expression);
type是转换的目标类型而expression是要转换的值。如果type是 引用类型,则结果是左值。
cast-name是以下的几种:
1)static_cast:
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast
double slope = static_cast<double>(j)/i; // 将运算对象j强制转换成double类型使表达式执行浮点数除法
当需要把一个较大算术类型赋值给较小的类型是,static_cast非常有用。

2)const_cast:
const_cast只能改变运算对象的底层const
const char *pc;
char *p = const_cast<char*>(pc); // 正确,但是通过p写值是未定义的行为
对于将常量对象转换成非常量的行为,一般称为“去掉const性质”。一旦去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。
如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的。然而如果对象是一个常量,在使用const_cast执行写操作就会产生未定义的后果
只有const_cast能改变表达式的常量属性,其他形式的命名强制类型转换改变表达式的常量属性都将发生错误。
同样,也不能用const_cast改变表达式的类型。
const char *cp;
char *q = static_cast<char*>(cp); // 错误,不能使用static_cast转换掉const性质
static_cast<string>(cp); // 正确,字符串字面值转换成string类型
const_cast<string>(cp); // 错误,const_cast只能改变常量属性,不能改变类型

3)reinterpret_cast:
4)dynamic_cast 

旧式的强制类型转换:
type(expr); // 函数形式的强制类型转换
(type) expr; // c语言风格的强制类型转换









猜你喜欢

转载自blog.csdn.net/csdn_dzh/article/details/80974338