C++ Primer 总结索引 | 第四章:表达式

1、介绍 由语言本身定义、并用于内置类型运算对象 的运算符,以及 几种标准库定义的运算符

1、基础

1.1 基本概念

1、C++定义的运算符: 一元运算符 二元运算符 三元运算符 函数调用
作用于 一个运算对象的运算符 是一元运算符,如 取地址符(&)和 解引用符(*
作用于 两个运算对象的运算符 是二元运算符,如 相等运算符(==)和 乘法运算符(*
作用于 三个运算对象的运算符 是三元运算符
函数调用 也是一种特殊运算符,它对 运算对象的数量 没有限制

以符号*为例,同一个符号到底是 一元运算符 还是 二元运算符 由上下文决定,两种用法 互不相干

2、组合运算符 和 运算对象:运算符的 优先级、结合律、运算对象的求值顺序

3、运算对象转换:一般的二元运算符 都要求 两个运算对象的类型相同。很多时候 即使运算对象的类型不相同 没关系,只要它们能 被转换成同一种类型 即可

转换规则:整数 能转成 浮点数,浮点数 也能转成 整数,但是 指针 不能转换成 浮点数;小整数类型(如bool、char、short等)通常会 提升成 较大的整数类型,主要是int

4、重载运算符:C++语言定义了运算符作用于 内置类型和复合类型的运算对象时执行的操作。当运算符 作用于 类类型的运算对象时,用户可以自行 定义其含义,为已存在的运算符 赋予了另外一种含义
IO库的>>和<<运算符 以及 string对象、vector对象 和 迭代器 使用的运算符都是 重载运算符

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

5、左值 和 右值:
1、可能 位于赋值号(=)左侧的表达式就是 左值,只能 位于赋值号右侧的表达式就是 右值
2、有名称的,可以通过 地址符 获取到存储地址的表达式 即为左值,反之是右值
当一个对象被用作 右值的时候,用的是对象的值;当对象被用作 左值的时候,用的是 对象的身份(在内存中的位置)
需要右值的地方 可以用左值来代替,但是 不能把右值当成左值使用

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

6、使用关键词decltype时,左值和右值 有所不同
如果表达式的求值结果是左值,decltype作用于 该表达式(不是变量)得到一个 引用类型
例:假定p的类型是int*,因为 解引用运算符生成 左值,所以decltype(*p)的结果是 int&

另一方面,因为 取地址运算符 生成右值,所以decltype(&p)的结果是int**(指向指针的指针)

1.2 优先级 和 结合律

1、复合表达式:含有两个 或 多个运算符的表达式
求复合表达式的值 首先要将运算符和运算对象 合理组合在一起,优先级 和 结合律 决定了运算对象的组合方式。也就是说,决定了表达式中 每个运算符对应的运算对象 来自表达式的哪一部分。表达式中 括号无视上述规则

2、表达式最终的值 依赖于 其子表达式的组合方式:高优先级运算符的运算对象 要比 低优先级的运算对象 更紧密地结合在一起。如果优先级相同,其组合规则 由结合律确定
例:乘法和除法的优先级相同 且 都高于加法的优先级。因此,乘法 和 除法的运算对象 会首先组合在一起,然后才能轮到 加法和减法的运算对象
算术运算符 满足 左结合律,如果运算符的优先级相同,将按照 从左到右的顺序 组合运算对象(与数学结果一致)

3、优先级 和 结合律的影响:
1)优先级 规定部分先后顺序,会影响 程序正确性

int ia[] = {0, 2, 4, 6, 8}; 
int last = *(ia + 4); // 把last初始化成8,即ia[4]
last = *ia + 4; // last = 4,等价于ia[0] + 4

2)结合律 规定部分先后顺序,在优先级相同的情况下,看结合律。对表达式的影响:典型输入输出运算。io相关的运算符满足 左结合律,可以把几个io运算符 组合在一条表达式中

cin >> v1 >> v2; // 先读入v1,再读入v2

1.3 求值顺序

1、优先级 和 结合律 规定了 运算对象的组合方式,但是没有说明对象 按照什么顺序求值。在大多数情况下,不会明确求值顺序
如:int i = f1() * f2();
f1和f2一定会在 执行乘法前被调用,但是无法知道先调用f1 还是f2

2、对于没有指定运算顺序的运算符 来说,如果 表达式指向并修改了 同一个对象,将会引发错误 并产生 未定义的行为
如:<<运算符 没有明确规定 何时以及如何对运算对象求值

int i = 0;
cout << i << " " << ++i << endl; // 未定义的

因为程序是未定义的,所以无法 推断它的行为。编译器 可能先求++i的值 再求i的值,输出结果是1 1;也可能先求i的值 再求++i的值,输出结果是0 1。因为结果不可预知,不论编译成什么样的代码都是错误的

