c++基础之表达式

这次接着更新《c++ primer》 这本书的读书笔记,上一篇博文更新到了书中的第三章,本次将记录书中的第四章——表达式

左值与右值

在理解表达式之前需要先理解c++中左值和右值的概念。
c++ 的表达式要么是右值,要么是左值,这两个名词是从c语言中继承过来的,在c语言中,左值指的是可以位于赋值语句左侧的表达式,右值则不能。在c++中二者的区别就相对复杂一些了。
在c++要区分左值和右值,可以采取一个原则:一般来说当一个对象被用作左值时,用的是对象的地址,也就是在内存中的位置,而右值可以采取排他性原则,只要不是左值的都是右值。
不同运算符对运算对象的要求各不相同,有的要求左值、有的要求右值;返回值也有差异,有的作为左值返回,有的作为右值返回。一个重要的原则是:凡事需要右值的地方可以使用左值来代替,但是不能把左值当成右值来使用。
一般下列运算符需要用到左值

  1. 赋值运算符的左侧需要一个左值。返回的结果也是一个左值
  2. 取地址运算符作用于一个左值运算对象,返回一个指向该对象的指针,结果是一个右值
  3. 内置解引用运算符、下表运算符迭代器解引用运算符、string、vector的下标运算符的求值结果都是左值
  4. 内置类型和迭代器的递增递减运算符作用于左值对象,其前置版本所得到的结果也是左值

优先级与结合律

复合表达式是指含有两个或者多个运算符的表达式,计算复合表达式的值需要将运算符和运算对象合理的组织在一起,优先级与结合律决定了运算对象的组合方式。

表达式中的括号无视运算优先级与结合律的规则,如果表达式中有括号,先运算括号中的内容。

表达式的最终值取决与子表达式的结合方式,在计算表达式的值时,先看运算符的优先级,先处理优先级高的子表达式,而优先级相同的情况下,则由其结合律规则决定

1
2
3
3 + 4 * 5 //根据运算符的优先级,乘法高于加法,所以先计算4 * 5 为20,再计算3 + 20 得到23
20 - 15 - 3 //先看运算符的优先级,都是减法优先级相同,再看结合律,减法的结合律是从左到右,所以先计算20 -15 得到 5,然后再计算5 - 3 得到2
6 + 3 * 4 / 2 + 2 //先看运算符的优先集,乘法除法的优先级大于加法,而乘法除法的结合律都是从左到右结合,所以这个表达式先计算 3 * 4 得到12,再计算 12 / 2 得到 6 ,最后加法的结合律也是从左到右,最后计算 6 + 6 + 2 得到 14

求值顺序

优先级规定了运算对象的组合方式,但是并没有规定运算对象按照什么顺序求值,在大多数情况下不会明确指定求值顺序。例如在表达式 int i = f1() * f2(); 中,先计算函数的返回值,然后再将结果赋值进行乘法运算,最后将结果赋值给i变量,但是究竟是先计算f1函数还是先计算f2函数,这个c++标准没有明确规定。

对于没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为,例如

1
2
int i = 0; 
int j = i + ++i;

根据结合律,会先计算i和 ++i但是不确定是该先计算i还是先计算++i 这里会产生未定义行为。如果先计算i则表达式可以转化为 j = 0 + 1 如果先计算 ++i,则表达式可以转化为 j = 1 + 1;

有4中表达式明确规定了求值顺序

  1. 逻辑与(&&):只有当左侧的结果为真时,才计算右侧的结果
  2. 逻辑或(||):只有当左侧的运算结果为假时,才会计算右侧结果
  3. 三目运算符(?:)当条件为真时,计算:左侧的表达式,否则计算右侧的表达式
  4. 逗号表达式:运算顺序是从左到右,最后返回最右侧的表达式的值

在处理复合表达式时,有下面两条准则:

  1. 在不清楚运算对象的优先级和结合律的时候,按照实际的结合逻辑使用括号
  2. 如果改变了某个运算对象的值,在表达式的其他地方不要使用这个运算对象,但是能明确知道求值顺序的时候这个规则就不适用了

算术运算符

