C语言浅学-运算符、表达式和语句
上一篇博客我们结束了对格式化字符串的输入输出的介绍和学习,这一篇博客我们将学习如何来处理数据,处理方式:算数运算、比较值的大小、修改变量、各种逻辑的组合关系等。
Tip:此篇中我们还学习循环这个编程中最强大的特性。
循环
为什么我们需要使用循环,因为若是完成单一的工作,编写程序的工作量远不如人完成的快速有效率,计算机需要帮助我们完成的就是重复计算的工作,C中有相当多的方法可以去做重复计算,这里先简单的介绍一种——while循环。
如下所示,在有或没有循环两种情况下的代码的效率:
无循环,只能输出单个的结果。
加入while循环,可以输出多个结果。
从这里我们可以得出结论,在进行重复运算时,计算机相较于人有显著的优势。
while循环的原理:
当程序第一次到达循环时,会检查圆括号中的条件是否为真,在上述例子中条件表达式:shoe < 18.5。初始鞋码为3.0,条件为真,程序将进入循环执行,将尺码转化为英寸,进行打印结果,在最后给shoe增加1.0,返回while入口检查条件,while下两个花括号括起来的称为块。返回入口时会再次进行判断,直至循环结束。
基本运算符
在C中使用运算符表示算数运算,除开基本运算符以外,C没有指数运算符,不过C标准库提供一个pow()函数用于指数运算:pow(3.5, 2.2)表示3.5的2.2次幂。
Tip:在使用Pow()函数时需先导入math.h的头文件,且在编译时需要在gcc mi.c -o mi 后加上 -lm才能编译成功。(链接math头文件)。
赋值运算符:=
对于这个运算符应该都不陌生,不是常规数学中的等于号意味着相等。
1 | bmw = 2000; |
上述语句就是把2000这个数值赋给bmw这个变量,赋值行从右往左进行。
变量名和变量值的区别看似区别不大,那么下面这个语句:
1 | i = i + 1; |
这在数学上来说是完全行不通的,但是在赋值语句中就是常规的对变量加1并且赋值给 原变量的过程。在编写代码的过程中,= 号左侧必须是一个变量名,右侧是一个常量或者表达式。
几个术语:数据对象、左值、右值和运算符
数据对象:存储值的数据存储区域称为数据对象。一般使用变量名来标识对象,还有指定数组的元素、结构的成员、使用指针表达式等。(房间)
左值:用于标识特定数据对象的名称或者表达式。(房间号)
前篇提到过的 const 限定符也是变量名却不能被赋值,所以左值就变成了可修改的左值。
右值:指能赋值给可修改左值的量,且本身不是左值。可以是常量、变量或者可求值的表达式。
1 | int ex; |
一般来说其他语言会回避程序的三重赋值,但是在C中完全没问题。
加法运算符:+
用于加法运算,使得两侧的值相加:
1 | printf("%d", 4 + 20); |
输出的是24,而不是表达式 4 + 20。
1 | income = salary + bribes; |
这样的表达式也是可以的,程序会读取右边变量的值并将其相加,然后把和赋值给income。
减法运算符:-
同加法运算符理,这两个运算符被称为二元运算符,即需要两个运算符才能完成操作。
符号运算符
减号还可以用于标记一个值的代数符号。例如,执行下面的语句后,值会变成12:
1 | rocky = -12; |
以这种方式使用的负号被称为一元运算符。
Tip:在最新的标准中,增加了+的一元运算符,仅仅是编译不会报错。
乘法运算符:*
符号*表示乘法,并将结果进行赋值。
1 | cm = 2.54 * inch; |
在C中没有平方函数,我们就可以使用乘法来计算平方。
1 |
|
还有另外一个在棋盘中放麦粒也是类似:
1 |
|
在这个程序中还运用到了while循环。
除法运算符:/
C中使用符号/来表示除法,左侧属于被除数,右侧属于除数:
1 | four = 12.0/3.0; |
Tip:在C语言中,整数除法结果会有小数部分被丢弃,这一过程被称为截断。
实例:
1 |
|
Tip:C语言中对于负数的截断一般会舍弃小数部分,称为趋零截断,如-3.8会变成-3。
运算符优先级
在日常数学运算中,我们都知道要先乘除后加减,那么在C语言中的运算符也有对应的优先级。基本规则差距不大,如下表所示:
Tip:我们只需要注意+、-的两种不同用法即可
优先级和求值顺序
用一个简单的程序就能解释清楚这个问题:
1 |
|
上述程序的输出结果应当为-23,这和我们在实际数学应用上并无不同。
其他运算符
上面介绍的都是C语言中最常用的运算符,接下来我们介绍一些比较有用的运算符。
sizeof 运算符和size_t类型
在数据类型那篇文章里最后就介绍了这个运算符。
功能:以字节为单位返回运算对象大小。
运算对象:具体的数据对象(变量名)或者类型。
Tip:若是类型必须使用圆括号括起来。
1 |
|
上述程序就展示了这个运算符的功能以及size_t类型的数据。
在C语言中,sizeof 返回的就是size_t类型,属于无符号整型,不是一个新类型。
Tip:C还有一个typedef类型,可以允许使用者为现有的类型创建别名,后续再详细介绍。
1 | typedef double real; |
这样一来,real 就是double的别名了,real 类型的变量本质就是double类型。
1 | real deal;//使用typedef |
Tip:在最新的标准中,%zd 转换说明用于printf()显示size_t 类型,若是系统不支持,则可以使用%u 或者%lu来代替%zd。
求模运算符:%
功能:给出其左侧整数除以右侧整数的余数。
例:13%5,得3。
局限:只能用于整数,不能用于浮点数。
该运算符在有公约数且需要取余的程序中用处很大。(例如年月分秒)
1 |
|
上述程序是对正数进行求模,那么怎么对负数进行求模呢?
在最新的标准中,有了趋零截断的规则之后,第一个运算对象是负数,那么求模结果也是负数,若为正数,求模也为正数。
1 | 11 / 5得2,11 % 5得1; |
若是系统不支持最新的标准,但是只要两个数是整数,就可以使用a - (a/b)*b来计算 a % b。
递增运算符:++
功能:将其运算对象递增1。
使用方式:
1.出现在其作用的变量前面,属于前缀模式
2.出现在其作用的变量后面,属于后缀模式
有了这个运算符,我们可以对前面出现的鞋码测脚长的程序进行优化:
1 | shoe = 3.0; |
这是将shoe = shoe + 1 转换成 ++shoe,还可以进行进一步的精简:
1 | shoe = 2.0; |
但是在这里我们需要注意的是,由于是循环外的递增,所以我们需要将shoe的初始值改成2.0,这样我们的第一个输入值才会是3.0。
Tip:这样虽然精简了程序,但是也降低了程序的可读性,同时也使得后续排查错误显得更加难。
当然这两种的使用方式也有区别:
1 |
|
输出结果:
在两个运算符单独使用时,并没有区别,但是在运算表达式中时,后缀是先赋值后递增,而前缀是先递增后赋值。为了避免这样的错误,一般解决方法就是不要这样使用他们,换一种方法:
1 | b = i++;//不好 |
递减运算符:–
基本同递增运算符的理,每个++都可以用–来代替。
实例:
1 |
|
Tip:在这个程序中,< >这两个符号表示的是大于号和小于号,都属于关系运算符。
优先级
和代数运算一样,递增递减运算符也拥有很高的结合优先级,只有圆括号的优先级比他们高。
1 | y = 2; |
上述程序的值应当是30,因为n++是先使用了3而后再进行加1的操作。
贴心提示
在同一个程序中,不要一次使用过多的递增运算符,否则会被程序弄晕。
实例:
1 | while (num < 21) |
上述程序有可能会出现先递增再计算的后果。
1 | 5 25 //原来的结果 |
而且同一个语句也可能因为编译器不会按照预想的顺序来执行,所以最好的办法就是不要在一个语句中过多使用递增运算符。
Tip:1.如果一个变量出现在一个函数的多个参数中,不要对该变量使用递增或
递减运算符;
2.如果一个变量多次出现在一个表达式中,不要对该变量使用递增或递减
运算符。
表达式和语句
C的基本程序步骤由语句组成,而大多数语句都由表达式构成。
表达式
表达式(expression)由运算符和运算对象组成(前面介绍过,运算对象是运算符操作的对象)。
实例
1 | 4 |
每一个表达式都有一个值,若是有=(赋值运算符)即是按照符号的优先级进行计算得出最后的数值;若是><这些关系运算符,那么这些表达式的值最后都是0或者1,用以表示真假。
语句
语句(statement)是C程序的基本构建块。一条语句相当于一条完整的计算机指令。在C中,大部分语句都以分号结尾。
实例
1 | legs = 4 //表达式 |
最简单的语句是空语句:
1 | ;//空语句 |
Tip一条完整的指令不一定是一条语句。
常见语句实例
1 |
|
上述程序中包含了声明和表达式语句和迭代语句和跳转语句。
Tip:特殊的就是声明,它不是表达式语句,若是去掉分号也不是一个表达式。
while语句是一种迭代语句,也被称为结构化语句,相较于简单的赋值表达式语句更加复杂。
副作用和序列点
1 | states = 50; |
上述的语句的副作用就是将变量的值设置为50,其实我也没理解为啥是副作用,书上这么写的,说是对数据对象或文件的修改就叫副作用。
类似的,我们调用printf()函数时,显示信息其实是副作用,返回值是显示字符的个数。
序列点:程序执行的点。
作用:在该点上,所有的副作用都在进入下一步之前发生。
在 C语言中,语句中的分号标记了一个序列点。意思是,在一个语句中,赋值运算符、递增运算符和递减运算符对运算对象做的改变必须在程序执行下一条语句之前完成。
完整表达式:这个表达式不是另外一个更大表达式的子表达式。
1 | while (guests++ < 10) |
此段代码在运行打印函数之前,guests变量就已经递增了,后缀++只是保证了guests是在完成了与10的比较之后才进行递增。
1 | y = (4 + x++)+ (6 + x++); |
这个式子就会引发歧义,因为4 + x++ 不是一个完整的表达式,所以C无法保证在子表达式求值之后立马递增x,应当避免编写类似的语句。
复合语句(块)
复合语句:用花括号括起来的一条或多条语句,也称为块。
实例:
1 | //代码1 |
虽然看上去就只差了一对花括号,但是效果却是天差地别。
代码1:while循环结束之后,printf()函数只会被调用一次。
代码2:花括号的存在确保了两条语句都是循环的一部分,每次循环都会执行一次printf()函数。
总的来说:
表达式:由运算符和运算对象组成,最简单的表达式是不带运算符的一个常量或变量。
语句:主要分为简单语句和复合语句,简单语句就是以一个分号结尾;复合语句是由花括号括起来的一条或者多条语句构成。
类型转换
一般来说,在语句和表达式中应当使用类型相同的变量和常量,若是我们使用混合类型,C语言会采用自己的一套规则进行自动的类型转换,我们应当对此做出了解。
规则:
1.当类型转换出现在表达式中时,无论是无符号还是有符号的char或short都会被自动转成int,如有必要会被转换成无符号整型,这些都是从较小类型转换成较大类型,一般被称为升级。
2.涉及两种类型的运算,两个值一般会被转换成两种类型的更高级别。
3.类型的级别从高到低依次是是long double、double、float、unsignedlong
long、long long、unsigned long、long、unsigned int、int。short和char类型已经被升级为int或者unsigned int了。
4.在赋值表达式语句中,计算的最终结果会被转换成被赋值变量的类型,可能会导致降级。
5.当作为函数参数传递时,char和short会被自动转换成int,float会被自动转换成为double。
Tip:一般来说升级不会导致问题,类型降级才会引发一系列的问题。
若是待转换的值与目标类型不匹配时有以下规则:
1.目标类型是无符号整型,且待赋的值是整数时,额外的位将被忽略。例如,如果目标类型是 8 位unsigned char,待赋的值是原始值求模256。
2.如果目标类型是一个有符号整型,且待赋的值是整数,结果因实现而异。
3.如果目标类型是一个整型,且待赋的值是浮点数,该行为是未定义的。
实例
1 |
|
强制类型转换运算符
一般来说,我们应当避免自动类型转换,尤其是类型降级;但是小心使用,类型转换也是很方便的,前面提到的类型转换都是自动完成的。有时候,我们需要精确的类型转换,我们就需要用到强制类型转换(cast),即在某个量前置圆括号括起来的类型名。
通用形式:(type),type就是我们需要更改的类型名。
实例
1 | mice = 1.6 + 1.7; //mice是int类型 |
Tip:一般来说,不应该使用混合类型,偶尔这样做也是有用的,也需要我们自己来承担后果。
总结运算符:
赋值运算符:
= 将其右侧的值赋给左侧的变量
算术运算符:
+将其左侧的值与右侧的值相加
-将其左侧的值减去右侧的值
-作为一元运算符,改变其右侧值的符号
*将其左侧的值乘以右侧的值
/ 将其左侧的值除以右侧的值,如果两数都是整数,计算结果将被截断
% 当其左侧的值除以右侧的值时,取其余数(只能应用于整数)
++ 对其右侧的值加1(前缀模式),或对其左侧的值加1(后缀模式)
– 对其右侧的值减1(前缀模式),或对其左侧的值减1(后缀模式)
其他运算符:
sizeof 获得其右侧运算对象的大小(以字节为单位),运算对象可以是一个被圆括
号括起来的类型说明符,如sizeof(float),或者是一个具体的变量名、数组名等如
sizeof foo。
(类型名) 强制类型转换运算符将其右侧的值转换成圆括号中指定的类型,如(float)9
把整数9转换成浮点数9.0。
带参数的函数
带参数的函数应当是比较熟悉了,为了之后自己编写函数,我们需要进一步学习。
1 |
|
最终的结果
我们可以看到在函数头出现的是(int n),这就说明该函数是要接受整型参数,参数名应当遵循C语言的命名规则。
在声明参数的过程中就创建了形式参数的变量,在该例中,形式参数就是整型变量n,我们称函数调用传递的值为实际参数,简称为实参,函数在调用时就是把实参的值拷贝给了形参n。
Tip:实参和形参,形参是变量,实参是函数调用提供的值,实参被赋给相应的形参,另外变量名是函数私有的,若是使用times代替pound中的n,对后续使用整型变量times也不会产生影响,最好不要这样做。
从pound()函数的原型说明了两点:
1.该函数没有返回值(函数名前面有void关键字)
对void关键字的解释详见
C语言学习之void关键字_void是c语言关键字吗_忆梦初心的博客-CSDN博客
简单点说就是void前置表示函数可以没有return 0;这句话,若是void出现在参数位置,则说明函数是没有参数传入的。
2.该函数有一个int类型的参数。
现在函数声明越来越规范,但是以前的版本可以不用指明参数类型,所以C语言中还存在着void pound();这样的函数声明形式,但是这种形式在使用在pound(f)会出现错误,float会被升级为double,虽然能运行,但是输出结果不正确,这个时候只能使用pound((int)f);可以解决。
实例程序
1 |
|
最后所呈现的效果。
关键概念和小结
在C语言的环境中有大量的运算对象的优先级和结合律,主要是在共享一个运算对象时需要注意。
虽然C语言允许编写混合数值的表达式,尽管如此,不要养成依赖自动类型转换的习惯,应该显式选择合适的类型或使用强制类型转换。这样可以避免出现不必要的类型转换。
一般而言,运算符需要一个或多个运算对象才能完成运算生成一个值。只需要一个
运算对象的运算符(如负号和 sizeof)称为一元运算符,需要两个运算对象的运算符(如加法运算符和乘法运算符)称为二元运算符。
在C语言中,许多类型转换都是自动进行的。当char和short类型出现在表达式里或作为函数的参数(函数原型除外)时,都会被升级为int类型;float类型在函数参数中时,会被升级为double类型。
定义带一个参数的函数时,便在函数定义中声明了一个变量,或称为形式参数。然后,在函数调用中传入的值会被赋给这个变量。这样,在函数中就可以使用该值了。
到此就结束了对运算符、表达式和语句相关知识的介绍,下一篇将具体学习C语言的控制语句:循环。