3、有4种运算符 明确规定了 运算对象的求值顺序
逻辑与(&&)运算符,规定先求 左侧对象的值,只有当左侧运算对象的值为真 才继续求右侧运算对象的值
另外三种是:逻辑或(||)运算符、条件(?:)运算符、逗号(,)运算符

4、求值顺序、优先级、结合律:运算的求值顺序 与 优先级和结合律无关
如:在一条形如f() + g() * h() + j()的表达式中:
1)优先级规定:g()的返回值 和 h()的返回值相乘
2)结合律规定:f()的返回值 先与g()和h()的乘积相加,所得的结果再与j()的返回值相加
3)对于 这些函数的调用顺序 没有明确的规定
如果f、g、h和j 是无关函数,它们既 不会改变同一对象的状态 也不执行io任务,那么函数的调用顺序 不受限制
反之,如果其中某几个函数 影响同一对象,则它是一条错误的表达式,将 产生未定义的行为

5、处理符合表达式
1、拿不准的时候 使用括号
2、如果改变了 某个运算对象的值,在表达式的其他地方 不要再使用 这个运算对象

第2条规则有重要的例外:
当改变运算对象的子表达式 本身就是 另外一个子表达式的运算对象时 该规则无效
如:*++iter,递增运算符 改变iter的值,iter已改变的值 又是解引用运算符的 运算符的运算对象
此时 求值顺序不是问题,递增运算(改变运算对象的子表达式)必须先求值,然后才轮到 解引用计算

2、算数运算符

1、按照算术运算符的优先级 将其分组
一元运算符的优先级最高,接下来是 乘法,除法以及求余,优先级最低的是加法减法
所有列出的运算符 都满足 做结合律,当优先级相同时按照 从左往右的顺序 进行组合

优先级分组运算符功能用法
1+一元正号+ expr
1-一元负号- expr
2*乘法expr * expr
2/除法expr / expr
2%求余expr % expr
3+加法expr + expr
3-减法expr - expr

2、一般算术类型 都能作用于 任意算术类型(整型,浮点型)以及 任意能转换为算术类型的类型
算数运算符的运算对象 和 求值结果 都是右值。在求表达式之前,小整数类型的运算对象 被 提升为较大的整数类型,所有运算对象 最终会转换成 同一类型

一元正号运算符、加法运算符 和 减法运算符 都能作用于 指针。当一元正号运算符 作用于 一个指针或者算数值时,返回运算对象值的一个(提升后的)副本;一元负号运算符 对运算对象值 取负后,返回其(提升后的)副本

int i = 1024;
int k = -i; // k是-1024

bool b = true;
// 非0都为真
bool b2 = -b; // b2是true,印证了之前所说的布尔值不应该参与运算

对于 大部分运算符来说,布尔类型的运算对象 将被提升为int类型。如上所示,布尔变量b的值为真,参与运算时被提升为1,求负后就是-1,换回布尔值为1(值不为0)

3、溢出异常:计算结果超出 该类型所能表示范围。short类型占16位,则最大的short数值是32627

short short_value = 32767; 
short_value += 1; // 溢出
unsigned short usvalue = 65535;
usvalue += 1; // 溢出

4、整数相除结果还是整数,如果商含有 小数部分,直接弃除:int ivall = 21/6; // ivall是3,结果进行了删节,余数被抛弃
C++11新标准 规定商 一律向0取整(直接切除小数部分)

5、运算符%:取余 或 取模运算符,负责计算两个 整数 相除所得的余数,参与 取余运算 的运算对象必须是 整数类型

6、如果m % n不等于0,则它的符号和m相同
区别:(-m) / n和m / (-n)都等于-(m / n);m % (-n)等于m % n,(-m) % n等于-(m % n)

3、逻辑 和 关系运算符

1、关系运算符 作用于 算术类型 或 指针类型,逻辑运算符 作用于 任意能转换成 布尔值 的类型
逻辑运算符 和 关系运算符的返回值 都是布尔类型。值为0的运算对象(算术类型 或 指针类型)表示假,否则表示真

优先级分组结合律运算符功能用法
1!逻辑非!expr
2<小于expr < expr
2<=小于等于expr <= expr
2>大于expr > expr
2>=大于等于expr >= expr
3==相等expr == expr
3!=不相等expr != expr
4&&逻辑与expr && expr
5ll逻辑或expr ll expr

根据优先级,假设i、j和k是三个整数,i != j < k等价于i != (j < k)

2、逻辑与 和 逻辑或 运算符:
短路求值:逻辑与运算符 和 逻辑或 运算符 都是先求左侧运算对象的值 再求右侧运算对象的值,当且仅当 左侧运算对象无法确定表达式的结果时 才会计算右侧运算对象的值
可以 使用左侧运算对象 确保右侧运算对象求值过程 的正确性和安全性

s被声明成了 对常量的引用:for (const auto &s : text) 因为text的元素是string对象,可能非常大,所以 将s声明成 引用类型 可以避免对 元素的拷贝;因为 不需要对string对象做写操作,所以s被声明成 对常量的引用

