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

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

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

写在前面:

各位小伙伴还在为C语言的学习而苦恼嘛?

还在为没有知识体系而烦心嘛?

别急。因为~~~~

接下来的时间里,我会持续推出C语言的有关知识内容。

都是满满的干货,从零基础开始哦~,循序渐进,直至将C中知识基本全部学完。

欢迎关注我♥,订阅专栏 0基础C语言保姆教学,

就可以持续读到我的文章啦~~~~

本文为第6节—— *** 作符、表达式和语句(文末附前5章的链接呦)

参考书籍:《C Prime Plus》、《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位。

那又

什么叫二进制位?

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

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

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

至于右移 *** 作符:>>

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

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

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

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

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

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

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

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

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

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

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

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

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

② 补码 = 反码 + 1;

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

 而想要把补码变成原码,

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

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

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

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

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

3、位 *** 作符:&  |  ^

&                  |                    ^

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

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

什么意思呢?

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

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

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

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

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

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

 请问,这里会输出什么?

我们让代码执行起来:

 为什么呢?

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

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

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

所以,我们得到的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、单目 *** 作符

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

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

很简单,也很好理解。

在这里面,我们还有& 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。当然了,这是它的补码。

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

 (以上两张图片引用自《C Prime Plus》第六版中文版 P106-P107,在此鸣谢)

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

比如:

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,并返回其值。

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

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 worldn");

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

3. 访问一个结构的成员 

.                结构体.成员名

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

再来举一个例子

#include
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. 是否控制求值顺序。

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

 我去,又臭又长。

都要记吗?

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

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

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

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

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

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

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()

{

        ....;

        ....;

}

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

好啦,时隔两个月,本次的博客就到这里啦~~~

欢迎各位看官关注我@jxwd,订阅专栏,就能持续看到我的文章啦

C语言自学保姆教程——第一节--编译准备与第一个C程序_jxwd的博客-CSDN博客

0基础C保姆自学 第二节——初步认识C语言的全部知识框架_jxwd的博客-CSDN博客_c语言全部框架0基础C语言自学教程——第三节 分支与循环_jxwd的博客-CSDN博客

0基础C语言保姆教程——第4节 函数_jxwd的博客-CSDN博客

0基础C语言保姆教学——第五节 数组_jxwd的博客-CSDN博客0基础C保姆自学 第二节——初步认识C语言的全部知识框架_jxwd的博客-CSDN博客_c语言全部框架

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5702392.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存