C++ 学习之五、C++ 运算符

每种语言都有类似的运算符,如同数学运算符。

基本运算符

C++ 基本运算包括加、减、乘、除和求模运算。

  • 加法运算,运算符为 +。如 4 + 20
  • 减法运算,运算符为 -。如 20 - 3
  • 乘法运算,运算符为 *。如 5 * 6
  • 除法运算,运算符为 /。如 20 / 2
  • 求模运算,它等于第一个数除以第二个数后的余数,运算符为 %。如 19 % 3,结果为 1。

递增(++)与递减(–)运算符

常用于循环操作中,将循环计数加 1 或减 1。这两种运算符都有两种变体,前缀版本位于操作数之前,如 ++a,后缀版本位于操作数后面,如 a++。两个版本对操作数的影响是一样的,但是影响的时间不同。

前缀版本

前缀版本将操作数操作完成之后用于下一步运算。

1
2
3
4
using namespace std;
int a = 5;
cout << ++a; // 先将 a 值加 1,然后使用新的值来计算表达式,这里打印值为 6
cout << a; // a 的值最终为 6

后缀版本

后缀版本先使用操作数当前的值计算表达式,再将值加 1 或减 1。

1
2
3
4
using namespace std;
int a = 5;
cout << a++; // 先使用 a 当前值 5 计算表达式,这里打印值为 5,再将 a 的值加 1
cout << a; // a 的值最终为 6

Tips: 不要在同一条语句中对同一个值递增或递减多次,对这种语句,结果将是不确定的。

前后缀格式的效率

前缀格式和后缀格式在效率上有着微小的差别。

前缀函数将值加 1,然后返回结果。后缀函数首先复制一个副本,原变量加 1,然后将复制的副本返回。对于语言内置类型和当代编译器而言,这看似不是什么问题。但对于自定义的类而言,前缀版本的效率比后缀版本高。

递增递减运算符和指针

可以将该运算符用于指针和基本变量。运用到指针上时,将把指针的值增加或减小其指向的数据类型占用的字节数。

1
2
3
double arr[5] = {21.1, 23, 4, 54.6, 3.99};
double *pt = arr; // double 指针 pt 指向 arr[0]
++pt; // 指针值加 1,指向下一个元素 arr[1]

前缀递增、前缀递减和解除引用运算符的优先级相同,以从右到左的方式进行结合。后缀递增和后缀递减的优先级相同,但比前缀运算符的优先级高,这两个运算符以从左到右的方式进行结合。

1
2
3
4
5
// 紧接上面 pt 指向 arr[1] 的语句
double x = *++pt; // double 指针 pt 先加 1,再取值,pt 指向 arr[2], x = 4
++*pt; // 先解除引用,把值加 1,也就是说 arr[2] 的值 4 将变为 5,pt 值不变
(*pt)++; // 括号优先级高,先取值,再加,即上面运算得到的 5 再加 1,arr[2] 中的值将变为 6
x = *pt++; // 后缀运算符的等级高,这意味着 ++ 运用在 pt 上而非 *pt,然而后缀运算符是先将复制的副本返回,所以 *pt++ 的值为 *pt,然后将 pt+1,所以 *pt++ 的值为 6,然后将指针加 1

组合运算符

i = i + by 同等于使用组合运算符 i += by

以下为组合赋值运算符总结

操作符 作用(L 为左操作数,R 为右操作数)
+= 将 L+R 赋给 L
-= 将 L-R 赋给 L
*= 将 L*R 赋给 L
/= 将 L/R 赋给 L
%= 将 L%R 赋给 L

逗号运算符

逗号运算符允许将两个表达式放到 C++ 句法只允许放一个表达式的地方。逗号运算符最常见的用途是将两个或更多的表达式放到一个 for 循环表达式中。

C++ 为逗号运算符提供了两个特性。首先,它确保先计算第一个表达式,然后计算第二个表达式(逗号运算符是一个顺序点),其次,逗号表达式的值是第二部分的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
int main()
{
using namespace std;
cout << "Enter a word: ";
string word;
getline(cin, word);

char temp;
int i, j; // 这里不是逗号运算符,声明时逗号只是用于分隔
for (j = 0, i = word.size() - 1; j < i; --i, ++j) // 两次使用逗号运算符
{
temp = word[i];
word[i] = word[j];
word[j] = temp;
}
cout << word << "\nDone\n";
return 0;
}

逗号运算符的优先级最低。

1
2
carts = 17,240; // 由于优先级 (carts = 17), 240,后面部分没作用
carts = (17,240); // 逗号运算符的值为右边部分,所以 carts = 240

运算符重载

这里只介绍一下基本概念。

如除法运算符表示了 3 种不同的运算:int 除法、float 除法和 double 除法。C++ 根据上下文(这里是操作数的类型)来确定运算符的含义。使用相同的符号进行多种操作叫作运算符重载(operator overloading)。