算术运算符的求值对象和求值结果都是右值。
算术运算符的优先级顺序为:单目运算符(+表示取当前值,-表示取相反数) > 乘除法 > 加减法;结合律:采用从左至右结合的方式

算术运算符能作用与所有的算术类型,算术类型的数据在运算前会被转化为精度较大的类型(运算对象只有byte,char, short时会被统一转化为int),在转化为同一类型后执行再进行运算

1
2
3
bool b = false;
int k = 1;
bool i = +k + -b;

在上述代码中,bool类型参与算术运算时,会将true变为1,false变为0,然后针对0和1进行操作,根据优先级得到 i = 1 + 0; 最后再将算术类型转化为bool类型赋值,i最终为true

除法运算中如果除数和被除数符号相同,商为正数,否则为负数,c++11 标准中规定负数商一律向0取整

取余运算,要求除数和被除数都是整数,如果m/n的结果不为0,则m%n的结果符号与m相同

(m/n)*n + m%n = m
(-m)/n=m/(-n)=-(m/n)
(-m)%n=-(m%n); m%(-n)=m%n

逻辑运算符

逻辑运算符作用与任何能转化为boo类型的运算对象上

优先级为 逻辑非 > 大于/小于/大于等于/小于等于 > 相等/不等 > 逻辑与 > 逻辑非

逻辑运算符一般的语言中都有,而且用法基本类似,这里就不再详细说明了,需要注意的是:

  1. 使用非bool类型来做判断时,不要写成 if(!val) 或者 if(val == true);同样的使用bool类型来判断时,也不要写成 if(val == true) 或者 if(val == 1)
  2. 在进行数值相等的比较时,为了防止少写=,习惯上把常量写在前面例如 if(1 == val)

赋值运算符

赋值运算符一般作用与初始化给对象赋值或者在后续修改对象的值,但是需要注意区分二者的不同,这点在初始化或者给类对象赋初始值的时候尤其重要

赋值运算符的左侧必须是一个可修改的左值。

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

1
2
3
4
int i, j;
i = j = 10;
const k = 10; //这里是初始化,不是赋值
k = i; //错误,左侧需要可以修改的左值

新的c++ 标准中允许使用初始化列表来给对象进行赋值

1
2
3
4
i = {3.14}; //错误,使用初始化列表时,不能出现精度丢失
i = 3.14; //正确,值为3
vector<int> vi;
vi = {0, 1, 2, 3, 4, 5};

对于内置类型,初始化列表赋值时,列表中最多只能有一个值,而且值的精度不能大于左侧对象的精度

赋值运算符满足右结合律,对于多重赋值语句中的每一个对象,它的类型或者与右边的对象相同,或者可以又右边对象的类型转化得到

赋值运算符的优先级较低

赋值运算符也包括复合赋值运算符,例如 += 、-=、*= /=

递增和递减运算符

递增和递减运算符为对象的加一和减一提供了一种简洁的书写形式。这两个运算符还可以应用于迭代器。

递增和递减运算符有前置版本和后置版本,前置版本是先加一,然后将改变后对象的值作为求值结果;后置版本是先将对象的结果作为求值结果返回,然后再改变对象的值。

在性能上,在涉及复杂的迭代运算时,前置版本会大大优于后置版本,因此尽量养成使用前置版本的习惯。

1
2
3
4
5
auto pbeg = v.begin()
while(pbeg != v.end() && *pbeg >= 0)
{
cout << *pbeg++ << endl;
}

这里后置递增运算符的优先级要大于解引用的优先级,所以这里等价于 *(pbeg++),即先进行后置递增运算,但是返回变化之前的迭代器,然后将变化之前的迭代器进行解引用操作,得到具体元素的值

递增和递减运算符可以修改对象的值,而一般的运算符没有严格规定求值的顺序,所以在复合表达式中需要额外注意,不要在可能修改变量值的位置访问该变量

1
2
3
4
5
6
string s = "hello world";
auto beg = s.begin();
while(beg != s.end() && !isspace(*beg))
{
*beg = toupper(*beg++);
}

