1.操作符的分类
- 算数操作符:+ 、- 、 * 、 / 、 %
- 移位操作符:>>、 <<
- 位操作符:& 、| 、^
- 赋值操作符:=、+=、-=、/=、%=、<<=、>>=、&=、|=、^=
- 单目操作符:!、++、- -、&、*、+、-、~、sizeof、(类型)
- 关系操作符:>、>=、<、<=、==、!=
- 逻辑操作符:&&、||
- 条件操作符:? :
- 逗号表达式: ,
- 下标引用:[ ]
- 函数调用:( )
- 结构成员访问: . 、->
- 操作符中的一些操作符和二进制有关系,让我们先了解一下二进制的知识。
2.二进制和进制转换
2进制、8进制、10进制、16进制是数值的不同表示形式而已。
例如:数字13的各种表示形式:
13的2进制:1101
13的8进制:15(8进制的数值前面写:0)
13的10进制:13
13的16进制:D (16进制的数值前面写:0x)
2.1二进制转十进制
在十进制中123表示的是:一百二十三,因为10进制的每一位都是有权重的,10进制的数字从右往左是个位、十位、百位……,每一位的权重是:10^0,10^1,10^2……
所以在十进制中:123=1*10^2+2*10^1+3*10^0=100+20+3,同样二进制也是一样的,权重为:2^0, 2^1 , 2^2……,那么二进制中的1101,对应的就是:
2.1.1 十进制转二进制数字
2.2 二进制转八进制和十六进制
2.2.1 二进制转八进制
8进制中每一位是 0~7,各自写成2进制,最多有3个2进制位就可以了,比如7:111,所以在二进制序列中从最右边一次往左边每3个二进制位换算成1个8进制位,剩下不够3个二进制位直接换算。
2.2.2 二进制转十六进制
同样,16进制的数字:0~9,a~f,各自写成二进制位,最多有4个2进制位就足够了,因为
F(15)用:1111 来表示。剩下不够4个二进制位直接(在最前面添0)换算。
3.原码、反码、补码
整数的2进制表示方法有3种:即原码、反码和补码
有符号整数的三种表示方法都有符号位和数值位两部分,二进制序列中,最高位的1位被当做符号位,剩余的都是数值位。
符号位都是用0表示 “正”,用1表示“负”。
正整数的原码、反码、补码都相同。
负整数的三种表示方法各不相同。
原码:直接将数值按照正负数的形式翻译成二进制得到就是原码。
反码:将原码的符号位不变,其它位依次取反,就可以得到反码了。
补码:在反码的基础上+1就可以得到补码。
将补码转换为原码的操作:取反,+1的操作。
数据在内存中的存放的是补码。这是因为CPU只有加法器,但是又要处理加法和减法,有统一的计算规则,只有用补码计算出来的结果才是正确的,直接原码计算时会出现错误的。
4.移位操作符
<< 左移操作符
>> 右移操作符
注意:移位操作符的操作数只能是整数。
4.1左移操作符
移位规则:左边抛弃,右边添0
4.2右移操作符
移位规则:分两种情况
1.逻辑右移:左边用0填充,右边丢弃(比较粗暴)
2.算术右移:左边用原该值得符号位填充,右边丢弃
5.位操作符:&、|、^、~
位操作符有:
& 按位与
| 按位或
^ 按位异或
~ 按位取反
注意:他们的操作数必须是整数
编写代码1,实现两个整数的交换。
#include<stdio.h>
int main()
{int a = 3;int b = 5;int c = 0;printf("a=%d\n", a);printf("b=%d\n", b);c = a;a = b;b = c;printf("a=%d\n", a);printf("b=%d\n", b);return 0;
}
上面的方法是我们容易想到的方法,采用创建第三个变量来实现两个整数的交换,这也是最常用的方法。
#include<stdio.h>
int main()
{int a = 3;int b = 5;int c = 0;printf("a=%d\n", a);printf("b=%d\n", b);a = a + b;//a=8b = a - b;//b=8-5=3=aa = a - b;//a=8-3=5=b//这种方法的缺点:a和b如果非常大,求和后的值超过了整型的最大值return 0;
}
int main()
{int a = 3;int b = 5;int c = 0;printf("a=%d\n", a);printf("b=%d\n", b);//异或操作符只适用于整数的异或,但是如果是浮点数就不行了a = a ^ b;b = a ^ b;//(a^b)^b=a^b^b=aa = a ^ b;//a^b^a=bprintf("a=%d\n", a);printf("b=%d\n", b);//异或的特点://a^a=0//0^a=areturn 0;}
用异或来实现两个数的转换是实在是太难想到了 ,缺点是:异或操作符只适用于整数的异或,但是如果是浮点数就不行了。
编写代码2,实现计算存储在内存中的数据的1的个数:
#include<stdio.h>
int main()
{//方法1:int num = 17;int count = 0;while (num){if (num % 2 == 1)count++;num = num / 2;}printf("%d\n", count);// //方法2:int num = 17;int i = 0;int count = 0;for (i = 0; i < 32; i++){if (((num >> i) & 1 )== 1)//移位操作移动的就是补码,所以不考虑正负数count++;}printf("%d\n", count);return 0;
}
什么时候我们才考虑正负数呢?
//int count_one( int n) //当我们输入-1的时候就会出现问题
int count_one(unsigned int n) //将int改为unsigned int 类型,这样的话,当计算机处理-1时对应的补码就会变为无符号的数字,//全部为有效位,那么就是1111111111111……1111(32个1)这样就不会出现错误//这个时候就需要考虑正负数了
{ int count = 0;while (n){if (n % 2 == 1)count++;n = n / 2;}return count;
}
//什么时候考虑正负数呢?
int main()
{int num = 0;scanf("%d", &num);//求一个整数在内存中的二进制中的1的个数int c=count_one(num);printf("%d\n", c);return 0;
}
利用:n=n&(n-1)
效果:就是将n的二进制位的最右边的1去掉我们还有一种方法来实现计算存储在内存中1的个数:
// n=15
// 1111 - n
// 1110 - n-1
// 1110 - n
// 1101 - n-1
// 1100 - n
// 1011 - n-1
// 1000 - n
// 0111 - n-1
// 0000 - n
//
// 所以上面上面执行几次就有几个n,所探究二进制中有几个1还有一种写法
//
int count_one(int n)
{ int count = 0;while (n){n = n & (n - 1);count++;}return count;
}int main()
{int num = 0;scanf("%d", &num);//求一个整数在内存中的二进制中的1的个数int c=count_one(num);printf("%d\n", c);return 0;
}
编写代码3,实现二进制位置0或置为1
把某一个数的二进制的第5个位置为1或者是置为0
#include<stdio.h>
int main()
{int num = 13;//8 4 2 1//1 1 0 1//00000000000000000000000000001101 - 13 将他的第五位置为1// (1<<4)--->……10000 - 或上这个数就可以将原来的数的第五位变为1//00000000000000000000000000011101 //将第五位置为1后又置为0,变为之前的数字//00000000000000000000000000011101 -13//11111111111111111111111111101111 - 与上这样的数字就可以变回原来的数字了//00000000000000000000000000001101//11111111111111111111111111101111这个数字是怎样得到的?其实是将下面的数字取反得到的//00000000000000000000000000010000//这样得来的:~(1<<4)num = (num | (1 << 4));printf("%d\n", num);num = (num & ~(1 << 4));printf("%d\n", num);return 0;
}
6.单目操作符
单目操作符:只有一个操作数。
7.逗号表达式
表达式1 , 表达式2 , 表达式3 , …… , 表达式n
逗号表达式,用逗号将多个表达式隔开,依次从左往右执行,整个表达式的结果是最后一个表达式的结果。
#include<stdio.h>
int main()
{int a = 1;int b = 2;int c = (a > b, a = b + 10, a, b = a + 1);// 0 a=12 1 b=13 所以 c =13printf("%d\n", c);//13return 0;
}
8.下标访问[ ]、函数调用( )
8.1 [ ]下标引用操作符
操作数:一个数组名 + 一个索引值(下标)
int arr[10]; //创建数组
arr[3]=10; //实用下标引用操作符
[ ] 的两个操作数是 arr 和 3
8.2 函数调用操作符
接受一个或多个操作数;第一个操作数是函数名,剩余的操作数是传递给函数的参数。
#include<stdio.h>
int main()
{int arr[10] = { 0 };arr[5] = 6;//[ ]下标引用操作符// arr 和 5 是两个操作数int c = Add(3, 5);//( )函数调用操作符 - 函数名和参数都是操作数test();//( )函数调用操作符return 0;
}
注意:sizeof 是操作符,不是函数
#include<stdio.h>
int main()
{int a = 10;printf("%zd\n", sizeof(a));printf("%zd\n", sizeof a );//两种都可以执行,说明sizeof是操作符,而不是函数//函数的调用操作符是不可以省略的,而sizeof 的()是可以省略的return 0;}
9.结构成员访问操作符
9.1结构体
C语言已经提供了内置类型,例如:char、short、int、long、float、double等等,但是这些内置类型是远远不够的,假设我想描述一本书,一个学生,这个时候单一的类型是不够的。
描述一个学生需要名字、学号、年龄、身高、体重、爱好等等;同样要描述一本书也是不够的,所以这个时候就要用到结构体这一类型,让我们自己来创造适合的类型。
结构是一些值得集合,这些值称为成员变量。结构的每一个成员可以是不同的类型的变量,如:标量、指针、数组、指针,其他结构体。
9.1.1结构的声明
描述一个学生:
struct Stu
{char name[20];//姓名int age;//年龄char insert[20];//爱好char id[20];//学号
};//分号不能丢
9.1.2结构体变量的定义和初始化
//结构体的声明
struct Stu
{char name[20];//姓名int age;//年龄char insert[20];//爱好char id[20];//学号
};//分号不能丢//结构体变量的定义和初始化
struct Point //类型说明
{int x;int y;}P1; //声明类型的同时定义变量 P1
struct Point P2;//定义结构体变量 P2//初始化
struct Point P3 = { 19,24 };
9.2结构成员访问操作符
9.2.1结构成员的直接访问
结构成员的直接访问是通过点操作符( . )访问的。点操作符接受两个操作数。
使用方法:结构体变量 . 成员名
9.2.2结构成员的间接访问
有时候我们得到的不是一个结构体的变量,而是得到了一个只想结构体的指针。这个时候我们就要用间接操作符来表示。
使用方法:结构体指针->成员名
举例:
//学生:姓名+年龄+成绩
struct Stu {char name[20];int age;float score;}s3 = { "lisi",23,81.64f }, s4 = {"xiaoming",32,67.4};//全局变量int main()
{struct Stu s1 = {"张三",19,100.0f};//局部变量struct Stu s2 = {"lihua",21,89.45f};// . 结构成员访问操作符//结构体变量 . 成员名printf("%s %d %f\n\n", s1.name, s1.age, s1.score);printf("%d %f %s\n\n", s4.age, s4.score, s4.name);//结构体指针struct Stu* ps = &s1;//取出s1的地址printf("%s %d %f\n", ps->name, ps->age, ps->score);//结构体指针 -> 成员名return 0;
}
10.操作符的属性:优先级、结合性
在C语言中的操作符有两个重要属性:优先级和结合性,这两个属性决定了表达式中的计算顺序。
10.1优先级
优先级指的是,如果一个表达式包含多个运算符,哪个运算符先执行,这个时候我们就要查看各种运算符的优先级,各种运算符的优先级是不一样的。
15 - 3 * 2;
上面的例子中我们可以看见既有减法运算符( - ),同样也有乘法运算符( * )。由于乘法运算符的优先级高于加法,所以会先计算 3 * 2,而不是先计算 15 - 3 。
10.2结合性
当在同一个表达式中,两个运算符的优先级相同时,这个时候就要根据结合性来看执行的顺序大部分的运算符都是左结合(从左到右依次执行),少数的运算符是右结合(从右到左依次执行),比如赋值运算符( = )。
12 / 6 * 3;
上面的例子中, / 和 * 的优先级相同,它们都是左结合运算符,所以从左到右执行,先计算 12 / 6,再计算 *3
运算符的优先级顺序有很多,下面是部分被运算符的优先级(从高到低排列),建议记住这些就可以了,其他的操作符在使用时在查表就可以了。
圆括号:( )
自增运算符:++,自减运算符:- -
单目运算符:+(正)、-(负)
乘法( * ),除法( / )
加法( + ),减法( - )
关系运算符(<、>等)
赋值运算符(=)
由于圆括号的优先级最高,所以可以随意改变其它运算符的优先级。
11.表达式求值
11.1整型提升
C语言中整型算术运算总是至少以默认整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种称为整型提升。
整型提升的意义:
即使是两个char类型的数据相加,在CPU执行时实际上是先转换为CPU内整型操作数的标准长度。通用的CPU是难以将8比特字节直接相加运算的,所以表达式中各种长度小于 int 长度的整型值,都必须转换为 int 或者是 unsigned int,然后再送入CPU中执行运算。
char a , b , c;
……
a = b + c;
b 和 c 的值先被提升为普通整型,然后再执行加法运算。加法运算完之后,结果发生截断,存储到a中。
如何进行整型提升?
- 有符号整数提升是按照变量的数据类型的符号位来提升的
- 无符号整数提升,高位补0
int main()
{//char类型的取值范围是-128~127//char类型是一个字节,1个字节是8bit位char c1 = 125;//00000000000000000000000001111101 -125//发生截断//01111101char c2 = 10;//00000000000000000000000000001010 -10//发生截断//00001010////00000000000000000000000001111101 -c1 有符号的char //00000000000000000000000000001010 -c2 有符号的char//00000000000000000000000010000111 -c3 会发生截断//10000111 -c3 补码需要转换为原码char c3 = c1 + c2;// 整型提升//11111111111111111111111110000111 -补码//10000000000000000000000001111000//10000000000000000000000001111001 -原码 --> -121printf("%d\n", c3);//121printf("%d\n", c1 + c2);//00000000000000000000000001111101//00000000000000000000000000001010//00000000000000000000000010000111 -135return 0;
}
11.2 算数转换
如果某个操作符的各个操作数类型不同,就要将一个类型转换为另一个类型,否则操作无法正常进行。下面的层次体系称为寻常算数转换。
- long double
- double
- float
- unsigned long int
- long int
- unsigned int
- int
如果某个操作数类型在上面的这个列表排名靠后,那么首先要转换为另外一个操作数的类型后爱执行运算。
11.3 问题表达式的解析
11.3.1 表达式1
a*b + c*d +e*f
上面的例子中,只知道 * 比 + 的优先级高,只能保证相邻的运算符的先后顺序,即: * 先比 +计算早,但是不能保证不相邻的运算符之间执行的先后顺序。
计算顺序可能有以下几种情况:
11.3.2表达式2
c + --c;
我们虽然知道 - - 运算在 +运算的前面,但是我们不知道 + 操作符的左操作数的获取是在右操作数之前还是之后求值,所以结果是有歧义的。
11.3.3 表达式3
int fun()
{static int count = 1;//static具有累加的效果 //所以count出这个函数后并不会销毁return ++count;
}// count 2 -> 3 ->4int main()
{int answer;answer = fun() - fun() * fun();//在VS上运行:2-3*4=-10//但在其他的编译器上面是不同的,有可能会出现:4-2*3=-2//因为函数调用顺序不知道,只是确保了操作符两边有数字而已,//但是计算的顺序是严格按照优先级的顺序来计算的//所以最好不要写步骤过于复杂的代码,要写也最好分开步骤来写printf("%d\n", answer);return 0;
}
11.3.4 表达式4
//同上一个代码也比较相同,放在不同的编译器上面会有不同的运行结果
int main()
{int i = 1;int ret = (++i) + (++i) + (++i);//14 - 4+4+4 i=2、3、4 最后i被赋值为4 //再执行+运算符的操作:4+4+4=12printf("%d\n", ret);printf("%d\n", i);return 0;
}
//所以不要写如此复杂的代码,容易出现错误要写的话就分开写
11.4 总结
虽然有了操作符的优先级和结合性,但是写代码的时候最好不要写很复杂的表达式,容易出现问题,最好分几步来实现操作。