0基础C语言保姆教程——第六节 操作符、表达式和语句

简介: 加法或者减法都是双目操作符,就是必须要求有两个操作对象才能够用它,这也很好理解,加法、减法要两个数才能加减对吧。

目录


一、运算符


1、算术运算符 :  +   -   *   /    %  


2、移位操作符:


3、位操作符:&  |  ^


4、赋值操作符  =


复合赋值符 : +=  -=  *=  /=  %=  ^=  &=  <<=  >>=  |=


5、关系操作符


6、逻辑操作符


7、单目操作符


--:自减1。分为前置和后置。前置为先自减,后使用;而后置则为先使用,后自减。


++:自增1。分为前置和后置。前置为先自增,后使用;而后置则为先使用,后自增。


(类型名):表示强制将原来的数据类型转变为新的数据类型。


类型转换分为显式类型转换和隐式类型转换


8、sizeof和size_t类型


9、条件操作符


10、逗号表达式


11、下标引用、函数调用和结构成员


12、操作符的优先级与结合性


13、代码分析


14、布尔值


二、表达式


1、逻辑表达式


2、关系表达式


3、算术表达式


4、赋值表达式


5、每个表达式都有一个值。


三、语句


副作用和序列点


副作用


序列点


复合语句(块)


一、运算符

1、算术运算符 :  +   -   *   /    %  

+   -   *   /    %  


+ / - :


它们俩完成的是用于两个数加法或者两个数的减法的运算。


比如:

printf("%d",3+5);


这里打印出来的就是8,而不是3+5。原因很简单,就是这里进行了加法的运算。


减法同理。


一般情况下,加法或者减法都是双目操作符,就是必须要求有两个操作对象才能够用它,这也很好理解,加法、减法要两个数才能加减对吧。


但是呢,- 也可以直接添置在一个数的前面,就意为着一个负号,这也是可以的。


而 + 在C90标准之前,是不允许直接加在一个字面常量(就是一个数,或者字符,统称字面常量)的前面的,就比如  int a = +10;  这在C90的标准前是不允许的,但是C90引入了这样一种使用的方法,也就是说,在C90以后,这样写也是合法的了。


所以说,- 也可以做单目操作符,而+在C90之后的标准也可以做单目操作符。


* 和 /


关于乘法和除法,包括前面的加减,在运算的时候都有一个原则,就是类型“低等”的向类型“高等”的转换。就比如,


我有3.0*6这么一个表达式,


那么在编译的时候,就会默认先将6这样一个整型进行整形提升,变成浮点型(因为3.0是浮点型)。另外三种运算符同理。


同时,还要说一下,类型等级的高低关系是这样的:

char(short)->int ->unsigned int -> long -> unsigned long ->float ->double


除法同理。


与此同时,我们便可以理解,像5/3这样,它得到的结果是1就不难理解了。


%:取模操作符


这个操作符的含义是取模,或者叫取余数。


举个例子,5%3,那么得到的结果就是2。


另外,对于%这个操作符需要注意下面两点:


1、只能用于整型的运算。因为浮点型是无法运算的。当然,像这么一种的写法也是可以的:

因为在内存中,字符型是以ASCII码的形式存储的。


2、在对负数取模时,在C99之前,并没有明确的规定,具体的实现要看所处的编译环境。而在C99之后,规定了“趋零截断”这么一种说法。意思就是字面说的那样,趋向0(或者说经过0)就截断、不再算了。如果不能理解,那就这样记吧:整数取模得到的是整数,负数取模得到的就是负数。


比如:11%5得1;  11%-2得1;   -11%-5得-1;-11%5得-1


(注:你的编译器必须要很好的支持C99标准才可以;比如vs就不行。在vscode或者直接在linux下用gcc是可以的)


2、移位操作符:

<< 左移           >> 右移


 我们首先来说说左移操作符 << :


比如,我有这么一个代码:



如图,那么为什么a<<1后变成了2呢?


