* 推荐配合书本一起食用,我会尽量记载更多的知识点,但出于时间限制,我也会进行取舍,书本中的内容会有大段内容跳过的现象,请谅解!*
目录
表达式由一个或多个运算对象组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。把一个运算符和一个或多个表达式组合起来可以生成较复杂的表达式。
4.1 基础
本部分先把概念罗列出来,后面再逐一讲解。
4.1.1 基本概念
C++定义了一元运算符和二元运算符。作用于一个运算符对象的是一元运算符;作用于两个运算对象的是二元运算符。除此之外还有一个三元运算符。函数的调用也是一种特殊的运算符,它对运算对象的数量没有限制。
一些符号既能作为一元运算符又能作为二元运算符(比如 * )。对于这类符号来说,它的两种用法互不相干,完全可以当成两个不同的符号。
组合运算符和运算对象
对于含有多个运算符的复杂表达式来说,要理解它,首先要理解运算符的优先级、结合律以及运算对象的求值顺序。比如下面这条表达式,具体是什么含义呢?(后面再讲解)
5 + 10 * 20 / 2;
运算对象转换
在表达式的求值过程中,运算对象常被转化类型。运算对象的类型非表达式所需,但如能转化为目标类型,就可以正常运行。
类型转化的规则很杂,好在合乎情理、不难理解。注意,小整数类型(如bool、char、short等)常被提升成较大的整数类型,主要是int。后面4.11部分也会对此做出讲解。
重载运算符
当运算符作用于类类型的运算对象时,用户可以自行定义其含义。因为这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为重载运算符。标准库当中很多类类型定义的对象,之所以能够使用大量的运算符,都是重载运算符的结果。具体如何实现重载运算符应该是书本靠后的内容,一般在学习重载运算符之前,我们会先学习重载函数。
左值和右值
C++表达式要不然是左值,要不然就是右值。
在C++语言中,当一个对象被用作右值时,用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(在内存中的位置)。
不同的运算符的运算对象的要求各不同。一个重要的原则就是,在需要右的值的地方可以用左值代替,但是不能不右值当成左值来用。
有这么一个沿袭至C语言的说法,不能取址的表达式是右值,该说明有一定的参考性,但不论是以前还是现在,这个说法都是不完全准确的。
使用decltype关键字的时候,左值和右值也有所不同。比如:
int *p;
decltype(*p); //其结果是 int&
decltype(&p); //其结果是 int**
容我来尝试解答一下这个问题。产生不同结果的主要原因不是左值和右值的差别,而是表达式的返回值本身就不同。
我们常说对一个指针解引用,得到的是指针所指对象本身,*p 表达式的返回值是一个左值,但如何让返回值是一个左值呢?返回引用类型是关键,所以 *p 的返回值类型是 int& ,decltype(*p)得到的类型也便是int&。(请结合参考2.5.3节,第62页)
对一个对象解引用,得到的是该对象的地址,是一个指向该对象的匿名指针,对象解引用的返回值因此也是一个右值。p本身是一个指针,那么&p表达式得到的就是一个匿名的指向指针的指针,即int**类型。因此 decltype(&p) 得到的类型为int**。
在C++里关于左值和右值的讨论还很多,左值和右值的定义确实比较模糊。最有争议的右值,会因所属类型是基础类型还是自定义类型,体现出不一样的性质。
这里推荐一篇文章:c++中的左值与右值 - twoon - 博客园
4.1.2 优先级和结合律
其实这块将的内容简单且基础,不过是用更加专业的说法去描述了些都懂的道理。
复合表达式是指含有两个或多个运算符的表达式。求复合表达式需要将运算符和运算对象合理的组合在一起,而优先级和结合律决定了运算符和对象的组合方式。
★根据运算符的优先级,表达式 3+4*5 的结果应该是23,不是35
★根据运算符的结合律,表达式20 - 15 - 3 的结果应该是2,不是8
再举个稍微复杂点的列子
6 + 3 * 4 / 2 + 2
如果完全从左到右依次计算,得到的结果将是 20,但综合考虑优先级和结合律的结果是 14。上述表达式等同于:
(6 + ((3 * 4) / 2) + 2)
括号无视优先级与结合律
括号无视普通的组合规则,表达式中括号括起来的部分被当成一个单元来求值,然后再与其它部分一起按照优先级组合。
4.1.3 求值顺序
优先级确定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。在大多数情况下,不会明确指定求值顺序。
但有四种运算符明确规定了求值顺序。第一种是逻辑与(&&)运算符,它要求先求左侧运算对象的值,只有当左侧运算对象的值为真时才继续求右侧运算对象的值。另外三种分别是逻辑或(||)运算符、条件(? :)运算符(它也常被称为三目运算符),和逗号(,)运算符。
求值顺序、优先级、结合律
运算对象的求值顺序与优先级以及结合律无关,一条形如 f() + g() * h() + j() 的表达式中
★ 优先级规定,g()的返回值优先和h()的返回值相乘
★结合律规定,f()的返回值优先和g()与h()的乘积相加,所得结果再与j()的返回值相加
★ 对于这些函数的调用顺序没有明确规定。
计算一个表达式,优先级和结合律只决定了组合方式,组合的优先性不代表求值的优先性,组合关系确定好后,我们会面临求值问题(即计算(子)表达式的问题),哪个组合先计算呢?抱歉并没有明确规定。这里规定的空缺,交给了各个不同的编译器自行处理,所以不同的编译器可能有不同的解决方案。一些复合表达式 形如
6 + 3 * 4 / 2 +2
该表达式的结果就是唯一的,因为它的求值是一环套一环的,有且仅有一种计算方式,就算没有规定求值顺序,不同编译器也不约而同的采取了同样的求值顺序。再看,
f() + g() * h() + j()
在优先级和结合律的规定下,它的组合方式已经被限定死了,但要知道,函数的调用也是一种表达式,这么一个简单的语句,其实里面已经嵌套出了很多个子表达式了。它的求值顺序在某些部分是可以固定的,但某些部分确实又是可以随机的,比如,必需先算出函数调用表达式的结果,才能再进行算术运算符的运算,这是固定的体现。4个函数到底谁先调用,仅现有信息来看,确实没有定数了,这是随机的体现。这时不同的编译器可能就有不同的解决方案,但不同的解决方案,也不代表一定会出现不确定的结果。只有当某个运算对象的值被改变了,该运算对象还被多次使用,这时不确定的风险才可能体现出来。
4.2 算术运算符
表格请参照书本
没做特殊说明时,算术运算符都能作用于任意算术类型 以及 任意能转换为算术类型的类型。算术运算符的运算对象和求值结果都是右值。算术运算符也全部满足左结合律。在表达式运算之前,小整数类型的运算对象被提升为较大的整数类型,所以有的运算对象最终会转换成同一类型。
当一元正号运算符作用于一个指针或者算术值时,返回运算对象值的一个(提升后的)副本。(该副本是一个右值,是一个匿名对象)
一元符号运算符对运算对象取负值之后,返回其(提升后的)副本:
int i = 1024;
int k = -i; //毫无疑问的,k的值为-1024
bool b = true;
bool b2 = -b; //出乎意料的,b2还是true
对大多数运算符来说,布尔类型的运算对象将被提升为int类型。b为true,参与运算时将被提升为整数值1,对它求负后的结果是-1,将-1再转换为布尔值并将其作为b2的初始值。任何非0的数的布尔值都是真,故b2还是true。
整数相除结果还是整数,也就是说,如果商有小数部分,直接弃掉。
运算符%俗称 “取模” 或 “取余” 运算符,负责计算两个整数相除所得的余数,参与取余的运算对象必须是整数类型:
int ival = 10;
doubel dval = 3.14;
ival % 12; //没问题
ival % dval //错误,运算对象是浮点型
dval % ival //错误,运算对象是浮点型
在除法运算中,同号商为正,否则为负。C++早期版本允许结果为负值的商向上或向下取整,C++11新标准规定商一律向0取整,即前面所说的,小数部分直接去掉。
C++语言早期版本允许m%n的符号匹配n的符号,而商向负无穷一侧取整,这一方式在新标准中已经被禁止了。除了-m 导致溢出的特殊情况,其它时候(-m) / n 和 m/(-n)都等于-(m/n),m%(-n)等于m%n,(-m)%n等于(m%n) 。
4.3 逻辑和关系运算符
这两类运算符,运算对象和求值结果都是右值。
短路求值
对逻辑运算符与来说,当且仅当左侧运算符为真时才会再求右侧运算对象的值。
对逻辑运算符或来说,当且仅当左侧运算符为假时才会再求右侧运算对象的值。
短路求值,这一特点,有很强的实际意义,不仅提高了程序的运行效率,还可以有效避免诸如内存溢出的严重错误。(确保了当前索引位置安全的情况下,才会去访问该索引位置)
举一个例子,一个名为text的vector对象中存有若干个string对象,想要遍历输出这些对象,如果当前输出的字符串为空串或者以‘.’结尾,那么进行换行操作,否则,用空格隔开这些string对象。
for (const auto& s : text) //范围for,遍历text对象中的每一个元素
{
cout << s;
if (s.empty() || s[s.size() - 1] == '.') //如果当前字符串为空输出空格,如果当前字符串不为空,但是字符串的最后一个元素是句号,都输出一个换行符
cout << endl;
else //不然就用一个空格隔开
cout << " ";
}
这里补充说明两点:
一个是 const auto& s,这里选择引用的原因在于,text的元素是string占用的空间可能很大,但用引用的方式就可以避免对元素的拷贝。用const修饰的原因在于,我们没有修改vector对象的元素的意图,加上const更加保险。
再一个是 if (s.empty() || s[s.size() - 1] == '.') ,判空在前,查找句号在后,这里必须这样,只有在string对象非空时我们才用下标运算符去访问它,不然可能会引发缓冲区溢出错误。
关系运算符
关系运算符比较对象的大小关系,并返回布尔值。关系运算符都满足左结合律。
如果我们想要在 i比j小,j又比k小的多重条件下,执行if条件下的语句,我们可能会尝试这么写
if(i < j < k) {......} //无论i和j为多少,只要k大于1一定为真
但结果并非我们所设想的,我们来逐步分析一下。按照左结合律,我们先进行 i<j 的判断,如果i真的小于j,我们会得到一个布尔类型的返回值,它为true,否则,为false。这么一个布尔类型的返回值会再和k进行比较,此时,该布尔类型的返回值会提升为int类型,要么为0,要么为1,所以k只要大于1,就可以得到返回值为真的最终结果。真确的写法应该为:
if(i < j && j< k) {......}
4.4 赋值运算符
赋值运算符的左侧对象必须是一个可修改的左值。
赋值运算符的结果是它的左侧运算对象,并且是一个左值。如果赋值运算符左右两个运算对象的类型不同,则右侧运算对象将尽可能转化为左侧运算对象的类型(如果,右侧无法转化过来,那么会报错)
int k = 0; //初始化而非赋值
k = 10; //结果:类型是int,值是10
k = 3.14; //结果:类型是int,值是3
k = "hello"; //错误,不存在适当的转化
C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象:
k = {10}; //结果:类型是int,值是10
k = {3.14}; //错误:窄化转换(也可称为收缩转换)
vector<int> vi; //声明定义一个vector<int>类对象vi,初始化为空
vi = {0,1,2,3,4} //vi现在包含5个元素了,分别为0到4
无论左侧运算对象的类型是什么,初始值列表都可以为空。此时,编译器创建一个值初始化的临时量讲其赋给左侧对象。(书本这里说的有些过于绝对了,当左侧对象需要一个自定义类来赋值,但这个自定义恰好又没有默认构造函数时,会引发错误。引发错误代码如下:)
#include<iostream>
#include<vector>
using std::vector;
class person
{
public:
person(int age) { _age = age; }
private:
int _age;
};
int main()
{
vector<person>vp(10); //error:“person::person”: 没有合适的默认构造函数可用
vp = {}; //error:“person::person”: 没有合适的默认构造函数可用
system("pause");
return 0;
}
赋值运算符满足右结合律
赋值运算符满足右结合律,这一点和其它二元运算符不太一样。
int ival, jval;
ival = jval = 0; //正确,都被赋值为0
因为遵循右结合律,所以靠右的子表达式jval = 0 先计算,其返回是jval本身,ival再与返回值jval进行赋值运算,最后值都变为了0
赋值运算符优先级略过
复合赋值运算符
每种运算符都有相应的复合赋值形式:
+= -= *= /= %= // 算术运算符
<<= >>= &= ^= |= // 位运算符,参见4.8节
任意一种复合运算符都完全等价于
a = a op b; //op表示一种运算符
唯一的区别是右侧运算对象的求值次数:使用复合运算符只求值一次,使用普通运算符求值两次。两次包括:一次是作为右边子表达式的一部分求值,另一次是作为赋值运算的左侧运算对象求值。这种区别可能只对程序的运行效率有小许的影响。
4.5 递增和递减运算符
递增运算符(++)和递减运算符(--)为对象的加1和减1操作提供了一种简洁的书写形式。这两个运算符还可以应用于迭代器,因为很多迭代器本身不支持算术运算,所以此时递增递减运算符除了书写简单外,还是必须的。
前置版本和后置版本的区别这里就不论述了。
建议,除非必须,否则不用递增递减运算符的后置版本
前置版本的递增运算,把值加1后直接返回了改变后的运算对象本身。与之相比,后置版本需要将原始值储存下来以便于返回这个未修改的内容。如果我们不需要 修改前的 值,那么后置版本的操作就是一种浪费。
我们可能听过一种很笼统的说法。前置递增,先加1再输出(或是其它操作),后置递增,先输出(或是其它操作)再加1。咋一看好像是这么一回事,但这是一种很误导人的错误说法。两种递增运算符都有较高的优先级,其中后置递增的优先级比前置递的优先级还要高,只不过后置递增返回的是运算对象改变前的副本,其它的操作都是对副本的操作。
在一条语句中混用解引用和递增运算符
让我们来通过一个案例了解解引用和递增运算符混用的巧妙。先有一个vector<int>类的对象vi,vi内包含若干int类型数据,讲容器内的所有元素遍历输出:
vector<int>vi;
for (int i = 0; i < 10; i++)
{
vi.push_back(i);
}
auto it = vi.begin();
while (it != vi.end())
cout << *it++ << endl;
后置递增运算符的优先级高于解引用运算符,因此 *it++ 等价于 *(it++) 。子表达式it++先进行运算,it的值加1,并且返回了加1前的it的副本,接着解引用运算符再对该副本进行运算。其得到的结果便是it在本次循环开始时所指的元素,同时迭代器it也实现了后移。
运算对象可按任意顺序求值
该部分要说明的问题,我在4.1.3 部分已用较大的篇幅给出了我自己的理解,希望对你有帮助。
4.6 成员访问运算符
点运算符和箭头运算符都可以用于访问成员,其中,点运算符获取对象的一个成员;箭头运算符与点运算符有关,表达式 ptr->mem 等价于 (*ptr).mem
如果你对 既然点运算符和解引用运算符的组合能够代替箭头运算符,为什么还要箭头运算符感到疑惑,这篇文章将帮助你理解:
C语言为什么要有->运算符,有.运算符不就够了吗? - 刘冲的博客 (popkx.com)
4.7 条件运算符
条件运算符(? :) 允许我们把简单的if-else逻辑嵌入到单个表达式当中,条件运算符按照如下形式使用:
cond ? expr1 : expr2
其中cond是条件判断的表达式,而exper1和expr2是两个类型相同或者可能转化为某个公共类型的表达式。条件运算符的求值过程是:首先求cond的值,如果条件为真对expr1求值并返回该值,如果条件为假对expr2求值并返回该值。
当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值;否则运算结果是右值。
嵌套条件运算符
嘿!这种方式还未曾尝试过,不过并不难理解。
允许条件运算符的内部再嵌套另一个条件运算符。直接举例,一目了然,使用嵌套条件运算符来完成成绩的评估,分别包括优秀、合格、不合格:
string finalgrade;
finalgrade = (grade < 60) ? "不及格" : (grade < 90) ? "及格" : "优秀";
cout << "分数:" << grade << " 属于:" << finalgrade << endl;
随着条件运算符嵌套的层数增多,代码的可读性急剧下降。因此,条件运算的嵌套最好别超过两道三层。
在输出表达式中使用条件运算符
cout << ((grade < 60) ? "不及格" : "及格"); //目标效果,输出 不及格 或 及格
cout << (grade < 60) ? "不及格" : "及格"; //非目标效果,输出结果为0或1
cout << grade < 60 ? "不及格" : "及格"); //错误,试图将cout对象与60进行比较
更详细的解释请见书本。
4.8 位运算符
位运算符作用域整数类型的运算对象,并把运算对象看成是二进制位的集合。位运算符提供检查和设置二进制位的功能。
一般来说,运算对象是“小整型”,则它会被提升为较大整数类型。运算对象可以是带符号的,也可以是无符号的,如果运算对象是带符号的且它的值为负,那么位运算符如何处理运算对象的“符号位”依赖于机器。而且,此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。
关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型。
位运算符的具体作用说明略过,请参考书本,书中有图文讲解,利于理解。
移位运算符(又叫IO运算符)满足左结合律
尽管很多程序员从未直接用过位运算符,但几乎所用人都用过它的重载版本来进行IO操作。重载运算符的优先级和结合律都与它的内置版本一样,,因此即使程序猿用不到移位运算符的内置含义,任有必要了解其优先级和结合律。
移位运算符满足左结合律,一下两种写法实则等效:
cout<<"hello"<<" world"<<endl;
( (cout<<"hello")<<" world")<<endl;
移位运算符的优先级居中,因此在使用多个运算符时有必要在适当的位置加上括号:
cout << 42 + 10; //正确:+的优先级更高,输出的结果为 42+10 子表达式的返回值
cout << (42 < 10); //正确:括号的加入使得运算对象如我们所期望的方式组合在一起,输出的结果将是1
cout << 10 < 42; //错误:左移运算符的优先级比<高,会先求子表达式cout<< 10的值,其返回结果是cout,试图将cout与42比较是一种未定义行为
练习 4.25
这里涉及到一个目前书本并未使用的知识点,叫做补码。与补码紧密相关的还有原码和反码。现简单讲解一下:
补码是用来解决负数在计算机中的表示问题的。正数的原码,反码和补码都是一样的。但对于负数而言三者是不一样的,负数的反码就是在源码的基础上,除了符号位不变,其它位都取反(1变0,0变1);而求负数的补码只需在其反码的基础上加1即可。
更多相关的知识可参考:
原码 反码 补码 概念 原理 详解 [MD] - 白乾涛 - 博客园
4.9 sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数。sizeof运算符满足右结合律,其所得值是一个size_t类型的常量表达式。运算符的运算对象有两种形式:
sizeof(type)
sizeof expr
在第二种形式中,sizeof返回的是表达式结果类型所占的大小。特殊的是,sizeof并不会实际计算其运算对象的值。出于这一性质,形如:
sizeof *p; //就算p是一个无效指针,依然不会引发错误
sizeof不要解引用指针也能知道它所指对象的类型
C++新标准允许我们使用作用域运算符来获取类成员的大小。形如:
sizeof Sales_data :: revenue; //不通过对象获取到了类成员的大小
通常情况下只有通过类对象才能访问到类的成员,但是sizeof运算符无需我们提供一个具体的对象,因为获取类的成员大小无须真的获取该成员。
还有较为特殊的一点就是,对数组的名的使用,常尝会把数组名转换为指向其首元的指针,但sizeof运算符不会把数组转换为指针来处理。因此,对数组执行sizeof运算得到的是整个数组所占的空间大小。
通过szieof运算符获取数组的大小也是一种常规操作。用整个数组的大小,除于数组的一个元素的大小即可。
4.10 逗号运算符
逗号运算符含有两个运算对象,按照从左向右的顺序依次求值。逗号运算符也是少数规定了求值顺序的运算符。
对于逗号运算符来说,首先对左侧表达式求值,然后将求值结果丢弃。逗号运算符真正的结果是右侧表达式的值。如果右侧对象是左值,那么最终结果也是左值。
这里想说明的一点,不是见到逗号就可以称之为逗号运算符,在初始化列表或函数参数列表中的逗号是列表元素的分隔符,它不是逗号运算符。
同样的也还有,不要见到等号就称之为赋值运算符,在初始化语句中的等号,表示的是采取拷贝初始化的方式,它不是赋值运算符。
4.11 类型转换
在C++语言中,某些类型之间有关联。如果两种类型可以相互转换,那么它们就算关联的。以下面的语句为例:
int ival = 3.541 + 3;
加法运算的两个运算对象类型不同,一个为double类型,一个为int类型。C++语言不会将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值。上述的类型转化是自动执行的,无需程序员介入,有时甚至不需要程序员了解。因此,它们被称作隐式转换。
算术类型之间的隐式转化的准则是尽可能避免精度的流失。上例当中,3就将被转换为double类型,执行浮点类型间的加法运算,所得结果也为double类型。
再然后,是完成初始化任务。由于初始化对象的类型无法改变,所以doube类型的初始值又被转换为int类型。
何时发生隐式类型转换
✦大多数表达式中,比int小的整型值首先提升为较大的整数类型。
✦在条件中,非布尔值转化为布尔类型。
✦在初始化过程当中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
✦如果算术运算或关系运算的运算对象有多种类型,需要转化成同一类型。
✦函数调用时也会发生类型转化。
4.11.1 算术转化
算术转化的含义是把一种算术类型转换成另外一种算术类型。算术转化的规则定义了一套类型转换的层次,其中运算符的运算对象将转化成最宽的类型。
整型提升
整型提升负责把小整数转换成较大的整数类型。所谓小整型一般就是一下几种:
bool, char, signed char, unsigned char, short, unsigned short
只要它们所有可能的值都能储存在int里,它们就会提升称为int类型;否则,提升成为unsigned int类型。
较大的char类型如:wchar_t, char16_t,char32_t
它们会提升成为int, unsigned int, long, unsigned long, long long, unsigned long long 中能容纳原类型所有可能值的最小一种类型。
无符号类型的运算对象
① 如果两个运算对象同为带符号类型 或 同为无符号类型,但是两个类型大小不同,则小类型转化为大类型
形如: int 和 long 运算,转化为 long 和 long 运算;
unsigned int 和 unsigned long 运算,转化为 unsigned long 和 unsigned long 运算。
② 两个运算对象,一个无符号的,一个带符号的,但是两个类型大小相同,则带符号的类型转化为无符号的类型
形如:int 和 unsigned int 运算,转化为 unsigned int 和 unsgined int 运算。
③ 两个运算对象,一个无符号的,一个带符号的,且带符号的类型比无符号的类型大
形如:unsigned int 和 long 运算
这种情况最为复杂,其转化结果依赖于机器。如果无符号类型的所有值都能存在于带符号类型中(也即带符号的类型所占空间大一些),则无符号类型的运算对象转化成带符号类型。
上述情款转化为:long 和 long 运算
如果不能(即带符号的类型和无符号的类型所占的空间一样大),带符号类型的运算对象转化成无符号类型。
上述情款转化为:unsigned int 和 unsigned int 运算
如果感觉上述规则记忆起来比较繁琐,那不妨尝试归纳下根本原则。
1.可表示数据范围(不考虑正负)小的类型会转化为可表示数据范围大的类型。
2.在两个不同数据类型可表示数据范围大小相同的情况下,小类型转化为大类型。
* 其中可表示数据范围(不考虑正负)的大小由 非符号位bit的多少 所决定。
* 大类型所占大小 大于或等于 小类型,大小类型是相对的,从小到大分别是:
short、int、long、long long (请结合参考整型提升)
综合考虑以上两点,例如在一个int类型和long类型均占4字节、32比特的机器上。转换的层级将是:
int 转 long 转 unsigned int 转 unsigned long
理解算术转换
举例见书本,如果你能理解上面的原则归纳,相信书本中的这些例子理解起来并无难度。
4.11.2 其它隐式类型转换
数组转化为指针:在大多数用到数组的表达式中,数组自动的抓换成指向数组首元素的指针。
指针的转化:C++还规定了几种其他的指针转换方式,包括常量整数值 0 或者字面值 nullptr 能转换成任意指针类型;指向任意非常量常量的指针也能转换成 void*;指向任意对象的指针能转换成cosnt void* 。在第15章还会讲到,在有继承关系的类型间还有另外一种指针转换方式。
转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动抓换的机制。如果指针或算术类型的值为0,转换结果是false;否则转换结果是true。
转换成常量:允许将指向非常量类型的指针转换成指向常量类型的指针,对于引用也是如此。但是上述转换反过来是一种错误行为。例如:
int i;
const int *p = &i; //非常量的地址转换成const的地址
int *p2 = p; //错误:不允许const转换成非常量
类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。例如前面已经使用过的:
string s = “hello world”; //字符串字面值转化为string类型
while(cin>>s); //while的条件部分把cin转换成布尔值
4.11.3 显式转化
命名的强制类型转化
一个命名的强制类型转换具有以下形式:
cast-name<type>(expression);
其中 type是转换的目标类型,而expression是要转换的值。如果type是引用类型,则结果是左值(不然,其返回的结果应该是转换类型后的匿名对象,是一个右值)。cast-name 是 static_cast, dynamic_cast, const_cast 和 reinterpret_cast 中的一种。
static_cast
任何具有明确定义的类型转换,只要不包含 底层const,都可以使用static_cast 。例如,通过强制类型转换,以进行浮点数除法:
int i = 10;
int j = 3;
double d = static_cast<double>(i) / j; //进行强制类型转换以便执行浮点数除法
cout << d << endl; //结果为:3.33333
当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。此时,强制类型转换告诉读者和编译器:我知道并且我不在乎潜在的精度损失,规避了把较大算术类型赋值给较小算术类型时的警告,如下:
i = d; //警告 C4244 “ = ”: 从“double”转换到“int”,可能丢失数据
i = static_cast<int>(d); //没有警告,编译器认为我们采取强制类型转换已了解并不在乎精度丢失的风险
static_cast对于编译器无法自动执行的类型转换也非常有用。例如,我们可以使用static_cast 找回存在于void* 指针所指的值,如下:
void* p2 = &i;
cout << *p2 << endl; //错误,表达式必须是指向完整类型的指针
cout << *(static_cast<int*>(p2)) << endl;
const_cast
cosnt_cast 只能改变运算对象的底层const(可认为它是仅用于指针和引用的强制类型转换方式)。
const char *pc; //底层const
char *p = const_cast<char*>(pc); //正确,但是通过p写值是未定行为。
对于将常量对象转换为非常量对象的行为,我们一般称其为 “去掉const性质” 。一旦我们去掉l某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量(不是顶层const),使用强制类型转换获得写限权是合法行为。然而如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。
只有const_cast 能改变表达式的常量属性,但const_cast无法改变表达式的类型。如下:
const char* cp;
char* q = static_cast<char*>(cp); //错误,static_cast 无法丢掉常量或其它类型限定符
static_cast<string>(cp); //正确,将字符串字面值转换为string
const_cast<string>(cp); //错误,const_cast只能改变常量属性
cosnt_cast常常用于有函数重载的上下文中,会在后面的章节中讲解。
reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。(估计你八成步知道这句话表达了什么) 举个例子说明:
int i = 10, *p = &i;
char *pc = reinterpret_cast<char*>(ip);
最终的结果是我们成功让一个本应指向char类型的指针指向了一个int类型的对象 i ,并且编译器不会对此行为产生警告。这样强制转换也极易引发错误,例如:
string s(*pc); //可能会导致异常行为。
使用reinterpret是非常危险的。
综合来说,无论哪种强制类型转化都是不推荐使用的。
旧形式的强制类型转化
在早期版本的C++语言中,显式的进行强制类型转换包含两种形式:
type(expr); //函数形式的强制类型转换
(type)expr; //C语言风格的强制类型转换
4.12 运算优先级表
请参照书本,如果后续有空,我会搬运过来。目前本章记录完结。
参考书籍 《C++ Primer (第5版)》