运算符和类型转换#
一个例子#
有了变量,我们就可以进行算术运算了,下面是一个例子。
/*arith_op.c*/
#include <stdio.h>
int main(void)
{
    int lhs = 10, rhs = 20;
    printf("%d\n", lhs + rhs);
    return 0;
}
30
当然,我们希望类型之间可以相互转换,下面给出了一个例子:
/*type_conv.c*/
#include <stdio.h>
int main(void)
{
    int var_int = 1;
    float var_float = (float)var_int;
    printf("%f\n", var_float);
    return 0;
}
1.000000
运算符(Operator)和表达式(Expression)#
表达式是由操作数(operand)和运算符组成。其中,操作数可以是:
- 字面量、常量
 - (已声明的)标识符
 - 另一个表达式
 
等1。而运算符负责串联起操作数,组成新的表达式。
表达式表示了一个值。左值表达式还表示了这个值的对象标识,可供赋值。 如果我们像数学中那样将运算符看成一个函数的话(如 \(a + b\) 看成 \(f_+(a, b)\))。 我们也可以说运算符“返回”了一个值。
所以赋值运算符返回的值是什么?
赋值运算符返回的值是被赋值的那个值。如a = 2返回的就是2,a = b返回的就是b的值。
赋值运算符是右结合的,也就是说a = b = c如同先处理b = c,
再处理a = c的值。
赋值表达式是右值表达式,不能被赋值。(a = b) = c是非法的。
运算符还可能会有除了返回值以外的其他作用,称为副作用(side effect)。 赋值运算符就是典型的具有副作用的运算符。其返回值是所赋之值,副作用是给变量赋值。
算术运算(Arithmetic Operation)#
算术运算是C中的一类最基本的运算。它们返回值是运算的结果,没有副作用。 C语言中有以下的算术运算符:
- 一元运算符
- 正号
+ - 负号
- 
 - 正号
 - 四则运算与取模
- 加法
+ - 减法
- - 乘法
* - 除法
/ - 取模
% 
 - 加法
 - 位运算
- 逐位非
~ - 逐位与
& - 逐位或
| - 逐位异或
^ - 逐位左移
<< - 逐位右移
>> 
 - 逐位非
 
