通过这篇文章我们将熟练掌握C语言中的各种操作符,表达式求值是如何使用操作符的,表达式求值该注意一些什么呢? 这里概念性的东西比较少,主要是通过代码演示,会正确使用各种操作符(12815字)
目录
一、操作符
分类:
- 算术操作符
- 移位操作符
- 位操作符
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符
- 逗号表达式
- 下标引用、函数调用和结构成员
1.算数操作符
+ - * / %
注意:
- 除了% 操作符之外,其他的几个操作符都可以作用于整数和浮点数。
- % 操作符的两个操作数必须为整数。返回的是整除之后的余数
如下代码:
int main()
{
3.5 % 2.5;
return 0;
}
发现编译器报错了
3. 对于/ 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除
法
如下代码:
#include <stdio.h>
int main()
{
printf("%d\n", 5 / 2);
printf("%f\n", 5.0 / 2);
return 0;
}
运行结果:
2. 移位操作符
<< 左移操作符
>> 右移操作符
-
左移操作符
左边丢弃,右边补零
如下代码:
#include <stdio.h>
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 <stdio.h>
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;//
3.位操作符
& //按位与
| //按位或
^ //按位异或
注:他们的操作数必须是整数
按位与
我们来看下面的代码:
#include <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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;
}
运行结果:
4. 赋值操作符
相信经过一段时间的学习,对赋值操作已经有了一定的了解,这里再啰嗦两句
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;
}
5.复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
将运算操作符写成了复合的形式,这样写更加简洁
如下示例:
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;
6.单目操作符
我们通过前面的学习会发现+,-等这些操作符都是有两个操作数,称为双目操作符,那么这里的单目操作符就是只有一个操作数的操作符
! 逻辑反操作
- 负值
+ 正值
& 取地址
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 <stdio.h>
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 <stdio.h>
int main()
{
int a = 0;
//分析:
//a的原码:00000000000000000000000000000000
//补码: 00000000000000000000000000000000
//按位取反 11111111111111111111111111111111
//反码; 11111111111111111111111111111110
//原码: 10000000000000000000000000000001
//所以结果应该是-1
printf("%d\n", ~a);
}
运行结果:
5.自增,自减操作符
#include <stdio.h>
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 <stdio.h>
int main()
{
int a = (int)3.14;
return 0;
}
7.关系操作符
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
注意:
- >=,<=,!=,==这几个由两个符号组成的操作符中间不能有空格
- 等于==和赋值=分清楚
8.逻辑操作符
&& 逻辑与
|| 逻辑或
逻辑操作符只关心操作数的真假
1.逻辑与 && ,如果两边的值都为真,那么结果为真,如果其中有一个为假,那么结果为假
2.逻辑或 || ,如果两边有一个值为真,那么结果为真,如果两个都为假,那么结果为假
代码演示:
#include <stdio.h>
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 <stdio.h>
int main()
{
int age = 0;
printf("请输入年龄\n");
scanf("%d", &age);
if (18 <= age <= 30)
{
printf("青年人\n");
}
return 0;
}
发现当输入70的时候仍然能够打印成年人
主要就是在if里面如果要这样判断一个范围的话不能这样用,而应该用逻辑与&&
一道笔试题
#include <stdio.h>
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,右边就不需要再算了
9.条件操作符
这里的条件操作符有三个操作数,所以也叫三目操作符。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简洁很多
10.逗号表达式
逗号隔开的一串表达式。逗号表达式的结果从左向右依次计算,但表达式的结果是最后一个表达式的结果
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
11. 下标引用、函数调用和结构成员
1. [ ] 下标引用操作符
操作数:一个数组名 + 一个索引值
这个下标引用操作符在学数组的时候就已经很了解了,这里主要再说一下它的一些其他使用方式
#include <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
int main()
{
int i = 10;
i = i-- - --i * (i = -3) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
这段代码来源于《C和指针》
作者将这段代码在11种不同的编译器中编译出来的结果都是不一样的
示范4
#include <stdio.h>
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语言,每个知识点后面都会单独写博客更加详细的介绍
欢迎大家关注!!!
一起学习交流 !!!
让我们将编程进行到底!!!
--------------整理不易,请三连支持------------------