第五节 操作符
目录
- 一. 操作符分类
- 二. 算术操作符
- 三. 移位操作符
- 1. 二进制位
- 2. 左移操作符
- 3. 右移操作符
- 四. 位操作符
- 1. 按位与
- 2. 按位或
- 3. 按位异或
- 五. 赋值操作符
- 六. 单目操作符
- 1. 概述
- 2. sizeof是操作符而不是函数
- 3. ~对一个数的二进制按位取反
- 4. ++与- -
- 5. sizeof和数组
- 七. 关系操作符
- 八. 逻辑操作符
- 九. 条件操作符
- 十. 逗号表达式
- 十一. 下标引用、函数调用和结构成员
- 1. [ ] 下标引用操作符
- 2. ( ) 函数调用操作符
- 3. 访问一个结构的成员
- 十二. 表达式求值
- 1. 隐式类型转换
- 2. 算术转换
- 3. 操作符的属性
本章重点:
各种操作符的介绍
表达式求值
一. 操作符分类
算术操作符 + - * / %
移位操作符 >> <<
位操作符 & | ^
赋值操作符 = += -= *= /=
单目操作符 ! size of ++ --
关系操作符 > < >= <= == !=
逻辑操作符 && ||
条件操作符 ? :
逗号表达式 ,
下标引用、函数调用和结构成员 [] () . ->
二. 算术操作符
+ - * / %
- 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
- 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
- % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
三. 移位操作符
<< 左移操作符
>> 右移操作符
注:移位操作符的操作数只能是整数。
1. 二进制位
整数的二进制表现形式一共有三种,原码、反码和补码。
原码:按照数值的正负,直接写出的二进制序列就是原码。
反码:原码的符号位不变,其他位按位取反。
补码:反码的二进制+1就得到了补码。
对于有符号的整数来说,最高位的1位是符号位。
符号位是1表示负数
符号位是0表示正数。
对于无符号整数来说,没有符号位,所有位都是有效位。
对于正的整数,原码、反码和补码相同,无需计算。
对于负的整数,原码、反码和补码是需要计算的。
整数在内存中存储的都是补码的二进制序列。
整数在计算的时候也使用的是补码!
2. 左移操作符
位移规则:左边抛弃,右边补零
代码如下:
#include<stdio.h>
int main() {
int m = 7;
int n = m << 1;
printf("%d\n",m);
printf("%d",n);
return 0;
}
运行结果如下:
#include<stdio.h>
int main() {
int m = -7;
int n = m << 1;
printf("%d\n",m);
printf("%d",n);
return 0;
}
运行结果如下:
m在没被赋值的情况下自身的值不会被改变。
多写几组代码进行比对。发现,对于正整数而言,左移一位操作运算就是乘2。再次深入研究,发现正整数左移运算的效果就是将原整数的值乘以2的指定次幂。例如,将一个整数n左移k位,相当于将n乘以2的k次方。对于负整数而言,左移一位操作运算也是乘2。
3. 右移操作符
右移操作符分两种方式:一种是算数右移,另一种是逻辑右移。
逻辑右移:右边直接丢弃,左边补0
算数右移:左边用原该值的符号位填充,右边丢弃
不同的编译器,采用的右移方式也不同。绝大部分编译器采用的是算数右移。
vs2019采用的是算数右移,代码如下:
#include<stdio.h>
int main()
{
int a = -10;
int b = a >> 1;
printf("b=%d\n", b);
printf("a=%d\n", a);
return 0;
}
运行成功,结果如下:
注意:对于移位运算符,不要移动负数位,这个是标准未定义的。
例如:
int num = 10;
num>>-1;//error
四. 位操作符
位操作符有:
按位与 &
按位或 |
按位异或 ^
注意:他们的操作数必须是整数
1. 按位与
代码如下:
#include<stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a & b;//按(2进制)位与
//00000000000000000000000000000011 --- 3的补码
//10000000000000000000000000000101
//11111111111111111111111111111010
//11111111111111111111111111111011 --- -5的补码
//00000000000000000000000000000011 --- 3的补码
//00000000000000000000000000000011
printf("%d\n", c);
return 0;
}
运行代码成功,结果如下:
我们可以利用按位与得出二进制位的最后一位,代码如下:
#include<stdio.h>
int main()
{
int a = 3;
//a&1
//00000000000000000000000000000011 --- 3的补码
//00000000000000000000000000000001
return 0;
}
要是要用到其他二进制位,可以先位移,再按位与。
2. 按位或
代码如下:
#include<stdio.h>
int main()
{
int a = 3;
int b = -5;
//00000000000000000000000000000011 --- 3的补码
//10000000000000000000000000000101
//11111111111111111111111111111010
//11111111111111111111111111111011 --- -5的补码
int c = a | b;
//00000000000000000000000000000011 --- 3的补码
//11111111111111111111111111111011 --- -5的补码
//11111111111111111111111111111011
//11111111111111111111111111111010
//10000000000000000000000000000101 -5
//11111111111111111111111111111011
//10000000000000000000000000000100
//10000000000000000000000000000101
printf("%d\n", c);
return 0;
}
运行结果成功,结果如下:
3. 按位异或
相同为0,相异为1
代码如下:
#include<stdio.h>
int main()
{
int a = 3;
int b = -5;
//00000000000000000000000000000011 --- 3的补码
//10000000000000000000000000000101
//11111111111111111111111111111010
//11111111111111111111111111111011 --- -5的补码
int c = a ^ b;
//00000000000000000000000000000011
//11111111111111111111111111111011
//11111111111111111111111111111000
//10000000000000000000000000000111
//10000000000000000000000000001000
printf("%d\n", c);
return 0;
}
运行代码成功,结果如下:
注意:
a^a = 0
0^a = a
有一道很有意思的面试题,题目如下:
不能创建临时变量(第三个变量),实现两个数的交换。
代码如下:
#include<stdio.h>
int main() {
int a = 3, b = 5;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("a=%d,b=%d\n", a, b);
return 0;
}
运行代码成功,结果如下:
五. 赋值操作符
使用赋值操作符需要注意一些编码规范,例如:
a = x = y+1;
这样的连续赋值代码感觉怎么样?
那同样的语义,你看看:
x = y+1;
a = x;
这样的写法是不是更加清晰爽朗而且易于调试。
六. 单目操作符
1. 概述
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
– 前置、后置–
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
2. sizeof是操作符而不是函数
代码证明如下:
#include <stdio.h>
int main()
{
int a = 10;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof a);//后面的括号在括号中写的不是类型的时候,括号可以省略,这样就说sizeof不是函数。
return 0;
}
代码运行成功,结果如下:
3. ~对一个数的二进制按位取反
代码如下:
#include<stdio.h>
int main()
{
int a = 0;
printf("%d\n", ~a);
//0
//00000000000000000000000000000000
//11111111111111111111111111111111
//10000000000000000000000000000000
//10000000000000000000000000000001
//-1
return 0;
}
运行成功,结果如下:
4. ++与- -
++ 操作是是一种自增1的操作。
前置++:计算口诀:先+1,后使用
前置--:先-1,后使用
后置++:计算口诀:先使用,后+1
后置--:先使用,后-1
非常规自增自减:https://danbaku.blog.csdn.net/article/details/131756318
5. sizeof和数组
代码如下:
#include<stdio.h>
void test1(int arr[])
{
printf("%d\n", sizeof(arr));//(2)
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));//(4)
}
int main()
{
int arr[10] = { 0 };
char ch[10] = { 0 };
printf("%d\n", sizeof(arr));//(1)
printf("%d\n", sizeof(ch));//(3)
test1(arr);
test2(ch);
return 0;
}
运行成功,结果如下:
x86环境
x64环境
七. 关系操作符
>
>=
<
<=
!=
==
注意:在编程的过程中==和=不小心写错,导致的错误。
八. 逻辑操作符
&& 逻辑与
|| 逻辑或
注意:区分逻辑与和按位与,区分逻辑或和按位或
1&2------>0
1&&2---->1
1|2------->3
1||2------>1
360面试题一道,代码如下:
#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);
return 0;
}
运行成功,结果如下:
&&左边为假(0),右边就不算了。
#include <stdio.h>
int main()
{
int i = 0, a = 1, 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);
return 0;
}
代码运行成功,结果如下:
||左边为真(1),右边就不算了。
短路操作:
&& 左边操作数如果为假,右边无需计算
|| 左边操作数如果为真,右边无需计算
详见博客:https://danbaku.blog.csdn.net/article/details/135054294
九. 条件操作符
exp1 ? exp2 : exp3
条件操作符也叫三目运算符,常常用于两个数中找较大值。
十. 逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
代码如下:
#include<stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
printf("c=%d\n", c);
return 0;
}
运行代码成功,结果如下:
逗号表达式的实际应用:
可以将:
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
a = get_val();
count_val(a);
}
改写为:
while (a = get_val(), count_val(a), a > 0)
{
//业务处理
}
十一. 下标引用、函数调用和结构成员
1. [ ] 下标引用操作符
操作数:一个数组名 + 一个索引值
2. ( ) 函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
3. 访问一个结构的成员
. 结构体.成员名
-> 结构体指针->成员名
代码如下:
#include<stdio.h>
struct Book
{
char name[20];
int price;
};
//结构体变量.成员
//结构体指针->成员
void Print(struct Book* pb)
{
printf("%s %d\n", (*pb).name, (*pb).price);
printf("%s %d\n", pb->name, pb->price);
}
int main()
{
struct Book b = {"C语言指南", 55};
printf("%s %d\n", b.name, b.price);
Print(&b);
return 0;
}
代码运行成功,结果如下:
十二. 表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
1. 隐式类型转换
整形提升详见博客:https://danbaku.blog.csdn.net/article/details/135055042
2. 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算
3. 操作符的属性
复杂表达式的求值有三个影响的因素。
操作符的优先级
操作符的结合性
是否控制求值顺序
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
一些问题表达式详见博客:https://danbaku.blog.csdn.net/article/details/135083672
本篇博客为本人学习C语言时的详细笔记,如有错误之处,还望各位指正。
文章为原创,如要转载请注明出处