算术运算、类型转换


C++ 提供算术相关的加(+)、减(-)、乘(*)、除(/) 以及余除运算符(%)或称模数(Modulus)运算符,这类以数学运算为主的运算符,称为「算术运算符」(Arithmetic operator)。

这类运算符的使用基本上由左而右进行运算,遇到加减乘除的顺序问题时,也是先乘除后加减,必要时加上括号表示运算的先后顺序,例如这个代码会在控制台显示 7:

cout << (1 + 2 * 3);

编译器在读取代码时,是由左往右读取的,由于习惯将分子写在上面,而分母写在下面,使得初学者往往将之写成了:

cout << 1 + 2 + 3 / 4;

这个程序事实上会是这样运算的:1 + 2 + (3 / 4);为了避免错误,加上括号才是最保险的,例如:

cout << (1 + 2 + 3) / 4;

%运算符是余除运算符,它计算除法后的余数,一个例子是要产生指定位数的乱数,可以使用%运算符,假设乱数产生函数为rand,可以产生正整数乱数,可以如下产生 0 到 99 的乱数:

cout << rand() % 100;

也可以利用%来作循环计数,例如由 0 计数至 9 不断循环:

counter = (counter + 1) % 10;

算术运算符使用不难,但要注意类型转换的问题,先看看这段程序会印出什么结果?

int number = 10;
cout << number / 3;

答案不是 3.3333,而是 3,小数点之后的部份被自动消去了,这是因为number是整数,而除数 3 也是整数,运算出来的程序被自动转换为整数了, 那下面这个程序呢?

double number = 10.0;
cout << number / 3;

这个程序的结果会显示 3.3333,这是 C++ 做了类型的隐式转换(Implicit conversion),在一个类型混杂的表达式中,长度较长的数据类型会成为目标类型,较小的类型会自动提升,因而在上例中 3 会被提升为 3.0 再进行运算,结果就可以显示无误,这样的转换又称算术转换(Arithmetic conversion)。

在一个指定的动作中,左值会成为目标类型,当右值类型比左值类型长度小时,右值会自动提升为目标类型,例如:

int num = 10;
double number = num;

在上例中,number的值最后会是 10.0,在指定的动作时,如果右值类型比左值类型类型长度大时,超出可存储范围的部份会被自动消去,例如将浮点数指定给整数变量,小数的部份会被自动消去,例子如下,程序会显示 3 而不是 3.14:

int num = 0;
double number = 3.14;
num = number;
cout << num;

由于会失去精度,若想要编译器在这类情况提出警讯,可以在编译时加上-Wconversion实参。

算术运算必要时,得进行类型的显式转换(Explicitly conversion),例如底下会显示 3:

int a = 10;
int b = 3;
cout << a / b; // 显示 3

这是因为ab都是int,计算结果也就是int,想得到小数的结果,必须显式地转换类型,方式之一是使用旧式的 C 转型(cast)语法:

cout << (double) a / b; // 显示 3.33333

或者是使用函数标示方式:

cout << double(a) / b; // 显示 3.33333

显式转型的目的是提供编译器信息,就以上而言,就是告诉编译器,将a的值提升为double;类似地,如果编译时加上了-Wconversion实参,若指定会失去精度就会发出警讯,若某些场合中,这确实就是你想要的,也可以显式转型,这样编译器就会住嘴了。例如:

int num = 0;
double number = 3.14;
num = int(number);     // 编译时加上 `-Wconversion` 实参也不会有警讯
cout << num;

对于基本类型来说,这样就足够了,不过这种转型是强制性的,也就是加上以上的转型语法,不管什么情况,编译器就都噤声了,如果因为编译时加上-Wconversion实参,有些开发者只是为了消除这类警讯,不管三七二十一都用这种方式强制转型,执行时期就可能因为精度遗失而发生问题,而 C++ 中还有指针、类等类型,无差别地强制转换(例如将Dog类指针转为Cat类指针),可能导致执行时期错误或不可预期的结果。

在 C++ 中为了避免这类问题,定义了四种用于不同场合的具名转型(named casting):

  • static_cast
  • const_cast
  • reinterpret_cast
  • dynamic_cast

其中static_cast的一部份应用场合,就是算术运算时的显式转换,例如:

int a = 10;
int b = 3;
cout << static_cast<double>(a) / b; // 显示 3.33333

或者是:

int num = 0;
double number = 3.14;
num = static_cast<int>(number);     // 编译时加上 `-Wconversion` 实参也不会有警讯
cout << num;

表面上看来,static_cast也是单纯叫编译器住嘴,实际上不然,例如以下在编译时会发生错误:

const double PI = 3.14159;
double *pi = &PI;  // error: invalid conversion from 'const double*' to 'double*'

C 风格转型语法加上后,编译器会完全闭嘴:

const double PI = 3.14159;
double *pi = (double*) &PI; // 没有错误也没有警讯

然而,static_cast会有编译错误:

const double PI = 3.14159;
double *pi = static_cast<double*>(&PI); // error: invalid static_cast from type 'const double*' to type 'double*'

目前还没谈到指针,然而可以先知道的是,PI是个const修饰过的变量,存储的值是只读的,以上代码试图将只读的内存空间地址指定给pi,如果之后试图对pi地址处的数据做变动,执行时期会有不可预期的结果,为此编译器不能通过编译,若真要通过编译,得使用const_cast

const double PI = 3.14159;
double *pi = const_cast<double*>(&PI);

当然,这只是叫编译器住嘴罢了,后续代码也是别对pi地址处的数据做变动,以避免执行时期不可预期的结果。

其他有关 C++ 具名转型,后续在适当的地方还会谈到,简单来说,C++ 希望开发者可以依不同的场合选择的具名转型,以便在编译时期提供不同粒度的检查,而不是像 C 风格的方式一律住嘴。


展开阅读全文