3、关系运算符:比较运算对象的大小关系 并 返回布尔值,都满足 左结合律。因为 关系运算符的求值结果是 布尔值,所以将 几个关系运算符连写在一起 会产生意想不到的结果:

// 拿i < j的布尔值结果 和 k比较
if (i < j < k) // 若k大于1则为恒为真

如果想i < j < k的时候为条件为真:if (i < j && j < k) { /* ... */ }

4、相等性测试 与 布尔字面值:
想测试 一个算数对象 或 指针对象的真值,直接将其 作为if语句的条件

if (val) { /* ... */ }
if (!val) { /* ... */ }

编译器将val转成 布尔值。val为0时 第一个条件为真,val不为0 第二个条件为真
对于if (val == true) { /* ... */ } 如果val不是布尔值,这样的比较 失去原来意义。如果val不是 布尔值,那么进行比较之前 会首先把 true 转换成 val 的类型,如果val 不是布尔类型,代码等价于 if (val == 1) { /* ... */ }

例:

const char *cp = "Hello World";
if (cp && *cp) // 为true,cp不为空 或 指向不非法,*cp = 'H'

5、相等性运算符 求值顺序 不明确

4、赋值运算符

1、赋值运算符的左侧运算对象 必须是一个可修改的左值
如果给定const int ci = i; // 初始化而非 赋值ci = k; // 错误,ci不可修改 int i = 0, j = 0, k = 0; i + j = k; // 非法,算数表达式是右值
如果赋值运算符的左右 两个运算对象类型不同,则 右侧运算对象 将 转换成 左侧运算对象的类型:k = 3.14; // 结果是k的值为3

2、C++11新标准允许 使用花括号括起来的 初始值列表 作为赋值语句的右侧运算对象。如果 左侧是内置类型,那么 初始值列表 最多只能包含 一个值,而且该值即使转换的话 其所占空间 也不应该大于目标类型的空间 k = { 3.14 }; // 错误:窄化转换
对于 类类型 来说,赋值运算的细节 由类本身决定。比如:vector模板 重载了 赋值运算符 并且可以接收 初始值列表,当赋值发生时 用右侧运算对象的元素 替换 左侧运算对象的元素

无论 左侧运算对象的类型是什么,初始值列表 都可以为空。此时,编译器 创建一个值初始化的临时量 并将其赋给 左侧运算对象 vector<int> vec = {};

3、赋值运算满足右结合律:这一点区别于 别的二元运算符

int ival, jval; 
ival = jval = 0; // 正确,都被赋值成0

靠右的赋值运算 jval = 0 作为 靠左的赋值运算符的 右侧运算对象。又因为 赋值运算符 返回的是 其左侧运算对象,所以 靠右的赋值运算的结果(即jval)被赋给了ival
对于 多重赋值语句中的每一个对象,它的类型 或者与右边对象的类型相同、或者可由右边对象的类型转换得到

string s1, s2;
s1 = s2 = "OK"; // 字符串字面值"OK"转换成string对象

int ival, *pval;
ival = pval = 0; // 错误:不能把指针的值赋给int,尽管0能赋给任何对象

int i; double d;
d = i = 3.5; // i = 3, d = 3.0

4、赋值运算的优先级较低,低于 关系运算符的优先级,所以 条件语句的赋值部分 通常应该加上 括号

while ((i = get_value()) != 42)

其处理过程 是首先将get_value函数的返回值 赋给i,然后比较i和42是否相等
如果 不加括号,比较运算符!=的运算对象 将是get_value函数的返回值 和 42,比较的结果 不论真假 将以布尔型的形式 赋值给i

5、相等运算符 和 赋值运算符:C++允许用赋值运算 作为条件 if (i = j) 此时含义是:if语句的条件部分 把j的值赋给i,然后 检查赋值结果是否为真,如果j不为0,条件将为真 如:if (i = 1024) // 永远为真
如果想判断 i和j是否相等:if (i == j)

6、复合赋值运算符:对对象施以某种运算,然后把运算结果 再赋值给 该对象,每种运算符 都有对应的 复合赋值形式:算术运算符略;<<= >>= &= ^= |= // 位运算符 都几乎完全等价于 a = a op b; 唯一的区别是:左侧运算对象 的求值次数:使用符合运算符只求一次,使用普通运算符求两次(一次是作为 右边子表达式的一部分 求值,另一次 是作为赋值运算符的 左侧对象求值)

5、递增递减运算符

1、可用于迭代器,即使 本身不支持算术运算

2、有两种形式:前置版本 和 后置版本
前置版本 首先将运算对象加1(或减1),然后 将改变之后的对象 作为求值结果
后置版本 也会将运算对象加1(或减1),但是 求值结果是 运算对象改变之前 那个值的 副本

