通过这篇文章我们将熟练掌握C语言中的各种操作符,表达式求值是如何使用操作符的,表达式求值该注意一些什么呢? 这里概念性的东西比较少,主要是通过代码演示,会正确使用各种操作符(12815字)
目录
一、操作符
1.算数操作符
2. 移位操作符
左移操作符
右移操作符
3.位操作符
按位与
按位或
按位异或
变态的笔试题
一道笔试题
4. 赋值操作符
5.复合赋值符
6.单目操作符
7.关系操作符
8.逻辑操作符
一道笔试题
9.条件操作符
10.逗号表达式
11. 下标引用、函数调用和结构成员
二、表达式求值
隐式类型转换
整型提升
整型提升的意义
如何进行整型提升
算术转换
操作符的属性
复杂表达式的求值有三个影响的因素
错误示范
分类:
+ - * / %
注意:
如下代码:
int main()
{
3.5 % 2.5;
return 0;
}
发现编译器报错了
3. 对于/ 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除
法
如下代码:
#include
int main()
{
printf("%d\n", 5 / 2);
printf("%f\n", 5.0 / 2);
return 0;
}
运行结果:
<< 左移操作符
>> 右移操作符
左边丢弃,右边补零
如下代码:
#include
int main()
{
int a = 7;
int b = a << 1;
printf("%d\n", b);
return 0;
}
分析:
要知道移位移的是二进制位
这里的a是整型变量,4个字节32bit,所以7写成二进制应该是
00000000000000000000000000000111
向左移动一位,就是左边丢弃一位,右边补零
00000000000000000000000000001110
如下图:
运行结果:
右移操作分为两种
逻辑右移和上面的左移操作类似,这边不再啰嗦
主要看一下算数右移
我们知道,有符数分为正数和负数,它们的符号位分别为:
正数----0
负数----1
也就是说如果正数进行算数右移的话,应该是右边丢弃,左边补0
负数进行算数右移的话,应该是右边丢弃,左边补1
我们通过以下代码来验证一下算数右移
#include
int main()
{
int a = -1;
int b = a >> 1;
printf("%d\n", b);
return 0;
}
分析:
这里我们还需要知道的是,整数在内存中存放的是二进制,并且是以补码形式来存的
这里的-1
原码:10000000000000000000000000000001
反码:11111111111111111111111111111110
补码:11111111111111111111111111111111
将它进行算数右移的话就是右边丢弃,左边补1
结果:11111111111111111111111111111111
仍然还是-1
运行结果:
也从这里可以看出来我们的编译器进行的是算数右移
注意:对于移位运算符,不要移动负数位,这个是标准未定义的
如下代码:
int num = 20;
num >> -1;//
& //按位与
| //按位或
^ //按位异或
注:他们的操作数必须是整数
我们来看下面的代码:
#include
int main()
{
int a = 3;
int b = 5;
int c = a & b;
printf("%d\n", c);
return 0;
}
分析:
按位与这里的位指的是二进制位
每位数一一对应,全1为1,有0为0
3的二进制为
00000000000000000000000000000011
5的二进制为
00000000000000000000000000000101
然后将3和5对应位相与
结果就是
00000000000000000000000000000001
运行结果:
有1为1,全0为0
代码如下:
#include
int main()
{
int a = 3;
int b = 5;
int c = a | b;
printf("%d\n", c);
return 0;
}
分析:
3的二进制:00000000000000000000000000000011
5的二进制:00000000000000000000000000000101
相或结果就是:00000000000000000000000000000111
结果为7
运行验证:
不同为0,相同为1
代码如下:
#include
int main()
{
int a = 3;
int b = 5;
int c = a ^ b;
printf("%d\n", c);
return 0;
}
分析:
3的二进制:00000000000000000000000000000011
5的二进制:00000000000000000000000000000101
相异或结果就是:00000000000000000000000000000110
结果为6
运行验证:
题目:不能创建临时变量(第三个变量),实现两个数的交换
之前我们实现两个数的交换都是通过创建第三个变量来实现交换的,如下代码:
代码1:
#include
int main()
{
int a = 1;
int b = 2;
int t = 0; //创建临时变量
t = a;
a = b;
b = t;
printf("a==%d\n", a);
printf("b==%d\n", b);
}
运行结果:
上面这就是我们通常的操作,但是如果不创建第三个变量,如何做到交换两数呢?
我们通过异或操作符来达到交换两数的目的
代码2:
#include
int main()
{
int a = 3;
int b = 5;
printf("a == %d ", a);
printf("b == %d\n", b);
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("a == %d ", a);
printf("b == %d\n", b);
return 0;
}
运行结果
分析:
如下图示:
会发现三次异或之后a,b两数完成了交换
这是为什么呢?
我们可以将第一次a^b的结果110看作是一个密码,这个密码和b异或之后翻译出来的值为011就是原本a的值,再将这个密码和a异或翻译出来的值就是原本b的值
相当于是这样:
a^b^a==b
a^b^b==a
举例:
011^011==000
000^111==111
两个相同的数字异或的结果为0,用这个0作为密码去和其他一个数字异或,结果还是这个数字
所以我们也可以做如下总结:
a^a==0
0^a==a
注意:一般在开发中我们都是用临时变量的方法来交换两数
两种方法比较:
创建临时变量:代码可读性高,代码执行效率相对较高
异或:代码可读性差,代码执行效率相对较低
编写代码实现:求一个整数存储在内存中的二进制中1的个数
方法1:
分析:
通过将这个数与1按位与的方法,可以知道这个数最后一位是不是1,然后将这个数右移一位,再继续使用这种方法来看最后一位是不是1,但这种方法必须是循环32次(整型32位)
#include
int main()
{
int n = 6;
int i = 0;
int count = 0;
for (i = 0; i < 32; i++)
{
if (((n >> i) & 1) == 1)
count++;
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
运行结果 这种方法不论有多少个1,都必须循环32次,效率较低
方法2:
分析:
先来看看以下几组计算:
111^110==110 n&(n-1)
110^101==100
100^011==0
可以发现n&(n-1)相当于是把n中最右边的1给消掉了
那么我们其实就可以通过n=n&(n-1)这样的方式来求得1的个数,赋值了几次,1就是几
#include
int main()
{
int n = 6;
int i = 0;
int count = 0;
while (n)
{
count++;
n = n & (n - 1);
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
运行结果:
相信经过一段时间的学习,对赋值操作已经有了一定的了解,这里再啰嗦两句
1.赋值操作符可以连续使用
int main()
{
int a = 1;
int b = 2;
int x = b = a; //连续赋值
}
但是建议不要出现这种写法,这种不便于调试与阅读
以下的代码更易调试
int main()
{
int a = 1;
int b = 2;
int x = 0;
b = a;
x = b;
}
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
将运算操作符写成了复合的形式,这样写更加简洁
如下示例:
a = a + 3;
a += 3;
b = b / 3;
b /= 3;
c = c % 2;
c %= 2;
d = d >> 1;
d >>= 1;
e = e & 1;
e &= 1;
我们通过前面的学习会发现+,-等这些操作符都是有两个操作数,称为双目操作符,那么这里的单目操作符就是只有一个操作数的操作符
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
1.逻辑反操作 ! 将一个数或表达式的真值改变,原来为假,进行逻辑反操作之后变为真
int main()
{
int a = 5;
int b = 0;
printf("a == %d\n", a);
printf("!a == %d\n", !a);
printf("b == %d\n", b);
printf("!b == %d\n", !b);
return 0;
}
运行结果:
注意在C语言里面 非0表示真,0表示假
使用如下示例:
#include
int main()
{
int exp = 0;
if (!exp)
{
printf("hello\n");
}
return 0;
}
分析:
这里exp原本是0,0表示假,在if语句的判断中,!exp将exp的真值变为真,所以就会执行if语句里面的语句,打印hello
运行结果:
2.取地址操作符 &
这个操作符用来取出变量的地址
如下代码
int main()
{
int a = 10;
int* pa = &a; //创建指针变量将a的地址存进去
*pa = 15;
printf("%d\n", a);
return 0;
}
分析:
这段代码中我们通过&a拿到a的地址并将其存进指针变量pa里面,然后通过解引用操作符 * 将pa所指向的对象内容修改为15
运行结果:
指针图示:
3.sizeof
可能很多人都把sizeof当作函数了,但其实sizeof是一个操作符,不是函数。可以用来计算类型的大小
如下代码:
int main()
{
int a = 5;
int arr[10] = { 0 };
printf("%d\n", sizeof a); //通过变量名来计算的时候sizeof的括号可以省略
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(int[10])); //这两种方式都能用来计算数组的大小,数组名去掉就是数组的类型
return 0;
}
运行结果:
注意第二种求数组大小的方法---数组名去掉就是数组的类型
4. ~ 这是对一个数的二进制位按位取反
如下代码:
#include
int main()
{
int a = 0;
//分析:
//a的原码:00000000000000000000000000000000
//补码: 00000000000000000000000000000000
//按位取反 11111111111111111111111111111111
//反码; 11111111111111111111111111111110
//原码: 10000000000000000000000000000001
//所以结果应该是-1
printf("%d\n", ~a);
}
运行结果:
5.自增,自减操作符
#include
int main()
{
int a = 0;
int b = a++; //后置++,先使用a的值,然后再让a加1
int c = 1;
int d = ++c; //前置++,先让c加1,然后再把c的值赋给d
printf("a=%d,b=%d\n",a,b);
printf("c=%d,d=%d\n", c, d);
}
注意自增操作符是带有副作用的
如下代码:想要让b实现a+1的效果
int main()
{
int a = 15;
int b = 0;
b = a + 1; // 方法1
//b = ++a; //方法2
}
这两种方法都可以让b达到a+1的效果,但是第二种方法产生副作用,让a的值也自增了1
6.强制类型转换操作符
如下代码:
3.14是double类型的,将double类型的数据赋给int类型,我们发现编译器报了一个警告
那么我们应该怎么解决呢? 这个时候就可以用强制类型转换,将3.14强制转换成整型
#include
int main()
{
int a = (int)3.14;
return 0;
}
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
注意:
&& 逻辑与
|| 逻辑或
逻辑操作符只关心操作数的真假
1.逻辑与 && ,如果两边的值都为真,那么结果为真,如果其中有一个为假,那么结果为假
2.逻辑或 || ,如果两边有一个值为真,那么结果为真,如果两个都为假,那么结果为假
代码演示:
#include
int main()
{
int a = 5;
int b = 3;
int c = 0;
int d = 0;
printf("%d,%d\n", a && b, a && c);
printf("%d,%d\n", a || b, d || c);
}
运行结果:
现在再看一下我们之前遇到过的一个问题:
判断年龄在18--30之间的为青年
#include
int main()
{
int age = 0;
printf("请输入年龄\n");
scanf("%d", &age);
if (18 <= age <= 30)
{
printf("青年人\n");
}
return 0;
}
发现当输入70的时候仍然能够打印成年人
主要就是在if里面如果要这样判断一个范围的话不能这样用,而应该用逻辑与&&
#include
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
i = a++||++b||d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
想想这道题的打印结果是什么呢? 是2 3 3 5吗?
分析:
要知道,在逻辑与&&运算中,只要有一个为假,则整体为假,这个式子中a++的值为0,为假,右边就不用再算了,不需要把所有式子都算完,所以第一个printf的结果应该是1 2 3 4
第二个运算的式子中a++值为1,为真,后面的几个就不需要再算了,直接整体为真,第二个printf的打印结果应该是
总结:
逻辑与 && 来说,如果左边为0,右边就不用再算了
逻辑或 || 来说,左边如果为1,右边就不需要再算了
这里的条件操作符有三个操作数,所以也叫三目操作符。exp1 ? exp2 : exp3
含义:表达式1的结果如果为真,就执行表达式2,如果表达式1的结果为假,就执行表达式3
我们之前写代码求两个数的最大值都是通过if语句来判断,现在我们完全可以通过条件操作符来计算,可以极大的简化代码
通过以下两段代码的对比来体验一下:
代码1:
int main()
{
int a = 1;
int b = 2;
int max = 0;
if (a > b)
{
max = a;
}
else
{
max = b;
}
return 0;
}
代码2:
int main()
{
int a = 1;
int b = 2;
int max = 0;
a > b ? a : b;
return 0;
}
这两段代码实现的是同样的功能,但明显可以发现代码2比代码1简洁很多
逗号隔开的一串表达式。逗号表达式的结果从左向右依次计算,但表达式的结果是最后一个表达式的结果
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
printf("%d\n", c);
return 0;
}
执行完之后c的值是多少呢?
分析:
整个逗号表达式中,从左向右依次计算,首先a>b进行了判断,不产生结果,然后a=b+10,a变成了12,再执行了a,不产生结果,b=a+1执行之后,b的值变成了13,所以c的值为13
1. [ ] 下标引用操作符
操作数:一个数组名 + 一个索引值
这个下标引用操作符在学数组的时候就已经很了解了,这里主要再说一下它的一些其他使用方式
#include
int main()
{
int arr[5] = { 4,3,2,1,0 };
printf("%p\n", arr + 4); //数组名表示首元素地址
printf("%p\n", &arr[4]);
printf("%d\n", *(arr + 4));
printf("%d\n", arr[4]);
printf("%d\n", 4[arr]);
return 0;
}
注意最后一种写法
2.( ) 函数调用操作符
我们平时在调用函数的时候必须要有这个圆括号,不能省略,里面可以放函数的参数
3.访问一个结构体的成员
C语言里面有两种方式可以访问结构体成员,一个是
结构体变量.结构体成员
结构体指针->结构体成员
我们可以通过以下代码再来体验一下
#include
struct Peo
{
char name[20];
int age;
};
int main()
{
struct Peo p = { "张三",18 }; //创建结构体变量并初始化
struct Peo* ps = &p; //创建一个结构体指针
//结构体变量.结构体成员
printf("姓名:%s 年龄:%d", p.name, p.age); //用.来访问
//结构体指针->结构体成员
printf("姓名:%s 年龄:%d", ps->name, ps->age); //用指针访问
return 0;
}
表达式求值的顺序一部分是由操作符的优先级和结合性决定,同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型
偷偷进行类型的转换(并不会直观的展现出来)
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升
表达式中可以使用整数的地方,就可以使用枚举类型,或有符号或无符号的字符、短整数、整数位域。如果一个int可以表示上述类型,则该值被转化为int类型的值;否则,该值被转化为unsigned int类型的值。这一过程被称作integral promotion。
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度
一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长
度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转
换为int或unsigned int,然后才能送入CPU去执行运算。
我们通过下面的例子来说明
#include
int main()
{
char a = 3;
char b = 127;
char c = a + b;
printf("%d\n", c);
return 0;
}
分析:
这里的char c = a + b这句执行的时候会发生整型提升
首先a和b要进行加法运算,a和b都要提升为整型
a和b原本为char类型,8bit
00000011-----a
01111111-----b
进行整型提升,因为 char 为有符号的 char,所以整形提升的时候,高位补充符号位,即为0
00000000000000000000000000000011-----a提升后
00000000000000000000000001111111-----b提升后
00000000000000000000000010000010-----相加结果
要赋给c,但c是char类型,只能存8位,存进去的应该是
10000010-----c里面
在进行打印的时候%d以整型进行打印,这里还要发生整型提升char 为有符号的 char,所以整形提升的时候,高位补充符号位
11111111111111111111111110000010-----c提升之后的补码
11111111111111111111111110000001-----c提升之后的反码
10000000000000000000000001111110-----c提升之后的原码
为-126
运行结果:
参与运算的操作数类型大于int
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算
注意:算术转换要合理,要不然会有一些潜在的问题(精度丢失等)
注意:两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性
操作符 | 描述 | 用法示例 | 结果类型 | 结合性 | 是否控制求值顺序 |
() | 聚组 | (表达式) | 与表达 式同 |
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 | 是 |
示范1:
a*b + c*d + e*f
这是一个有问题的代码
分析:
我们都知道 * 优先级比 + 高,但是存在的问题是:
- 这三个乘号谁先算
- 第三个*和第一个+谁先算
出现多种计算方式
示范2:
c + --c;
分析:
这个表达式先算+还是先算--可以由优先级来确定
存在的问题是谁先准备好
到底是先准备好c还是先准备好--c,这是很重要的
有以下两种
int c = 2; c + --c; //先算--c //--c执行后c是1 //表达式就是1 + 1
2.
int c = 2; c + --c; //先准备好c //整个表达式就成了2 + 1
这两种计算出来的结果是不一样的
示范3:
#include
int main()
{
int i = 10;
i = i-- - --i * (i = -3) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
这段代码来源于《C和指针》
作者将这段代码在11种不同的编译器中编译出来的结果都是不一样的
示范4
#include
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();这个表达式是个问题表达式,我们只能知道先算*再算-,但到底先调用哪个fun()是不确定的
注意:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
虽然已经掌握了操作符的优先级,结合性,以及控制求值顺序这些属性,但还是没有办法确定出每个表达式的唯一求值方式。由此可见我们平时在写代码的时候应该要避免写出问题表达式,写出的表达式要由唯一确定的计算路径
--------------------------------------------------------------------------------
-------------------------C语言操作符部分完结-----------------------------
关于C语言,每个知识点后面都会单独写博客更加详细的介绍
欢迎大家关注!!!
一起学习交流 !!!
让我们将编程进行到底!!!
--------------整理不易,请三连支持------------------