写在前面
这一章比较重要,讲的是表达式,其中重要的概念有:
-
表达式的组成:表达式由运算对象(operand)和运算符(operator)组成。运算对象可以是字面值、变量或者是更复杂的表达式。运算符用于指定如何对运算对象进行操作。
-
运算符类型:
- 一元运算符:作用于单个运算对象的运算符,例如取地址符(&)和解引用符(*)。
- 二元运算符:作用于两个运算对象的运算符,例如相等运算符(==)和乘法运算符(*)。
- 三元运算符:作用于三个运算对象的运算符,C++中唯一的三元运算符是条件运算符(?:)。
- 函数调用:也是一种特殊的运算符,对运算对象的数量没有限制。
-
运算符优先级和结合律:在含有多个运算符的复杂表达式中,运算符的优先级决定了它们的求值顺序。结合律决定了当多个具有相同优先级的运算符同时出现时,它们的组合方式。
-
运算对象的求值顺序:在表达式中,运算对象的求值顺序可能会影响表达式的结果,特别是在涉及副作用(如赋值或函数调用)的情况下。
-
类型转换:在表达式求值过程中,运算对象可能会由一种类型转换成另一种类型。C++提供了隐式类型转换和显式类型转换(类型转换运算符)两种方式。隐式类型转换是编译器自动进行的,而显式类型转换需要程序员明确指定。
-
类型提升:小整数类型(如bool、char、short等)在表达式求值时通常会被提升(promoted)到较大的整数类型,主要是int。
-
条件运算符:C++中的条件运算符(?:)是一个三元运算符,用于基于条件表达式的结果选择两个可能的值之一。
-
运算符重载:C++允许程序员为自定义的类类型重载运算符,使得类的对象可以使用与内置类型相似的运算符。
让我们一个一个来讲。
4.1 基础
4.1.1 基本概念
首先得了解一些表达式的基础,在C++中有三种常见的运算符,分别是一元运算符、二元运算符和三元运算符,我们可以理解为小学数学的n元方程,代表作用于n个元素。
比如一元运算符只作用于一个元素,如取地址符&
,解引用符(间接引用)*
等。
二元运算符需要两个元素方可触发,比如做比较的 >
<
和 ==
等。
三元运算符比较少见但并不是没有,比如我们常写的 元素1? 元素2:元素3
,这个运算符的其实就是ifelse的简写,读出来就是:如果元素1为真则输出元素2,否则输出元素3。
运算对象转换
这个我们在前面的章节已经提到过了,如果两个可以运算不同类型的值用运算符计算,输出结果会被统一成一个类型。
重载运算符
这个现在讲有点早,不过可以这么理解,我们都知道,在某个函数作用域外已经定义了的变量可以在函数作用域内重定义,操作符也是这样的,像是string
、vector
的使用运算就是使用了重载运算符
左值和右值
说到这就不得不说一下c语言的概念了,在c语言中,左值和右值就是字面意思,左值就是在运算符左边可以被赋值的值右值则不行。
但是!这是c++,部分左值是不可以被赋值的,比如常量(被const修饰的变量);而部分表达式的运算结果虽然是对象,但是却是右值。
对此,在c++中可以进行简单归纳:当一个元素作为左值时用的是其身份,而用作右值的时候用的是其值。
4.1.2 优先级和结合律
这个简直就是小学知识,像是+ - * /
这种表达式,先乘除后加减,但是如果遇到()
则括号内优先运算,因此,有时候表达式的元素位置会影响到表达式结果。
4.1.3 求值顺序
虽然啊,优先级规定了运算对象的组合方式,但是对运算对象本身谁先求谁后求这个倒没有刻意规定,除了四种表达式规定一定要先求左边的元素为真后再求右边的,分别是&&
, ||
,?:
和,
4. 2 算术运算符
老规矩,看表
上表中按照运算符的优先级将其分组。一元运算符的优先级最高,接下来是乘法和除法,优先级最低的是加法和减法。优先级高的运算符比优先级低的运算符组合得更紧密。
这里有一个关键点要注意一下,就是用表达式时要注意其可能溢出的情况,一种情况在于数学层面的如0成为除数的情况,另一种则是进行运算后结果大于其类型规定值的情况。
如short a = 32767
,如果我令其做+1
运算,其结果在部分电脑就会显示为-32768
这时发生了“环绕“,而在部分其他系统中1,可能会出现更多无法预知的异常。
4.3 逻辑和关系运算符
还是一样,先看表
这些运算符的共同特点是,都会返回一个布尔值。
短路求值
这个词应该很经常听到,其意义是逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。
4.4 赋值运算符
首先,赋值运算符的左侧运算对象必须是一个可修改的左值。
其次,赋值运算符满足右结合律,这一点与其他二元运算符不太一样,也就是说类似int a = b = 0;
这样的等式是成立的,结果都会被赋值为0。
赋值运算的优先级相对较低,因此在计算的时候我们会习惯性给他加上一个()
不要混淆逻辑运算符==
和赋值运算符=
复合赋值的情况也很常见,比如我们常用的++i
就可以理解为i = i+1
4. 5 递增和递減运算符
递增运算符(++)和递减运算符(–)为对象的加1和减1操作提供了一种简洁的书写形式。这两个运算符还可应用于迭代器,因为很多迭代器本身不支持算术运算,所以此时递增和递减运算符除了书写简洁外还是必须的。
这个在很早很早以前就有使用,其中又分前置版本和后置版本,一般的,在不立即取值的情况,前置版本性能可能更优。而且前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。
除非你需要先输出原本值再++
的情况,比如
int i = 0;
while(i <= 10)
{
std::cout << i << std::endl;
++i;
}
这种情况可以简化为
int i = 0;
while(i <= 10)
{
std::cout << i++ << std::endl;
}
输出结果一致
这也符合书中说的美德
4.6 成员访问运算符
.
运算符和->
运算符都可以访问类成员,但是.
运算符一般用于访问类成员,而->
运算符与.
运算符有关,比如ptr->mem
等价于(*ptr).mem
.
总结来说箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值:反之,如果成员所属的对象是右值,那么结果是右值。
4. 7 条件运算符
这个就是之提到过的三元运算符,条件运算符?:
允许我们把简单的if else
逻辑嵌入到单个表达式当中,条件运算符 按照如下形式使用
有几点注意一下
- 允许在条件运算符的内部嵌套另外一个条件运算符
- 在输出表达式中也可以使用条件运算符
4.8 位运算符
看表
这些操作符的操作层面是二进制数,比如~
就是按位取反,如果是~12
那么输出的值就是-13
,因为12
转化成0b12 = 0000 0000 0000 0000 0000 0000 0000 1100
按位取反后得0b~12 = 1111 1111 1111 1111 1111 1111 1111 0011
以十进制显示(取反码后再显示补码)就是-13
,很好理解吧。
移位运算符
>>
和<<
分别是右移和左移运算符,超出部分丢弃,后续章节会讲到,这个运算符在标准输入输出库被重载了
位与、位或、位异或运算符
&
:两个都真才为真
|
:两个都假才为假
^
:两个相异为真,相同为假
移位运算符(又叫10运算符)满足左结合律
因此可以实现 cout << a << b << endl;
补充个点,原码反码补码
在计算有符号数时,为了方便计算机计算,我们使用了这玩意儿进行计算
几个概念:原码就是直接将数据按类型给转化为二进制,比如说我是64位系统,整型占8个字节,那么12
的原码就是0000 0000 0000 0000 0000 0000 0000 1100
,第一位位符号为,所以-13
的原码是1000 0000 0000 0000 0000 0000 0000 0000 1101
在计算过程中,我们都是按补码运算的,记住一句话,任何数字的反码都是原码,其余符号位不变按位取反,正数的补码还是原码,负数的补码为反码+1
因此我们可以求得12的补码还是0000 0000 0000 0000 0000 0000 0000 1100
,然后按位取反得到了另一个补码1111 1111 1111 1111 1111 1111 1111 0011
,此时看符号位就知道是负数,所以我们需要-1
,得到反码1111 1111 1111 1111 1111 1111 1111 0010
除符号位外按位取反得1000 0000 0000 0000 0000 0000 0000 1101
就是-13
了
此时我们可以
4.9 sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数,sizeof运算符满足右结合律,其所得的值是一个size_t类型的常量表达式。
其中有两种使用方式:sizeof(A)
和sizeof B
,第一种用法返回的是表达式结果类型大小,第二种返回的是表达式大小。
一些示例:
几条规则:
- 对于
char
类型或者char
类型的表达式,执行sizeof
运算结果为 1,因为char
类型的大小通常是一个字节。 - 对于引用类型,执行
sizeof
运算结果为被引用对象所占空间的大小,而不是引用本身的大小。 - 对于指针,执行
sizeof
运算结果为指针本身所占空间的大小,而不是指针所指向对象的大小。 - 对于解引用指针,执行
sizeof
运算结果为指针指向的对象所占空间的大小。即使指针无效也可以执行此操作。 - 对于数组,执行
sizeof
运算结果为整个数组所占空间的大小,等价于对数组中所有元素各执行一次sizeof
运算并将所得结果求和。注意,sizeof
运算不会把数组转换成指针来处理。 - 对于
string
对象或vector
对象,执行sizeof
运算结果只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。
4.10 逗号运算符
逗号运算符(comma operator)含有两个运算对象,按照从左向右的顺序依次求值。
和逻辑与、逻辑或以及条件运算符一样,逗号运算符也规定了运算对象求值的顺序。
对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值。
4.11 类型转换
隐式转换
- 在大多数表达式中,比int 类型小的整型值首先提升为较大的整数类型。
- 在条件中,非布尔值转换成布尔类型。
- 初始化过程中,初始值转换成变量的类型:在赋值语句中,右侧运算对象转换成左 侧运算对象的类型。
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 函数调用时也会发生类型转换。
4.11.1 算术转换
整型提升
整型提升(integralpromotion)负责把小整数类型转换成较大的整数类型。对于bool、char、signed char、unsigned char、short和unsigned short等类型来说,只要它们所有可能的值都能存在int里,它们就会提升成int类型;否则,提升成unsigned int类型
4.11.2 其他隐式类型转换
- 数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针
- 指针的转换:C++还规定了几种其他的指针转换方式,包括常量整数值。或者字面值
nullptr
能转换成任意指针类型;指向任意非常量的指针能转换成void*
;指向任意对象的指针能转换成const void*
。 - 转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果是false;否则转换结果是true
- 转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。
- 类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换
4.11.3 显式转换
命名的强制类型转换
一个命名的强制类型转换具有如下形式
cast-name<类型>(元素)
cast-name
是static_cast
、dynamic_cast
、const_cast
和reinterpret_cast
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。
const_cast
const_cast只能改变运算对象的底层const
reinterpret_cast
reinterpret _cast通常为运算对象的位模式提供较低层次上的重新解释。
旧式的强制类型转换
在早期版本的C++语言中,显式地进行强制类型转换包含两种形式:
type(expr);
(type)expr;
根据所涉及的类型不同,旧式的强制类型转换分别具有与const_cast、static_castXIreinterpret_cast相似的行为。当我们在某处执行旧式的强制类型转换时,如果换成const_cast和static_cast也合法,则其行为与对应的命名转换一致。如果替换后不合法,则旧式强制类型转换执行与reinterpret_cast类似的功能
4.12 运算符优先级表
写在后面
没想到这一章这么简单,那我觉得我今天还可以再更新一章!
同学们可以试着用今天学到的知识优化一下一开始的计算器。