int i = 0, j;
j = ++i; // j = 1, i = 1 前置版本
j = ++i; // j = 1, i = 2 后置版本

除非必须,否则 不用递增运算符的后置版本:后置版本 需要将原始值 存储下来 以便于返回未修改的内容

3、在一条语句中 混用解引用和递增运算符:*pbeg++

auto pbeg = v.begin();
// 输出元素 直至遇到 第一个负值为止
while (pbeg != v.end() && *beg >= 0)
	cout << *pbeg++ << endl; // 输出当前值 并将pbeg向前移动一个元素

后置递增运算符的优先级 高于 解引用运算符,因此 *pbeg++ 等价于 *(pbeg++)
pbeg++ 把 pbeg的值加1,然后 返回pbeg的初始值的副本 作为其求值的结果,解引用的对象 是pbeg未增加之前的值
简洁是一种美德

4、运算对象 可按任意顺序 求值:大多数运算符都没有规定 运算对象的求值顺序,在一般情况下无影响。然后,如果 一条子表达式 改变了某个运算对象的值,另一个子表达式 又要使用该值,运算对象的求值顺序 就很关键。因为 递增运算符 和 递减运算符 会改变运算对象的值,所以 要提防 在复合表达式中 错用这两个运算符

使用for循环 将输入的第一个单词 改写成 大写形式

for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
	*it = toupper(*it); // 将当前字符 改写成 大写形式

如果 把解引用it 和 递增it 两项任务分开来完成。用一个 看似等价的while循环 进行代替

// 该循环的行为是 未定义的
while (beg != s.end() && !isspace(*beg))
	*beg = toupper(*beg++); // 错误,该赋值语句 未定义

赋值运算符 左右两端的运算对象 都用到了beg,并且 右侧的运算对象 还改变了beg的值

*beg = toupper(*beg); // 如果先求 左侧的值
*(beg + 1) = toupper(*beg); // 如果先求 右侧的值

5、有运算顺序的逻辑运算符 和 没有运算顺序的逻辑运算符:假设 ptr 的类型是指向 int 的指针、vec 的类型是vector、ival 的类型是int

ptr != 0 && *ptr++ // 没问题,指针ptr不为空时,判断指针所指的值是不是不为0
ival++ && ival // 没问题,判断ival和ival+1是不是不为0

vec[ival++] <= vec[ival] // 有问题,未定义的,应该改为 vec[ival] <= vec[ival+1]

6、成员访问运算符

1、点运算符(.)和 箭头运算符(->)都可用于 访问成员,表达式ptr -> mem等价于(*ptr).mem 注意括号

string s1 = "a string", *p = &s1; // string类型的指针p

解引用 运算符的优先级 低于 点运算符

// 运行p的size成员,然后 解引用size的结果
*p.size(); // 错误:p是一个指针,没有名为size的成员

箭头运算符 作用于 一个指针类型的运算对象,结果是一个左值。点运算符 分为两种情况:如果成员所属的对象 是左值,那么结果是左值;如果成员所属对象是 右值,那么结果 是右值

2、iter 的类型是 vector<string>::iterator

*iter++; // 合法,求iter的解引用,并把iter指向下一个位置,递增运算符的优先级高
(*iter)++; // 不合法,*iter为字符串,不能++
*iter.empty(); // 不合法,iter没有empty()成员,点运算符的优先级高
iter->empty(); // 合法,iter所指向的值是否为空
++*iter; // 不合法,字符串不能++
iter++->empty(); // 合法,先判断是否为空,再iter++,箭头运算符的优先级高

7、条件运算符

1、条件运算符 可以把简单的 if-else 逻辑 嵌入到单个表达式当中 cond ? expr1 : expr2; 其中,cond是判断条件的表达式,expr1 和 expr2 是两个类型相同 或 可能转换为某个公共类型的表达式

执行过程:首先求cond的值,如果 条件为真 对expr1求值 并返回该值,否则 对expr2求值 并返回该值
例:判断成绩是否合格:string finalgrade = (grade < 60) ? "fail" : "pass";

当 条件运算符的两个表达式 都是左值 或者 能转换为 同一种左值类型时,运算结果是 左值;否则 运算结果是 右值

2、嵌套条件运算符:条件运算符的内部 嵌套另一个 条件运算符,即 条件运算符 可以作为 另外一个条件运算符 的cond 或 expr

finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass";

第一个条件 检查是否在90分以上,如果是,执行符号?后面的表达式;如果否,执行:后面的分支,这个分支本身就是一个表达式

条件运算符 满足右结合律,意味着 运算对象 按 从右往左的顺序组合。因此,靠右边的条件运算(比较成绩是否小于60)构成了 靠左边的条件运算的:分支
条件运算 嵌套 最好不要超过三层

3、在输出表达式中 使用条件运算符:条件运算符的优先级非常低,当一条长 表达式 嵌套了条件运算子表达式 时,通常要在 其两端加括号

