操作符详解
目录:
- 操作符分类
- 二进制和进制转换
- 源码、反码、补码
- 移位操作符
- 位操作符:&、|、^、~
- 单目操作符
- 逗号表达式
- 下标访问[],和函数调用()
- 结构成员访问操作符
- 操作符的属性:优先级和结合性
- 表达式求值
1、操作符分类
- 算数操作符:
+ 、 — 、 * 、 / 、 %
- 移位操作符:
<< >>
- 位操作符:
& | ^
- 赋值操作符:
=、 +=、 -=、 *=、 /=、 %=、 <<=、 >>=、 &=、 |=、 ^=
- 单目操作符:
!、 ++、 --、 *、 +、 -、 ~、 sizeof(类型)
- 关系操作符:
>、 >=、 <、 <=、 ==、 !=
- 逻辑操作符:
&&, ||
- 条件操作符:
? :
- 逗号表达式:
,
- 下标引用:
[]
- 函数调用:
()
- 结构成员访问:
. ->
2、2进制转换
我们通常遇到 二进制
8进制
10进制
16进制
转换的问题, 简单来说,分别是满二进一,满8进1,以此类推退。揭晓来我们来详细讲解。
2.1 2进制转10进制
通过10进制来理解2进制,10进制的每一位代表着他们的权重,通过计算权重值的和,我们可以得到在这个10进制的数值大小。
同理,我们同样利用权重来理解和计算2机制:例如2机制的1101
二进制转换10进制,利用辗转相除法:
2.2 2进制转换8进制和16进制
2.21 2进制转8进制
3个2进制位转换成10进制位后,表示的数字范围是0~7,8进制数每位数字表示范围是0到7,所以每3个2进制位可以表示成一个8进制位,剩余不够3个2进制位直接换就行。
2.22 2进制转换16进制
16进制数字的每一位是0~9,a到f。如果写成2进制,最多有4个二进制位就足够了。
3、源码、反码、补码
整数的2进制表示方法有三种:源码、反码、和补码
有符号整数的二进制由符号位 和数值位 两部分构成,2进制序列中,最高位的1位当符号位,,剩余的都是符号位。符号位中0 表示正数,1 表示负数。
整数的源码、反码、补码相同
负整数的源码、反码、补码各不相同
**源码:**即整数的2进制序列位
反码: 源码的符号位不变,其他位按位取反便是反码
**补码:**反码+1得到补码 对于整形来说数据在内存中存放的方式是补码
补码按位取反后+1,便得到源码
4、移位操作符
**<<左移操作符 **
>>右移操作符
移位操作符的操作数只能是整数
4.1 左移操作符
移动规则:左边抛弃,右边补0
# include<stdio.h>
int main()
{
int num = 10;
int n = num << 1;
printf("n = %d\n", n);
printf("num = %d\n", num);
return 0;
}
4.2 右移操作符
右移规则分为两种:
1、逻辑右移: 左边用0填充,右边丢弃
2、算数右移: 左边用符号位填充,右边丢弃
一般采用算数右移,但具体采用哪种,应该以编译器为主
# include<stdio.h>
int main()
{
int num = 10;
int n = num >> 1;
printf("n = %d\n", n);
printf("num = %d\n", num);
return 0;
}
注意: 对于移位运算符,不要移动负数位,这是标准为定义行为
5、位操作符:&、 |、 ^、 ~
他们的操作数必须是整数
& 按位与 都为1才为1,
| 按位或 有一个为1便为1
^ 按位异或 相同为0, 相异为1
- 按位取反 0变1,1变0
# include<stdio.h>
int main()
{
int num1 = -3;
int num2 = 5;
printf("%d\n", num1 & num2);
printf("%d\n", num1 | num2);
printf("%d\n", num1 ^ num2);
printf("%d\n", ~0);
return 0;
}
关于操作符,我们已经讲解到这里了, 那么他们到底有什么用处呢, 接下来,我们讲几个例子。
例一:
不创建临时变量(第三个变量), 实现两个整数的交换
# include<stdio.h>
int main()
{
int a = 20;
int b = 30;
a = a^b;
b = a^b;
a = a^b;
printf("a = %d b = %d\n", a,b);
return 0;
}
按位异或^, 满足a^0 = a; a^a = 0; a^ b ^a = a ^ a ^ b = 0 ^ b = b
例二:
求一个整数存储在内存中的2进制序列中的1的个数
// 方法一:
# include<stdio.h>
int main()
{
int num = 10;
int count = 0;
while(num)
{
if(num % 2 == 1)
{
count++;
num /= 2;
}
}
printf("2进制中1的个数 = %d\n", count);
return 0;
}
// 方法一的弊端:无法统计负数
//方法二:
# include<stdio.h>
int main()
{
int num = -1;
int count = 0; // 统计
int i = 0;
for(i = 0; i < 32; i++)
{
if(num & (1 << i)) // 判断二进制序列中每一位是否是1
{
count++;
}
}
printf("2进制中1的个数 = %d\n", count);
return 0;
}
// 方法三:
# include<stdio.h>
int main()
{
int num = -5;
int i = 0;
int count = 0;
while(num)
{
count++;
num = num&num(num - 1);
}
printf("二级制中1的个数 = %d\n", count);
return 0;
}
例三:
二进制位置0或者置1
编写代码将13的二进制序列的第5位修改成1,然后在修改成0
13的二进制序列:00000000000000000000000000001101
将第5位置为1后:00000000000000000000000000011101
将第5位再置为0:00000000000000000000000000001101
# include<stdio.h>
int main()
{
int a = 13;
a = a | (1<<4);
printf("a = %d\n", a);
a = a & ~(1<<4);
printf("a = %d\n", a);
return 0;
}
6、单目操作符
单目操作符有: !、++、--、&、*、+、-、~ 、sizeof、(类型)
单目操作符只有一个操作数
7、逗号表达式
exp1, exp2, exp3, …….expN
逗号表达式,就是由逗号隔开的多个表达式, 从左向右依次执行,整个表达式的结果是最后一个表达式的结果,同时前面表达式也会影响最后表达式的计算结果.
8、下标访问[], 函数调用()
8.1 [] 下标引用操作符
操作数: 一个数组名 + 一个索引值(下标)
int arr[10];
arr[9] = 10;
// [ ] 的操作数是arr和9
// 因此也可以写成
9[arr] = 10; // 如果见到不要惊讶
8.2 函数调用操作符
接受一个或者多个操作数: 第一个操作数是函数名,剩余的操作数是传给函数的参数
#include <stdio.h>
void test1()
{
printf("hehe\n");
}
void test2(const char *str)
{
printf("%s\n", str);
}
int main()
{
test1(); //这⾥的()就是作为函数调⽤操作符。
test2("hello bit.");//这⾥的()就是函数调⽤操作符。
return 0;
}
9. 结构成员访问操作符
9.1 结构体
C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类
型还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。
结构体是一些值的集合,这些值称为成员变量.结构的每个成员可以是不同的类型的变量,如:
标量、数组、指针,甚⾄是其他结构体。
9.1.1 结构声明
struct tag {
member-list
member-list
member-list
...
} variable-list ;
tag 是结构体标签
member-list 是标准的变量定义,比如 int i; 或者 float f;,或者其他有效的变量定义。
variable-list 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。下面是声明 Book 结构的方式:
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book;
9.1.2 结构体变量的定义和初始化
// 代码1: 变量的定义
struct point
{
int x;
int y;
}p1; // 申明变量的同时定义变量p1
struct point p2; // 定义结构体变量p2
// 代码2: 初始化
struct point p3 = {10, 20};
struct Stu // 类型声明
{
char name[15];
int age;
};
struct Stu s1 = {"zhangsan", 20}; // 初始化
sturct Stu s2 = {.age = 20, ,name = "list"}; // 指定顺序初始化
// 代码3
struct Node
{
int data;
struct point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; // 结构体嵌套初始化
struct Node n2 = {20, {5,6}, NULL}; // 结构体嵌套初始化
9.2 结构成员访问操作符
9.2.1 结构体成员的直接访问
结构体成员的直接访问是通过 .
操作符访问的, 但操作符接受两个操作数
# include<stdio.h>
struct point
{
int x;
int y;
}p = {1,2};
int main()
{
printf("x: %d y: %d\n", p.x, p.y);
return 0;
}
结构成员变量.成员名
9.2.2 结构体成员的间接访问
有时候我们得到的不是一个结构体的变量,而是得到了一个指向结构体的指针
# include<stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3,3};
struct Point *Ptr = &p;
Ptr -> x = 20;
Ptr -> y = 10;
printf("x =%d y = %d\n", Ptr->x, Ptr->y);
return 0;
}
10. 操作符的属性: 优先级, 结合性
优先级和结合性,这两个属性决定了表达式求值的计算顺序.
10.1 优先级
优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执⾏。各种运算符的优先级是
不⼀样的。
10.2 结合性
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符
是左结合,还是右结合,决定执⾏顺序.
11. 表达式求值
11.1 整型提升
C语言中整型算术运算总是至少以缺省(默认)整型类型的精度来进⾏的。为了获得这个精度,表达式中的字符和短整型操作数在使⽤之前被转换为普通整型,这种转换称为整型提升。
正向提升的意义
表达式的整型运算要在CPU的相应运算器件内执⾏,CPU内整型运算器(ALU)的操作数的字节⻓度⼀
般就是int的字节⻓度,同时也是CPU的通⽤寄存器的⻓度。
因此,即使两个char类型的相加,在CPU执⾏时实际上也要先转换为CPU内整型操作数的标准⻓
度。
通⽤CPU(general-purpose CPU)是难以直接实现两个8⽐特字节直接相加运算(虽然机器指令中
可能有这种字节相加指令)。所以,表达式中各种⻓度可能⼩于int⻓度的整型值,都必须先转换为
int或unsigned int,然后才能送⼊CPU去
如何整型提升
- 有符号整数提升是按照变量的数据类型的符号位来进行提升的
- 无符号整数提升,高位补0
!
11.2 算数转换
如果某个操作数的各个操作数属于不同的类型,那么除非其中一个操作数转为另一个操作数的类型,否则操作就无法进行,下面的层次体系称为寻常算数转换
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上⾯这个列表中排名靠后,那么⾸先要转换为另外⼀个操作数的类型后执⾏
特字节直接相加运算(虽然机器指令中
可能有这种字节相加指令)。所以,表达式中各种⻓度可能⼩于int⻓度的整型值,都必须先转换为
int或unsigned int,然后才能送⼊CPU去
如何整型提升
- 有符号整数提升是按照变量的数据类型的符号位来进行提升的
- 无符号整数提升,高位补0
[外链图片转存中…(img-crOWBIha-1722269332440)]
[外链图片转存中…(img-0iOuhBjK-1722269332440)]
11.2 算数转换
如果某个操作数的各个操作数属于不同的类型,那么除非其中一个操作数转为另一个操作数的类型,否则操作就无法进行,下面的层次体系称为寻常算数转换
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上⾯这个列表中排名靠后,那么⾸先要转换为另外⼀个操作数的类型后执⾏
运算。