前言:在前面的学习中,我们不仅学习了函数,数组等,也了解过一些操作符。今天就让我们一起来深入学习操作符。
1 操作符的分类
.
算术操作符:+、-、*、/、%
.
移位操作符:<<、>>
.
位操作符:&、|、^、~
.
赋值操作符:=、+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=
.
单目操作符:!、++、--、&、*、+、-、~、sizeof、(类型)
.
关系操作符:>、<、>=、<=、==、!=
.
逻辑操作符:&&、||
.
条件操作符:? :
.
逗号表达式:,
.
下标引用:[]
.
函数调用:()
.
结构成员访问:. 、->
在学习今天的操作符之前,先来学习一下二进制和进制转换的知识。
2 二进制
在生活中,最常见的就是10进制了。除此之外,还有2进制,8进制,16进制。这些进制都是数值的不同表现形式而已。
比如:数值15的各种进制表示形式
15的2进制:1111
15的8进制:17
15的10进制:15
15的16进制:F
计算机能够识别的指令就是二进制,因此我们重点来介绍二进制。
首先我们从10进制讲起,也是生活中最常用的。
.
10进制中满10进1。
.
10进制中的每一位数字都是由0~9的数字组成。
二进制也是一样的。
.
二进制中满2进1。
.
二进制中每一位数字都是由0~1的数字组成。
特别要注意的是16进制。
.
16进制中的每一位数字都是由0~9和A,B,C,D,E,F组成的。
3 进制转换
10进制的123表示的值是一百二十三,为什么是这个值呢?其实10进制的每一位都是有权重的。10进制从右向左分别是个位,十位,百位…,每一位的权重分别是10^0,10^1,10^2
。
二进制和十进制是类似的。只不过二进制的每一位权重从右向左分别是2^0,2^1,2^2
。
.
10进制转为2进制
比如:10进制的125
.
2进制转换8进制
8进制中的每一位数字都是由0~7组成的,0~7中的每一个数字都可以用3位二进制位表示
。所以2进制转换8进制的时候,从二进制序列中右边低位开始向左每3个二进制位会换算一个8进制位,剩余不够3个二进制位的直接换算。
2进制------8进制
000--------0
001--------1
010--------2
011--------3
100--------4
101--------5
110--------6
111--------7
例如:2进制的01101011转换为8进制:0153,0开头的数字,会被当做8进制。
.
2进制转16进制
16进制的数字每一位都是由0~9,a~f组成的,16进制的每一位数字都可以用4位2进制位表示
。所以在2进制转16进制的时候,从2进制序列中右边低位开始向左每4位会转换一个16进制位,剩余不够4个二进制位的直接换算。
2进制---------16进制
0000----------0
0001----------1
0010----------2
0011----------3
0100----------4
......
1010----------a
1011----------b
1100----------c
1101----------d
1110----------e
1111----------f
例如:2进制的01101011转换为16进制:0x6b,16进制表示的时候前面加0x。
4 原码,反码,补码
整数的2进制表示方法有3种:原码,反码,补码。
有符号整数的3种表示方法均有符号位和数值位两部分,在2进制序列中最高位的一位是符号位,剩余位是数值位。
符号位都是用0表示“正”,用1表示“负”。
正整数的原,反,补码都相同。
负整数的3种表示方法各有不同。
.
原码:根据数值按照正负数的形式直接翻译成二进制序列得到的就是原码
。
.
反码:原码的符号位不变,其他位依次按位取反
。
.
补码:反码+1
。
通过补码得到原码也可以通过取反,+1的操作
得到原码。
对于整形来说,数据存放在内存中其实存放的是补码
。
为什么呢?
在计算机系统中,数值一律用补码来表示和存储
。原因在于使用补码,可以将符号位和数值位统一处理。同时加法和减法也可以统一处理(cpu只有加法器),此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
#include<stdio.h>
int main()
{
int a = 10;
//正数的原码,反码,补码是相同的
//00000000000000000000000000001010
int b = -5;
//10000000000000000000000000000101原码
//11111111111111111111111111111010反码
//11111111111111111111111111111011补码
return 0;
}
5 移位操作符
.
<<
左移操作符:将二进制补码序列的每一位向左移动,右 边补0
.
>>
右移操作符:分为逻辑右移和算术右移
逻辑右移:二进制补码序列的每一位向右移动,左边补0
算术右移:二进制补码序列的每一位向右移动,左边用该值的符号位补充
(大多数编译器采用算术右移)
它们的操作数必须是整数。
#include<stdio.h>
int main()
{
int a = 10;
//00000000000000000000000000001010
printf("%d\n", a << 1);//20
//00000000000000000000000000010100 ------- 20
return 0;
}
画图分析
#include<stdio.h>
int main()
{
int a = 10;
//00000000000000000000000000001010
printf("%d\n", a >> 1);//5
//00000000000000000000000000000101
return 0;
}
观察上述代码,可以看到<<左移操作符有*2的效果,>>右移操作符有/2的效果
。
6 位操作符
.
&按位与:有0为0,全1为1
。
.
| 按位或:有1为1,全0为0
。
.
^按位异或:相同为0,相异为1
。(支持交换律)
.
~按位取反:二进制序列中的每一位都要进行取反操作
。
它们的操作数必须是整数。
#include<stdio.h>
int main()
{
int a = 10;
//00000000000000000000000000001010
int b = -5;
//10000000000000000000000000000101原码
//11111111111111111111111111111010反码
//11111111111111111111111111111011补码
printf("a&b=%d\n", a & b);//10
// 00000000000000000000000000001010
// &11111111111111111111111111111011
// 00000000000000000000000000001010 //10
int n = -2;
//10000000000000000000000000000010原码
//11111111111111111111111111111101反码
//11111111111111111111111111111110补码
//00000000000000000000000000000001 ~a
printf("~n=%d\n", ~n);//1
int m = 0;
//00000000000000000000000000000000
//11111111111111111111111111111111 补码 ~b
//10000000000000000000000000000000
//10000000000000000000000000000001 //-1
printf("~m=%d\n", ~m);//-1
return 0;
}
接下来我们来看一道变态的面试题。
不创建临时变量,实现两个整数的交换。
第一种方法
#include<stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
a = a ^ b;//此时原来a的值已经被覆盖
b = a ^ b;//a^b就相当于a^b^b,根据按位异或的规则,此时b的值被改成a,b里面存放原来a的值
a = a ^ b;//a^b就等于a^b^b=a^b^a,a的值被改成b的值,a里面存放原来b的值
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
第二种方法
#include<stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
a = a + b;//此时a里面存储的是a+b的值,原来a的值已经被覆盖
b = a - b;//a-b等价于a+b-b=a(这里的a是原来a的值),b的值已经被原来a的值覆盖
a = a - b;//a-b等价于a+b-b就等于a+b-a,所以a里面存储的是原来b的值
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
7 逗号表达式
逗号表达式,就是用逗号隔开的多个表达式。
exp1,exp2,exp3,...expN
逗号表达式,从左向右依次执行。整个表达式的结果就是最后一个表达式的结果
。
#include<stdio.h>
int main()
{
int a = 5;
int b = 3;
// 4 8 1
int c = (a = b + 1, b = 2 * a, b > a);
printf("%d\n", c);//1
return 0;
}
8 下标访问[ ],函数调用()
.
[ ]下标引用操作符
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", arr[6]);//7
//[]下标引用操作符,arr,6是操作数
return 0;
}
.
函数调用操作符()
#include<stdio.h>
int Fact(int n)
{
if (n <= 2)
{
return 1;
}
else
{
return Fact(n - 1) + Fact(n - 2);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fact(n);//()是函数调用操作符,Fact,n是操作数,函数调用操作符至少有一个操作数,是函数名,可以无参,也可以有多个参数
printf("第%d个斐波那契数是%d\n", n, ret);
return 0;
}
9 结构成员访问操作符
C语言已经提供了内置类型(char,short,int,float,double等)
,但是只有这些是不够的。描述一个学生需要姓名,,性别,年龄,学号,身高,体重等。单一的内置类型是不行的。C语言为了解决这个问题,增加了结构体这种自定义类型。
.
结构的声明:
struct tag
{
member_list;
}variable_list;
.
结构体变量的定义和初始化
#include<stdio.h>
struct stu//结构体类型的声明
{
char name[15];
int age;
}s1;//结构体变量s1
struct book
{
int price;
char author[20];
struct stu s3;//结构体嵌套
};
int main()
{
struct stu s1 = { "zhangsan",20 };//初始化
struct stu s2 = { .age = 18,.name = "xiaoming" };//指定初始化顺序
printf("%s,%d\n", s1.name, s1.age);
printf("%s,%d\n", s2.name, s2.age);
struct book b1 = { 66,"luoguanzhong",{"lisi",16} };
//结构体嵌套初始化
printf("%d,%s,%s,%d", b1.price, b1.author, b1.s3.name, b1.s3.age);
return 0;
}
.
结构成员访问操作符
结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。
使用方式:结构体变量.成员名
。
结构体成员的间接访问
有时候我们得到的不是一个结构体变量,而是一个指向结构体的指针。
使用方式:结构体指针->成员名
#include<stdio.h>
#include<string.h>
struct stu
{
char name[20];//名字
int age;//年龄
};
void set_stu(struct stu* ps1)//结构体指针
{
strcpy(ps1->name, "lisi");//字符串拷贝函数,是一个库函数,需要包含头文件string.h
ps1->age = 22;
}
void print_stu(struct stu s1)
{
printf("%s,%d", s1.name, s1.age);
}
int main()
{
struct stu s1 = { "zhangsan",20 };
set_stu(&s1);//设置学生的信息
print_stu(s1);//打印信息
return 0;
}
10 操作符的属性:优先级,结合性
C语言的操作符有两个重要的属性:优先级,结合性,这两个属性决定了表达式求值的计算顺序
。
.
优先级
优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不一样的。
.
结合性
如果两个运算符优先级相同,那么优先级就无法确定执行顺序了。这时候就要看结合性了,根据运算符是左结合还是右结合决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右向左执行),比如赋值运算符(=)。
下面是部分运算符的优先级顺序(按照优先级由高到低排列)。
.
圆括号(())
.
自增运算符(++),自减运算符(--)
.
单目运算符(+和-)
.
乘法(*),除法(/)
.
加法(+),减法(-)
.
关系运算符(>,<等)
.
赋值运算符(=)
参考:https://zh.cppreference.com/w/c/language/operator_precedence?
11 整型提升
C语言中整型算术运算总是至少以缺省整型类型的精度来计算。为了获得这个精度,表达式中的字符和短整型操作数在使用之前会转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int类型的字节长度,同时也是CPU的通用寄存器的长度
。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度
。
通⽤CPU(general-purpose?CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int
,才能输入CPU执行运算。
如何进行整型提升呢?
.
有符号整数提升是按照变量的数据类型的符号位来提升的
.
无符号整数提升,高位补0
#include<stdio.h>
int main()
{
char a = 1;
//00000000000000000000000000000001------- 1是一个整型
// char类型,1个byte,8个bit
//00000001发生截断
char b = -1;
//10000000000000000000000000000001原码
//11111111111111111111111111111110反码
//11111111111111111111111111111111补码
//char类型,1个byte,8个bit
//11111111
char c = a + b;
//相加之前,先整型提升
// a ---- 00000000000000000000000000000001整型提升
// b ---- 11111111111111111111111111111111整型提升
// 100000000000000000000000000000000
// char类型只能够存储8个bit
// c ---- 00000000
printf("%d\n", c);//0
//%d打印整数,先发生整型提升
//00000000000000000000000000000000
return 0;
}
12 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的类型转换为另一个操作数的类型
,否则操作就无法执行。
1 long double
2 double
3 float
4 unsigned long int
5 long int
6 unsigned int
7 int
如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后执行运算。否则,操作就无法执行。
#include<stdio.h>
int main()
{
//%d应该写成%f,否则会报错,这里只是为了验证类型的转换
printf("%d\n", 3 + 6.12);//int转换为double
return 0;
}
结语:今天的操作符详解到此告一段落,觉得还不错的为小编点点赞吧。