cout << ((grade < 60) ? "fail" : "pass"); // 输出pass / fail

// grade和60的比较结果是 <<运算符的对象,如果 grade < 60为真 输出1,否则输出0。<<运算符的返回值是 cout,接下来cout作为 条件运算符的条件
cout << (grade < 60) ? "fail" : "pass"; // 输出 1 或 0
// 等价于
cout << (grade < 60); // 输出 1 或 0
cout ? "fail" : "pass"; // 根据cout的值是true 还是 false产生对应的字面值

cout << grade < 60 ? "fail" : "pass"; // 错误:比较cout 和60
// 等价于
cout << grade; // 小于运算符 的优先级 低于 移位运算符,先输出grade
cout < 60 ? "fail" : "pass"; // 比较cout 和 60,错误

例:运算符的优先级(优先级高于 赋值 、 逗号运算符 ,低于其他 运算符)问题

string s = "word";
string pl = s + s[s.size() - 1] == 's' ? "" : "s" ;

+的优先级大于 == 和 ?: 所以会先执行s + (s[s.size() - 1],与程序本意不符

8、位运算符

1、位运算符 作用于 整数类型 的运算对象,并且 把运算对象 看成是二进制位的集合,满足 左结合律

优先级分组运算符功能用法
1~位求反~expr
2<<左移expr1 << expr2
2>>右移expr1 >> expr2
3&位与expr & expr
4^位异或expr ^ expr
5l位或expr l expr

如果运算对象是 小整形,它的值 会被自动提升成 较大的整数类型。运算对象 可以是带符号的,也可以是 无符号的

如果运算对象是带符号的 且它的值为负,那么 位运算符如何处理运算对象的 符号位 依赖于机器。此时的左移操作可能会改变 符号位的值,是一种 未定义的行为
所以 符号位如何处理 没有明确的规定,仅 将位运算符 用于处理 无符号类型

2、移位运算符:处理输入输出操作时,使用的是 标准IO库 定义的<<运算符 和 >>运算符 的重载版本
两种运算符 内置含义是:对其运算对象 执行基于 二进制位的 移动操作。首先 令左侧运算对象的内容 按照 右侧运算对象的要求 移动指定位数,然后 将经过移动(可能还进行了 提升)左侧运算对象 的拷贝 作为求值结果

右侧的运算对象 不能为负,值 必须严格小于 结果的位数,否则会产生 未定义的行为。二进制位 或向左移(<<) 或向右移(>>),移出边界之外的位 就被舍弃掉了
二进制位 左移 右移
左移运算符(<<)在右侧插入值为0 的二进制位

右移运算符(>>)的行为 则依赖于 其左侧运算对象的类型:
如果 该运算对象是 无符号类型,在左侧插入值为0 的二进制位
如果 该运算对象是 带符号类型,在左侧插入 符号位的副本 或 值为0的二进制位

字符’q’ 的二进制形式是 01110001,那么表达式~‘q’ << 6的值是什么
未定义的行为。(~优先于<<,执行~时,会先将char转换为int,接着按位求反~后,得到一个负值。如果运算对象是带符号的且它的值为负,那么运算符如何处理运算对象的“符号位”依赖于机器)

3、位求反运算符(~):将 运算对象 逐位求反 后生成一个新值
位求反运算符
char型的运算对象 首先提升成 int类型,提升时 运算对象原来的位 保持不变,往高位 添加0即可,随后将 提升后的位 逐位求反

4、位与、位或、位异或 运算符:
与(&)、或(l)、异或(^) 在两个运算符上 逐位执行相应的逻辑操作
与(&)、或(l)、异或(^) 在两个运算符上 逐位执行相应的逻辑操作
位异或:两个运算对象 的对应位置 有且只有一个为1 则运算结果中 该位为1,否则为0(两元素相等为 0,不等为 1)
位运算符和逻辑运算符 别搞混了(如& 和 &&)

unsigned long ul1 = 3, ul2 = 7; // 011, 111
ul1 & ul2  // 011
ul1 | ul2  // 111
ul1 && ul2 // true
ul1 || ul2 // true

5、使用 位运算符

unsigned long quiz1 = 0; // 把这个值的二进制表达形式 当成是位的集合 使用

需要使quiz1 只有第27位是1,其余位不变
1UL:unsigned long类型的 整数字面值为1

1UL << 27 // 生成一个值只有第27位为1,1UL << 27 等同于 1UL * (2^27)

quiz1 |= 1UL << 27; // quiz1 只有第27位是1,其余位不变

把quiz1 第27位重新 置为0,其余位不变

quiz1 &= ~(1UL << 27);

查看 第27位 的值,quiz1 的第27位是1,计算结果就是 非0(真);否则结果是0

bool status = quiz1 & (1UL << 27);

6、移位运算符(IO运算符) 满足 左结合律,优先级不高不低:使用移位运算符的重载类型 进行输入输出操作。重载运算符的优先级 和 结合律 都和其内置版本 相同

左结合律:((cout << "hi") << "there") << endl;

移位运算符的优先级 比 算数运算符 优先级低,比 关系运算符、条件运算符 和 赋值运算符 的优先级高

cout << 42 + 10; // 正确:+的优先级 更高,输出 求和结果
cout << 10 < 42; // 错误:试图比较cout 和 42

cout << 10 < 42; 把数字10写到cout,然后将 结果(即cout)与42进行比较,等价于(cout << 10) < 42

9、sizeof运算符

1、返回一条表达式 或 一个类型名字 多占 字节数。sizeof运算符 满足 右结合律,所得值是一个size_t类型的 常量表达式

其运算对象 有两种形式:1、sizeof (type) 2、sizeof expr
在第二种形式中,sizeof返回的是 表达式结果类型的大小,sizeof并不实际计算 运算对象的值,所以即使 其运算对象 是一个无效(未初始化)的指针 也没事,解引用一个无效指针 也没事,是一种安全行为,指针并没被使用,也不需要 真的解引用指针

Sales_data data, *p; 
sizeof data; // data的类型的大小,等价于sizeof(Sales_data)
sizeof p; // 指针所占空间大小
sizeof *p; // p所指类型的大小,即sizeof(Sales_data)
sizeof data.revenue; // Sales_data的revenue成员 对应类型的大小
sizeof Sales_data::revenue; // 另一种获取revenue大小的方式

sizeof满足右结合律,且与*运算符的优先级一样,所以 对于sizeof *p按照从左往右组合,等价于sizeof (*p)

2、C++11新标准 允许使用 作用域运算符 来获取类成员的大小。通常只有 通过类的对象 才能访问到类的成员,但是sizeof运算符 不需要提供 一个具体的对象,因为 要想知道 类成员的大小 无需获取该成员

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

用数组的大小 除以 单个元素的大小 得到元素个数
因为sizeof返回值 是一个常量表达式,所以 可以用sizeof的结果 声明数组的维度

// sizeof(ia) / sizeof(*ia)返回ia的元素数量
constexpr size_t sz = sizeof(ia) / sizeof(*ia);
int arr2[sz]; // 正确:sizeof返回一个 常量表达式,只有常量才能定义数组的维数

4、int指针 所占字节数为4

5、算数运算符、箭头运算符、比较运算符 与 sizeof的优先级关系

sizeof x + y // (sizeof x) + y      
sizeof p->mem[i] // sizeof(p->mem[i])
sizeof a < b // (sizeof a) < b    
sizeof f() // sizeof(f())

10、逗号运算符

1、含有 两个运算对象,按照从左向右的顺序 依次求值。和 逻辑与、逻辑或 以及 条件运算符 一样,逗号运算符 也规定了 运算对象的求值顺序:首先对 左侧的表达式求值,然后 把求值结果丢掉。逗号运算符真正的结果是 右侧表达式的值。如果 右侧运算对象是 左值,那么最终的求值结果 也是左值

2、someValue ? ++x, ++y : --x, --y;:因为逗号表达式的 优先级最低,编译器会认为冒号后面的–x属于 三目运算符中的语句,而–y属于 一个单独的语句。也就是( someValue ? ++x, ++y : --x), --y; 因此,如果someValue为真,则执行++x,++y,以及 单独的语句–y,最后得到的结果是 y本身。如果someValue为假,则执行–x,以及 单独的语句–y,最终的结果是 --y的值

11、类型转换

1、两种类型有关联:两种类型 可以互相转换,即可以用 另一种关联类型的 对象或值 来替代

2、隐式转换:下例 先根据类型转换规则 将运算对象类型 统一后再求值,然后 初始值 被转换成 左值的类型。上述两次类型转换是 自动执行的

int ival = 3.541 + 3; // 编译器可能警告 该运算损失精度

隐式转换 被设计得尽可能 避免损失精度

3、何时发生 隐式类型转换:
以下情况 编译器 会自动地转换运算对象的类型
1)在大多数表达式中,比 int 小的整型值 首先提升为 较大的整数类型
2)在条件中,非布尔值 转换成 布尔类型
3)初始化过程中,初始值 转换成 变量的类型;在 赋值语句中,右侧运算对象 转换成 左侧运算对象 的类型
4)如果 算数运算 或 关系运算的运算对象 有多种类型,需要 转换成 一种类型
5)函数调用时 也会发生 类型转换