Attention
所有的算术表达式都是右值。它们无法被赋值。
一元(Unary)算术运算符#
一元算术运算符
+ 表达式
- 表达式
一元算术运算符将表达式保持原值(正号+)或取相反数(符号-),并进行整数提升
(见下)。
以下是一个例子:
#include <stdio.h>
int main(void)
{
    int var = -1;
    printf("%d\n", var);
    return 0;
}  
这里将整数字面量和负号结合起来初始化一个变量。这样可以初始化一个负数值。
Tip
在赋值的时候,我们可以将一个变量的值经过计算后结果重新赋给这个变量。如:
a = -a;
意思是取出a的值,变为相反数,再赋值给a。也就是,a变为原来的相反数。
四则运算符与取模运算符#
四则运算符与取模运算符
表达式 + 表达式
表达式 - 表达式
表达式 * 表达式
表达式 / 表达式
整数表达式 % 整数表达式
在左右表达式都是算术类型的情况下,四则运算符都如同在数学中的定义,只有以下不同:
除法#
对于除法来说,如果它的左右表达式的类型都是整数,那么结果为代数商(非分数),向零截断。
如以下的例子:
#include <stdio.h>
int main(void)
{
    int foo = -7 / 3, bar = 7 / 3;
    printf("%d %d\n", foo, bar);
    return 0;
}  
Undefined-behavior
整数除法中,如果被除表达式是0,则会是未定义行为。
会输出:-2 2。
如果左右表达式类型为浮点数,则会算出一个浮点数。如以下的例子:
#include <stdio.h>
int main(void)
{
    float foo = -7.f / 3.f;
    printf("%f\n", foo);
    return 0;
}
会输出:-2.333333。
Attention
如果编译器的浮点数算术遵循IEEE-754,则非零浮点数除以0.0结果是INFINITY,
除以-0.0结果是-INFINITY,0.0 / 0.0结果是NaN。否则为未定义行为。
取余#
Attention
取余只能运用在整数类型上。右操作数不能是0。
取余运算与商有密切关系。如果q = a / b,那么a % b与a - q * b相等。
对于正整数,这就是通常意义上的余数。同样,这适用于负数的情况。
这个例子会输出-1:
#include <stdio.h>
int main(void)
{
    int foo = -7 % 3;
    printf("%d\n", foo);
    return 0;
}
不难发现,取余结果的符号与左操作数同号。
浮点无穷大计算#
浮点无穷大遵循以下的规律运算: - 同号无穷大相减或异号无穷大相加得出NaN。 - 无穷大乘0得出NaN,否则根据符号得出(相应符号)无穷大。 - 无穷大除以有限值得到(相应符号)无穷大,有限值除以无穷大得到(相应符号)零。
NaN计算#
如果NaN参与了任何的四则运算,结果都是NaN。 NaN的相反数是(另一)NaN。
溢出(Overflow)#
由于C语言的整数类型是存在范围的的,所以会遇到整数溢出的情况(分为上溢和下溢)。
对于无符号整数的情况,如果它的位宽为\(N\),则结果就会是对\(2 ^ N\)的模。
如以下的例子:
#include <stdio.h>
int main(void)
{
    unsigned a = 65536u;
    unsigned b = a * a;
    printf("%u\n", b);
    return 0;
}
在32位的环境下得到的是0.因为 $$ 65536 ^ 2 = (2 ^ {16}) ^ 2 = 2 ^ {32}. $$ 模\(2 ^ {32}\)刚好是0.
Implementation-defined
对于有符号整数,溢出后的结果是实现定义的。
Attention
这里的溢出对于运算的中间结果也成立。
浮点数在中间计算的过程中一般float量都是扩到double上运算(视编译器而定)。
较难发生溢出的情况。当然,浮点数还会有另外的问题:
浮点运算的不精确性#
看以下的例子:
#include <stdio.h>
int main(void)
{
    double a = 1e300;
    double b = a + 1.f;
    printf("%f\n", b - a);
    return 0;
}
这个例子会输出0.000000. 直觉上,b比a大1,应该输出1. 但是,由于a和b都是很大的数,
加上1来说,对尾数没有改变。所以相减会等于0.
在精确度要求不高,数量级相差不大的计算中,浮点数还是有其优势的。
位运算符#
位运算符是在整数表达式上对整数的位进行操纵运算符。
无符号整数的二进制表示
如同十进制的表示,每一个无符号整数也可以用二进制表示。二进制的每一位表示2的某个幂。
从左向右依次是:$$ \cdots, 2^3, 2^2, 2^1, 2^0 .$$
如5的二进制表示可以表示为:\(101_{2}\),其意义是\(2^2 + 2^0 = 5\)。
在32位的unsigned int类型中就存储为:
0000 0000 0000 0000 0000 0000 0000 0101
计算机内部就是这么储存无符号整数的。对于有符号整数数,有多种内部表示方法,在此不赘述。
逐位逻辑运算符#
逐位逻辑运算符就是对整数的位进行操纵。
逐位逻辑运算符
~整数表达式
整数表达式 & 整数表达式
整数表达式 | 整数表达式
整数表达式 ^ 整数表达式
取反操作将所有的位取反,也就是说将每一位0变成1、1变成0. 取反操作的真值表如下:
| 输入 | 输出 | 
|---|---|
| 0 | 1 | 
| 1 | 0 | 
什么是真值表(truth table)?
每一种位运算都可以由输入输出决定。真值表列出所有可能的输入,并给出了对应的输出。 每一个单元格都是0(假)或1(真),所以叫真值表。
我们来试一下以下的程序:
#include <stdio.h>
int main(void)
{
    unsigned a = 3;
    printf("%u\n", ~a);
    return 0;
}
在32位下会输出4294967292. 我们来看看3(上)和4294967292(下)的二进制表示:
0000 0000 0000 0000 0000 0000 0000 0011
1111 1111 1111 1111 1111 1111 1111 1100
已经很明白了。
接下来我们介绍逐位与(&)、逐位或(|)和逐位异或(^),它们都是二元运算符。
Attention
C语言中^代表逐位异或,不是乘幂。
与的真值表如下:
| 输入 | 输入 | 输出 | 
|---|---|---|
| 0 | 0 | 0 | 
| 0 | 1 | 0 | 
| 1 | 0 | 0 | 
| 1 | 1 | 1 | 
“与”的意思直观理解就是:当两个位都为1(真)时,输出1(真)。其他情况都是0(假)。 这正是“与”的字面意思。
或的真值表如下:
| 输入 | 输入 | 输出 | 
|---|---|---|
| 0 | 0 | 0 | 
| 0 | 1 | 1 | 
| 1 | 0 | 1 | 
| 1 | 1 | 1 | 
“与”的意思直观理解就是:当两个位有一个为1(真)时,就输出1(真)。这正是“或”的字面意思。
来看一个例子:
#include <stdio.h>
int main(void)
{
    unsigned a = 5;
    unsigned b = 3;
    printf("%u %u\n", a | b, a & b);
    return 0;
}
输出7 1. 来看看这些数的内部表示:
0...0 0011 -> 3
0...0 0101 -> 5
0...0 0111 -> 7: 3 | 5
0...0 0001 -> 1: 3 & 5
我们可以利用与和或做一下掩码操作。掩码就是找一个特殊的数,用它和其他的数做与运算, 能够保持那个数的某些位,而其他的位统统变为0. 这个数的设计也特别简单: 需要保留的位设为1,其他设为0,这就是做掩码的数。
Exercise
设计一个保留整数最右边三位的掩码。你可以使用9(1001)来试一下,应该与这个数的与是1. (提示:这个数在本节已经出现过。)
或运算的一个应用是:将一个数所需要的位设置为1,其他位保持不变。这个数的设计也特别简单: 需要设置的位设为1,其他设为0。
最后是异或运算,这是它的真值表:
| 输入 | 输入 | 输出 | 
|---|---|---|
| 0 | 0 | 0 | 
| 0 | 1 | 1 | 
| 1 | 0 | 1 | 
| 1 | 1 | 0 | 
异或运算的逻辑是:如果两个位只有一个是1(真),则输出1(真),否则为0(假)。
使用异或运算进行原地交换
使用异或运算可以原地交换两个整数的值,不需要辅助变量,下面是一个例子:
#include <stdio.h>
int main(void)
{
    unsigned a = 5;
    unsigned b = 3;
    a = a ^ b;
    b = b ^ a;
    a = b ^ a;
    printf("%u %u\n", a, b);
    return 0;
}
你可以想想这种方法的原理。
移位运算符#
逐位逻辑运算符
整数表达式 << 整数表达式
整数表达式 >> 整数表达式
移位运算符将整数的所有位向左/向右移动。它的右表达式表示了要移动多少位。 右移在右侧的位补零,无符号整数左移在左侧补零,超出的部分(对于无符号整数)则截断。
#include <stdio.h>
int main(void)
{
    unsigned a = 5;
    printf("%u %u\n", 5 << 2, 5 >> 1);
    return 0;
}
输出是20 2.来看看这些数的内部表示:
0...0 0000 0101 -> 5
0...0 0001 0100 -> 20: 5 << 2
0...0 0000 0010 -> 2: 5 >> 1
Undefined-behavior
如果右表达式是负数,或者超出左表达式类型的位宽的,则是未定义行为。
左移/右移\(N\)位相当于乘/整数除\(2^N\),对于有符号类型的正数也是如此。 它提供了一个乘除2的幂的好方法,通常来说,移位运算比乘除运算要快。
Undefined-behavior
如果对于无符号整数,在整数提升后(见下)仍然无法表达移位后的值,则是未定义行为。
Implementation-defined
对于负数,移位的结果是实现定义的。大多数编译器选择符号位扩展,会保留正负性。
优先级(Precedence)与结合性(Associativity)#
和数学中一样,运算符具有优先级。运算符的优先级根数学中的类似。 我们把现在遇到的运算符列举如下:
| 优先级 | 运算符 | 结合性 | 
|---|---|---|
| 高 | +(正号)、-(负号)、~ | 
右 | 
*、/、% | 
左 | |
+(加号)、-(减号) | 
左 | |
<<、>> | 
左 | |
& | 
左 | |
^ | 
左 | |
| | 
左 | |
| 低 | = | 
右 | 
结合性指的是同级运算的优先级。左结合说明最左边的运算符优先、从左至右, 右结合说明最右边的运算符优先、从右至左。
我们可以用小括号()包围表达式,使它最优先,如同数学中那样。
小括号
(表达式)
Attention
[]和{}不能像()那样作为提高优先级使用,它们有别的意义。
Good-practice
通常,除了=、%和四则运算之外,我们都使用小括号来表示优先级。
因为这些负号的优先级不是显然的,也省得我们背优先级表。
类型转换(Type Conversion)#
尽管C语言不允许通过赋值取改变一个值的类型,也不允许类型不匹配的类型相互赋值。 但是我们可以使用类型转换将一个类型的值转换为另外的类型。
类型转换分为两种:显式转换和隐式转换。顾名思义,显示转换需要我们显示地去指定, 而隐式转换不需要我们去指定,编译器会为我们自动生成。
即使有了类型转换机制,在C中仍然存在不能完成的类型转换。好在,算术类型都存在互相的转换。
算术类型转换规则#
C语言中的算术类型转换时,遵守以下的规则:
整数与浮点类型的相互转换#
- 整数向浮点类型转换时,若能够用浮点数表示则保留原值,若不能则转换为临近的值。
 - 浮点数向整数转换时,向零取整。
 