因为:<<表示的是指将某个元素(或者叫对象)的二进制位左移多少多少位。


比如上面的a<<1,意思就是将a的二进制位左移1位。


那又


什么叫二进制位?


其实,我们在第二章的时候略微介绍过,我们在此再来说说:

image.png

如上图,a是一个int类型,我们说过,一个int类型占4个字节,也就是32个比特位,那么如果a=1,则它用二进制的形式来表示就是00000000 00000000 00000000 00000001。


那么,a<<1就是将其整体向左移动一位,然后超出的舍弃,少的补0。就像这样:

image.png



至于右移操作符:>>


需要明确的是,右移操作符包含两种方式,一种是逻辑右移,一种是算术右移。


(1)逻辑右移:左边直接用0填充,右边丢弃。


(2)算术右移:左边用符号位填充,右边丢弃。


而大多数情况采用的都是算术右移的方式来实现。


在我们的msvc的环境中,采用的就是算术右移的方式。


我们在举一个例子前,先进行一个知识点补充:


在这里,补充一个数据在内存中存储的一个原码、反码、补码的知识:


通俗一点的来说,原码就是打印在屏幕上,让你能够看见的;


而补码是在计算机中实际存储的,


反码是原码和补码的一个桥梁。


1)而对于一个正数,其原码、反码、补码是相同的。


2)对于一个负数,三者之间存在着这样一种关系:


① 反码 = 原码的符号位不变,其余位按位取反;


② 补码 = 反码 + 1;


下面是一个例子,便于读者理解:

image.png



而想要把补码变成原码,


那么就是反过来,就是用补码的二进制减一,再将其符号位不变,其他位按位取反。


那么,下面这个例子就能看懂了:



如图,-1在内存中存储为11111111 11111111 11111111 11111111,而它右移一位,打印出来的仍然是-1,则说明,右移过后的存储的数字仍然是11111111 11111111 11111111 11111111,所以,它采用的是算术右移。


注意:移位的时候是不可以移动负数位的。比如:2>>-1。这样的情况是标准的未定义。


还有,移位操作符和下面的位操作的都是只能用于整型(或者char)


3、位操作符:&  |  ^

&                  |                    ^


同样的道理,这里的位操作符的操作对象也是数据的二进制位。


其中&位按位与;|表示按位或;^表示按位异或 ;  ~表示按位取反。 (前面都是对两个数操作得到一个新的数,而 ~ 是对于一个数进行操作的)


什么意思呢?


我们以0表示假,非0表示真,那么:


&的规则:两个数的某一位都为真,得到的这一位才是真。


|的规则:两个数的某一位只要有一个为真,得到的这一位就是真。


^的规则:两个数的某一位相同则得到的这一位为假,相异则得到的这一位真。


~的规则:一位一位看,真变假,假变真。


那我们就再举一个例子吧:



请问,这里会输出什么?


我们让代码执行起来:



为什么呢?


首先,我们先写出3和-5在内存中的补码:

image.png



而我们对于进行按位与运算的实际上是它们的补码。

image.png



根据我们所说到的&的操作规则,看二进制位的每一位,如果两个数同时为真才为真,一假就为假。


所以,我们得到的3&5的补码就是:


00000000 00000000 00000000 00000011


那么,根据转换规则,其原码就是


00000000 00000000 00000000 00000011(因为它是正数,正数的原码、反码、补码相同)


按位或和按位异或的操作相同,在这里就不做过多的赘述。


(如果还是不知道其基本的规则,可参考第二节0基础C保姆自学 第二节——初步认识C语言的全部知识框架_jxwd的博客-CSDN博客中10-4里面的位操作符)


4、赋值操作符  =

这个操作符,别看它简单,但值得我们好好学习一下


1、首先,它叫赋值操作符,不是判断等于。


2、其次,它是将等号右边的值赋值给等号左边的值。


3、它的操作符的运算顺序是从右向左,先右后左。


4、它的左值必须可修改。


