IO
运算符)满足左结合律sizeof
运算符&
)和解引用符(*
);==
)和乘法运算符(*
)。要理解含有多个运算符的复杂表达式的含义,首先要理解运算符的:
小整数类型(如bool
、char
、short
等)通常会被**提升(promoted)**成较大的整数类型,主要是int
。
重载运算符(overloaded operator): IO库的>>
和<<
运算符以及string
对象、vector
对象和迭代器
使用的运算符都是重载运算符。
当运算符作用域类类型的运算对象时,用户可以自行定义其含义。因为这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为重载运算符(overloaded operator)。
左值(lvalue) vs 右值(rvalue)
一个简单的归纳:当一个对象被用作右值的时候,用的是对象的值(内容);
当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
在使用关键字decltype
的时候,左值和右值也有所不同。如果表达式的求值结果是左值,decltype
作用于该表达式(不是变量)得到一个引用类型。
举例:
int ival = 42;
int *p = &ival; // p 存放着变量 ival 的地址,或者说 p 是指向变量 ival 的指针
cout << *p; // 由符号 * 得到指针 p 所指的对象,输出 42
假定p
的类型是int*
,因为解引用运算符生成左值,所以decltype(*p)
的结果是int&
。
另一方面,因为取地址运算符生成右值,所以decltype(&p)
的结果是int**
int ia[] = {
0, 2, 4, 6, 8}; // 含有 5 各整数的数组
int last = *(ia + 4); // 把 last 初始化成 9,也就是ia[4]的值
last = *ia + 4; // last = 4, 等价于 ia[0] + 4
对于那些没有指定执行顺序的运算符来说,如果表达是指向并修改了同一个对象,将会引发错误并产生未定义的行为。举个简单的例子,<<
运算符没有明确规定何时以及如何对运算对象求值,因此下面的输出表达式是未定义的:
int i = 0;
cout << i << " " << ++i << endl; // 未定的
因为程序是未定义的,所以我们无法推断它的行为。编译器可能先求++i
的值再求i
的值,此时输出结果是1 1
;也可能先求i
的值再求++i
的值,输出结果是0 1
;甚至编译器还可能做完全不同的操作。
因为此表达式的行为不可预知,因此不论编译器生成什么样的代码程序都是错误的。
练习4.19:假设 ptr
的类型是指向 int
的指针、vec
的类型是 vector
、ival
的类型是 int
,说明下面的表达式是何含义?如果有表达式不正确,为什么?应该如何修改?
(a) ptr != 0 && *ptr++
(a)的含义是先判定指针 ptr
是否为空,如果不为空,继续判断指针 ptr
所指的整数是否为非 0 数。
如果非 0,则该表达式的最终求值结果为真;否则为假。
最后把指针 ptr
向后移动一位。
该表达式从语法上分析是合法的,但是最后的指针移位操作不一定有意义。
如果 ptr
所指的是整形数组中的某个元素,则 ptr
可以按照预期移动到下一个元素。
如果 ptr
所指的只是一个独立的整数变量,则移动指针操作将产生未定义的结果。
(b) ival++ && ival
(b)的含义是先检查 ival
的值是否非 0,如果非 0 继续检查(ival + 1)
的值是否非 0。
只有当两个值都是非 0 值时,表达式的求值结果为真;否则为假。
在4.1.3节中我们学习到,如果二元运算符的两个运算对象设计同一个对象并改变对象的值,则这是一种不好的程序写法,应该改写。
所以按照程序的原意,本式应该改写成 ival && (ival + 1)
© vec[ival++] <= vec[ival]
©的含义是比较 vec[ival]
和 vec[ival + 1]
的大小,如果前者较小则求值结果为真,否则为假。
与 (b) 式一样,本式也出现了二元运算符的两个运算对象设计同一个对象并改变对象值的情况,
应该改写为 vec[ival] <= vec[ival + 1]
有 4 种运算符明缺规定了运算对象的求值顺序(P123):
逻辑与(&&
) 和 逻辑或(||
):
都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为短路求值(short-circuit evaluation)。
条件(?:
):
逗号(,
):
运算对象的求值顺序与优先级和结合律无关。
一元运算符:
+ expr
, - expr
乘法、除法和求余:
expr * expr
, expr / expr
, expr % expr
加法和减法:
expr + expr
, expr - expr
一元运算符的优先级最高,接下来是乘法和除法,优先级最低的是加法和减法。
当一元运算符作用于一个指针或者算术值时,返回运算对象值的一个(提升后的)副本:
int i = 1024;
int k = -i; // k 是 -1024
bool b = true;
bool b2 = -b; // b2 是 true! b2 = -1 ≠ 0,所以仍是 true
提示:溢出和其他算术运算异常:
当计算的结果超出该类型所能表示的范围时就会产生溢出。因为在计算机中存储某种类型的内存空间有限,所以该类型的表示能力(范围)也是有限的,当计算的结果值超出这个范围时,就会产生未定义的数值,这种错误称为溢出。
假设某个机器的short
类型占16
位,则最大的short
数值是32767
。在这样一台机器上,下面的复合赋值语句将产生溢出:
short short_value = 32767;
short_value += 1;
cout << "short_value: " << short_value << endl;
给short_value
复制的语句是未定义的,这是因为表示一个带符号数32768
需要17
位,但是short
类型只有16
位。很多系统在编译和运行时都不报溢出错误,像其他未定义的行为一样,溢出的结果是不可预知的。在我们的系统中,程序的输出结果是:short_value: -32768
。
该值发生了“环绕(wrapped around)”,符号位本来是0
,由于溢出被改成了1
,于是结果变成一个负值。
如果商含有小数部分,直接弃除:
int ival1 = 21/6; // ival1 是 3,结果进行了删节,余数被抛弃掉了
int ival2 = 21/7; // ival2 是 3,没有余数,结果是整数值
运算符%
俗称“取余”或“取模”运算符:
除了-m
导致溢出的特殊情况,其他时候
(-m)/n
和m/(-n)
都等于-(m/n)
,m%(-n)
等于m%n
,(-m)%n
等于-(m%n)
// 具体示例如下:
-21 % -8; /* 结果是 -5 */
21 % -5; /* 结果是 1 */
-30 / 3 * 21 % 4 /* 结果是 -2 */
&&
) 和 逻辑或(||
):比如P85的循环条件:
index != s.size() && !isspace(s[index])
/* 首先检查 index 是否到达 string 对象的末尾,以此确保只有当 index 在合理范围之内才会计算右侧运算对象的值。 */
举例:使用逻辑或运算符的例子,假定有一个存储着若干string
对象的vector
对象,要求输出string
对象的内容并且在遇到空字符串或者以句号结束的字符串时进行换行。使用基于范围的for
循环处理string
对象中的每个元素:
// s 是对常量的引用;元素既没有被拷贝也不会被改变
for (const auto &s : text) {
cout << s;
// 遇到空字符串或者以句号结束的字符串进行换行
if (s.empty() || s[s.size() - 1] == '.')
cout << endl;
else
cout << " "; // 否则用空格给开
}
练习4.9:解释在下面的 if 语句中条件部分的判断过程。
/* 练习4.9:解释在下面的 if 语句中条件部分的判断过程。*/
const char *cp = "Hello World";
if (cp && *cp)
解答:cp
是指向字符串的指针,因此上式的条件部分含义是首先检查指针是否有效。如果cp
为空指针或无效指针,则条件不满足。如果cp
有效,即cp
指向了内存中的某个有效地址,继续解引用指针cp
并检查cp
所指的对象是否为空字符'\0'
,如果cp
所指的对象不是空字符则条件满足;否则不满足。
在本例中,显然初始状态下 cp
指向了字符串的首字符,是有效的;同时当前cp
所指的对象是字符'H'
,不是空字符,所以if
的条件部分为真。
练习4.10:为while
循环写一个条件,使其从标准输入中读取整数,遇到42
时停止。
// 最简洁的版本
while (cin >> num && num != 42)
该语句首先检查从输入流读取数据是否正常,然后判断当前读入的数字是否是42
,遇到42
则条件不满足,退出循环。
// 另一种形式
int num;
while (cin >> num)
{
if (num == 42)
break;
// 其他操作
}
// 输出 vec 的首元素(如果有的话)
if (!vec.empty())
cout << vec[0];
因为关系运算的求值结果是布尔值,所以将几个关系运算符连写在一起会产生意想不到的结果:
// 哎哟!这个条件居然拿 i < j 的布尔值结果和 k 比较!
if (i < j < k) // 若 k 大于 1 则为真!
// 正确:当 i 小于 j 并且 j 小于 k 时条件为真
if (i < j && j < k) {
/*...*/ }
练习4_12:假设i
、j
和k
是三个整数,说明表达式i!=j
C++规定<
、<=
、>
、>=
的优先级高于==
和!=
,因此上式的求职过程等同于i!=(j
j
和k
的大小,得到的结果是一个布尔值(1
或0
);然后判断i
的值与之是否相等。
if (val) {
/* ...*/} // 如果 val 是任意的非 0 值,条件为真
if (!val) {
/* ...*/} // 如果 val 是 0,条件为真
左侧运算对象必须是一个可修改的左值;
如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型:如当k
为int
,k = 3.14159
的结果是,由于k
类型是int
,值是3
。
C++新标准允许使用花括号括起来的初始值里诶博爱作为赋值语句的右侧运算对象:
k = {
3.14}; // 错误:窄化转换
vector<int> vi; // 初始为空
vi = {
0,1,2,3,4,5,6,7,8,9}; // vi 现在含有10个元素了,值从0到9
因为赋值运算的优先级相对较低,所以通常需要给赋值部分加上括号:
如果我们想不断循环读取数据直至遇到42
为止:
// 这是一种形式繁琐、容易出错的写法
int i = get_value(); // 得到第一个值
while (i != 42) {
// 其他处理……
i = get_value(); // 得到剩下的值
}
// 更好的写法:条件部分表达得更加清晰
// 首先将 get_value函数的返回值赋给 i,然后比较 i 和 42 是否相等
while ((i = get_value()) != 42) {
// 其他处理……
}
我们想不断循环读取数据直至遇到42
为止,其处理过程是首先将get_value
函数的返回值赋给i
,然后比较i
和42
是否相等。
如果不加括号的话含义会有很大变化,比较运算符!=
的运算对象将是get_value
函数的返回值及42
,比较的结果不论真假将以布尔值的形式赋值给i
。
练习4.14:执行下述 if 语句后将发生什么情况?
/*练习4.14:执行下述 if 语句后将发生什么情况?*/
if (42 = i) //...
/*第一条语句发生编译错误,因为赋值运算符的左侧运算对象必须是左值,字面常量 42 显然不是左值,不能作为左侧运算对象。*/
if (i = 42) //...
/*第二条语句,应该写成 i == 42;
而 i = 42 的意思是把 42 赋值给 i,然后判断 i 的值是否为真。因为所有非 0 整数转换成布尔值时都对应 `true`,所以该条件是恒为真的。
*/
每种运算符都有相应的复合赋值形式:
+=
-=
*=
/=
%=
// 算术运算符
<<=
>>=
&=
^=
|=
// 位运算符
递增和递减运算符有两种形式:前置版本和后置版本。
前置版本将对象本身作为左值返回,
后置版本则将对象原始值的副本作为右值返回。
1
(或减1
),然后将改变后的对象作为求值结果。1
(或减1
),但是求值结果是运算对象改变之前那个值的副本。int i = 0, j;
j = ++i; // j = 1, i = 1: 前置版本得到递增之后的值
j = i++; // j = 1, i = 2: 后置版本得到递增之前的值
建议:除非必须,否则不用递增递减运算符的后置版本:
前置版本的递增运算符避免了不必要的工作,它把值加1
后直接返回改变了的运算对象。
与之相比,后置版本需要原始值存储下来以便于返回这个未修改的内容。
如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。
对于整数和指针类型来说,编译器可能对这种额外的工作进行一定的优化;
但是对于复杂的迭代器类型,这种额外的工作就消耗巨大了。
建议养成使用前置版本的习惯,这样不仅不需要担心性能的问题,而且更重要的是写出的代码会更符合编程的初衷。
如果我们想在一条符合表达式中既将变量加1
又能使用它原来的值,这时就可以使用递增和递减运算符的后置版本。
举个例子,可以使用后置的递增运算符来控制循环输出一个vector
对象内容直至遇到(但不包括)第一个负值为止:
auto pbeg = v.begin();
// 输出元素直至遇到第一个负值为止
while (pbeg != v.end() && *beg >= 0)
cout << *pbeg++ << endl; // 输出当前值并将 pbeg 向前移动一个元素
分析:后置递增运算福的优先级高于解引用运算符,因此*pbeg++
等价于*(pbeg++)
。pbeg++
把pbeg
的值加1
,然后返回pbeg
的初始值的副本作为其求值结果,此时解引用运算符的运算对象是pbeg
为增加之前的值。最终这条语句输出pbeg
开始时指向的那个元素,并将之真向前移动一个位置。
这种用法完全是基于一个事实,即后置递增运算符返回初始的未加1
的值。如果返回的是加1
之后的值,解引用该值将产生错误的结果。不但无法输出第一个元素,而且更糟糕的是如果序列选中没有负值,程序将可能试图解引用一个根本不存在的元素。
练习4.18:如果第132页那个输出vector对象元素的while循环使用前置递增运算符,将得到什么结果?
前置递增运算符先讲运算对象加1
,然后把改变后的对象作为求值结果;
后置递增运算符也将运算对象加1
,但是求值结果是运算对象改变之前那个值的副本。
简言之,如果一条表达式中出现了递增运算符,则其计算规律是:
++
在前,先加1
,后参与运算;
++
在后,先参与运算,后加1
。
基于上述分析,本体不应该把while
循环的后置递增运算符改为前置递增运算符。
如果这样做了,会产生两个错误结果:
一是无法输出vector
对象的第一个元素;
二是当所有元素都不为负时,移动到最后一个元素的地方,程序试图继续向前移动迭代器并解引用一个根本不存在的元素。
建议:简洁可以成为一种美德:
书写 cout << *iter++ << endl;
要比书写下面的等价语句更简洁、也更少出错
cout << *iter << endl;
++iter;
如果一条子表达式改变了某个运算对象的值,另一条子表达式又要使用该值的话,运算对象的求值顺序就很关键了。
因为递增运算符和递减运算符会改变运算对象的值,所以要提防在复合表达式中错用这两个运算符。
// 举例:使用 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); // 如果先求右侧的值
// 也可能采取别的什么方式处理它。
练习4.31:前置版本和后置版本的联系和区别。练习PDF 101页:
vector<int>::size_type cnt = ivec.size();
// 将从 size 到 1 的值赋给 ivec 的元素
// for (vector::size_type ix = 0; ix != ivec.size(); ++ix, --cnt)
for (vector<int>::size_type ix = 0; ix != ivec.size(); ix++, cnt--)
ivec[ix] = cnt;
本体从程序运行结果来说,使用前置版本或后置版本是一样的,这是因为递增递减运算符与真正使用这两个变量的语句位于不同的表达式中,所以不会有什么影响。
点运算符 和 箭头运算符 都可用于访问成员,其中,点运算符获取类对象的一个成员;箭头运算符与点运算符有关,表达式 ptr->mem
等价于(*ptr).mem
:
string s1 = "a string", *p = &s1;
auto n = s1.size(); // 运行 string 对象 s1 的 size 成员
n = (*p).size(); // 运行 p 所指对象的 size 成员
n = p->size(); // 等价于 (*p).size()
因为 解引用运算符 的优先级低于 点运算符,所以执行解引用运算的子表达式两段必须加上括号。如果没加括号,代码的含义就大不相同了:
// 运行 p 的 size 成员,然后解引用 size 的结果
*p.size(); // 错误:p 是一个指针,它没有名为 size 的成员
这条表达式试图访问对象p
的size
成员,但是p
本身是一个指针且不包含任何成员,所以上述语句无法通过编译。
练习 4.20:假设iter
的类型是vector
,说明下面的表达是是否合法。如果合法,表达式的含义是什么?如果不合法,错在何处?
【出题思路】考查 成员访问运算符 与 递增运算符 和 解引用运算符 的优先级关系。
(a) *iter++;
a 是合法的,后置递增运算符的优先级高于解引用运算符,其含义是解引用当前迭代器所处位置的对象内容,然后把迭代器的位置向后移动一位。
(b) (*iter)++;
b 是非法的,解引用iter
得到vector
对象当前的元素,结果是一个string
,显然string
没有后置递增操作。
© *iter.empty()
c 是非法的,解引用运算符的优先级低于点运算符,所以该式先计算 iter.empty()
,而迭代器并没有定义 empty()
函数,所以无法通过编译。
(d) iter->empty();
d 是合法的,iter->empty;
等价于 (*iter).empty();
。解引用迭代器得到迭代器当前所指的元素,结果是一个string
,显然字符串可以判断是否为空,empty
函数在此处有效。
(e) ++*iter;
e 是非法的。该式先解引用 iter
,得到迭代器当前所指的元素,结果是一个 string
,显然 string
没有后置递增操作。
(f) iter+±>empty();
f 是合法的。iter++->empty();
等价于 (*iter++).empty();
。含义是解引用迭代器当前位置的对象内容,得到一个字符串,判断该字符串是否为空,然后把迭代器向后移动一位。
条件运算符(?:
):cond? expr1 : expr2
举例:string finalgrade = (grade < 60) ? "fail" : "pass";
允许在条件运算符的内部嵌套另外一个条件运算符。举例:使用一对嵌套的条件运算符可以将成绩分成三挡:优秀(high pass)、合格(pass)、和不合格(fail):
finalgrade = (grade > 90) ? "high pass"
: (grade < 60) ? "fail" : "pass";
条件运算符满足右结合律,意味着运算对象(一般)按照从右向左的顺序组合。
因此在上面的代码中,靠右边的条件运算(比较成绩是否小于 60)构成了靠左边的条件运算的:
分支。
条件运算符的优先级非常低,因此通常需要在它两端加上括号。
cout << ((grade < 60) ? "fail" : "pass"); // 输出 pass 或者 fail
cout << (grade < 60) ? "fail" : "pass"; // 输出 1 或者 0!
cout << grade < 60 ? "fail" : "pass"; // 错误:试图比较 cout 和 60
在第二条表达式中,grade
和60
的比较结果是<<
运算符的运算对象,因此如果grade<60
为真输出1
,否则输出0
。<<
运算符的返回值是cout
,接下来cout
作为条件运算符的条件。也就是说,第二条表达式等价于
cout << (grade < 60); // 输出 1 或者 0
cout ? "fail" : "pass"; // 根据 cout 的值是 true 还是 false 产生对应的字面值
因为第三条表达式等价于下面的语句,所以它是错误的:
cout << grade; // 小于运算符的优先级低于移位运算符,所以先输出 grade
cout << 60 ? "fail" : "pass"; // 然后比较 cout 和 60!
练习4.21:编写一段程序,使用条件运算符从 vector中找到哪些元素的值是奇数,然后将这些奇数值翻倍。
/* 练习4.21:编写一段程序,使用条件运算符从 vector中找到哪些元素的值是奇数,然后将这些奇数值翻倍。*/
#include
#include
#include
#include
using namespace std;
int main()
{
vector<int> vInt;
const int sz = 10; // 使用 sz 作为数组的维度
srand( (unsigned) time (NULL)); // 生成随机数种子
// 使用普通 for 循环为数组赋初值
cout << "The inital value of the array is: " << endl;
for (int i = 0; i != sz; ++i)
{
vInt.push_back(rand() % 100); // 生成 100 以内的随机数
cout << vInt[i] << " "; // 使用下标运算符输出数组内容
}
cout << endl;
// 使用范围 for 循环把数组中的奇数翻倍
for (auto &val : vInt)
val = (val % 2 != 0) ? val*2 : val; // 条件表达式
// 使用范围for循环和迭代器输出数组的当前值
cout << "The adjusted array is: " << endl;
for (auto it = vInt.cbegin(); it != vInt.cend(); ++it)
cout << *it << " ";
cout << endl;
return 0;
}
表4.3:位运算符(左结合律)
运算符 | 功能 | 用法 |
---|---|---|
~ |
位求反 | ~expr |
<< |
左移 | expr1 << expr2 |
>> |
右移 | expr1 >> expr2 |
& |
位与 | expr & expr |
^ |
位异或 | expr ^ expr |
| |
位或 | expr | expr |
位求反运算符(~
)将运算对象逐位求反后生成一个新值,将1
置为0
、将0
置为1
。
位与运算符(&
):如果都是1
,则结果为1
,否则为0
;
位或运算符(|
):如果至少有一个为1
,则结果为1
,否则为0
;
位异或运算符(^
):如果两个运算对象的对应位置有且只有一个为1
,则运算结果中该位为1
,否则为0
。
WARNING: 有一种常见错误是把位运算符和逻辑运算符搞混了,比如
位与(&
) 和 逻辑与(&&
)、
位或(|
) 和 逻辑或(||
)、
位求反(~
) 和 逻辑非(!
)。
练习 4.27:下列表达式的结果是什么?
unsigned long ul1 = 3, ul2 = 7;
ul1 转换为二进制形式是:
00000000 00000000 00000000 00000011
,
ul2 转换为二进制形式是:
00000000 00000000 00000000 00000111
。
(a) ul1 & ul2
按位与,结果是:
00000000 00000000 00000000 00000011
,即 3
。
(b) ul1 | ul2
按位或,结果是:
00000000 00000000 00000000 00000111
,即 7
。
© ul1 && ul2
逻辑与,所有非 0 整数对应的布尔值都是true
,所以该式等价于true && true
,结果为true
。
(d) ul1 || ul2
逻辑或,所有非 0 整数对应的布尔值都是true
,所以该式等价于true || true
,结果为true
。
看不懂,待整理 (P137)
练习4.25:如果一台机器上int
占32
位、char
占8
位,用的是Latin-1字符集,其中字符'q'
的二进制形式是01110001
,那么表达式~'q'<<6
的值是什么?
在位运算符中,运算符~
的优先级高于<<
,因此先对q
按位求反,因为位运算符的位运算对象应该是整数类型,所以字符'q'
首先转换为整数类型。
如题所示:char
占8
位而int
占32
位,
所以字符'q'
转换后得到
00000000 00000000 00000000 01110001
,
按位求反得到
11111111 11111111 1111111 10001110
;
接着执行移位操作,得到
11111111 11111111 11100011 10000000
。
转换成十进制形式是
-7296
。
C++规定整数按照其补码形式存储,对上式求补,得到
10000000 00000000 00011100 10000000
,
即最总结过的二进制形式。
【注】:一个数在bai计算机中的二进制表示形式,叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1。
原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值。
反码的表示方法是:正数的反码是其本身;负数的反码是在其原码的基础上, 符号位不变,其余各个位取反。
补码的表示方法是:正数的补码就是其本身;负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1 (即在反码的基础上+1)。
IO
运算符)满足左结合律移位运算符的优先级不高不低,介于中间:
比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。
cout << 42 + 10; // 正确:+的优先级更高,因此输出求和结果
cout << (10 < 42); // 正确:括号使运算对象按照我们的期望组合在一起,输出 1
cout << 10 < 42; // 错误:试图比较 cout 和 42!
// 最后一个 cout 的含义其实是 (cout << 10) < 42;
sizeof
运算符sizeof
运算符返回一条表达式或一个类型名字所占的字节数。所得的值是一个size_t
类型。
运算符的运算对象有两种形式:
sizeof (type)
size expr
第二种形式中,sizeof
返回的是表达式结果类型的大小,sizeof
并不实际计算其运算对象的值。
Sales_data data, *p;
sizeof(Sales_data); // 存储 Salese_data 类型的对象所占的空间大小
sizeof data; // data 的类型的大小,即 sizeof(Sales_data)
sizeof p; // 指针所占的空间大小
sizeof *p; // p 所指类型的空间大小,即 sizeof(Sales_data)
sizeof data.revenue; // Sales_data 的 revenue 成员的UI应类型的大小
sizeof Sales_data::revenue; // 另一种获取 revenue 大小的方式
sizeof *p
按照从右向左的顺序组合,等价于 sizeof(*p)
。
因为sizeof
不会实际求运算对象的值,所以即使p
是一个无效(即未初始化)的指针,也不会有什么影响。在sizeof
的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。sizeof
不需要真的解引用指针也能知道它所指对象的类型。
sizeof
运算能得到整个数组的大小,所以可以用数组的大小除以单个元素的大小得到数组中元素的个数:
// sizeof(ia) / sizeof(*ia) 返回 ia 的元素数量
constexpr size_t sz = sizeof(ia) / sizeof(*ia);
int arr2[sz]; // 正确
// 因为 sizeof 的返回值是一个常量表达式,所以我们可以用 sizeof 的结果声明数组的维度。
练习4.29:当sizeof
的运算对象是数组名、数组内容、指针时,了解其区别。
/* 练习4.29:推断下面代码的输出结果并说明理由。实际运行这段程序,结果和你想象的一样吗?如果不一样,为什么?*/
int x[10]; int *p = x;
cout << sizeof(x) / sizeof(*x) << endl;
cout << sizeof(p) / sizeof(*p) << endl;
【解答】
第一条:
sizeof(x)
的运算对象x
是数组的名字,求值结果是整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof
运算并对所的结果求和。尤其需要注意,sizeof
运算符不会把数组转换成指针来处理。
在本例中,x
是一个int
数组且包含10
个元素,所以sizeof(x)
的求值结果是10
个int
值所占的内存空间总和。
sizeof(*x)
的运算对象*x
是一条解引用表达式,此处的x
既是数组的名称,也表示指向数组首元素的指针,解引用该指针得到指针所指的内容,在本例中是一个int
。所以sizeof(*x)
在这里等价于sizeof(int)
,即int
所占的内存空间。
sizeof(x) / sizeof(*x)
可以理解为数组x
所占的全部空间除以其中一个元素所占的空间,得到的结果应该是数组x
的元素总数。本题所示的方法是计算得到数组容量的一种常规方法。
第二条:
sizeof(p)
的运算对象p
是一个指针,求值结果是指针所占的空间大小。
sizeof(*p)
的运算对象*p
是指针p
所指的对象,即int
变量x
,所以求值结果是int
指所占的空间大小。
在此编译环境中,int
占4
字节,指针也占4
字节,所以输出结果是:
10
1
练习4.30:在下述表达式的适当位置加上括号,使得加上括号之后表达式的含义与原来的含义相同。
(a) sizeof x + y
© sizeof a < b
由于sizeof
运算符的优先级高于加法运算符的优先级,也高于关系运算符的优先级,所以应该改为:
sizeof(x + y)
sizeof(a < b
(b) sizeof p->mem[i]
(d) sizeof f()
b的含义是限定味道指针p
所指的对象,然后求该对象和总名为mem
的数组成员第i
个元素的尺寸。因为成员选择运算符的优先级高于sizeof
的优先级,所以无须加括号。
d的含义是求函数f()
返回值所占内存空间的大小,因为函数调用运算符的优先级高于sizesof
的有夏季,所以无须加括号。
练习4.33:根据4.12节中的表(第147页)说明下面这条表达式的含义。
someValue ? ++x, ++y : --x, --y
【出题思路】理解条件运算符和逗号运算符的优先级关系。
【解答】C++规定条件运算符的优先级高于逗号运算符,
所以someValue ? ++x, ++y : --x, --y
实际上等价于
(someValue ? ++x, ++y : --x), --y
。
它的求值过程是,首先判断someValue
是否为真,
如果为真,依次执行++x
和++y
,最后执行--y
;
如果为假,执行--x
和--y
。
#include
using namespace std;
int main()
{
int x = 10, y = 20;
// 检验条件为真的情况
bool someValue = true;
someValue ? ++x, ++y : --x, --y;
cout << x << endl;
cout << y << endl;
cout << someValue << endl;
x = 10, y = 20;
// 检验条件为假的情况
someValue = false;
someValue ? ++x, ++y : --x, --y;
cout << x << endl;
cout << y << endl;
cout << someValue << endl;
return 0;
}
/* Output:
11
20
1
9
19
0
*/
隐式转换 (implicit conversion)
算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。
例如:如果一个运算对象的类型是long double
,那么不论另外一个运算对象的类型是什么都会转换成long double
。
还有一种更普遍的情况,当表达式中既有浮点类型也有证书类型时,整数值将转换成相应的浮点类型。
整型提升,负责把小整数类型的转换成较大的整数类型。
P142 再复习
bool flag; char cval;
short sval; unsigned short usval;
int ival; unsigned int uival;
long lval; unsigned long ulval;
float fval; double dval;
3.14159L + 'a'; // 'a' 提升成 int,然后该 int 值转换成 long double
dval + ival; // ival 转换成 double
dval + fval; // fval 转换成 double
ival = dval; // dval 转换成(切除小数部分后)int
flag = dval; // 如果 dval 是 0,则 flag 是 false,否则 flag 是true
cval + fval; // cval 提升成 int,然后 int 值转换成 float
sval + cval; // sval 和 cval 都提升成 int
cval + lval; // cval 转换成 long
ival + ulval; // ival 转换成 unsigned long
usval + ival; // 根据 unsigned short 和 int 所占空间的大小进行提升
uival + ival; // 根据 unsigned int 和 long 所占空间的大小进行转换
练习4.34:根据本节给出的变量定义,说明在下面的表达式中将发生什么样的类型转换:
(a) if (fval)
float
型变量fval
自动转换成布尔值
(b) dval = fval + ival;
ival
转换成float
,与fval
求和后所得的结果进一步转换为double
类型。
© dval + ival * cval;
cval
执行整型提升转换为int
,与ival
相乘后所得的结果转换为double
类型,最后再与dval
相加。
练习4.35:假设有如下的定义: 请回答在下面的表达式中发生了隐式类型转换吗?如果有,指出来。
char cval;
int ival;
unsigned int ui;
float fval;
double dval;
(a) cval = ‘a’ + 3;
字符'a'
提升为int
,与3
相加所得的结果再转换为char
并赋给cval
。
(b) fval = ui - ival * 1.0;
ival
转换为double
,与1.0
相乘的结果也是double
类型,ui
转换为double
类型后与乘法得到的结果相减,最终的结果转换为float
并赋给fval
。
© dval = ui * fval;
ui
转换为float
,与fval
相乘的结果转换为double
类型并赋给dval
。
(d) cval = ival + fval + dval;
ival
转换为float
,与fval
相加所得的结果转换为double
类型,再与dval
相加后结果转换为char
类型。
数组转换成指针,在大多数用到数组的表达式中,数组自动转换成指向数组收元素的指针:
int ia[10]; // 含有 10 个整数的数组
int *ip = ia; // ia 转换成指向数组首元素的指针
指针的转换,包括常量整数值0
或者字面值nullptr
能转换成任意指针类型;
指向任意非常量的指针能转换成void*
;
指向任意对象的指针能转换成const void*
。
转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值是0
,转换结果是false
;否则转换结果是true
。
char *cp = get_string();
if (cp) /* ...*/ // 如果指针cp不是0,条件为真
while (*cp) /* ...*/ // 如果 *cp 不是空字符,条件为真
转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。
int i;
const int &j = i; // 非常量转换成 const int 的引用
const int *p = &i; // 非常量的地址转换成 const 地址
int &r = j, *q = p; // 错误:不允许 const 转换成非常量
相反的转换并不存在,因为它试图删除掉底层的const
。
类类型定义的转换
// 一处是在需要标准库 string 类型的地方使用 C风格字符串
string s, t = "a value"; // 字符串字面值转换成 string 类型
// 另一处是在条件部分读入 istream
while (cin >> s); // while 的条件部分把 cin 转换成布尔值
/*所得布尔值到底是什么由输入流的状态决定,如果最后一次读入成功,转换得到的布尔值是 true;相反,如果最后一次读入不成功,转换得到的布尔值是 false。*/
强制类型转换(cast):显式地将对象强制转换成另外一种类型。
WARNNING: 虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。
int i, j;do
double slope = i/j;
(1) 告诉编译器我们知道并且不在乎潜在的精度损失
一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息;但是当我们执行了显式地的类型转换后,警告信息就会被关闭了。
(2) static_cast
对于编译器无法自动执行的类型转换也非常有用。
例如,我们可以使用static_cast
找回存在于void*
指针中的值:
void *p = &d; // 正确:任何非常量对象的地址都能存入 void*
// 正确:将 void* 转换回初始的指针类型
double *dp = static_cast<double*>(p);
const char *pc;
char *p = const_cast<char*>(pc); // 正确:但是通过 p 写值是未定义的行为
对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const
性质(cast away the const)”。一旦我们去掉了某个对象的const
性质,编译器就不再组织我们对该对象进行写操作了。
如果对象本身不是一个常量,使用强制类型转换获得写权限是合法行为。
然而如果对象是一个常量,再使用const_cast
执行写操作就会产生未定义的后果。
P146
结合律和运算符 | 功能 | 用法 | 参考页码 |
---|---|---|---|
左 :: |
全局作用域 | ::name |
256 |
左 :: |
类作用域 | class::name |
79 |
左 :: |
命名空间作用域 | namespace::name |
74 |
左 . |
成员选择 | object.member |
20 |
左 -> |
成员选择 | pointer->member |
98 |
左 [] |
下标 | expr[expr] |
104 |
左 () |
函数调用 | name(expr_list) |
20 |
左 () |
类型构造 | type(expr_list) |
145 |
右 ++ |
后置递增运算 | lvalue++ |
131 |
右 -- |
后置递减运算 | lvalue-- |
131 |
右 typeid |
类型ID | typeid(type) |
731 |
右 typeid |
运行时类型ID | typeid(expr) |
731 |
右 explicit cast |
类型转换 | cast_name |
144 |
右 ++ |
前置递增运算 | ++lvalue |
131 |
右 -- |
前置递减运算 | --lvalue |
131 |
右 ~ |
位求反 | ~expr |
136 |
右 ! |
逻辑非 | !expr |
126 |
右 - |
一元负号 | -expr |
124 |
右 + |
一元正号 | +expr |
124 |
右 * |
解引用 | *expr |
48 |
右 & |
取地址 | &lvalue |
47 |
右 () |
类型转换 | (type)expr |
145 |
右 sizeof |
对象的大小 | sizeof expr |
139 |
右 sizeof |
类型的大小 | sizeof(type) |
139 |
右 Sizeof... |
参数包的大小 | sizeof...(name) |
619 |
右 new |
创建对象 | new type |
407 |
右 new[] |
创建数组 | new type[size] |
407 |
右 delete |
释放对象 | delete expr |
409 |
右 delete[] |
释放数组 | delete[] expr |
409 |
右 noexcept |
能否抛出异常 | noexcept(expr) |
690 |
左 ->* |
指向成员选择的指针 | ptr->*ptr_to_member |
740 |
左 .* |
指向成员选择的指针 | obj.*ptr_to_member |
740 |
左 * |
乘法 | expr * expr |
124 |
左 * |
除法 | expr / expr |
124 |
左 % |
取模(取余) | expr % expr |
124 |
左 + |
加法 | expr + expr |
124 |
左 - |
减法 | expr - expr |
124 |
左 << |
向左移位 | expr << expr |
136 |
右 >> |
向右移位 | expr >> expr |
136 |
左 < |
小于 | expr < expr |
126 |
左 <= |
小于等于 | expr <= expr |
126 |
左 > |
大于 | expr > expr |
126 |
左 >= |
大于等于 | expr >= expr |
126 |
左 == |
相等 | expr == expr |
126 |
左 != |
不相等 | expr != expr |
126 |
左 & |
位与 | expr & expr |
136 |
左 ^ |
位异或 | expr ^ expr |
136 |
左 | |
位或 | expr | expr |
136 |
左 && |
逻辑与 | expr && expr |
126 |
左 || |
逻辑或 | expr || expr |
126 |
右 ?: |
条件 | expr ? expr : expr |
129 |
右 = |
赋值 | lvalue = expr |
129 |
右 *= , /= , %= 右 += m -= 右 <<= , >>= 右 &= , |= , ^= |
复合赋值 | lvalue += expr 等 |
129 |
右 throw |
抛出异常 | throw expr |
173 |
左 , |
逗号 | expr, expr |
140 |