运算符优先级

当两个运算符作用于同一个操作数时,需要考虑先运算哪部分,这里就需要了解运算符优先级。所有的运算优先级如下

C++ 运算优先级

运算时的类型转换

各种算术运算涉及到的硬件编译指令都是各不相同的,因此计算机需要处理大量不同的情况,尤其对不同的类型进行运算时。为处理这种潜在的混乱,C++ 自动执行很多类型转换。

  • 将一种算术类型的值赋给另一种算术类型变量时,C++ 将对值进行转换。
  • 表达式中包含不同类型时,C++ 将对值进行转换。
  • 将参数传递给函数时,C++ 将对值进行转换。

初始化和赋值进行的转换

C++ 允许将一种类型的值赋给另一种类型的变量。该值将被转换为接收变量的类型。如

1
2
double a = 3.456;   // a 为 double 类型
int b = a; // 将 double 类型的值赋给 int 类型,将去掉 double 类型小数部分,所以 b 值为 3

这种类型的转换在一些情况下可能出现问题

转换 潜在的问题
将较大的浮点类型转换为较小的浮点类型,如将 double 转换为 float 精度(有效位数)降低,值可能超出目标类型的取值范围,在这种情况下,结果将是不确定的
将浮点型转换为整型 小数部分丢失,原来的值可能超出目标类型的取值范围,在这种情况下,结果将是不确定的
将较大的整型转换为较小的整型,如将 long 转换为 short 原来的值可能超出目标类型的取值范围,通常只复制右边的字节

Tips: 将 0 赋给 bool 变量时,将被转换为 false,而非零值将被转换为 true

以 { } 方式初始化时进行的转换(C++11)

C++ 将使用 { } 的初始化称为列表初始化(list-initialization),与前一种初始化转换方式相比,它对类型转换的要求更严格。列表初始化不允许缩窄(narrowing),也即不允许转换为无法表示该值的数据类型。例如:

  • 不允许浮点型转换为整型。
  • 不同的整型之间的转换或将整型转换为浮点型可能被允许,条件是编译器知道目标变量能够正常地存储赋给它的值。
1
2
3
4
5
6
const int code = 66;    // const 修饰的常量,code 值固定不可更改
int x = 66;
char c1 {31325}; // 非法,不允许缩窄
char c2 {66}; // 合法,char 类型能处理值 66
char c3 {code}; // 合法,code 为常量 66,可以被 char 处理
char c4 {x}; // 非法,区别于上一段代码,x 值虽然为 66,但编译器看来,x 是一个 int 变量,其值可能很大。编译器不会跟踪后面阶段可能发生的情况;此处列表初始化时,并不是严格要求是 const 常量,如果 { } 中为变量,需要求被赋值的变量类型能处理该值,例如如果 x 为 char 类型变量,这个初始化则是正确的。

表达式中的转换

当同一个表达式中包含两种不同的算术类型时,C++ 将执行两种自动转换。首先,一些类型在出现时便会自动转换;其次,有些类型在与其他类型同时出现在表达式中时将被转换。

一些类型在出现时便会进行自动转换

在计算表达式时,C++ 将 boolcharunsigned charsigned charshort 值转换为 int。具体地说,true 被转换为 1,false 被转换为 0。这些转换被称为整型提升(integral promotion)。例如:

1
2
3
short chickens = 20;
short ducks = 35;
short fowl = chickens + ducks; // C++ 程序取得 chickens 和 ducks 的值,并将它们转换为 int 类型,计算完后,将值转换为 short 类型赋给 fowl

还有一些其它整型提升:如果 shortint 短,则 unsigned short 类型将被转换为 int;如果两种类型的长度相同,则 unsigned short 类型将被转换为 unsigned int。这种规则确保了在对 unsigned short 进行提升时不会损失数据。

将不同的类型进行算术运算时,也会进行一些转换

当运算涉及到两种类型时,较小的类型将被转换为较大的类型。其中规则为:

long double > double > float > 整型提升

传递参数时的转换

传递参数时的类型转换通常由 C++ 函数原型控制。然而,也可以取消原型对参数传递的控制,尽管这样做并不明智。在这种情况下,C++ 将对 char 和 short 类型(signed 和 unsigned)应用整型提升。另外,为保持与传统 C 语言中大量代码的兼容性,在将参数传递给取消原型对参数控制的函数时,C++ 将 float 提升为 double。

强制类型转换

C++ 允许通过强制类型转换机制显式地进行类型转换。

普通强制类型转换语法

1
2
(typeName) value    // 将 value 强制转换为 typeName 类型,如 (int) 'Q',兼容 C 语言
typeName (value) // 如 int ('Q'),C++ 格式,基于面向对象的语法

C++ 引入的 4 个强制类型转换运算符

引用的强制类型转换运算符的使用要求较为严格

1
static_cast<typeName> (value)   // 将 value 转换为 typeName 类型