我们来解释一下上面的几个概念。


第一个没有什么好说的。它是该操作符的定义。


第二个是该操作符的具体实现的方式。比如说,int a = 10的意思就是我拿到10的值,然后把它赋值给a


第三个是说等号的右边是先进行计算的,运算的顺序不再是从左向右,而是从右向左。


第四个涉及到一个左值(lvalue)的概念:


我们可以这样来理解:


左值是等号左边的值,可修改。


右值是等号右边的值,有时可修改,有时不可修改。


那么,什么样的值可以作为左值,什么样的值可以作为右值呢?


在C中,不可修改的常量不可以作为左值来使用。


它们通常有const修饰的常变量、#define定义的常量(即宏)、枚举常量和字面常量(就是具体的数值或者是字符)


(值得注意的是,它们有的在定义中是可以被赋值的。因为这是它们的一个初始化过程。就比如:const int a = 10)


我们定义的变量,如果不是以上类型,基本上是都可以作为左值的。比如,我就普普通通的定义了一个int b; 那么很显然,这里的b就是一个普通的变量,它是可以被修改的。


需要指出的是,如果两个变量进行了运算,它们是不可以作为左值的。


什么意思?


举个例子:


比如

int a = 10, int b = 20;


那么a 和b实际上都对应着一块内存。但是,我如果写这样一个表达式:a+b,那么这里a+b的结果就不再会是一个可修改的变量了。也就是说,a+b是不可以用来作为左值的。


原因是什么?其实很简单:


在编译器看来,a+b的意思就是找出内存a对应的值,再找出内存对应的值,然后把二者做加法,但是得到的结果编译器并不会为其去开辟一块新的内存空间,它只是一个临时的值(或者理解为临时的字面常量)而已。所以不可被再次修改了。


每个变量有两种属性,一种是数值属性,还有一种是类型属性。


而右值呢?


其实,理论上所有的值都可以作为右值。你想把什么值赋给别人,那你就把它作为右值就好啦。


复合赋值符 : +=  -=  *=  /=  %=  ^=  &=  <<=  >>=  |=

这些操作符是什么意思呢?其实很简单,它们的规则都是一样的。就是自己对自己运算完了以后,再把值赋给自己。


举个例子:


a += 2;


它实际上就是等价于a = a+2;


再举一个例子:


a %= 2;


那么它的意思就是 a = a%2;


其它也的都一样,依此类推即可。在这里就不再做过多的赘述。


5、关系操作符

像 < > <= >= == 这样用于判断表达式的关系的叫做关系操作符。


这里,我们需要提到一下什么叫做真,什么叫做假:


在一个关系表达式中,如果判断的条件成立,则称为真,否则,则称为假。


为真返回1,为假返回0。


6、逻辑操作符

&&           ||          !


&& :表示并且,类似于数学中的,即&&两边的两个关系表达式全为真,最后的结果才能为真。一假即假,全真才为真。


|| :表示或者,类似于数学中的,即||两边的两个关系表达式一真即为真。


!:表示真假互置,即如果原来表达式为真(即exp1为真),那么加上!后变为假(即!exp1为假)


需要注意的是,像这么一个表达式:exp1&& exp2如果exp1为假,那么编译器就不会再去计算exp2了。因为编译器认为无论exp2是真是假,结果都为假,所以编译器为了优化,exp2就不会进行运算了。


exp1||exp2同理,如果exp1为真,那么exp2也就不会算了。




7、单目操作符

单目操作符的意思就是操作元素只有一个。


像这些,都是属于单目操作符。

image.png



很简单,也很好理解。


在这里面,我们还有& sizeof和-- ++ *()没有介绍。我们在这里介绍-- ++ 和(),剩下的我们说到指针的时候读者就自然明白了。


--:自减1。分为前置和后置。前置为先自减,后使用;而后置则为先使用,后自减。

++:自增1。分为前置和后置。前置为先自增,后使用;而后置则为先使用,后自增。

