C语言-操作符(详细)和表达式求值

本篇是对各种操作符和表达式的求值的介绍

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<

还有当我们在读取多组数时,一般的代码为

while(scanf("%d",&n)!=EOF);
{
    …………;
}

但是我们也可以用这种方法写读取多组数

while(~scanf("%d",&n))
{
    …………;
}

原理是因为scanf读取失败时会返回一个EOF,而EOF的值是(-1),(-1)的补码全都是1,所以~(-1)的补码就全为0,表示0,而0又表示假,所以上述代码可以使循环停止。

6.关系操作符

>
>=
<
<=
==
!=

非常直观,没有什么难以理解的

需要注意的是:==不能用来判断两个字符串是否相等,"abcd"=="efg"比较的是这两个字符串的首字符的地址,比较字符串大小有一个专门的函数---strcmp。strcmp比较的是两个字符串对应位置上字符的大小,而不是长度,在比较时,如果字符串对应位置的字符相等则比较下一位,直到对应位置的字符不相等,则较大字符所在的字符串就是大的字符串。

7.逻辑操作符

&&   逻辑与  (见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

上面的代码为什么输出是1 2 3 4而不是1 3 3 5呢?  因为对&&来说,如果左边的操作数为0,那么它的结果肯定是0,所以右边的值就不会计算了,对 || 也是一样,如果左边的操作数为1,那么值肯定为1,右边的操作数也不会计算了。

8.条件操作符

exp1?exp2:exp3

exp1为真,执行exp2,结果为exp2的结果

exp1为假,执行exp3,结果为exp3的结果

9.逗号表达式

exp1,exp2,exp3……,expN;

从左到右依次执行,整个表达式的结果为最后一个表达式的结果(前面的每个表达式都是要执行的)

10.下标引用操作符与函数调用操作符

[]    下标引用
()    函数调用

下标引用操作符就是我们在调用数组中的元素时用的[ ]。

arr[4] <==> *(arr+4) <==> *(4+arr) <==> 4[arr]

上面的代码更加说明了【】是一个操作符,数组名和下标是它的两个操作数。

函数调用操作符()的操作数为函数名和参数。

11.结构体成员操作符

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

我们来通过下面的代码对这两个操作符进行一个区分

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;
}

 上面的代码会打印出三组数据,他们都是一样的,说明我们传的是结构体时,要用 结构体.成员名来访问,当我们传的是结构体指针时,可以用 结构体指针->成员名 来访问,也可以解引用指针然后用(*结构体指针).成员名 来访问

12.表达式求值

隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。 为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度。 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。

 简单来说就是CPU中最小的计算单位是一个整形的大小,所以当小于一个整形大小的变量要进行运算时,计算机要先把它变成整型进行运算,然后再变回去。

那么计算机是如何进行整形提升的呢?我们看下面的代码

int main()
{
    char c1=3;
    char c2=127;
    char c3=c1+c2;
    printf("%d\n",c3);
    return 0;
}

c1,c2都是字符型,而3,127都是整型,char中存放不下,所以会发生截断,下面我们来分析一下计算机的计算过程

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

上面这个代码的输出结果也说明了当c要进入表达式进行计算时,就会进行整型提升。

算数转换

当操作符对大于等于整型的不同类型的数进行操作时,会把其中一个数的类型转换成另一个数的类型,否则就无法进行操作,下面的层次体系称为寻常算术转换。

long double
double
float
ubsigned long int
long int
ubsigned int
int

上面的类型的转换优先级是从上到下排列的,即当一个long double类型与int类型的数据运算时,会把int转换成long double类型。

13.操作符的属性

(1)操作符的优先级

(2)操作符的结合性

(3)是否控制求值序列

 操作符的优先级有一个明确的表格,大家有兴趣可以去查找一下

只有相邻的两个操作符猜讨论优先级,当相邻的两个操作符优先级相同时,结合性起作用

结合性就像下面的代码:

a+b+c

两个操作符都是+,优先级相同,那么就通过他们的结合性判断先后顺序,+操作符是从左到右结合的,所以该式的计算顺序是a+b,再加c。

大多数操作符都是不控制求值顺序的

只有以下四个

逻辑与

逻辑或

条件操作符

逗号表达式

 就像上面所说,以逻辑与为例,当逻辑与的左边的操作数为0时,表达式必为0,所以不计算右边的表达式,这就是控制求值顺序。

下面有几个典型的错误表达式

a*b+c*d+e*f

这个表达式是有多种执行顺序的,如果abcdef都是变量,那么无论哪种路径都不会影响最终的值,但是当abcdef都是表达式的时候,并且如果他们当中还使用了一下相同的变量时,这时候不同的计算路径就很可能产生不同的结果,所以这是一个错误的表达式。

c=++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);
}

上述代码的问题在于每次调用fun函数时返回的值都不同,但是我们无法确定到底应该先调用那个fun函数。

综上所述,我们在写表达式的时候一定要注意,要根据操作符的属性,写出能够确定唯一执行路径的表达式,否则就是错误的表达式。

你可能感兴趣的