11.1 算术转换

1、算术转换:把一种 算术类型 转换成 另外一种 算术类型。算术转换规则:运算符的运算对象 转换成 最宽的类型
例:如果 一个运算对象的类型时 long double,那么 不论另外一个运算对象的类型 是什么 都会转换为long double;再如 当表达式中 既有浮点 又有整数 时,整数值 会转换成 相应的浮点类型

2、类型提升:把 小整数类型 转换成 较大的整数类型。对于 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中最小的一种类型,前提是 转换后的类型 要能容纳 原类型所有可能的值

3、无符号类型的运算对象 (都是取大的)
1)如果类型 不匹配,执行 整型提升。如果 两个提升后的运算对象的类型 要么都是带符号的、要么都是无符号的 ,则小类型的运算对象 转换成 较大的类型

2)如果 一个运算对象是 无符号类型、另外一个运算对象是 带符号类型,而且 其中的无符号类型 不小于 带符号类型,带符号的运算对象 转换成 无符号的。假设两个类型 分别是unsigned int和int,则int类型的运算对象 转换成unsigned int类型。如果int型的值 恰好为负,有未定义行为(补码)

3)带符号类型 大于 无符号类型时,如果 无符号类型的所有值 都能存在该 带符号类型中,则 无符号类型的运算对象 转换成 带符号类型;如果 不能,带符号类型 转换成 无符号类型
如:两个运算对象 分别是long、unsigned int,并且 int和long大小相同,则 long转换成unsigned long类型;如果 long类型 占用的空间比 int更多,则 unsigned int 转成long