Undefined-behavior
如果这种转换后的值不能被转换后的类型表示,则行为未定义。通常发生于超出范围的情况。
如将1e300转换为整数类型。
算术类型的转换等级#
算术类型的转换等级从某种程度表示了某个类型的范围大小。通常来说,等级越高,范围越大。
总的来说转换等级有以下的规律:
- 浮点类型等级比所有整数类型高
 - 整数类型有符号和对应的无符号类型等级相等
 
以下是类型转换等级列表:2
| 等级 | 类型 | 
|---|---|
| 高(浮点) | long double | 
| (浮点) | double | 
| (浮点) | float | 
| (整数) | long long、unsigned long long | 
| (整数) | long、unsigned long | 
| (整数) | int、unsigned | 
| (整数) | short、unsigned short | 
| (整数) | char、signed char、unsigned char | 
| 低(布尔) | _Bool | 
整数转换#
整数类型之间可以进行整数转换,规则如下:
- 如果转换前的值能被转换后的类型表示,则值不变
 - 若转换到是无符号的类型,其位宽为\(N\),则转换后的值是原值对\(2^N\)取模的结果。
 
Implementation-defined
对于转到有符号类型,且转换后的类型不能表示原值的情况,为实现定义。
隐式转换(Implicit Conversion)#
隐式转换跟随“语义”发生。存在以下类型的隐式转换,它们都根据算术类型转换规则进行:
- 整数提升
 - 通常算术转换
 - 赋值转换
 