上述例子由于赋值运算符未定义两侧运算对象的求值顺序,可能先求值左侧,那么循环中的语句等效于 beg = toupper(beg); 如果先求值右侧,则等效于 (beg + 1) = toupper(beg);

条件运算符

条件运算符也叫做三目运算符。

1
cond ? expr1:expr2;

条件运算符也可以嵌套使用, 条件运算符满足右结合律。随着嵌套层数的增加,代码的可读性极具下降,因此条件运算的嵌套最好不要超过三层。

条件运算符的优先级非常低,一般使用的时候建议加上括号

1
2
3
cout << ((grade > 60) ? "pass" : "fail"); // 输出pass 或者 fail
cout << (grade > 60)? "pass" : "fail"; // 输出 1或者0,运算结果 是 "pass" 或者 "fail"
cout << grade > 60 ? "pass" : "fail"; // 试图将cout 与 60 进行比较,错误

位运算符

位运算是作用与对象的二进制值的,理论上它可以处理任何对象,但是为了代码安全和可读性,建议只处理整型数据,而且最好是无符号整型

运算符 功能 用法
~ 按位求反 ~expr
<< 左移 expr << expr2
>> 右移 expr >> expr2
& 位与 expr & expr2
^ 位异或 expr ^ expr2
位或

sizeof 运算符

sizeof 返回一个类型或者一个表达式所占的字节数。它满足右结合律

针对表达式,sizeof并不计算表达式的值,只返回表达式结果类型的大小

由于sizeof 不计算表达式的值,因此即使在sizeof中解引用指针也不会有什么影响

逗号表达式

逗号运算符含有两个表达式,按照从左至右的顺序依次求值

逗号表达式先对左侧表达式进行求值,然后丢弃返回的结果,然后再对右侧表达式进行求值。逗号表达式的返回值是右侧的表达式的值

类型转换

何时发生隐式转换

  1. 大多数情况下,比int小的整型值会被转化为int
  2. 条件中,非布尔值会被转化为布尔类型
  3. 初始化过程中,初始值转化为变量类型;赋值语句中右侧运算对象转化成左侧运算对象的类型
  4. 如果是算术运算或者关系运算的运算对象有多种类型,需要转化为同一种类型。而且会尽量往精度较大的一方转化
  5. 调用函数时也可能会发生类型转化

算术类型转换

算术转换总是朝着精度更高的一级转换

较小的整型会被转化为int,较大的整型会被转化为long、unsigned long、unsigned longlong 等

其他隐式类型转换

除了算术类型的隐式转换外,还有下面几种

  1. 数组转化为指针:当数组被用作 decltype、sizeof、取地址符一级typeid 等运算符的运算对象时,该转换不会发生
  2. 指针的转化:常量整数0和nullptr可以转化为任意类型的指针,指向任意非常量的指针能转化成void,指向任意对象的指针能转化为const void
  3. 转化为布尔类型: 算术类型或者指针,值为0或者nullptr的被转化为false,其他的值被转化为true
  4. 转化为常量:常量的指针或者引用可以指向非常量对象,反过来则不行;
  5. 类类型定义的转化:由程序员预先定义,在需要转化时,由编译器自动调用进行转化

显式类型转换

显式类型转换绕过了编译器的类型检查,是不安全的一种转化方式

显示类型转换的语法规则如下:

1
cast-name<type>(express);

其中type是目标类型,express是要转化的值,如果type是引用类型则结果是一个左值。cast-name是 static_cast、dynamic_cast、const_cast 和 reinterpret_cast 中的一种

  1. static_cast 只要不包含底层const,都可以使用static_cast,在对指针进行强制类型转化时,要保证转化前与转化后指针所指向的对象类型相同,用于同类型数据之前的转化,如算术类型之前的相互转化。
  2. const_cast 只能改变运算对象的底层const、与static_const互相补充
  3. reinterpret_cast 重新解释比特位,通常为运算对象的位模式提供较低层次上的重新解释。一般用于指针之间的转化,它没有限制,任何类型间都可以进行转化。但是也十分危险
  4. dynamic_cast 动态类型转化,主要用于多重继承类类型之间的转化