int ival; unsigned long ulval;
3.14159L + 'a'; // 'a'提升成int,然后该int值转换成 long double
// 'a'是char型的字符常量,能表示一个数字值,具体多少 完全依赖于 机器上的字符集
ival + ulval; // ival转换成unsigned long
char cval;
int ival;
unsigned int ui;
float fval;
double dval;

cval = 'a' + 3;                             // ‘a’先转化为int进行计算,得到的结果再转化为char
fval = ui - ival * 1.0;                //  ival先转化为double,ui也转化为double,double再变为float
dval = ui * fval;                           // unsigned int先提升为float,float再转为double
cval = ival + fval + dval;             // ival先转为float,float再转为double,最后double转为char
// 算术运算符 符合左结合律

11.2 其他隐式类型转换

1、数组 转换成 指针:在大多数用到数组的表达式 中,数组 自动转换成 指向 数组首元素的指针;当在 表达式中 使用函数类型时 会发生类似转换

int ia[10]; 
int* ip = ia; // ia转换成 指向数组首元素的指针

2、数组 不转换成 指针:当数组 被用作decltype关键字的参数,或者 作为取地址符(&)、sizeof 及 typeid等 运算符的运算对象时,上述 转换不会发生;如果用一个引用 初始化一个数组时 如:int (*Parray)[10] = &arr; ,上述 转换不会发生

3、指针的转换:C++还规定了 其他几种 指针转换方式,包括 常量整数值0 或 字面值nullptr 能转换成 任意指针类型;指向 非常量的指针 能转换成void*;指向任意对象的指针 能转换成 const void*

4、转换成 布尔类型:如果 指针 或 算术类型的值为0,转换的结果是false;否则转换的结果是 true

char *cp = get_string();
if (cp) // 如果指针cp不是0(空指针),条件为真
while (*cp) // 如果*cp不是空字符,条件为真

5、转换成 常量:允许 将指向非常量类型的指针 转换成 指向相应的 常量类型 的指针,对于引用 也是这样
如果 T 是一种类型,就能将 指向T的指针或引用 分别转换成 指向const T的指针 或引用

int i;
const int &j = i; // 非常量转换成const int的引用
const int *p = &i; // 非常量的地址 转换成const的地址
int &r = j, *q = p; // 错误:不允许const转换成 非常量,相反的转换是不存在的,因为它试图删除底层const

6、类类型定义的转换:类类型能定义 由编译器自动执行的转换,不过 编译器每次只能执行 一种类类型的转换
之前使用过的 类类型转换:一处是 在需要标准库string类型的地方 使用C风格字符串;另外一处是 在条件部分读入istream

string s, t = "a value"; // 字符串字面值 转换成 string类型
while (cin >> s) // while 的条件部分 把cin转换成布尔值(布尔值 由输入流的状态决定,取决于 最后一次读入 是否成功)

11.3 显式转换

1、命名的强制类型转换 cast-name<type>(expression):type是转换的目标类型 而expression是要转换的值。如果 type 是引用类型,则结果是 左值。cast-name 是 static_cast、dynamic_cast、const_cast 和 reinterpret_cast 中的一种,指定了是那种 转换

2、static_cast:任何 有明确定义的 类型转换,只要 不包含底层const,都可以使用 static_cast

int j, i;
// 进行 强制类型转换 以便执行浮点数除法
double slope = static_cast<double>(j) / i;

1)需要把 一个较大的算术类型 赋值给 较小的类型时,static_cast 可以避免 编译器给出 警告信息
2)static_cast 对于 编译器无法自动执行的类型转换 也很有用,比如 可以使用 static_cast 找回存在于 void* 指针中的值,但是应该确保 指针的值保持不变(即 强制转换的结果 与 原始的地址值 相等,类型一旦不相等,产生未定义的结果)

