本篇是对各种操作符和表达式的求值的介绍
1.算数操作符
2.移位操作符
3.位操作符
4.赋值操作符
5.单目操作符
6.关系操作符
7.逻辑操作符
8.条件操作符
9.逗号表达式
10.下标引用操作符与函数调用操作符
11.结构体成员操作符
12.表达式求值
13.操作符的属性
1.算数操作符
+ 加法
- 减法
* 乘法
/ 取商
% 取余
算数操作符与数学类似,值得注意的是 / 和 % 。
float a,b;
a=10/3; //值为3
b=10/3.0; //3.3333333
/ 的两个操作数如果都是整数,那么进行的就是整数除法,两个操作数中至少有一个是小数时,才进行小数除法。而 % 的两个操作数都必须是整形。
2.移位操作符
<< 左移
>> 右移
移位操作符移动的是二进制位
整数的二进制位有三种形式:
原码
反码
补码
一个整形的大小是4个字节,所以占32个bit位,所以一个整形的二进制位一共有32位。第一位表示符号位,0表示整数,1表示负数。
整数在内存中都是以二进制的补码储存的
原码就是把整数从十进制转换成二进制数,然后补齐到32位得到
反码就是原码的符号位不变,其他的位全部按位取反得到的
补码就是把反码加一得到的
而正数的的原码,反码,补码都是相同的
所以,我们要计算的只有负数的补码
int a=0;
a<<1 //把a左移一位
我们知道,位移操作符是对整数的二进制位进行位移,而这个二进制位就是二进制的补码。
对负数进行操作时,要先算出负数的补码,在进行操作,然后再转换成原码就可以得到操作后的值
左移的规则
把补码整体向左移,左边的丢掉,右边补零
右移的规则
算数右移:右边丢弃,左边补原符号位
逻辑右移: 右边丢弃,左边补零
在右移时,到底进行算数右移还是逻辑右移是取决于编译器的。(我当前的编译器是算数右移)
此外,还有一个未定义的规则:不要移动负数位
int a=0;
a>>-1;
3.位操作符
& 按位与
| 按位或
^ 按位异或
与位移操作符一样,也是对整数的二进制补码进行操作,
a&b a与b的二进制补码按位与(见零位零)。注意与取地址&区分,取地址&是单目操作符
a|b a与b的二进制补码按位或(见一为一)。
a^b a与b的二进制补码按位异或(相异为一,相同为零)。
由上面的定义我们可以知道
a^a=0;
a^0=a;
当我们想交换a,b两个数时,我们可以使用这样一种方法(只能用于整数)
a=a^b;
b=a^b;
a=a^b;
下面我们做一道例题:求出一个数在内存中存储的1的个数
思考:假设我们要判断一个数的在内存中存储的补码的最后一位是不是1,那我们只需要让这个数按位与一个1,如果结果是0,说明最后一位是0,如果结果是1,说明最后一位是1,那么当我们想要验证别的位时,我们只需要把1左移,然后把每一位都验证一边即可
int main()
{
int n=0;
scanf("%d",&n);
int i,count;
for(i=0;i<32;i++)
if(1==(n<
4.赋值操作符
=
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
这个比较好理解,就是赋值,需要注意的是
连续赋值:
a=x=y+1;
先把y+1赋给x,再把x赋给a
复合赋值:
a=a+2 <==> a+=2;
a=a>>1 <==> a>>=1;
a=a&4 <==> a&=4;
5.单目操作符
! 逻辑反
- 负值
+ 正值
& 取地址
sizeof 计算类型创建的变量所占的内存大小,单位是字节
~ 对一个数的二进制位按位取反(包括符号位)
-- 前置,先减后用,后置,先用后减
++ 前置,先加后用,后置,先用后加
* 解引用(间接访问) *&a <==> a
(类型) 强制类型转换
逻辑反:C语言中,在c99之前,是没有表示真假的类型的,我们用0表示假,非0表示真,在c99中引入了布尔类型头文件是
bool flag1=ture;
bool flag2=false;
逻辑反就是把真变成假,假变成真。
取地址与解引用:取地址是取一个变量的地址,解引用是通过这个地址找到这个变量,所以他们经常成对使用
int a=10;
int* pa=&a;
*pa=5;
printf("%d",a);//输出结果为5
sizeof:可能会有初学者觉得它是一个函数,因为确实使用的时候也很像是函数,但其实sizeof是一个操作符,我们可以通过下面的代码验证。
int main()
{
int a=10;
printf("%d\n",sizeof a);
printf("%d\n",sizeof(int));
return 0; //输出结果都是4
}
当我们省略a两边的括号时,代码依然可以跑起来,这就说明了sizeof不是函数,因为函数的括号是绝对不能省略的(这种省略的写法只是为了大家好区分sizeof是不是函数,并不支持大家这样去写,并且sizeof括号里如果是类型的话,也是不能省略括号的)
sizeof还有一个属性
int a=10;
short s=0;
printf("%d\n",sizeof(s=a+2));//输出2
printf("%d\n",s); //输出0
为什么第二个没有输出12呢,因为sizeof()括号中的表达式是不参与计算的,因为我们的电脑在执行我们写的源文件时,会先编译,再生成链接,最后生成.exe文件。而sizeof是在编译期间处理的,表达式则是在程序编好后才运行的。所以上面的代码中s的值没有被改变。
~按位取反:当我们想把某一个数的某一个二进制位变为1时,我们可以让这个数按位或一个1左移n位 a|(1< 还有当我们在读取多组数时,一般的代码为 但是我们也可以用这种方法写读取多组数 原理是因为scanf读取失败时会返回一个EOF,而EOF的值是(-1),(-1)的补码全都是1,所以~(-1)的补码就全为0,表示0,而0又表示假,所以上述代码可以使循环停止。 6.关系操作符 非常直观,没有什么难以理解的 需要注意的是:==不能用来判断两个字符串是否相等,"abcd"=="efg"比较的是这两个字符串的首字符的地址,比较字符串大小有一个专门的函数---strcmp。strcmp比较的是两个字符串对应位置上字符的大小,而不是长度,在比较时,如果字符串对应位置的字符相等则比较下一位,直到对应位置的字符不相等,则较大字符所在的字符串就是大的字符串。 7.逻辑操作符 我们看下面的代码 上面的代码为什么输出是1 2 3 4而不是1 3 3 5呢? 因为对&&来说,如果左边的操作数为0,那么它的结果肯定是0,所以右边的值就不会计算了,对 || 也是一样,如果左边的操作数为1,那么值肯定为1,右边的操作数也不会计算了。 8.条件操作符 exp1为真,执行exp2,结果为exp2的结果 exp1为假,执行exp3,结果为exp3的结果 9.逗号表达式 从左到右依次执行,整个表达式的结果为最后一个表达式的结果(前面的每个表达式都是要执行的) 10.下标引用操作符与函数调用操作符 下标引用操作符就是我们在调用数组中的元素时用的[ ]。 上面的代码更加说明了【】是一个操作符,数组名和下标是它的两个操作数。 函数调用操作符()的操作数为函数名和参数。 11.结构体成员操作符 我们来通过下面的代码对这两个操作符进行一个区分 上面的代码会打印出三组数据,他们都是一样的,说明我们传的是结构体时,要用 结构体.成员名来访问,当我们传的是结构体指针时,可以用 结构体指针->成员名 来访问,也可以解引用指针然后用(*结构体指针).成员名 来访问 12.表达式求值 隐式类型转换 C的整型算术运算总是至少以缺省整型类型的精度来进行的。 为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。 表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度。 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。 简单来说就是CPU中最小的计算单位是一个整形的大小,所以当小于一个整形大小的变量要进行运算时,计算机要先把它变成整型进行运算,然后再变回去。 那么计算机是如何进行整形提升的呢?我们看下面的代码 c1,c2都是字符型,而3,127都是整型,char中存放不下,所以会发生截断,下面我们来分析一下计算机的计算过程 上面我们就分析清楚了计算机到底是怎么进行整型提升的。 上面这个代码的输出结果也说明了当c要进入表达式进行计算时,就会进行整型提升。 算数转换 当操作符对大于等于整型的不同类型的数进行操作时,会把其中一个数的类型转换成另一个数的类型,否则就无法进行操作,下面的层次体系称为寻常算术转换。 上面的类型的转换优先级是从上到下排列的,即当一个long double类型与int类型的数据运算时,会把int转换成long double类型。 13.操作符的属性 (1)操作符的优先级 (2)操作符的结合性 (3)是否控制求值序列 操作符的优先级有一个明确的表格,大家有兴趣可以去查找一下 只有相邻的两个操作符猜讨论优先级,当相邻的两个操作符优先级相同时,结合性起作用 结合性就像下面的代码: 两个操作符都是+,优先级相同,那么就通过他们的结合性判断先后顺序,+操作符是从左到右结合的,所以该式的计算顺序是a+b,再加c。 大多数操作符都是不控制求值顺序的 只有以下四个 逻辑与 逻辑或 条件操作符 逗号表达式 就像上面所说,以逻辑与为例,当逻辑与的左边的操作数为0时,表达式必为0,所以不计算右边的表达式,这就是控制求值顺序。 下面有几个典型的错误表达式 这个表达式是有多种执行顺序的,如果abcdef都是变量,那么无论哪种路径都不会影响最终的值,但是当abcdef都是表达式的时候,并且如果他们当中还使用了一下相同的变量时,这时候不同的计算路径就很可能产生不同的结果,所以这是一个错误的表达式。 这个代码的问题是我们不知道左边的c是在自加之前准备好还是自加之后,所以也有问题。 这个代码在不同的编译器里有不同的结果,所以也是存在问题。 上述代码的问题在于每次调用fun函数时返回的值都不同,但是我们无法确定到底应该先调用那个fun函数。 综上所述,我们在写表达式的时候一定要注意,要根据操作符的属性,写出能够确定唯一执行路径的表达式,否则就是错误的表达式。while(scanf("%d",&n)!=EOF);
{
…………;
}
while(~scanf("%d",&n))
{
…………;
}
>
>=
<
<=
==
!=
&& 逻辑与 (见0为0)
|| 逻辑或 (见1为1)
int i=0;a=0;b=2;c=3;d=4;
i=a++ && ++b && d++;
printf("%d %d %d %d\n",a,b,c,d);//输出为1 2 3 4
exp1?exp2:exp3
exp1,exp2,exp3……,expN;
[] 下标引用
() 函数调用
arr[4] <==> *(arr+4) <==> *(4+arr) <==> 4[arr]
. 结构体.成员名
-> 结构体指针->成员名
struct Stu
{
char name[20];
int age;
float score;
};
void print1(struct Stu ss)
{
printf("%s %d %f\n",ss.name,ss.age,ss.score);
}
void print2(struct Stu* ps)
{
printf("%s %d %f\n",(*ps).name,(*ps).age,(*ps).score);
printf("%s %d %f\n",ps->name,ps->age,ps->score);
int main()
{
struct Stu s={"李四",20,95.5};
print1(s);
print2(&s);
return 0;
}
int main()
{
char c1=3;
char c2=127;
char c3=c1+c2;
printf("%d\n",c3);
return 0;
}
char c1=3;
//00000000 00000000 00000000 00000011--3的补码
//00000011--c1
char c2=127;
//00000000 00000000 00000000 01111111--127的补码
//01111111--c2
//那么我们如何进行整型提升呢?
//负数:高位补1
//正数:高位补0
//无符号位:高位补0
char c3=c1+c2;
//00000000 00000000 00000000 10000010--130的补码
//10000010--c3
//符号位为1,表示负数,所以高位补1
//11111111 11111111 11111111 10000010--c3整型提升后的补码
//11111111 11111111 11111111 10000001--c3整形提升后的反码
//10000000 00000000 00000000 01111110--c3整形提升后的原码--(-126)
printf(%d\n",c3);//值为-126
char c=1;
printf("%u\n",sizeof(c)); //1
printf("%u\n",sizeof(+c)); //4
printf("%u\n",sizeof(-c)); //4
long double
double
float
ubsigned long int
long int
ubsigned int
int
a+b+c
a*b+c*d+e*f
c=++c
int i=10;
i=i-- - --i*(i=-3)*i++ + ++i;
printf("i=%d\n",i);
int fun()
{
static int count=1;
return ++count;
}
int main()
{
int answer;
answer=fun()-fun()*fun();
printf("%d\n",answer);
}