C语言学习(五)运算符、表达式和语句

5.2 基本运算符

基本算术运算的运算符: =、 +、 -、 *和/

C 没有指数运算符。不过, C 的标准数学库提供了一个pow()函数用于指数运算。 例如, pow(3.5,
2.2)返回3.5的2.2次幂) 。

C使用符号/来表示除法。 /左侧的值是被除数, 右侧的值是除数。 例如, 下面four的值是4.0:

four = 12.0/3.0;

整数除法和浮点数除法不同。 浮点数除法的结果是浮点数, 而整数除法的结果是整数。 整数是没有小数部分的数。 这使得5除以3很让人头痛, 因为实际结果有小数部分。 在C语言中, 整数除法结果的小数部分被丢弃, 这一过程被称为截断(truncation) 。

运行程序清单5.6中的程序, 看看截断的情况, 体会整数除法和浮点数除法的区别。

/* divide.c -- 演示除法 */
#include 
int main(void)
{ 
printf("integer division: 5/4 is %d \n", 5 / 4);
printf("integer division: 6/3 is %d \n", 6 / 3);
printf("integer division: 7/4 is %d \n", 7 / 4);
printf("floating division: 7./4. is %1.2f \n", 7. / 4.);
printf("mixed division: 7./4 is %1.2f \n", 7. / 4);
return 0;
}

程序清单5.6中包含一个“混合类型”的示例, 即浮点值除以整型值。 C相对其他一些语言而言, 在类型管理上比较宽容。 尽管如此, 一般情况下还是要避免使用混合类型。 该程序的输出如下:

integer division: 5/4 is 1
integer division: 6/3 is 2
integer division: 7/4 is 1
floating division: 7./4. is 1.75
mixed division: 7./4 is 1.75

注意, 整数除法会截断计算结果的小数部分(丢弃整个小数部分) , 不会四舍五入结果。 混合整数和浮点数计算的结果是浮点数。 实际上, 计算机不能真正用浮点数除以整数, 编译器会把两个运算对象转换成相同的类型。
本例中, 在进行除法运算前, 整数会被转换成浮点数。

在C99以前,不同的实现采用不同的方法。 但是C99规定使用趋零截断。 所以, 应把-3.8转换成-3

5.3.1 sizeof运算符和size_t类型

sizeof运算符以字节为单位返回运算对象的大小(在C中, 1字节定义为char类型占用的空间大小。 过去, 1字节通常是8位, 但是一些字符集可能使用更大的字节) 。 运算对象可以是具体的数据对象(如, 变量名) 或类型。 如果运算对象是类型(如,float) , 则必须用圆括号将其括起来。