void* p = &d; // 正确:任何非常量对象的地址 都能存入void*
// 正确:将void*转换回 初始的指针类型
double *dp = static_cast<double*>(p); 

假设i是int类型,d是double类型,书写表达式i *= d使其执行整数类型的乘法而非浮点数的乘法:i *= static_cast<int> (d);

3、const_cast:只能改变 运算对象的底层const

const char *pc;
char *p = const_cast<char*>(pc); // 正确,但是通过p写值 是未定义的行为,尽管编译器不会再阻止

如果 本身不是一个常量,使用 强制类型转换 获得写权限 是合法行为。然而 如果对象是一个常量,再使用 const_cast 执行写操作 就会产生未定义的结果

4、只有 const_cast 能改变表达式的 常量属性,使用 其他形式的命名强制类型转换 改变表达式的常量属性 都将引发编译错误;同理,也不能用 const_cast 改变表达式的类型

const char *cp;
// 错误:static_cast 不能转换掉const性质
char *q = static_cast<char*>(cp);

static_cast<string>(cp); // 正确:字符串字面值 转换成string属性 
const_cast<string>(cp); // 错误:const_cast只改变 常量属性

static_cast<string>(cp); // 正确:字符串字面值 转换成string属性
const_cast 常常用于有 函数重载的上下文中

5、reinterpret_cast:通常为 运算对象的位模式(位是一个个位置,一个个地址) 提供较低层次上的 重新解释

int *ip;
char *pc = reinterpret_cast<char*>(ip); 

必须牢记pc所指 的真实对象 是一个int而非 字符。如果把pc当成普通字符指针使用 就可能在运行时 发生错误:string str(pc); // 运行时异常

使用 reinterpret_cast 是危险的,关键问题是 类型改变了,编译器 没给出任何警告 或者 错误的提示信息

6、尽量避免 强制类型转换。强制类型转换 干扰了正常的类型检查。有重载函数的上下文中 使用 const_cast 无可厚非
就算实在 无法避免,也应该 尽量限制类型转换 值的作用域,并且 记录对相关类型的所有假定

7、旧式的强制类型转换:有两种形式

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

旧式的强制类型转换 覆盖 与const_cast、static_cast 或 reinterpret_cast 同样的功能
如:char *pc = (char*) ip; // ip是指向整数的指针 的效果 与使用reinterpret_cast 一样

8、

int i;
double d;
const string *ps;
char *pc;
void *pv;

pv = (void *)ps;            // pv = const_cast<string *>(ps); 去除底层const属性。
i = int(*pc);                  // i = static_cast<int>(*pc);
pv = &d;                      // pv = static_cast<void *>(&d);
pc = (char *)pv;           // pc = reinterpret_cast<char *> (pv);

12、运算符优先级列表

优先级分组结合律运算符功能用法
1::全局作用域::name
1::类作用域class::name
1::命名空间作用域namespace::name
2.成员选择object.member
2->成员选择pointer->member
2[ ]下标expr[expr]
2( )函数调用name(expr_list)
2( )类型构造type(expr_list)
3++后置递增计算lvalue++
3- -后置递减运算lvalue- -
3typeid类型idtypeid(type)
3typeid运行时类型idtypeid(expr)
3explicit cast类型转换cast_name<type>(expr)
4++前置递增运算++lvalue
4- -前置递减运算符- -lvalue
4~位求反~expr
4!逻辑非!expr
4-一元负号-expr
4+一元正号+expr
4*解引用*expr
4&取地址&lvalue
4( )类型转换(type)expr
4sizeof对象大小sizeof expr
4sizeof类型的大小sizeof(type)
4sizeof…参数包的大小sizeof…(name)
4new创建对象new type
4new[ ]创建数组new type[size]
4delete释放对象delete expr
4delete[ ]释放数组delete[ ] expr
4noexcept能否抛出异常noexcept(expr)
5->*指向成员选择的指针ptr->*ptr_to_member
5.*指向成员选择的指针obj.*ptr_to_member
6*乘法expr * expr
6/除法expr / expr
6%取模(取余)expr % expr
7+加法expr + expr
7-减法expr - expr
8<<向左移位expr << expr
8>>向右移位expr >> expr
9<小于expr < expr
9<=小于等于expr <= expr
9>大于expr > expr
9>=大于等于expr >= expr
10==相等expr == expr
10!=不相等expr != expr
11&位与expr & expr
12^位异或expr ^ expr
13l位或expr l expr
14&&逻辑与expr && expr
15ll逻辑或expr ll expr
16?:条件expr ? expr : expr
17=赋值lvalue = expr
18*=,/=,%=复合赋值lvalue += expr等
18+=,-=
18<<=,>>=
18&=,l=,^=
19throw抛出异常throw expr
20,逗号expr, expr

*lvalue表示左值

  • 18
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值