目录
一、运算符
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。当然了,这是它的补码。
以上是笔者所理解的通俗的含义。如果想看官方的解释,我比较懒,直接拍照了。
再来说一下显式类型转换,也就是我们通常所说的强制类型转换。
比如:
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 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. 是否控制求值顺序。
两个相邻的操作符先执行哪个,取决于他们的优先级。而如果两者的优先级相同,取决于他们的结合性。
我去,又臭又长。
都要记吗?
当然不是,其实,它还是有规律可循的。
总体来说,是逻辑非>算术操作符>关系操作符>逻辑操作符>赋值操作符
并且,赋值操作符都是从右往左计算。
如果是复合的赋值操作符,那运算符就更低了。
剩下的几个特例,可以再特殊的记忆一下。
接下来,再说一说几个有问题的代码。
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()
{
....;
....;
}
编译风格提示:最好在花括号内有缩进的习惯,这样可以帮助人们更好地识别结构和解释指令,提高代码的可读性。