// sizeof.c -- 使用sizeof运算符
// 使用C99新增的%zd转换说明 -- 如果编译器不支持%zd, 请将其改成%u%lu
#include 
int main(void)
{ 
int n = 0;
size_t intsize;
intsize = sizeof (int);
printf("n = %d, n has %zd bytes; all ints have %zdbytes.\n",
n, sizeof n, intsize);

C语言规定, sizeof 返回 size_t 类型的值。 这是一个无符号整数类型,但它不是新类型。size_t是语言定义的标准类型。 C有一个typedef机制, 允许程序员为现有类型创建别名。 例如,

typedef double real;

这样, real就是double的别名。 现在, 可以声明一个real类型的变量:

real deal; // 使用typedef

编译器查看real时会发现, 在typedef声明中real已成为double的别名, 于是把deal创建为double 类型的变量。
类似地, C 头文件系统可以使用 typedef把 size_t 作为 unsigned int 或unsigned long的别名。
这样, 在使用size_t类型时, 编译器会根据不同的系统替换标准类型。

C99 做了进一步调整, 新增了%zd 转换说明用于 printf()显示 size_t 类型的值。 如果系统不支持%zd, 可使用%u或%lu代替%zd。

5.3.2 求模运算符: %

例如, 13 % 5(读作“13求模5”)得3
求模运算符只能用于整数, 不能用于浮点数。

min = sec / SEC_PER_MIN; // 截断分钟数
left = sec % SEC_PER_MIN; // 剩下的秒数

负数求模如何进行? C99规定“趋零截断”之前, 该问题的处理方法很多。 但自从有了这条规则之后, 如果第1个运算对象是负数, 那么求模的结果为负数; 如果第1个运算对象是正数, 那么求模的结果也是正数:

11 / 5211 % 5得1
11 / -5得-211 % -2得1
-11 / -52, -11 % -5得-1
-11 / 5得-2, -11 % 5得-1

如果当前系统不支持C99标准, 会显示不同的结果。 实际上, 标准规定: 无论何种情况, 只要a和b都是整数值, 便可通过a - (a/b)*b来计算a%b。
例如, 可以这样计算-11%5:

-11 - (-11/5) * 5 = -11 -(-2)*5 = -11 -(-10) = -1

简洁
递增运算符的另一个优点是, 通常它生成的机器语言代码效率更高, 因为它和实际的机器语言指令很相似。 尽管如此, 随着商家推出的C编译器越来越智能, 这一优势可能会消失。 一个智能的编译器可以把x = x + 1当作++x对待。

递增运算符和递减运算符都有很高的结合优先级, 只有圆括号的优先级比它们高。 因此, x*y++表示的是(x)*(y++), 而不是(x+y)++。不过后者无效, 因为递增和递减运算符只能影响一个变量

如果一次用太多递增运算符, 自己都会糊涂。

while (num < 21)
{ 
printf("%10d %10d\n", num, num*num++);   //不要自己这样写,给自己找麻烦
}

在C语言中, 编译器可以自行选择先对函数中的哪个参数求值。 这样做提高了编译器的效率, 但是如果在函数的参数中使用了递增运算符, 就会有一些问题。

类似这样的语句, 也会导致一些麻烦:

ans = num/2 + 5*(1 + num++);

同样, 该语句的问题是: 编译器可能不会按预想的顺序来执行。 你可能认为, 先计算第1项(num/2) , 接着计算第2项(5*(1 + num++)) 。 但是,编译器可能先计算第2项, 递增num, 然后在num/2中使用num递增后的新值。 因此, 无法保证编译器到底先计算哪一项。

还有一种情况, 也不确定:

n = 3;
y = n++ + n++;

5.4 表达式和语句

表达式(expression) 由运算符和运算对象组成(运算对象是运算符操作的对象) 。
下面是一些表达式:

4 -6 4+
21
a*(b + c/d)/20
q = 5*2
x = ++q % 3
q > 3

C 表达式的一个最重要的特性是, 每个表达式都有一个值。

表达式q = 5*2作为一个整体的值是10。 那么, 表达式q > 3的值是多少? 这种关系表达式的值不是0就是1

5.4.2 语句

语句(statement) 是C程序的基本构建块。 一条语句相当于一条完整的计算机指令。 在C中, 大部分语句都以分号结尾。 因此,

legs = 4

只是一个表达式(它可能是一个较大表达式的一部分) , 而下面的代码
则是一条语句:

legs = 4;

最简单的语句是空语句:

; //空语句

C把末尾加上一个分号的表达式都看作是一条语句(即, 表达式语
句) 。 因此, 像下面这样写也没问题:

8;
3 + 4;

但是, 这些语句在程序中什么也不做, 不算是真正有用的语句。 更确切地说, 语句可以改变值或调用函数:

x = 25;
++x;
y = sqrt(x);

虽然一条语句(或者至少是一条有用的语句) 相当于一条完整的指令,但并不是所有的指令都是语句。 考虑下面的语句:

x = 6 + (y = 5);

该语句中的子表达式y = 5是一条完整的指令, 但是它只是语句的一部分。 因为一条完整的指令不一定是一条语句, 所以分号用于识别在这种情况下的语句(即, 简单语句) 。

/* addemup.c -- 几种常见的语句 */
#include 
int main(void) /* 计算前20个整数的和 */
{ 
int count, sum; /* 声明[1] */
count = 0; /* 表达式语句 */
sum = 0; /* 表达式语句 */
while (count++ < 20) /* 迭代语句 */
sum = sum + count;
printf("sum = %d\n", sum); /* 表达式语句[2] */
return 0; /* 跳转语句 */
}

我们还是要提醒读者: 声明创建了名称和类型, 并为其分配内存位置。 注意, 声明不是表达式语句。 也就是说, 如果删除声明后面的分号, 剩下的部分不是一个表达式, 也没有值:

int port /* 不是表达式, 没有值 */

副作用和序列点

我们再讨论一个C语言的术语副作用(side effect) 。 副作用是对数据对象或文件的修改。 例如, 语句:

states = 50;

它的副作用是将变量的值设置为50。 副作用? 这似乎更像是主要目的!
但是从C语言的角度看, 主要目的是对表达式求值。 给出表达式4 + 6, C会对其求值得10; 给出表达式states = 50, C会对其求值得50。 对该表达式求值的副作用是把变量states的值改为50。
跟赋值运算符一样, 递增和递减运算符也有副作用, 使用它们的主要目的就是使用其副作用。

类似地, 调用 printf()函数时, 它显示的信息其实是副作用(printf()的返回值是待显示字符的个数) 。

序列点(sequence point) 是程序执行的点, 在该点上, 所有的副作用都在进入下一步之前发生。
在 C语言中, 语句中的分号标记了一个序列点。
意思是, 在一个语句中, 赋值运算符、 递增运算符和递减运算符对运算对象做的改变必须在程序执行下一条语句之前完成。

序列点有助于分析后缀递增何时发生。 例如, 考虑下面的代码:

while (guests++ < 10)
printf("%d \n", guests);

对于该例, C语言的初学者认为“先使用值, 再递增它”的意思是, 在printf()语句中先使用guests, 再递增它。
但是, 表达式guests++ < 10是一个完整的表达式, 因为它是while循环的测试条件, 所以该表达式的结束就是一个序列点。 因此, C 保证了在程序转至执行 printf()之前发生副作用(即, 递增guests) 。 同时, 使用后缀形式保证了guests在完成与10的比较后才进行递增。

现在, 考虑下面这条语句:

y = (4 + x++) + (6 + x++);

表达式4 + x++不是一个完整的表达式, 所以C无法保证x在子表达式4 + x++求值后立即递增x。 这里, 完整表达式是整个赋值表达式语句, 分号标记了序列点。
此语句有问题,要尽量避免编写类似的语句。

5.4.3 复合语句(块)

复合语句(compound statement) 是用花括号括起来的一条或多条语句,复合语句也称为块(block) 。
花括号确保两条语句都是while循环的一部分, 每执行一次循环就调用一次printf()函数。 根据while语句的结构, 整个复合语句被视为一条语句。

语句:

到目前为止, 读者接触到的语句可分为简单语句和复合语句。

简单语句以一个分号结尾。 如下所示:

赋值表达式语句: toes = 12;
函数表达式语句: printf("%d\n", toes);
空语句: ; /* 什么也不做 */

复合语句(或块) 由花括号括起来的一条或多条语句组成。 如下面的while语句所示:

while (years < 100)
{ 
wisdom = wisdom * 1.05;
printf("%d %d\n", years, wisdom);
years = years + 1;
}

5.5 类型转换

通常, 在语句和表达式中应使用类型相同的变量和常量。 但是, 如果使用混合类型, C 不会像 Pascal那样停在那里死掉, 而是采用一套规则进行自动类型转换。

1.当类型转换出现在表达式时, 无论是unsigned还是signed的char和short都会被自动转换成int, 如有必要会被转换成unsigned int(如果short与int的大小相同, unsigned short就比int大。 这种情况下, unsigned short会被转换成unsigned int) 。 由于都是从较小类型转换为较大类型, 所以这些转换被称为升级(promotion) 。

2.涉及两种类型的运算, 两个值会被分别转换成两种类型的更高级别。

3.类型的级别从高至低依次是long double、 double、 float、 unsignedlong long、 long long、 unsigned long、 long、 unsigned int、 int。 例外的情况是, 当long 和 int 的大小相同时, unsigned int比long的级别高。 之所以short和char类型没有列出, 是因为它们已经被升级到int或unsigned int。

4.在赋值表达式语句中, 计算的最终结果会被转换成被赋值变量的类型。 这个过程可能导致类型升级或降级(demotion) 。 所谓降级, 是指把一种类型转换成更低级别的类型。

5.当作为函数参数传递时, char和short被转换成int, float被转换成double。 以后将介绍, 函数原型会覆盖自动升级。

类型升级通常都不会有什么问题, 但是类型降级会导致真正的麻烦。 原因很简单: 较低类型可能放不下整个数字。


如果待转换的值与目标类型不匹配怎么办? 这取决于转换涉及的类型。
待赋值的值与目标类型不匹配时, 规则如下。

1.目标类型是无符号整型, 且待赋的值是整数时, 额外的位将被忽略。 例如, 如果目标类型是 8 位unsigned char, 待赋的值是原始值求模256。
2.如果目标类型是一个有符号整型, 且待赋的值是整数, 结果因实现而异。
3.如果目标类型是一个整型, 且待赋的值是浮点数, 该行为是未定义的。

如果把一个浮点值转换成整数类型会怎样?
当浮点类型被降级为整数类型时, 原来的浮点值会被截断。 例如, 23.12和23.99都会被截断为23, -23.5会被截断为-23。

程序清单5.14 convert.c程序

/* convert.c -- 自动类型转换 */
#include 
int main(void)
{ 
char ch;
int i;
float fl;
fl = i = ch = 'C'; /* 第9行 */
printf("ch = %c, i = %d, fl = %2.2f\n", ch, i, fl); /* 第10行 */
//    ch = C, i = 67, fl = 67.00
ch = ch + 1; /* 第11行 */
i = fl + 2 * ch; /* 第12行 */
fl = 2.0 * ch + i; /* 第13行 */
printf("ch = %c, i = %d, fl = %2.2f\n", ch, i, fl); /* 第14行 */
//   ch = D, i = 203, fl = 339.00
ch = 1107; /* 第15行 */
printf("Now ch = %c\n", ch); /* 第16行 */
//   Now ch = S
ch = 80.89; /* 第17行 */
printf("Now ch = %c\n", ch); /* 第18行 */
//   Now ch = P
return 0;
} 

第9行和第10行: 字符’C’被作为1字节的ASCII值储存在ch中。 整数变量i接受由’C’转换的整数, 即按4字节储存67。 最后, fl接受由67转换的浮点数67.00。

第11行和第14行: 字符变量’C’被转换成整数67, 然后加1。 计算结果是4字节整数68, 被截断成1字节储存在ch中。 根据%c转换说明打印时, 68被解释成’D’的ASCII码。

第12行和第14行: ch的值被转换成4字节的整数(68) , 然后2乘以ch。为了和fl相加, 乘积整数(136) 被转换成浮点数。 计算结果(203.00f) 被转换成int类型, 并储存在i中。

第13行和第14行: ch的值(’D’, 或68) 被转换成浮点数, 然后2乘以
ch。 为了做加法, i的值(203) 被转换为浮点类型。 计算结果(339.00) 被
储存在fl中。

第15行和第16行: 演示了类型降级的示例。 把ch设置为一个超出其类型范围的值, 忽略额外的位后, 最终ch的值是字符S的ASCII码。 或者, 更确切地说, ch的值是1107 % 265, 即83。

第17行和第18行: 演示了另一个类型降级的示例。 把ch设置为一个浮点数, 发生截断后, ch的值是字符P的ASCII码。

5.5.1 强制类型转换运算符

圆括号和它括起来的类型名构成了强制类型转换运算符(cast operator) , 其通用形式是:

(type)
mice = 1.6 + 1.7;
mice = (int)1.6 + (int)1.7;

第1 行使用自动类型转换。 首先, 1.6和1.7相加得3.3。 然后, 为了匹配int 类型的变量, 3.3被类型转换截断为整数3。
第2行, 1.6和1.7在相加之前都被转换成整数(1) , 所以把1+1的和赋给变量mice。 本质上, 两种类型转换都好不到哪里去, 要考虑程序的具体情况再做取舍。

类型说明符和表达式都必须加括号(单个变量可以不加括号),如把(int)(x+y)写成(int)x+y则成了把x转换成int型之后再与y相加了。

5.6 带参数的函数

程序清单5.15 pound.c程序
/* pound.c -- 定义一个带一个参数的函数 */
#include 
void pound(int n);// ANSI函数原型声明
int main(void)
{ 
int times = 5;
char ch = '!'; // ASCII码是33
float f = 6.0f;
pound(times); // int类型的参数
pound(ch); // 和pound((int)ch);相同
pound(f); // 和pound((int)f);相同
return 0;
}
void pound(int n) // ANSI风格函数头
{ // 表明该函数接受一个int类型的参数
while (n-- > 0)
printf("#");
printf("\n");
} 
运行该程序后, 输出如下:
#####
#################################
######

如果函数不接受任何参数, 函数头的圆括号中应该写上关键字 void。由于该函数接受一个 int 类型的参数, 所以圆括号中包含一个int类型变量n的声明。

该例中, 函数原型告诉编译器pound()需要一个int类型的参数。 相应地, 当编译器执行到pound(ch)表达式时, 会把参数ch自动转换成int类型。 在我们的系统中, 该参数从1字节的33变成4字节的33, 所以现在33的类型满足函数的要求。 与此类似, 最后一次调用是pound(f), 使得float类型的变量被转换成合适的类型。


虽然C允许编写混合数值类型的表达式, 但是算术运算要求运算对象都是相同的类型。 因此, C会进行自动类型转换。 尽管如此, 不要养成依赖自动类型转换的习惯, 应该显式选择合适的类型或使用强制类型转换。 这样,就不用担心出现不必要的自动类型转换。

只需要一个运算对象的运算符(如负号和 sizeof) 称为一元运算符, 需要两个运算对象的运算符(如加法运算符和乘法运算符) 称为二元运算符。

当char和short类型出现在表达式里或作为函数的参数(函数原型除外) 时, 都会被升级为int类型;float类型在函数参数中时, 会被升级为double类型。


程序清单5.16 running.c程序
// running.c -- A useful program for runners
#include 
const int S_PER_M = 60; // 1分钟的秒数
const int S_PER_H = 3600; // 1小时的分钟数
const double M_PER_K = 0.62137; // 1公里的英里数
int main(void)
{ 
double distk, distm; // 跑过的距离(分别以公里和英里为单位)
double rate; // 平均速度(以英里/小时为单位)
//推荐用double,不用float
int min, sec; // 跑步用时(以分钟和秒为单位)
int time; // 跑步用时(以秒为单位)
double mtime; // 跑1英里需要的时间, 以秒为单位
int mmin, msec; // 跑1英里需要的时间, 以分钟和秒为单位
printf("This program converts your time for a metric race\n");
printf("to a time for running a mile and to your average\n");
printf("speed in miles per hour.\n");
printf("Please enter, in kilometers, the distance run.\n");
scanf("%lf", &distk); // %lf表示读取一个double类型的值
printf("Next enter the time in minutes and seconds.\n");
printf("Begin by entering the minutes.\n");
scanf("%d", &min);
printf("Now enter the seconds.\n");
scanf("%d", &sec);
time = S_PER_M * min + sec; // 把时间转换成秒
distm = M_PER_K * distk; // 把公里转换成英里
rate = distm / time * S_PER_H; // 英里/秒×秒/小时 = 英里/小时
mtime = (double) time / distm; // 时间/距离 = 跑1英里所用的时间
mmin = (int) mtime / S_PER_M; // 求出分钟数
msec = (int) mtime % S_PER_M; // 求出剩余的秒数
//强制类型转换
printf("You ran %1.2f km (%1.2f miles) in %d min, %dsec.\n",
distk, distm, min, sec);
printf("That pace corresponds to running a mile in %dmin, ",
mmin);
printf("%d sec.\nYour average speed was %1.2f mph.\n", msec,
rate);
return 0;
}

为什么要进行类型转换?
因为程序在秒转换成分钟的部分需要整型参数, 但是在公里转换成英里的部分需要浮点运算。 我们使用强制类型转换运算符进行了显式转换。

实际上, 我们曾经利用自动类型转换编写这个程序, 即使用int类型的mtime来强制时间计算转换成整数形式。 但是, 在测试的11个系统中, 这个版本的程序在1个系统上无法运行, 这是由于编译器(版本比较老) 没有遵循C规则。 而使用强制类型转换就没有问题。 对读者而言, 强制类型转换强调了转换类型的意图, 对编译器而言也是如此。


你可能感兴趣的:(C语言)