这里想提到一点:


像这样的代码,是有问题的(特别指出某pxx刷题网站上竟然有这样的题)

int a = 3;
b = (++a)+(a++);


问你b的值是多少?


我们在不同的编译环境中会得出不同的结果。


我们就不试了。我们重点分析为什么会这样。


原因很简单。


第二个a到底是多少?是按3算还是按4算?


在不同的编译环境中不一样,所以也就会得出不同的结果。本质原因是计算的路径不唯一。


(类型名):表示强制将原来的数据类型转变为新的数据类型。

实际上,


类型转换分为显式类型转换和隐式类型转换

隐式类型转换:通俗的来说,就是编译器自己完成的转换。


在编译过程中,如果类型不相同:


则在运算的过程当中,其会遵循“由小变大”的原则,即等级低的会转换成等级高的。


在赋值的过程当中,其会将计算得到的转变为所要赋值的元素的类型。


而其中,类型的高低等级分别是long double > double > float > unsigned long long >long long > unsigned long > long > unsigned int > int > char(short)


什么意思?


我们来举一个例子:

int a = 3 + 3.2;


在这里,3是int ,3.2是double ,double > int,即double 的等级更高,所以,3首先要转换为3.0,然后与3.2相加,结果得到的是6.2,类型为double,而a 的类型为int ,需要将6.2转换成int类型。然后再赋值给a。


那么,它等级提升和等级降低的具体的规则是怎么样的呢?我们在数据类型的存储一章会详细地介绍相关内容,这里,我们说一下char和int 之间下转换,至于其他类型之间的转换,等介绍完数据存储便会理解。


我们知道,char占8个比特位,int占32个比特位 。


如果int 转换为char,那么很简单,直接截断前24位,只留下后8个比特位。


例如00000000 00111111 00001010 00000001


那么变为char类型的元素后,就是   00000001。


如果是char转变为int,高位补符号位。


比如,10000001     转换完后则变成 11111111 11111111 11111111 10000001。当然了,这是它的补码。


以上是笔者所理解的通俗的含义。如果想看官方的解释,我比较懒,直接拍照了。


再来说一下显式类型转换,也就是我们通常所说的强制类型转换。


比如:

int b = 10;
double a = (double)b;


就像这样,将b的类型强制转化为double,然后将值赋给a。


!!!!请注意!!!!


需要强调的是,在经过上述的代码之后,b的类型仍然是int。


因为强制类型转换转换的只是其值,并不会改变其本身的数据类型。


还需要注意一点:这样的写法很奇怪,也是不允许的

int a = 10;
(double) p = a;


即未定义的类型是不能用来强制转换的。道理 很简单,就是因为p都不知道是什么类型,怎么转换呢?就好像没有前任,哪里来的第二任?


8、sizeof和size_t类型

sizeof是一个操作符,不是 函数。这里需要强调。


sizeof返回以字节为单位的运算对象的大小。


比如sizeof(int) 就是4


sizeof(char) 就是1


而C规定,sizeof返回的类型为size_t,


size_t是unsigned int的别名。就是说,size_t就是unsigned int。


还需要注意的是,sizeof括号内是不进行运算的。


比如

int k = 3;
sizeof(k++);


执行完上述的代码后,k的值还是3。


那么我如果sizeof一个数组呢?那就是一个数组的大小。


比如

int a[10] = {0};
size_t b = sizeof(a);


 这里的b的值就是4*10=40


9、条件操作符

简而言之,exp1?exp2:exp3。就是判定exp1是否为真。若为真,则运算exp2,并返回其值,否则,就运算exp3,并返回其值。

image.png



如果我把这个式子改成条件操作符,那么就是

int b = a > 5 ? 3 : -3;


不是很难。


10、逗号表达式

exp1,exp2,exp3...expn


从左向右依次运算,返回最后一个表达式的值。


比如:

int a = 10;
int b = (a++,a++,a++);


执行完上述代码后,a的值为13;b的值是12。


