基础
左值和右值
左值可以位于赋值语句的左侧,右值则不能。
当一个对象被用作右值时,用的是对象的值(内容);当对象用作左值时,用的是对象的身份(在内存中的位置)。
使用关键字decltype,左值和右值也有所不同,如果表达式的求值结果是左值,decltype会得到一个引用类型。假设p类型int*,因为解引用生成左值,所以decltype(*p)的结果是int&。
求值顺序
优先级规定运算对象的组合方式,但没有说明运算符对象的求值顺序。在大多数情况下,不会明确指定求值顺序。例如,下面的表达式中,我们可以确定f1和f2一定会在乘法前调用,因为调用符优先级高于乘法,但我们无法知道f1和f2谁先调用。
1 | int i = f1() * f2(); |
对于没有明确指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将导致未定义行为。例如:
1 | int i = 0; |
编译器可能先求++i的值,再求i的值,因此输出结果是1 1;也可能先求i的值再求++i的值,输出结果是0 1,甚至编译器还可能做完全不同的操作。在单词程序中,编译器可能按照下面的任意一种方式处理表达式,也可能采用别的方式处理它。
1 | *beg = toupper(*beg); //如果先求左侧的值 |
明确规定了求值顺序的运算符:逻辑与(&&) 逻辑或(||) 条件运算(?:) 逗号(,)运算
处理复合表达式原则
1)使用括号强制让表达式的组合关系符合逻辑要求
2)如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象
规则2的例外,当改变对象的子表达式本身就是另外一个子表达式的运算对象时,该规则无效。比如常用的用法*++iter,因为递增运算必须先求值,然后才轮到解引用。
逻辑运算
逻辑与和或运算都使用短路求值(short-circuit evaluation)
。即只有左侧表达式无法确定运算结果时才会计算右侧表达式。
赋值运算
C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。
1 | vector<int> vi; |
因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分应该加上括号
切勿混淆相等运算符和赋值运算符
递增和递减运算符
递增和递减运算符有两种形式:前置版本和后置版本。前置版本现将运算对象加1或减1,然后将改变后的运算对象作为结果;后置版本也会将对象加1或减1,但求值结果是运算对象改变前的那个值的副本。
除非必须,否则不用递增递减运算符的后置版本
在一条语句中混用解引用和递增运算符,如下程序循环输出vector对象直到遇到第一个负值:
1 | auto pbeg = v.begin(); |
后置递增运算符的优先级高于解引用,因此*pbeg++等价于*(pbeg++)
。pbeg++把pbeg的值加1,然后返回pbeg的初始值的副本作为求值结果,所以解引用的结果是pbeg未增加前的值。最终,该语句输出pbeg开始时指向的那个元素,并将指针向前移动一个位置。
sizeof运算符
sizeof运算符返回一个表达式或者类型所占的字节数。sizeof运算符满足右结合律,返回值为size_t类型的常量表达式。sizeof运算符的两种形式:
1 | sizeof(type) |
因为sizeof不会实际求运算对象的值,所以在sizeof *p中即使p是个未初始化的指针也不会有什么影响。
C++11新标准允许我们使用作用域运算符来获得类成员的大小。因为sizeof运算符不需要我们提供一个具体的对象,所以如果需要知道类成员的大小并需要真的获取该成员。
sizeof运算结果总结:
对char类型或者类型为char的表达式执行sizeof运算,结果得到1
对引用类型执行sizeof运算得到被引用对象所占空间大小
对指针执行sizeof运算得到指针本身所占空间大小
对解引用指针执行sizeof运算得到指针指向对象所占空间大小,指针不需要有效
对数组执行sizeof运算得到整个数组所占空间大小,也即使sizeof运算不会把数组转换成指针来处理
对string或vector执行sizeof运算只返回该类型固定部分大小,不会计算对象中元素占用了多少空间
类型转换
在C++中,某些类型之间存在关联,如果两种类型相关联,那么它们就可以相互转换。
算术转换
整型提升负责把小整数类型转换成较大的整数类型。对于bool、char、signed char、unsigned char、short和unsigned short类型,只要它们所有可能值都能存在int里,它们就会提升成int类型,否则提升成unsigned int类型。
如果一个运算对象是无符号类型,另外一个是有符号类型,而且其中无符号类型不小于有符号类型,那么有符号的运算转换成无符号的。否则转换结果依赖于机器。
显式转换
强制类型转换干扰了正常的类型检查,所以程序员应该避免使用强制类型转换。特别是reinterpret_cast。当我们每次使用强制类型转换时,都应该反复考虑是否能以其他方式达到相同目标。如果无法避免,也应该尽量限制类型转换值的作用域,并且记录相关类型的所有假定,减少错误发生的机会。
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。强制类型转换告诉读者和编译器:我们知道并且不在乎潜在的精度损失。另外,static_cast对于编译器无法自动执行的类型转换也非常有用。例如,我们可以使用static_cast找回存在于void*
指针中的值,但我们必须确保转换后得到的类型就是指针所指的类型,否则,将产生未定义。另外,你不能将一个int*类型static_cast到一个double*类型,因为这个转换只有在int*已经被改为指向一个double*时才有意义
。
1 | int i, j; |
static_cast常见用法:
用于基本数据类型之间的转换,如把int转换成char,把int转换成enum,但转换的安全性需要开发人员保证
把void*指针转换成初始的指针类型
用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换
- 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
- 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的
const_cast
const_cast只能改变运算对象的底层const。对于将常量对象转换成非常量对象,我们成为“去掉const性质”。一旦去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是常量,使用const_cast获得写权限是合法行为,然而如果对象是一个常量,再使用const_cast执行写操作会产生未定义的后果。
1 | const char *pc; |
const_cast常用于函数重载的上下文中。
reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。通俗解释,reinterpret_cast运算符是用来处理无关类型之间的转换;它会产生一个新的值,这个值会有与原始参数(expressoin)有完全相同的比特位。例如,将int*转换为double*对于reinterpret_cast
来说是合法的,尽管结果是未定义的。类似地,将int转换为void*对于reinterpret_cast
完全合法,虽然它是不安全的。例如有如下转换:
1 | int *ip; |
我们必须牢记pc所指向的真实对象是一个int而非字符,如果把pc当成普通的字符指针使用就可能在运行时发生错误。主要问题是当我们用int的地址初始化pc时,类型改变了,但由于显示地声称这个转换合法,所以编译器没有给出任何的警告或者错误的提示信息。接下来使用pc时就会认为它的值是char*
类型,编译器没法知道它实际存放的是int*
类型。
查找这类问题的原因十分困难,如果将ip强制转换成pc的语句和用pc初始化string的语句位于不同的文件中,错误原因更加难以查找。
reinterpret_cast中的reinterpret过程依赖于具体的机器,所以对于reinterpret_cast运算符需要谨慎使用,下面列出常用的地方:
从指针转换成足够大以保有该指针值的整数类型(例如转换成std::uintptr_t)
从整型或者枚举类型转换为指针
从任何指向T1类型对象的指针能转换成指向另一类型T2对象的指针,等价于static_cast<T2*>(static_cast<void*>(expression))
从指向函数的指针转向另一个不同类型的指向函数的指针
从一个指向对象的指针转向另一个不同类型的指向对象的指针
从一个指向成员的指针转向另一个指向类成员的指针
旧式的强制类型转换
当我们在执行旧式的强制类型转换时,如果换成const_casthe和static_cast也合法,则其行为与对应的命名转换一致。如果替换后不合法,则旧式强制类型转换执行与reinterpret_cast类似的功能。与命令的强制类型转换相比,旧式的强制类型转换从表现形式上不够直观清晰,所以一旦转换过程出现问题,很难调试。