目录
表达式由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果(result)。字面值和变量是最简单的表达式(expression),其结果就是字面值和变量的值。把一个运算符(operator)和一个或多个运算对象组合起来可以生成较复杂的表达式。
4.1 基础
本节先介绍几个基础概念。
4.1.1 基本概念
C++ 定义了一元运算符(unary operator)和二元运算符(binary operator)。作用于一个运算对象的运算符是一元运算符,如取地址符(&)和解引用符(*);作用于两个运算对象的运算符是二元运算符,如相等运算符(==)和乘法运算符(*)。除此之外,还有个作用于三个运算对象的三元运算符。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
组合运算符和运算对象
对于含有多个运算符的复杂表达式来说,要想理解它的含义首先要理解运算符的优先级(precedence)、结合律(associativity)以及运算对象的求值顺序(order of evaluation)。
运算对象转换
在表达式求值的过程中,运算对象常常由一种类型转换成另外一种类型。
类型转换的规则虽然有点复杂,但是大都合乎情理、容易理解。例如,整数能转换成浮点数,浮点数也能转换成整数,但是指针不能转换成浮点数。让人稍微有点意外的是,小整数类型(如 bool、char 、short等)通常会被提升(promoted)成较大的整数类型,主要是 int。
重载运算符
C++ 语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用于可以自行定义其含义。因为这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为重载运算符(overloaded operator)。IO库的 >> 和 << 运算符以及 string 对象、vector 对象和迭代器使用的运算符都是重载的运算符。
我们使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的;但是运算对象的个数、运算符的优先级和结合律都是无法改变的。
左值和右值
C++ 的表达式要不然是右值(rvalue,读作“are-value”),要不然是左值(lvalue,读作“ell-value”)。
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是身份(在内存中的位置)。
4.1.2 优先级与结合律
复合表达式(compound expression)是指含义两个或多个运算符的表达式。求复合表达式的值需要首先根据优先级和结合律将运算符和运算对象合理地组合在一起。表达式中的括号无视上述规则。
括号无视优先级与结合律
括号无视普通的组合规则,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。
优先级与结合律有何影响
优先级会影响程序的正确性。
4.1.2 节练习
练习4.1:表达式 5+10*20/2的求值结果是多少?
105
在本题涉及的运算符中,优先级最高的是成员选择符和函数调用运算符,其次是解引用运算符,最后是加法运算符。因此添加括号后的等价式子是:
*(vec.begin()) (*(vec.begin()))+1
4.1.3 求值顺序
对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将对引发并产生未定义的行为。举个例子,<<运算符没有明确规定合适以及如何对运算对象求值,因此下面的输出表达式是未定义的。
int i = 0;
cout << i << " " << ++i << endl;//未定义的
求值顺序、优先级、结合律
运算对象的求值顺序与优先级和结合律无关,在一条形如 f() + g() * h() + j()的表达式中:
- 优先级规定,g()的返回值和 h()的返回值相乘。
- 结合律规定,f() 的返回值先与g()和h()的乘积相乘,所得结果再与j()的返回值相加。
- 对于这些函数的调用顺序没有明确规定。
建议:处理复合表达式
1、拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求。
2、如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。
4.1.3 节练习
这样的做法在一定程度上是可以接受的,前提是编写程序时注意前面两点建议。
4.2 算术运算符
运算符 | 功能 | 用法 |
+ | 一元正号 | + expr |
- | 一元负号 | - expr |
* | 乘法 | expr * expr |
/ | 除法 | expr / expr |
% | 求余 | expr % expr |
+ | 加法 | expr + expr |
- | 减法 | expr - expr |
一元运算符的优先级最高,其次是乘法、除法和求余,优先级最低的是加法和减法。优先级高的运算符比优先级低的运算符组合得更紧密。上面的所有运算符都满足左结合律,意味着当优先级相同时按照从左到右的顺序进行结合。
除非另做特殊说明,算术运算符都能作用于任意算术类型以及任意能转换为算术类型的类型。算术运算符的运算对象和求值结果都是右值。
一元正号运算符、加号运算符和减法运算符都能作用于指针。当一元正号运算符作用于一个指针或者算术值时,返回运算对象值的一个(提升后的)副本。
一元负号运算符对运算对象值取负后,返回其(提升后的)副本。
提示:溢出和其他算术异常
算术表达式有可能产生未定义的结果。一部分原因是数学性质本身:例如除数是0的情况;另外一部分则源于计算机的特点:例如溢出,当计算的结果超出该类型所能表示的范围时就会产生溢出。
运算符 % 俗称“取余”或“取模”运算符,负责计算两个整数相除所得的余数,参与取余运算的运算对象必须是整数类型。
在除法运算中,C++ 11 新标准规定商一律向 0 取整(即直接切除小数部分)。
4.2 节练习
//练习4.4:在下面表达式中添加括号,说明其求值的过程及最终结果。编写程序验证。
//16+75+0 =91
cout << 12 / 3 * 4 + 5 * 15 + 24 % 4 / 2 << endl;
//练习4.5:写出下列表达式的求值结果
cout << "(a): "<<-30 * 3 + 21 / 5 << endl;//-90+4=-86
cout << "(b): " << -30 +3 * 21 / 5 << endl;//-30+63/5=-30+12=-18
cout << "(c): " << -30 / 3 * 21 % 5 << endl;//-10*21%5=-210%5=0
cout << "(d): " << -30 / 3 * 21 %4 << endl;//-10*21%4=-210%4=-2
//写一条表达式用于确定一个整数是个数还是偶数
int num;
num % 2 == 0; //为真时偶数,为假是奇数
练习4.7:溢出是何含义?写出三条将导致溢出的表达式。
//会导致溢出的表达式
//假设编译器规定占32位
int i = 2147483647 + 1;
int j = -100000 * 300000;
int k = 2021 * 2021 * 2021 * 2021;
45 / 0;
4.3 逻辑和关系运算符
关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。值为 0 的运算对象(算术类型或指针类型)表示假,否则表示真。对于这两类运算符来说,运算对象和求值结果都是右值。
结合律 | 运算律 | 功能 | 用法 |
左 | ! | 逻辑非 | !expr |
左 | < | 小于 | expr < expr |
左 | <= | 小于等于 | expr <= expr |
左 | > | 大于 | expr > expr |
左 | >= | 大于等于 | expr >= expr |
左 | == | 相等 | expr == expr |
左 | != | 不相等 | expr != expr |
左 | && | 逻辑与 | expr && expr |
左 | || | 逻辑或 | expr || expr |
逻辑与和逻辑或运算符
逻辑与(&&):当且仅当两个运算对象都为真是结果为真;逻辑或(||):只要两个运算对象中的一个为真结果就为真。
逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为短路求值(short-circuit evaluation)。
- 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。
- 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
逻辑非运算符
逻辑非运算符(!)将运算对象的值取反后再返回。
关系运算符
关系运算符比较运算对象的大小关系并返回布尔值。关系运算符都满足左结合律。
相等性测试与布尔字面值
进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值 true 和 false 作为运算对象。
4.3 节练习
练习4.8:说明在逻辑与、逻辑或及相等性运算符中运算对象求值的顺序。
对于逻辑与运算符来说,当且仅当两个运算对象都为真时结果为真;对于逻辑或运算符来说,只要两个运算对象中的一个为真结果就为真,
逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略就是短路求值。其策略是:对于逻辑与运算符来说,当且仅当左侧运算对象为真时才计算右侧运算对象;对于逻辑或运算符来说,当且仅当左侧运算对象为假时才计算右侧运算对象。
相等性运算符的两个运算对象都需要求值,C++没有规定其求值顺序。
练习4.9:解释下面的if语句中条件部分的判断过程。
const char *cp = "hello world;
if(cp && *cp)
cp是指向字符串的指针,因此上式的条件部分含义是首先检查指针 cp 是否有效。如果 cp 为空指针或无效指针,则条件不满足。如果 cp 有效,即 cp 指向了内存中的某个有效地址,继续解引用指针 cp 并检查 cp 所指的对象是否为空字符'\0',如果 cp 所指的对象不是空字符则条件满足,否则不满足。
本题中, if 条件部分为真。
//练习4.10:为 while 循环写一个条件使其从标准输入中读取整数,遇到 42 为止
int num;
while (cin >> num && num != 42);
//练习4.11:书写一条表达式用于测试4个值a、b、c、d的关系,确保a大于b、b大于c、c大于d。
int a, b, c, d;
a > b && b > c && c > d;
练习4.12:假设 i,j 和 k 是三个整数,说明表达式 i != j < k 的含义。
C++规定<、<=、>、>=的优先级高于 == 和 != ,因此上式的求值过程等同于 i 1= (j<k)。即先比较 j 和 k 的大小,得到的结果是一个布尔值;然后再判断 i 的值是否与之相等。
4.4 赋值运算
赋值运算符的左侧运算对象必须是一个可修改的左值。
int i = 0, j = 0, k = 0; //初始化而非赋值
const int ci = i; //初始化而非赋值
//下面的赋值语句都是非法的
1024 = k; //错误:字面值是右值
i + j = k; //错误:算术表达式是右值
ci = k; //错误:ci 是常量(不可修改的)右值
赋值运算的结果是它的左侧运算对象,并且是一个左值。相应的,结果的类型就是左侧运算对象的类型。如果赋值运算符左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。
k = 0; //结果:类型是 int,值是0
k=3.14; //结果:类型是 int ,值是3
C++11 新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。
k = { 3.14 }; //错误:窄化转换
vector<int> vi; //初始化为空
vi = { 0,1,2,3,4,5,6,7,8,9 }; //vi 现在含有10个元素了,值从0到9
无论左侧运算对象的类型是什么,初始值列表都可以为空。此时,编译器创建一个值初始化 的临时变量并将其赋值给左侧运算对象。
赋值运算满足右结合律
赋值运算符满足右结合律。
int ival, jval;
ival = jval = 0; //正确:都被赋值为0
对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同、或者可由右边对象的类型转换得到。
赋值运算优先级较低
因为赋值运算的优先级相对较低,所以通常需要给赋值部分加上括号使其符合我们的原意。
//这是一种形式琐烦、容易出错的写法
int i = get_value(); //得到第一个值
while(i != 42){
//其他处理......
i = get_value(); //得到剩下的值
}
在这段代码中,首先调用 get_value 函数得到一个值,然后循环部分使用该值作为条件。在循环体内部,最后一条语句会再次调用 get_value 函数并不断重复循环。可以将上述代码以更简单直接的形式表达出来。
int i;
//更好的写法:条件部分表达得更加清晰
while((i = get_value()) != 42){
//其他处理.....
}
如果不加括号的话含义会有很大变化,比较运算符 != 的运算对象将是 get_value 函数的返回值及 42,比较的结果不论真假将以布尔值的形式赋值给 i,这显然不是我们期望的结果。
切勿混淆相等运算符和赋值运算符
复合赋值运算符
每种运算符都有相应的复合赋值形式:
+= -= *= /= %= //算术运算符
<<= >>= &= ^= |= //位运算符
4.4 节练习
(a) d=3,i=3 (b)i=3,d=3.5
第一条语句发生编译错误,因为赋值运算符的左侧运算对象必须是左值;
第二条语句的意思是把42赋给 i,然后判断 i 的值是否为真,这样的话恒定为真。
是非法的,因为 pi 和另外两个变量类型不同,而且也无法通过类型转换得到。可以修改为:
dval = ival = 0;
pi = 0;
(a)的原意是把getPtr()得到的指针赋值给 p ,然后判断 p 是否是一个空指针,但是因为赋值运算符的优先级低于不相等运算符,所以真正的表达式求值过程是先判断 get_Ptr()的返回值是否为空指针,如果是则 p = 0,否则 p = 1,最后以 p 的值作为 if 语句的条件。要想符合原意,应改为: if((p=getPtr()) != 0)
(b)应改为:if(i == 1024)
4.5 递增和递减运算符
递增运算符(++)和 递减运算符(--)为对象的加1和减1操作提供了一种简洁的书写形式。
递增和递减运算符有两种形式:前置版本和后置版本。前置版本会首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。后置对象也会将运算对象加1(或减1),但是求值结果是运算对象改变之前那个值的副本。
int i = 0, j;
j = ++i; //j = 1,i = 1:前置版本得到递增之后的值
j = i++; //j = 1,i = 2:后置版本得到递增之前的值
建议:除非必须,否则不用递增递减运算符的后置版本
在一条语句中混用解引用和递增运算符
auto p = v.begin();
while(p != v.end() && *p >= 0)
cout<< *p++ <<endl;
后置递增运算符的优先级高于解引用运算符,因此 *p++ 等价于 *(p++) 。 p++ 把 p的值加1,然后返回 p 的初始值的副本作为其求值结果,此时解引用运算符的运算对象是 p 未增加之前的值。最终这条语句输出 p 开始时指向的那个元素,并将指针向前移动一个位置。
这种用法完全是基于一个事实,即后置递增运算符返回初始的未加1的值。如果返回的是加1之后的值,解引用该值将产生错误的结果。不但无法输出第一个元素,而且如果序列中没有负值,程序将试图解引用一个根本不存在的元素。
运算对象可按任意顺序求值
//该循环的行为是未定义的
while(beg != s.end() && !isspace(*beg))
*beg = toupper(*beg++); //错误:该赋值语句未定义
4.5 节练习
(a)先判断指针 ptr 是否为空,如果不为空继续判断指针所指的整数是否为非 0 数。如果非 0 ,则表达式的最终求值结果为真;否则为假。最后把指针 ptr 向后移动一位。该表达式从语法上分析是合法的,但是最后的指针移位操作不一定有意义。如果 ptr 所指的是整型数组中的某个元素,则 ptr 可以安装预期移动到下一个元素。如果 ptr 所指的只是一个独立的整数变量,则移动指针将产生未定义的结果。
(b)先检查 ival 的值是否非0,如果非 0 继续检查(ival +1)的值是否非 0 。只有当两个值都是非 0 值时,表达式的求值结果为真;否则为假。本式应该改写为 ival && (ival+1)。
(c)比较 vec[ ival ]和vec[ ival + 1]的大小,如果前者较小则求值结果为真,否则为假。本式应该改写为 vec[ ival ] <= vec[ ival + 1]。
4.6 成员访问运算符
点运算符和箭头运算符都可用于访问成员,其中,点运算符获取类对象的一个成员;箭头运算符与点运算符有关,表达式 ptr->mem 等价于 (*ptr).mem。
string s1 = "a string", *p = &s1;
auto n = s1.size();
n = (*p).size(); //运行时 p 所指对象的size成员
n = p->size(); //等价于n = (*p).size();
因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。如果没加括号,代码的含义就不同了。
*p.size();//错误: p 是一个指针,它没有名为 size 的成员
箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。
(a)合法。后置递增运算符的优先级高于解引用运算符,其含义是解引用当前迭代器所处位置的对象内容,然后把迭代器的位置向后移一位。
(b)非法。解引用 iter 得到 vector 对象当前的元素,结果是一个 string ,显然 string 没有后置递增操作。
(c)非法。解引用运算符的优先级低于点运算符,所以该式先计算 iter.empty(),而迭代器并没有定义 empty 函数,所以无法通过编译。
(d)合法。等价于 (*iter).empty();解引用迭代器得到迭代器当前所指的元素,结果是一个string ,显然字符串可以判断是否为空, empty 函数在此处有效。
(e)非法。该式先解引用 iter,得到迭代器当前所指的元素,结果是一个 string ,显然 string m没有后置递增操作。
(d)合法。等价于(*iter++).empty();含义是解引用迭代器当前位置的对象内容,得到一个字符串,判断该字符串是否为空,然后把迭代器向后移动一位。
4.7 条件运算符
条件运算符( ?: )允许我们把简单的 if-else 逻辑嵌入到单个表达式当中,条件运算符按照如下形式使用:
cond ? expr1 : expr2;
其中 cond 是判断条件的表达式,而 expr1 和 expr2 是两个类型相同或可能转换为某个公共类型的表达式。条件运算符的执行过程是:首先求 cond 的值,如果条件为真对 expr1 求值并返回该值,否则对 expr2 求值并返回该值。举例如下。
string finalgrade = (grade < 60) ? "fail" : "pass";
嵌套条件运算符
允许在条件运算符的内部嵌套另外一个条件运算符。也就是说,条件表达式可以作为另外一个条件运算符的 cond 或 expr。举例如下。
finalgrade = (grade > 90) ? "high pass"
: (grade < 60) ? "fail" : "pass";
第一个条件检查成绩是否在 90 分以上,如果是,执行符号 ? 后面的表达式,得到 "high pass";如果否,执行符号 : 后面的分支。这个分支本身又是一个条件表达式,它检查成绩是否在 60 分以下,如果是,得到 "fail";否则得到"pass"。
条件运算符满足右结合律,意味着运算对象(一般)按照从右到左的顺序组合。因此在上面的代码中,靠右边的条件运算(比较成绩是否小于 60)构成了靠左边的条件运算的 : 分支。
在输出表达式中使用条件运算符
条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。
cout << ((grade < 60) ? "fail" : "pass");//输出 pass 或 fail
cout << (grade < 60) ? "fail" : "pass"; //输出 1 或者 0
cout << grade < 60 ? "fail" : "pass"; //错误:试图比较 cout 和 0
在第二条表达式中, grade 和 60 的比较结果是 << 运算符的运算结果,因此 grade<60为真输出1,否则输出0。 << 运算符的返回值是 cout,接下来 cout 作为条件运算符的条件。第二条表达式等价于:
cout << (grade < 60); //输出1或者0
cout ? "fali" : "pass"; //根据 cout 的值是 true 还是 false 产生对应的字面值
第三条表达式等价于下面的语句,所以它是错误的。
cout << grade; //小于运算符的优先级低于移位运算符,所以先输出 grade
cout << 60 ? "fail" : "pass";//然后比较 cout 和60!
4.7 节练习
//练习4.21:编写一段程序,使用条件运算符从 vector<int> 中找到哪些元素的值是奇数,然后将这些奇数值翻倍。
vector<int> v;
srand((unsigned)time(NULL));
cout << "v中的数为: ";
for (int i = 0; i < 10; ++i)
{
v.push_back(rand() %100);
cout << v[i] << " ";
}
cout << endl;
for (auto &val : v)//记得使用引用
{
val = (val % 2 != 0) ? val*2 : val;
}
cout << "将奇数值翻倍后:";
for (auto it = v.cbegin(); it != v.cend(); ++it)
{
cout << *it << " ";
}
cout << endl;
//练习4.22:扩展程序进一步将60分到75分之间的成绩设定为 low pass。
//要求该程序包含两个版本:一个版本只使用条件运算符
int grade;
string result;
result = (grade > 90) ? "high pass"
: (grade > 75) ? "pass"
: (grade < 60) ? "low pass" : "fail";
//另外一个使用1个或多个 if 语句
string result;
if (grade > 90)
{
result = "high pass";
}
else if ( grade > 75)
{
result = " pass";
}
else if (grade > 60)
{
result = "low pass";
}
else
{
result = "fail";
}
}
//练习4.23:因为运算符的优先级问题,下面这条表达式无法通过编译。指出它的问题在哪并修改。
string s = "word";
string p1 = s + s[s.size() - 1] == 's' ? " " : "s";
//修改后
string p1 = s + (s[s.size() - 1] == 's' ? " " : "s");
练习4.24:本节的示例程序将成绩划分成high pass、pass 和fail,它的依据是条件运算符满足右结合律。假如条件运算符满足的是左结合律,求值过程将是怎样的?
//原文的程序
finalgrade = (grade > 90) ? "high pass"
: (grade < 60) ? "fail" : "pass";
//根据左结合律的含义,该式等价于:
finalgrade = ((grade >90) ? "high pass" : (grade < 60))
? "fail" : "pass";
先考查 grade > 90 是否成立,如果成立,第一个条件表达式的值为"high pass";如果不成立,第一个条件表达式的值为 grade < 60。这句语句是无法编译通过的,因为条件运算符要求两个结果表达式的类型相同或者可以互相转化。即使假设语法上通过,也就是说,第一个条件表达式的求值结果分为 3 种,分别是 "high pass"、1 和 0。接下来根据第一个条件表达式的值求解第二个条件表达式,求值结果是 "fail" 或 "pass"。上述求值过程显然与我们的期望是不符的。
4.8 位运算符
位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。
运算符 | 功能 | 用法 |
- | 位求反 | - expr |
<< | 左移 | expr1 << expr2 |
>> | 右移 | expr1 >> expr2 |
& | 位与 | expr & expr |
^ | 位异或 | expr ^ expr |
| | 位或 | expr | expr |
一般来说,如果运算对象是“小整型”,则它的值会被自动提升成较大的整数的类型。运算对象可以说带符号的,也可以是无符号的。如果运算对象是带符号的且它的值为负,那么位运算符如何处理对象的“符号位”依赖于机器。而且,此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。
强烈建议仅将位运算用于处理无符号类型。
移位运算符
这两种运算符的内置含义是对其运算对象执行基于二进制位的移动操作,首先令左侧运算对象的内容按照右侧运算对象的要求移动指定位数,然后将经过移动的(可能还经过了提升的)左侧运算对象的拷贝作为求值结果。其中,右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则会产生未定义的行为。
二进制位或者向左移(<<)或者向右移(>>),移出边界之外的位就被舍弃掉了。
左移运算符(<<)在右侧插入值为 0 的二进制位。右移运算符(>>)的行为则依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在左侧插入值为 0 的二进制位;如果该运算对象是带符号类型,在左侧插入符号位的副本或值为 0 的二进制位,如何选择要视具体环境而定。
位求反运算符
位求反运算符(-)将运算对象逐位求反后生成一个新值,将 1 置为 0 、将 0 置为 1;
位与、位或、位异或运算符
与(&)、或(|)、异或(^)运算符在两个运算对象上逐位执行相应的逻辑操作。
对于 与运算符(&)来说,如果两个运算对象的对应位置都是 1 则运算结果中该为为 1 ,否则为 0 。对于 位或运算符(|)来说,如果两个运算对象的对应位置至少有一个为 1 则运算结果中该位为 1 ,否则为 0,。对于 位异或运算符(^)来说,如果两个运算对象的对应位置有且只有一个为 1 则运算结果中该为 为 1, 否则为 0。(相异为1,相同为0)。
使用位运算符
举例使用位运算符:假设班级中有30个学生,老师每周都会对学生进行一次小测验,测验的结果只有通过和不通过两种。我们用一个二进制位代表某个学生在一次测验中是否通过,显然去哪办的测验结果可以用一个无符号整数来表示。
unsigned long quizl = 0;//我们把这个值当成是位的集合来使用
如果第27名学生通过了测验,那么只需要将一个只有第27位是1其他都是0的数与 quizl 进行位或运算即可。
如果重新核对时发现第27名学生没有通过测验,那么可以将 quizl 与一个第27位是0、其他所有位都是1 的整数进行 与运算就能实现目的了。
检测第27名学生是否通过:将 quizl 和一个只有第27位是1的值按位求与,如果 quizl 的第27位是1,计算结果就是非0(真);否则结果是 0。
移位运算符(又叫 IO 运算符)满足左结合律
因为移位运算符满足左结合律,所以表达式:
cout << "hi" << " there" << endl;
的执行过程实际上等同于
((cout << "hi") << " there" ) << endl;
在这条语句中,运算对象 "hi" 和第一个 << 组合在一起,它的结果和第二个 << 组合在一起,接下来的结果再和第三个 << 组合在一起。
移位运算符的优先级比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。
cout << 42 +10; //正确: + 的优先级更高
cout << (10 < 42); //正确:括号使运算对象按照我们的期望组合在一起,输出1
cout << 10 < 42; //错误:试图比较 cout 和42
4.8 节练习
原本为 00000000 00000000 00000000 0110001先取反,变为11111111 11111111 11111111 10001110,再左移6位,得到:11111111 11111111 11100011 10000000
C++ 规定整数按照其补码形式存储,对上式求补,得到 10000000 0000000 0011100 10000000,转换成10进制数为 -7296。
班级中有30位学生,C++ 规定 unsigned long 在内存中至少占 32位,这样就足够存放 30个学生的信息。
如果用 unsigned int 作为 quizl 的类型,则由于 C++ guiding unsigned int 所占空间的最小值为16,所以在很多机器环境中,该数据类型不足以存放全部学生的信息,从而造成了信息丢失,无法满足题目的要求。
ull:0000 0011 ul2:0000 0111
(a)0000 0011,即3 (b)0000 0111,即7
(c)true (d) true
4.9 sizeof 运算符
sizeof 运算符返回一条表达式或一个类型名字所占的字节数。sizeof 运算符满足右结合律,其所得的值是一个 size_t 类型的常量表达式。运算符的运算对象有两种形式
sizeof (type)
sizeof expr
Sales_data data, *p;
sizeof(Sales_data );//存储Sales_data 类型的对象所占的空间大小
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)。
因为 sizeof 不会实际求运算对象的值,所以即使 p 是一个无效(即未初始化)的指针也不会有什么影响。
C++11 新标准允许我们使用作用域来获取类成员的大小。
sizeof运算符的结果部分地依赖于其所作用的类型:
- 对 char 或者类型为char的表达式执行 sizeof 运算,结果为 1。
- 对引用类型执行 sizeof 运算得到被引用对象所占空间的大小。
- 对指针执行 sizeof 运算得到指针本身所占空间的大小。
- 对解引用指针执行 sizeof 运算得到指针指向的对象所占空间的大小,指针不需有效。
- 对数组执行 sizeof 运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次 sizeof 运算并将所得结果求和。注意,sizeof 运算不会吧数组转换成指针来处理。
- 对 string 对象或 vector 对象执行 sizeof 运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。
string s1("hhhhh");
string s2("hhh");
cout << "sizeof s1:" << sizeof s1 << endl;
cout << "sizeof s2:" << sizeof s2 << endl;
cout << "sizeof string:" << sizeof(string) << endl;
可以用数组的大小除以单个元素的大小得到数组中元素的个数:
//sizeof(ia)/sizeof(*ia) 返回 ia 的元素个数
constexpr size_t sz = sizeof(ia)/sizeof(*ia);
int arr2[sz]; //正确:sizeof返回一个常量表达式
4.9 节练习
//练习4.28:编写一段程序,输出每一种内置类型所占空间的大小
cout << "类型名称\t" << "所占空间" << endl;
cout << "bool\t\t" << sizeof(bool) << endl;
cout << "char\t\t" << sizeof(char) << endl;
cout << "wchar_t\t\t" << sizeof(wchar_t) << endl;
cout << "char16_t\t" << sizeof(char16_t) << endl;
cout << "char32_t\t" << sizeof(char32_t) << endl;
cout << "short\t\t" << sizeof(short) << endl;
cout << "int\t\t" << sizeof(int) << endl;
cout << "long\t\t" << sizeof(long) << endl;
cout << "long long\t" << sizeof(long long) << endl;
cout << "float\t\t" << sizeof(float) << endl;
cout << "double\t\t" << sizeof(double) << endl;
cout << "long double\t" << sizeof(long double) << endl;
32位环境:(切换为64位编译环境,结果也没变,不知道为啥)
//练习4.29:推断出下面程序的输出结果并说明理由。实际运行这段程序。
int x[10];
int *p = x;
cout << sizeof(x) / sizeof(*x) << endl;//输出10
cout << sizeof(p) / sizeof(*p) << endl;//输出0,实际是1,因为指针占4字节
(a)sizeof(x + y) (b)无需加括号
(c) sizeof(a < b) (d) 无需加括号
4.10 逗号运算符
逗号运算符(comma operator)含有两个运算对象,按照从左向右的顺序一次求值。
对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。 如果右侧运算对象是左值,那么最终的求值结果也是左值。
//逗号运算符常被用在 for 循环中
vector<int> ivec;
vector<int>::size_type cnt = ivec.size();
//将把从 size 到1的值赋给 ivec 的元素
for (vector<int>::size_type ix = 0;
ix != ivec.size(); ++ix, --cnt)
ivec[ix] = cnt;
这个循环在 for 语句的表达式中递增 ix、递减 cnt,每次循环迭代 ix 和 cnt 相应改变。只要 ix 满足条件,我们就把当前元素设成 cnt 的当前值。
4.10 节练习
除非必须,否则不用递增(递减)运算符的后置版本。前置版本的递增运算符避免了不必要的工作,它把值加 1 后直接返回改变了的运算对象。与之相比,后置版本需要把原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改之前的值,那么后置版本的操作就是一种浪费。
首先定义一个常量表达式 size,它的值是 5;接着以 size 作为维度创建一个整型数组 ia,5个元素分别是 1-5。
for 语句头包括三部分:第一部分定义整型指针指向数组 ia 的首元素,并且定义了一个整数 ix,赋给它初值 0;第二部分判断循环终止条件,当 ix 没有达到 size 同时指针 ptr 没有指向数组最后一个元素的下一位置时,执行循环体;第三部分令变量 ix和指针 ptr 分别执行递增操作。
C++规定条件运算符的优先级高于逗号运算符,所以 这条表达式等价于 (someValue ? ++x,++y : --x), --y)。它的求值过程是,首先判断 someValue是否为真,如果为真,依次执行++x和++y,最后执行--y;如果为假,执行 --x和--y。
4.11 类型转换
在C++语言中,某些类型之间有关联。如果两种类型有关联,那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。换句话说,如果两种类型可以相互转换(conversion),那么它们就是关联的。
int val = 3.541 + 3; //编译器可能会警告该运算损失了精度
加法的两个运算对象类型不同,C++语言不会直接将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象统一后再求值。上述的类型转换是自动执行的,因此,它们被称做隐式转换(implicit conversion)。
算术类型之间的隐式转换被设计得尽可能避免损失精度。很多时候,如果表达式中既有整数类型的运算对象也有浮点数类型的运算对象,整型会转换成浮点型。
何时发生隐式类型转换
- 在大多数表达式中,比 int 类型小的整型值首先提升为较大的整数类型。
- 在条件中,非布尔值转换成布尔类型。
- 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 如第6章将要介绍的,函数调用时也会发生类型转换。
4.11.1 算术转换
算术转换(arithmetic conversion)的含义是把一种算术类型转换成另外一种算术类型。算术转换的和规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。例如,如果一个运算对象的类型是 long double,那么不论另外一个运算对象的类型是什么都会转换成 long double。
整型提升
整型提升(integral promotion)负责把小整数类型转换成较大的整数类型。
无符号的运算对象
如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。但是如果某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。
如果一个运算对象是无符号类型、另外一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。
剩下的一种情况是带符号类型大于无符号类型,此时转换的结果依赖于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。如果不能,那么带符号类型的运算对象转换成无符号类型。
理解算术转换
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
usval + ival; //根据 unsigned short 和 int 所占空间的大小进行提升
uival + lval; //根据 unsigned int 和 long 所占空间的大小进行提升
4.11.1 节练习
(a)自动转换成布尔值 (b)fval和ival先转换成float,然后运算结果转换成 double
(c)cval先转换成int,然后cval和ival的运算结果转换成double,再与dval相加
(a)发生了,字符'a'提升为int,与3相加所得的结果再转换为char并赋给cval (b)发生了 ,ival转换为 double,与1.0相乘的结果也是double类型,ui转换为double类型后与乘法得到的结果相减,最终的结果转换为float并赋给 fval
(c)发生了,ui转换为 float ,与 fval相乘的结果转换为 double类型并赋给dval;
(d)发生了,ival 转换成float ,与 fval相加所得的结果转换为 double类型,再与dval相加后结果转换为 char类型。
4.11.2 其他隐式类型转换
除了算术转换之外还有几种隐式类型转换,包括如下几种
数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针。
当数组被用作 decltype 关键字的参数,或者作为取地址符(&)、sizeof及 typeid等运算符的运算对象时,上述转换不会发生。
指针的转换:C++ 还规定了几种其他的指针转换方式,包括常量整数值 0 或者字面值 nullptr 能转换成任意指针类型;指向任意非常量的指针能转换成 void*;指向任意对象的指针能转换成 const void*。
转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果是 false;否则转换结果是 true。
转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果 T 是一种类型,我们就能将指向 T 的指针或引用分别转换成指向 const T的指针或引用。
类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。
string s,t="a,value"; //字符串字面值转换成 string 类型
while(cin>>s) //while 的条件部分把 cin 转换成布尔值
4.11.3 显式转换
有时我们希望显式地将对象强制转换成另外一种类型。例如,如果想在下面的代码中执行浮点数除法:
int i, j;
double slope = i / j;
就要使用某种方法将 i 和/或 j 显式地转换成 double ,这种方法称作强制类型转换(cast)。
命名的强制类型转换
一个命名的强制类型转换具有如下形式:
cast-name<type>(expression);
其中, type 是转换的目标类型而 expression 是要转换的值。如果 type 是引用类型,则结果是左值。cast-name 是 static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。dynamic_cast支持运行时类型识别。
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用 static_cast。
//进行轻质类型转换一遍执行浮点数除法
double slope = static_cast<double>(j) / i;
const_cast
const_cast 只能改变运算对象的底层 const。
const char *pc;
char *p = const_cast<char*>(pc);//正确:但是通过p写值是未定义的行为
对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉 const 性质(cast away the conat)”。一旦我们去掉了某个对象的const 性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。然而如果对象是一个常量,再使用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只改变常量属性
reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。假设有如下的转换:
int *ip;
char *pc = reinterpret_cast<char*>(ip);
我们必须时刻牢记 pc 所指的真实对象是一个 int 而非字符,如果把 pc 当成普通的字符指针使用就可能在运行时发生错误。例如:
string str(pc);
建议:避免强制类型转换
旧式的强制类型转换
在早期版本的C++语言中,显示地进行强制类型转换包含两种形式:
type(expr); //函数形式的强制类型转换
(type) expr; //C语言风格的强制类型转换
4.11.3 节练习
i *= static_cast<int>(d);
//(a)
pv = static_cast<void *>(const_cast<string *>(ps));
//(b)
i = static_cast<int>(*pc);
//(c)
pv = static_cast<void*>(&d);
//(d)
pc = static_cast<char*>(pv);
练习4.38:说明下面这条表达式的含义。
double slope = static_cast<double>(j/i);
把 j / i 的值强制类型转换成 double,然后赋值给slope。请注意,如果 i 和 j 的类型都是 int ,则 j / i 的求值结果仍然是 int,即使除不尽也只保留商的整数部分,最后再转换成 double类型。