11、下标引用、函数调用和结构成员

1. [ ] 下标引用操作符


主要是对于数组而言。


比如,

int a[10] = {10};
int b = a[2];


我们创建了这样一个数组,那么我们后面的a[2],这里的[ ]就是下标引用操作符。


2. ( ) 函数调用操作符


很简单,举个例子:


printf("goodbye world\n");

这里printf后面跟着的括号就是函数调用操作符。


3. 访问一个结构的成员


.                结构体.成员名


->              结构体指针->成员名


再来举一个例子

#include<stdio.h>
struct Stu
{ 
    char name[10]; 
    int age;
    char sex[5]; 
    double score;
}a,*p;
int main()
{
    scanf("%d",a.age);
    scanf("%d",p->age);
    return 0;
}


这里的a.age和p->age中的 . 和 ->便分别是结构体成员访问操作符。


如果非要去区分其名字,那么 . 叫成员操作符(分量操作符)


                                                ->叫指向操作符



12、操作符的优先级与结合性

复杂表达式的求值有三个影响的因素。


1. 操作符的优先级


2. 操作符的结合性


3. 是否控制求值顺序。


两个相邻的操作符先执行哪个,取决于他们的优先级。而如果两者的优先级相同,取决于他们的结合性。

微信图片_20221208181232.png



我去,又臭又长。


都要记吗?


当然不是,其实,它还是有规律可循的。


总体来说,是逻辑非>算术操作符>关系操作符>逻辑操作符>赋值操作符


并且,赋值操作符都是从右往左计算。


如果是复合的赋值操作符,那运算符就更低了。


剩下的几个特例,可以再特殊的记忆一下。


接下来,再说一说几个有问题的代码。


13、代码分析

这些代码写的都很奇怪,并且是有bug的。


比如:

a = b*c + d*e + f*g


乍一看,这好像是没有什么问题。但请仔细想想,它是有问题的。它的问题就是:运算的路径不唯一。


乘法在加法之前算,这是没有争议的。


但是,我可以先去算b*c + d*e中间的加法,再去算f*g中的乘法,也可以先算f*g的乘法,再算b*c + d*e中间的加法。这就引起了争议。如果它们都是一个个很复杂的表达式,最终的结果可能就不一样。


再看:

int c = 5;
c + ++c;


首先,++的运算符的优先级是高于+的,所以这里没有运算顺序的争议。但是,这里有取值的争议。前面的c到底是++c计算过了再取值,还是先取值,再计算++c呢 ?


这就引起争议了。


14、布尔值

说到布尔值,大多都是在C++中用的较多,C中在C99之前,我们只能用int在表示,就是这样

#define TRUE 1
#define FLASE 0


而在C99中,引进理论一种新的类型——布尔类型,从此,在C中就也有了布尔类型。


它的标识符为:_Bool,头文件为stdBool.h


二、表达式

表达式通俗来说,就是用操作符连接起来的式子,或者说是


由运算符和运算对象组成的式子。并且单独的一个运算对象(常量/变量)也可以叫做表达式


比如这些都是表达式:

-4
8
4+21
q > 3
x = ++q



1、逻辑表达式

用逻辑运算符将关系表达式或逻辑量连接起来的式子。


是指运算符为或||、与&&、非!的表达式。返回值为0或1,0表示false,非0表示true. 例如!0返回1,0&&1返回0,0||1返回1。


2、关系表达式

由关系运算符和操作数组成的表达式称为关系表达式。


且是指运算符为<,<=,>,>=,==,!=的表达式。返回值同样为0或1,例如a!=b,a>=0等


3、算术表达式

由算术运算符和操作数组成的表达式。


4、赋值表达式

含“=”“+=”等(复合)赋值运算符的表达式


...


不再赘述,一般就是由什么操作符组成,就是属于什么表达式。


并且一个表达式可以由若干个子表达式构成。


5、每个表达式都有一个值。

