C语言—操作符和表达式
操作符和表达式
- 操作符
-
- 算术操作符
- 移位操作符
- 位操作符
-
- 练习:
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符(三目操作符)
- 逗号表达式
- 下标引用、函数调用和结构成员的访问操作符
- 表达式求值
-
- 隐式类型转换
- 算术转换
- 操作符的属性
操作符
分类:
- 算术操作符
- 移位操作符
- 位操作符
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符
- 逗号表达式
- 下标引用、函数调用和结构成员访问的相关操作符
算术操作符
+ - * / %
算术操作符的使用:
注:直接写出的小数,编译器会默认认为是double类型的数
注:%和/是不支持被零除或被零求模的
总结:
- 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
- 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
- % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
移位操作符
<< 左移操作符
>> 右移操作符
左移操作符 移位规则:左边抛弃、右边补0
注:左移操作符左边抛弃、右边补0(包括符号位,移动到符号位是什么就是什么)
右移操作符 移位规则:
首先右移运算分两种:
- 逻辑移位 左边用0填充,右边丢弃
- 算术移位 左边用原该值的符号位填充,右边丢弃
可以看出在VS编译器下采用的是算术右移
注意:
- 右移操作符分为逻辑右移、算术右移,通常采用的是算术右移
- 对于移位运算符,不要移动负数位,这个是标准未定义的。
例如:
int num = 10;
num>>-1;//error
补充:
- 负整数在内存中是以二进制补码的形式存储的
- 整数的二进制表示形式有3种:原码、反码、补码
- 针对负整数二进制的表示形式:
-
- 原码:直接根据数值写出的二进制序列就是原码
-
- 反码:原码的符号位不变,其他位按按取反就是反码
-
- 补码:反码+1,就是补码
- 对于正整数来说原码、反码、补码是相同的
位操作符
& //按位与
| //按位或
^ //按位异或
按位与操作符的使用:
按位或操作符的使用:
按位异或操作符的使用:
注:
- 他们的操作数必须是整数。
- 任何两个相同的数异或一定是0;0与任何一个数异或还是它本身
- 这些操作符运算时符号位也会计算进去
练习:
不能创建临时变量(第三个变量),实现两个数的交换。
//版本一:
int main()
{int a = 3;int b = 5;printf("a = %d b = %d\\n", a, b);//数值太大会溢出a = a + b;b = a - b;a = a - b;printf("a = %d b = %d\\n", a, b);return 0;
}//版本二:
int main()
{int a = 3;int b = 5;//交换printf("a=%d b=%d\\n", a, b);a = a ^ b;b = a ^ b;a = a ^ b;printf("a=%d b=%d\\n", a, b);return 0;
}
版本一是有缺陷的,当两个数值过大时两数相加会发生溢出现象(因为int存储是存在范围的,如果过大会发生溢出)。但是版本二就不存在这种现象,因为没有进位就不存在溢出。
编写代码实现:求一个整数存储在内存中的二进制中1的个数。
#include <stdio.h>int main()
{int num = -1;int i = 0;int count = 0;//计数while(num){count++;num = num&(num-1);}printf("二进制中1的个数 = %d\\n",count);return 0;
}
赋值操作符
赋值操作符的使用:
double salary = 10000.0;
salary = 20000.0;//使用赋值操作符赋值。//赋值操作符可以连续使用
int a = 10;
int x = 0;
int y = 20;
a = x = y+1;//连续赋值//与上面的代码含义相同
x = y+1;
a = x;
//这样的写法更加清晰爽朗而且易于调试。
复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
这些运算符都可以写成复合的效果
复合赋值符的使用:
int x = 10;
x = x+10;
x += 10;//复合赋值
//上面两行代码是等价的
//其他运算符一样的道理。这样写更加简洁。
注意:在C语言中=赋值;==判断是否相等
单目操作符
单目操作符是只有一个操作数的操作符
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
逻辑反操作符的使用:
int main()
{int flag = 5;printf("%d\\n", !flag);//0int u = 0;printf("%d\\n", !u); //1//flag为真,打印heheif (flag){printf("hehe\\n");}//flag为假,打印hahaif (!flag){printf("haha\\n");}return 0;
}
注:如果非0的值运用逻辑反操作符得到的是0;如果0值运用逻辑反操作符得到的是1
正值负值操作符的使用:
int a=10;
a=-a;
a=+a; //一般+是省略的
sizeof运算符的使用:
注:sizeof既可以通过变量名计算大小也可以通过类型计算大小
sizeof和数组
#include <stdio.h>
void test1(int arr[])
{printf("%d\\n", sizeof(arr));//4/8
}
void test2(char ch[])
{printf("%d\\n", sizeof(ch));//4/8
}
int main()
{int arr[10] = {0};char ch[10] = {0};printf("%d\\n", sizeof(arr));//40printf("%d\\n", sizeof(ch));//10test1(arr);test2(ch);return 0;
}
练习:
源文件变成可执行程序需要经过编译、链接、运行。s=a+2这样的表达式运行是在运行期间运行的,但是sizeof计算s的大小时sizeof(s=a+2)的时候是在编译期间计算的。sizeof计算s=a+2这个表达式时就已经知道判断的结果s说了算,所以sizeof就把这个计算s所占空间大小的2已经计算好了。计算好之后这个表达式就已经处理完了,所以当真正最终跑到运行期间的时候这个地方s=a+2这个代码就没有了,这个地方相当于就是个2了,打印2是运行期间要做的事情。但是注意s=a+2这个表达式已经在编译期间处理过了所以运行期间s=a+2根本没有计算,因此s的值没有发生过任何改变。
注:sizeof括号中的表达式是不参与运算的
按位取反操作符的使用
练习:
将特定二进制位上的数置为0或者1
int main()
{int a = 13;//把a的二进制中的第5位置成1a = a | (1 << 4);//00000000000000000000000000001101//00000000000000000000000000010000//00000000000000000000000000011101printf("a = %d\\n", a);//29//把a的二进制中的第5位置成0a = a & ~(1 << 4);//00000000000000000000000000011101//11111111111111111111111111101111 //00000000000000000000000000010000//00000000000000000000000000001101printf("a = %d\\n", a);//13return 0;
}
前置、后置++、–操作符的使用:
int main()
{int a = 10;printf("%d\\n", a--);//10printf("%d\\n", a);//9//后置++,先使用,再++int b = a++; //9//前置++, 先++,后使用int c = ++a; //11//后置--,先使用,后--int d = a--; //11//前置--,先--,后使用int e = --a; //9return 0;
}
补充:
#include <stdio.h>int main()
{int a = 1;int b = (++a) + (++a) + (++a);printf("%d\\n", b);return 0;
}
这段代码的运行结果在Linux平台下的gcc编译器中的结果是10;在Windows平台下的VS2019编译器中的结果是12。
取地址、解引用操作符的使用
int main()
{int a = 10;printf("%p\\n", &a);//& - 取地址操作符int * pa = &a;//pa是用来存放地址的 - pa就是一个指针变量*pa = 20;//* - 解引用操作符 - 间接访问操作符printf("%d\\n", a);//20return 0;
}
强制类型转换的使用:
关系操作符
> 大于
>= 大于等于
< 小于
<= 小于等于
!= 不相等
== 相等
注:
- 在编程的过程中== (判断相等)和=(赋值)不小心写错,导致的错误。
- 比较两个字符串是否相等不能用==进行判断
逻辑操作符
&& 逻辑与
|| 逻辑或
逻辑与:两者之中只要有一个为假则为假,两者同时为真则为真
逻辑或:两者之中只要有一个为真则为真,两者同时为假则为假
区分逻辑与和按位与 区分逻辑或和按位或
1&2----->0
1&&2---->11|2----->3
1||2---->1
经典笔试题:
#include <stdio.h>
int main()
{int i = 0,a=0,b=2,c =3,d=4;i = a++ && ++b && d++; //1 2 3 4//int i = 0,a=1,b=2,c =3,d=4;//i = a++ && ++b && d++; //2 3 3 5//i = a++||++b||d++; //1 3 3 4printf("a = %d\\n b = %d\\n c = %d\\nd = %d\\n", a, b, c, d);return 0;
}
注:逻辑与操作符的特点是只要当前条件为假时,后面的条件不进行判断,整个结果为假;逻辑或操作符的特点是只要当前条件为真时,后面的条件不进行判断,整个结果为真
条件操作符(三目操作符)
三目操作符:有三个操作数的操作符
exp1 ? exp2 : exp3
表达式一的结果如果为真整个表达式的结果是表达式二的运行结果;如果为假整个表达式的结果是表达式三的运行结果
如果表达式一结果为真表达式二计算表达式三不计算(表达式二的结果是整个表达式的结果);如果表达式一的结果为假表达式二不计算表达式三计算(表达式三的结果是整个表达式的结果)
条件操作符的使用:
int main()
{int a = 3;int b = 0;int m = 0;//版本一if (a > b)m = a;elsem = b;//版本二//三目操作符m = (a > b ? a : b);//这两段代码表示的含义一样return 0;
}
逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式中的逗号称为逗号操作符
逗号表达式的使用:
//代码1
int a = 1;
int b = 2;//逗号表达式
int c = (a>b, a=b+10, a, b=a+1); //a=12 b=13 c=13//代码2
if (a =b + 1, c=a / 2, d > 0)//代码3
a = get_val();
count_val(a);
while (a > 0)
{//业务处理a = get_val();count_val(a);
}
//使用逗号表达式改写:
while (a = get_val(), count_val(a), a>0)
{//业务处理
}
//这两段代码含义相同,只是第一段代码比较冗余
注:逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
下标引用、函数调用和结构成员的访问操作符
[ ] 下标引用操作符
操作数:一个数组名 + 一个索引值
下标引用操作符的使用:
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; //[]定义数组时指定数组大小的一种语法格式// 0 1 2 3 4printf("%d\\n", arr[4]);//[] - 就是下标引用操作符//[] 访问数组具体的某一个元素//[] 的操作数是2个:arr , 4return 0;
}
( ) 函数调用操作符
函数调用操作符的使用:
//函数的定义
int Add(int x, int y)
{return x + y;
}
void test()
{}int main()
{int a = 10;int b = 20;//函数调用int ret = Add(a, b);//() - 函数调用操作符//()操作符的操作数有三个:Add、a、btest();//() - 函数调用操作符//()操作符的操作数有一个:testreturn 0;
}
注:( ) 函数调用操作符接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
访问一个结构的成员
. 结构体.成员名
-> 结构体指针->成员名
结构体成员访问操作符的使用:
//结构成员访问操作符
//.
//->//创建了一个自定义的类型
struct Book
{//结构体的成员(变量)char name[20];char id[20];int price;
};int main()
{struct Book b = {"C语言", "C20210509", 55};struct Book * pb = &b;//结构体指针->成员名printf("书名:%s\\n", pb->name);printf("书号:%s\\n", pb->id);printf("定价:%d\\n", pb->price);printf("书名:%s\\n", (*pb).name);printf("书号:%s\\n", (*pb).id);printf("定价:%d\\n", (*pb).price);//结构体变量名.成员名printf("书名:%s\\n", b.name);printf("书号:%s\\n", b.id);printf("定价:%d\\n", b.price);return 0;
}
表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
- 表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
- 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
- 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
整型提升的实例:
//实例1
char a,b,c;a = b + c;
b和c的值被提升为普通整型,然后再执行加法运算。加法运算完成之后,结果将被截断,然后再存储于a中
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001//无符号整形提升,高位补0
详解:
int main()
{char a = 3;//00000000000000000000000000000011//00000011 - achar b = 127;//00000000000000000000000001111111//01111111 - b//这里进行整形算术运算时a和b都是char类型的,都没有达到一个int的大小,这里就需要发生整形提升char c = a + b;//00000000000000000000000000000011//00000000000000000000000001111111//00000000000000000000000010000010//发生截断,因为char只能存储1个字节(8比特位),只能放低8比特位//10000010 - c//内存中存储的是补码,内存中计算是以补码的形式计算的。%d打印是以原码的形式进行打印的//11111111111111111111111110000010 - 补码//11111111111111111111111110000001 - 反码//10000000000000000000000001111110 - 原码//-126//%d有符号的整形打印,这里需要进行整型提升printf("%d\\n", c);//打印出来肉眼可见的是原码//-126return 0;
}
总结:表达式中的字符和短整型操作数在进行计算时,会进行整型提升。这是因为在运算时不管是字符和短整型都没有达到整形的大小,但是CPU计算的时候又是用整形的方式计算的,这个时候把字符和短整型提升成整形,那计算的精度也会变高。
注:非整型类型只要参与运算就会整型提升这句话是错的。float不需要整型提升的,自身的大小达不到整形大小才会进行整型提升的
整形提升的例子:
实例一:
实例一中的a,b要进行整形提升(因为要进行比较),但是c不需要整形提升(因为c是整型) a,b整形提升之后,变成了负数,所以表达式a == 0xb6,b == 0xb600 的结果是假,但是c不发生整形提升,则表达式 c==0xb6000000 的结果是真
实例二:
int main()
{char c = 1;printf("%u\\n", sizeof(c));//1printf("%u\\n", sizeof(+c));//4printf("%u\\n", sizeof(-c));//4printf("%u\\n", sizeof(!c));//4 这里以gcc编译器为准 gcc - 4(在vs中运行结果是1)return 0;
}
实例二中的c只要参与表达式运算就会发生整形提升,表达式 +c会发生提升,所以sizeof(+c) 是4个字节。表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节。表达式 !c 也会发生整形提升,所以 sizeof(!c) 是4个字节(这里以gcc为准,因为gcc编译器设计时更接近C语言标准)。但是sizeof( c ) ,就是1个字节。
注:
- sizeof()中的表达式是不参与运算的,没有真正运算,但是它看的是这个表达式如果运算之后产生结果的类型是什么(推算这个表达式计算完之后的结果是什么)
- 任何表达式都有两个属性:类属性(可以推导出来的)、值属性(计算出来的)
补充:sizeof函数返回的类型是无符号整数
算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
注:
- 上面这些类型是从下往上进行转换的
- 但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
操作符的属性
复杂表达式的求值有三个影响的因素:
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
int main()
{int a = 4;int b = 5;//int c = a + b * 7;//优先级决定了计算顺序int c = a + b + 7;//优先级不起作用,结合性决定了顺序return 0;
}
操作符优先级
操作符 | 描述 | 用法示例 | 结果类型 | 结合性 | 是否控制求值顺序 |
---|---|---|---|---|---|
() | 聚组 | (表达式) | 与表达式同 | N/A | 否 |
() | 函数调用 | rexp(rexp,…,rexp) | rexp | L-R | 否 |
[ ] | 下标引用 | rexp[rexp] | lexp | L-R | 否 |
. | 访问结构成员 | lexp.member_name | lexp | L-R | 否 |
-> | 访问结构指针成员 | rexp->member_name | lexp | L-R | 否 |
++ | 后缀自增 | lexp ++ | rexp | L-R | 否 |
– | 后缀自减 | lexp – | rexp | L-R | 否 |
! | 逻辑反 | ! rexp | rexp | R-L | 否 |
~ | 按位取反 | ~ rexp | rexp | R-L | 否 |
+ | 单目,表示正值 | + rexp | rexp | R-L | 否 |
- | 单目,表示负值 | - rexp | rexp | R-L | 否 |
++ | 前缀自增 | ++ lexp | rexp | R-L | 否 |
– | 前缀自减 | – lexp | rexp | R-L | 否 |
* | 间接访问 | * rexp | lexp | R-L | 否 |
& | 取地址 | & lexp | rexp | R-L | 否 |
sizeof | 取其长度,以字节表示 | sizeof rexp sizeof(类型) | rexp | R-L | 否 |
(类型) | 类型转换 | (类型) rexp | rexp | R-L | 否 |
* | 乘法 | rexp * rexp | rexp | L-R | 否 |
/ | 除法 | rexp / rexp | rexp | L-R | 否 |
% | 整数取余 | rexp % rexp | rexp | L-R | 否 |
+ | 加法 | rexp + rexp | rexp | L-R | 否 |
- | 减法 | rexp - rexp | rexp | L-R | 否 |
<< | 左移位 | rexp << rexp | rexp | L-R | 否 |
>> | 右移位 | rexp >> rexp | rexp | L-R | 否 |
> | 大于 | rexp > rexp | rexp | L-R | 否 |
>= | 大于等于 | rexp >= rexp | rexp | L-R | 否 |
< | 小于 | rexp < rexp | rexp | L-R | 否 |
<= | 小于等于 | rexp <= rexp | rexp | L-R | 否 |
== | 等于 | rexp == rexp | rexp | L-R | 否 |
!= | 不等于 | rexp != rexp | rexp | L-R | 否 |
& | 位与 | rexp & rexp | rexp | L-R | 否 |
^ | 位异或 | rexp ^ rexp | rexp | L-R | 否 |
| | 位或 | rexp | rexp | rexp | L-R | 否 |
&& | 逻辑与 | rexp && rexp | rexp | L-R | 是 |
|| | 逻辑或 | rexp || rexp | rexp | L-R | 是 |
? : | 条件操作符 | rexp ? rexp : rexp | rexp | N/A | 是 |
= | 赋值 | lexp = rexp | rexp | R-L | 否 |
+= | 以…加 | lexp += rexp | rexp | R-L | 否 |
-= | 以…减 | lexp -= rexp | rexp | R-L | 否 |
*= | 以…乘 | lexp *= rexp | rexp | R-L | 否 |
/= | 以…除 | lexp /= rexp | rexp | R-L | 否 |
%= | 以…取模 | lexp %= rexp | rexp | R-L | 否 |
<<= | 以…左移 | lexp <<= rexp | rexp | R-L | 否 |
>>= | 以…右移 | lexp >>= rexp | rexp | R-L | 否 |
&= | 以…与 | lexp &= rexp | rexp | R-L | 否 |
^= | 以…异或 | lexp ^= rexp | rexp | R-L | 否 |
|= | 以…或 | lexp |= rexp | rexp | R-L | 否 |
, | 逗号 | rexp,rexp | rexp | L-R | 是 |
有一些表达式是没有办法确定唯一的计算路径的——问题表达式
一些问题表达式:
实例一:
//表达式的求值部分由操作符的优先级决定。
a*b + c*d + e*f
注释:实例一在计算的时候,由于比+的优先级高,只能保证* 的计算是比+早,但是优先级并不能决定第三个* 比第一个+早执行。
所以表达式的计算顺序就可能是:
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f或者:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
实例二:
c + --c;
注释:同上,操作符的优先级只能决定自减–的运算在+的运算的前面,但是并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
实例三:
//非法表达式
int main()
{int i = 10;i = i-- - --i * ( i = -3 ) * i++ + ++i;printf("i = %d\\n", i);return 0;
}
实例三在不同编译器中测试结果:非法表达式程序的结果
值 | 编译器 |
---|---|
-128 | Tandy 6000 Xenix 3.2 |
-95 | Think C 5.02(Macintosh) |
-86 | IBM PowerPC AIX 3.2.5 |
-85 | Sun Sparc cc(K&C编译器) |
-63 | gcc,HP_UX 9.0,Power C 2.0.0 |
4 | Sun Sparc acc(K&C编译器) |
21 | Turbo C/C++ 4.5 |
22 | FreeBSD 2.1 R |
30 | Dec Alpha OSF1 2.0 |
36 | Dec VAX/VMS |
42 | Microsoft C 5.1 |
实例四:
int fun()
{static int count = 1;return ++count;
}
int main()
{int answer;answer = fun() - fun() * fun();printf( "%d\\n", answer);return 0;
}
这个代码是有实际的问题。虽然在大多数的编译器上求得结果都是相同的。但是上述代码 answer = fun() - fun() * fun(); 中只能通过操作符的优先级得知:先算乘法,再算减法。但函数的调用先后顺序无法通过操作符的优先级确定。
实例五:
#include <stdio.h>
int main()
{int i = 1;int ret = (++i) + (++i) + (++i);printf("%d\\n", ret);printf("%d\\n", i);return 0;
}
这个代码是有问题的。在Linux平台下gcc编译器中的结果是10(3+3+4);而在Windows平台下VS编译器中的结果是12(4+4+4)。看看同样的代码产生了不同的结果,因为这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的。因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。
总结:程序员写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
补充:
- 计算机中有一些寄存器 eg:eax、ebx、ecx、edx、ebp、esp
- 汇编代码在不同的硬件上汇编代码不一样