前二者都只将类型“扩大”,而赋值转换则比较任意。
整数提升(Integer Promotions)#
整数提升是指转换等级小于int类型的值都会转换为int或unsigned中的一者。
如果int能涵盖所有原来类型的值域则选用int,否则为unsigned。
以下情况存在整数提升:
- 一元算术运算符 
+、-、~的右表达式 - 移位运算符
<<和>>的左右表达式 
通常算术转换2#
如果算术运算符(除移位运算符之外)的两侧拥有不同的类型,则两边进行通常算术转换, 直到都变为一个公共的类型。结果表达式的类型就是公共的类型。 简而言之,通常算术遵循以下原则:
- 小于
int的整数类型进行整数提升 - 转换为较高等级的类型
 - 整数类型如果分别为有符号S和无符号U,则:
- S和U的类型如果一样等级,则转换为U。
 - 若否,则如果S能涵盖U的值域,则转换为S。
 - 否则,转换为对应S的无符号类型3
 
 
直观上理解,通常算术转换就是对左右两边进行范围扩大到一致,且倾向于选择无符号类型。
例如以下的表达式:
1 / 10.0
得到的结果是double类型的0.1(约值)。虽然除号左边是一个整数,但是由于右边是一个浮点类型,
所以左边转化为浮点类型,并执行浮点的除法运算。
而以下的表达式:
65536ll * 65536
不会溢出,得到结果是long long类型的4294967296(\(2^{32}\))。
以下的表达式:
-1 * 65536u
则会变为unsigned类型。
赋值转换#
赋值和初始化时,右表达式的类型会根据算术类型转换规则转化到左表达式的类型(可以不是扩大)。
如下面的赋值和初始化都是合法的:
char a = 'a';
unsigned u_one = 1;
float f_one = 1;
long long ll_one = 1;
ll_one = 1.0;
Exercise
试阐明每一行存在的类型转换和其规则。
以下的代码片段:
float a = 1 / 3;
会将a初始化为0.0而非三分之一。这是因为,赋值号右边的表达式中,除号两边都是整数,
所以是整数除法,结果是int类型的0,再转化为float类型,为0.0。
显式转换(Type Cast)#
显示类型转换
(类型) 表达式
不同于隐式转换,显示类型转换则是在代码中显式地将要转换的类型标识出来。 由于这种类型转换在何处都能用,不像隐式转换受“语境”影响,可以强制转换类型, 所以也叫强制类型转换。
下面是一个显示类型转换的例子:
int a = 3;
float b = (float)a;
float c = (float)5;
显示转换的优先级于正负号相当。下面的例子展示了如何将计算两个整数的浮点商。
int x = 1, y = 3;
float q = (float)x / y; /* q is 0.33 */
显示转换可以完成算术类型转换中所有的转换,除此之外还可以做到其他的转换, 这在我们之后接触到其他的类型就会说到。
本章练习#
Exercise
计算 \(\frac{1}{7}\)的值,精确到6位小数。
0.142857
#include <stdio.h>
int main(void)
{
    printf("%f", 1 / 7.0);
    return 0;
}
Exercise
定义一个int型变量input并赋值为一个三位正整数(任选)。
计算它的(十进制)每一位数之和并输出。
对于input为123的情况:
6
#include <stdio.h>
int main(void)
{
    int input = 123;
    int sum = input / 100 + input / 10 % 10 + input % 10;
    printf("%d", sum);
    return 0;
}
Exercise
不同的浮点类型精度不一样,(float)0.1与(double)0.1相差多少?
将结果乘以65536并输出。(保留6位小数)
0.000098
#include <stdio.h>
int main(void)
{
    printf("%f", (0.1f - 0.1) * 65536);
    return 0;
}
(注:这里直接使用了float类型的字面量0.1f)
Exercise
定义int类型变量input并赋值为一个正整数,使用位运算解决以下问题:
- 它除以16的整数商是多少?
 - 它除以16余多少?
 - 它是否为奇数(是则输出1,否则输出0)
 
结果用空格分隔。
(提示:\(16 = 2 ^ 4\),15的二进制为1111。)
对于input为1001时:
62 9 1
#include <stdio.h>
int main(void)
{
    int input = 1001;
    printf("%d %d %d", input >> 4, input & 15, input & 1);
    return 0;
}