要想获得这个值,那么就需要根据运算符的优先级,来执行相应的操作。


三、语句

语句式C程序的基本构建块。


一条语句相当于一个完整的计算机指令。(需要注意的是计算机指令不一定都是语句)


在C中,大多数语句以分号结尾。


因此,a = 10是一个表达式,


而a = 10;   就是一个语句了。


最简单的语句是空语句。


像这样的语句,也是合法的。


4;


3+5;


但是它们什么事也不做。不算是有用的语句。


更确切的来说,就是语句可以改变值或者调用函数。这也是语句的主要功能。


需要注意一点,声明不是语句。这一点与C++有所区别。


在C中,赋值和函数调用都是表达式,没有所谓的“赋值语句”、“函数调用语句”这样的区别,统称为表达式语句。


副作用和序列点

副作用

副作用是对文件或者对象的修改。


比如,

a = 10;



这里的副作用就是将变量a 的值变为10.没错,它就是副作用,虽然它看起来是我们的主要作用。而如果从主要作用来看,对C而言,其主要作用是对表达式求值。就好像给出4+5,它的主作用就是求得结果为9;所以上面的a = 10,其主作用实际上是计算求得值 10。


类似地,我们在调用printf()的时候,它的显示的信息实际上就是它的副作用。(它的主作用是返回打印的值的个数)


主作用发生在副作用之前。


序列点

就是程序执行的点,在该点上,所有的副作用都已经发生完了。也就是说,在一个序列点上,就是一个完整表达式结束的地方(完整表达式就是指该表达式不是更大的一个表达式的子表达式)


这个了解一下就可以了。我们就不在此多举例了。免得嫌笔者啰嗦。我们讲完下面这最后一个知识赶紧下班😀


复合语句(块)

复合语句就是用花括号括起来的一条或者多条语句。复合语句也称块。


比如


while()


{


       ....;


       ....;


}


编译风格提示:最好在花括号内有缩进的习惯,这样可以帮助人们更好地识别结构和解释指令,提高代码的可读性。



目录
相关文章
|
3月前
|
存储 C语言 索引
【C语言篇】操作符详解(下篇)
如果某个操作数的类型在上⾯这个列表中排名靠后,那么⾸先要转换为另外⼀个操作数的类型后执⾏运算。
74 0
|
3月前
|
程序员 编译器 C语言
【C语言篇】操作符详解(上篇)
这是合法表达式,不会报错,但是通常达不到想要的结果, 即不是保证变量 j 的值在 i 和 k 之间。因为关系运算符是从左到右计算,所以实际执⾏的是下⾯的表达式。
255 0
|
26天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
34 10
|
1月前
|
C语言 C++
保姆式教学C语言——数组
保姆式教学C语言——数组
17 0
保姆式教学C语言——数组
|
1月前
|
C语言 开发者
C语言实现猜数字小游戏(详细教程)
C语言实现猜数字小游戏(详细教程)
|
1月前
|
编译器 C语言 C++
VSCode安装配置C语言(保姆级教程)
VSCode安装配置C语言(保姆级教程)
|
1月前
|
存储 编译器 C语言
【C语言】简单介绍进制和操作符
【C语言】简单介绍进制和操作符
160 1
|
1月前
|
机器学习/深度学习 编译器 C语言
C语言刷题(中)(保姆式详解)
C语言刷题(中)(保姆式详解)
14 0
|
1月前
|
机器学习/深度学习 C语言
C语言必刷题上(保姆式详解)
C语言必刷题上(保姆式详解)
14 0
|
2月前
|
程序员 C语言
【C语言基础考研向】06运算符与表达式
本文介绍了C语言中的运算符分类、算术运算符及表达式、关系运算符与表达式以及运算符优先级等内容。首先概述了13种运算符类型,接着详细说明了算术运算符的优先级与使用规则,以及关系运算符和表达式的真假值表示,并给出了C语言运算符优先级表。最后附有课后习题帮助巩固理解。
104 10