前言:
操作符又称为运算符:作为运算对象的变量或者常量称为操作数。
操作符左侧的操作数称为左操作数,操作符右边的操作数称为右操作数。
操作符同时对两个操作数进行运算的称为“双目操作符”,操作符只对一个操作数进行运算的称为“单目操作符”。
一、目录
1.各种操作符的介绍
总结了C语言中各个操作符的介绍
2.表达式求值
总结了隐式类型转换中的整型提升和算数转换,还总结了复杂表达式的三种影响因素(操作符的属性)
二、各种操作符的介绍
1.算术操作符( + - * / % )
+(加法) -(减法) *(乘法) /(除法) %(求余数)
1.除了%操作符之外,其他的几个操作符都可以作用于整数和浮点数
2.对于/操作符如果两个操作数都为整数,那么结果为整数,而只要有浮点数执行,那么结果为浮点数
3.%操作符的两个操作数必须为整数,返回的是整数之后的余数
4.%操作符所得结果的符号与运算符左侧的操作数(被除数)的符号相同。如:
-5%2=–1 5%-2=1
2.移位操作符( << >> )
符号位‘1’表示负数,符号位‘0’表示正数
2.1 <<(左移操作符)
移位规则:左边抛弃,右边补0
如:
mun<<1 //实际上mun在没有被赋值的情况下,自身的值不会变
mun<<=1 //mun被赋值了,自身的值变了
2.2 >>(右移操作符)
整数的二进制表示形式:不管是正整数还是负整数都可以写出二进制补码,根据正负直接写出的二进制序列就是原码。
有三种表示形式:原码 、反码 、补码
1)正整数的原码、反码、补码是相同的。
2)负整数的原码、反码、补码是要计算的。
(1)原码—>反码(原码的符号位不变,其它位按位取反得到的就是反码)—>补码(反码+1就是补码)
3)整数在内存中存储的是补码
4)计算的时候也是用补码进行计算
移位规则:右移运算分为两种(C语言没有明确规定是算术右移还是逻辑右移,一般编译器上采用的是算术右移)
2.2.1逻辑移位
左边用0填充,右边丢弃
2.2.2算术移位
左边用原该值的符号位填充,右边丢弃
如:
//算术移位 #include <stdio.h> int main() { int a = -15; int b = a >> 1; //a用二进制表示的形式 10000000 00000000 00000000 00001111 原码 // a 11111111 11111111 11111111 11110000 反码 // a 11111111 11111111 11111111 11110001 补码 //a进行算术右移1位 11111111 11111111 11111111 11111000 补码 //b 11111111 11111111 11111111 11111000 补码 //b 11111111 11111111 11111111 11110111 反码 //b 10000000 00000000 00000000 00001000 原码//十进制表示形式为-8 printf("%d", b);//打印-8 return 0; }
3.位操作符( & | ^ )
&(按位与)、|(按位或)、^(按位异或)
#include <stdio.h> int main() { int num1 = 10; int num2 = 22; int a = num1 & num2;//num1和num2进行按位与运算 //num1 00000000 00000000 00000000 00001010 //num2 00000000 00000000 00000000 00010110 //a 00000000 00000000 00000000 00000010 //规则: 全1为1 int b = num1 | num2;//num1和num2进行按位或运算 // num1 00000000 00000000 00000000 00001010 // num2 00000000 00000000 00000000 00010110 // b 00000000 00000000 00000000 00011110 //规则: 有1则1 int c = num1 ^ num2;//num1和num2进行按位异或运算 // num1 00000000 00000000 00000000 00001010 // num2 00000000 00000000 00000000 00010110 // c 00000000 00000000 00000000 00011100 //规则: 有1则1,全1位0 printf("a=%d,b=%d,c=%d", a, b, c);//a=2,b=30,c=28 return 0; }
位操作符的问题:
问题一:不能创建临时变量(第三个变量),实现两个数的交换
#include<stdio.h> int main() { int a = 10; int b = 20; a = a ^ b; b = a ^ b; a = a ^ b; //原理a^a等于0,a^0等于a,如a=a^b,那么b=a^b^b,故b=a(按位异或支持乘法交换律) return 0; }
问题二:求一个整数存储在内存中的二进制中的1的个数
#include <stdio.h> int main() { int num; scanf("%d", &num); int count = 0; while (num) { count++; num = num & (num - 1);/*这个表达式会让num的二进制中最右边的 1消失。通过循环,当num为0时,就能用count统计‘1’的个数*/ } printf("%d", count); return 0; }
4.赋值操作符( += -= *= /= %= >>= <<= &= |= ^= )
赋值操作符是个很棒的操作符,它可以给自己重新赋值
int a = 120;//体重
a = 140;//不满意就赋值
//赋值操作符如下: int x = 10; x = x + 10; x += 10;//复合赋值 其他的运算符一样的道理
5.单目操作符( ! - + & sizeof ~ – ++ * (类型))
! 逻辑反操作 - 负值 + 正值 & 取地址 sizeof 操作数的类型长度(以字节为单位) ~ 对一个数的二进制位取反(按补码二进制位取反,符号位也取反) -- 前置、后置-- ++ 前置、后置++ * 间接访问操作符(解引用操作符) (类型) 强制类型转换
#include <stdio.h> int main() { int a = -10; int* p = NULL; int arr[10] = {0}; printf("%d\n", !2);//0 printf("%d\n", !0);//1 a = -a; p = &a; p = arr;//数组名相当于首元素的地址 printf("%d\n", sizeof(int));//4 printf("%d\n", sizeof(a));//4 printf("%d\n", sizeof a);//4 printf("%d\n", sizeof (arr));//40 printf("%d\n", sizeof (int[10]));//40 return 0; }
自增、自减运算 1.运算规则: ++i;--i; //前缀就先算 i++; i--; //后缀就后算 2.注意: 只能用变量 3.使用时谨防出错 j=++i;//i=i+1,j=i; j=i++;//j=i;i=i+1;
事例:
#include <stdio.h> int main() { int a = 10, x, y, z; x = a++ + a++; //a+a;++a;++a; a = 10; y = ++a + (++a);//++a;++a;a+a; a = 10; z = ++a + a++;//++a,a+a,++a; printf("x=%d,y=%d,z=%d", x, y, z);//打印 x=20,y=24,z=22 return 0; }
6.关系操作费( > >= < <= != ==)
> 大于 >= 大于等于 < 小于 <= 小于等于 != 不等于(用于测试不相等) == 等于(用于测试相等) 注意:只能是字符比较
7.逻辑操作符( && || )
&& 逻辑与 || 逻辑或 注意: 1.区分逻辑与和按位与 1&2——>0 1&&2——>1 2.区分逻辑或和按位或 1|2——>3 1||2——>1 说明: 1.逻辑运算的结果只有真、假两种,分别用整数1、0表示 2.逻辑运算对象的值,可以是任何数据类型,非0则为真,0表示假 3.分支或循环中的条件,可用数字(任意类型)表示,且非0数值 表示真,0表示假.如: 100&&200 //1 !(4*5)==0 //1 2&&8==1 //0 0||9==3*3 //1
/*逻辑表达式短路特性:计算逻辑表达式时,若计算到 某步已经确定整个表达式的值,则表达式中后面部分将 不再被执行(总是先算表达式1)如: <表达式1>&&<表达式2> 当<表达式1>为0时,<表达式2>不执行 <表达式1>||<表达式2> 当<表达式1>为1时(非0时),<表达式2>不执行 */ #include <stdio.h> int main() { int i = 0, a = 0, b = 2, c = 3, d = 4; i = a++ && ++b && d++; // i=a++||++b||d++;(发生了短路,结果为a=1,b=3,c=3,d=4) printf("a=%d,b=%d,c=%d,d=%d\n", a, b, c, d);//结果为a=1,b=2,c=3,d=4(发生了短路) return 0; }
8.条件操作符( exp1 ? exp2 : exp3 )
#include <stdio.h> //问题:找到两个数中较大的值 int main() { int m, a=20, b=30; if (a > b) printf("最大值为:%d\n", a); else printf("最大值为:%d\n", b); //转换为条件表达式 m = a > b ? a : b; printf("最大值为:%d\n", m); return 0; }
9.逗号表达式( exp1, exp2, exp3, exp4, …expN )
逗号表达式:用“,”将几个表达式连接起来而形成的表达式
一般形式为:表达式1,表达式2,表达式3,……表达式N
注意:
逗号表达式,按从左到右次序计算各表达式的值,整个逗号表达式的值是
最后一个表达式的值
例子:写出以下表达式的值 a = 8 * 2, a * 4; //表达式的值为64(a*4的结果),a的值为16 (a = 8 * 2, a * 4), a * 2; //表达式的值为32(a*2的结果),a的值为16 a = b = 5, 5 * 2; //表达式的值为10(5*2的结果),a和b的值都为5 a = (b = 5, 5 * 2); //表达式为赋值表达式,值为10(5*2的结果),a的值为10,b的值为5
10.下标引用、函数调用和结构成员( [] () . )
操作数:一个数组名+一个索引值
如: int arr[10];//创建数组 arr[9] = 10;//给元素赋值 []的两个操作数是arr和9.
10.2 ()(函数调用操作符)
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数
#include <stdio.h> void test1(void) { printf("hehe\n");//打印hehe } void test2(const char* str) { printf("%s\n", str);//打印hello hello } int main() { test1();//()就是函数调用操作符,最少有一个操作数test1 test2("hello hello");//()就是函数调用操作符 return 0; }
10.3 .(访问一个结构的成员)
. 结构体.成员名
-> 结构体指针->成员名
#include <stdio.h> struct stu // 类型名 { char name[10];// | int age;// | 成员名 char sex[5];// | double score;// | }; void set_age1(struct stu a) { a.age = 18; printf("%d\n", a.age);//打印18 } void set_age2(struct stu* a) { a->age = 18; printf("%d\n", a->age); //打印18 } int main() { struct stu st; struct stu* pst = &st; st.age = 23; //结果成员访问 set_age1(st);//值传递(传值调用) printf("%d\n", st.age);//打印23 pst->age=23; //结构成员访问 set_age2(pst);//地址传递(传址调用) printf("%d\n", pst->age);//打印18 return 0; }
三、 表达式求值
表达式的求值顺序一般是由操作符的优先级和结合性决定的;同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
1.隐式类型转换
1.1整型提升
c的整型算术运算总是至少以缺省整型类型的精度来进行的;为了提升这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。(只有字符和短整型操作数)
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
如:
char a,b,c;
b=1;
c=2;
a=b+c;
b和c的值先提升为普通整型,然后在执行加法运算。
整型提升是按照变量的数据类型的符号位来提升的
// 负数的整形提升 char c1 = -1; 变量c1的二进制位(补码)中只有8个比特位:1111111 因为char 为有符号的char 所以整形提升的时候,高位补充符号位,即为1 11111111111111111111111111111111 提升之后的结果是: //正数的整形提升 char c2 = 1; 变量c2的二进制位(补码)中只有8个比特位:00000001 因为char为有符号的char 所以整形提升的时候,高位补充符号位,即为0提升之后的结果是 : 00000000000000000000000000000001 //无符号整型提升高位补零
例子1:
#include <stdio.h> int main() { char c1 = 5; char c2 = 127; // c1 00000000 00000000 00000000 00000101 // c1 00000101(截断) // c2 00000000 00000000 00000000 01111111 // c2 01111111 char c3 = c1 + c2; // 根据符号位整型提升 // c1 00000000 00000000 00000000 00000101 // c2 00000000 00000000 00000000 01111111 // c3 00000000 00000000 00000000 10000100 //截断 // c3 10000100 补码的形式 //%d 10进制的形式打印有符号得整数 //c3 11111111 11111111 11111111 10000100 补码的形式 //c3 11111111 11111111 11111111 10000011 反码的形式 //c3 10000000 00000000 00000000 01111100 原码的形式 printf("%d\n", c3); return 0; }
例子2:
#include <stdio.h> int main() { char c = 1; printf("%u\n", sizeof(c));//打印结果为1 printf("%u\n", sizeof(+c));//打印结果为4 printf(" %u\n", sizeof(-c));//打印结果为4 return 0; }
结论:c只要进行了表达式运算,就会发生整型提升,表达式+c,就会发生提升,所以sizeof(+c)是4个字节;表达式-c也发生了整型提升,所以sizeof(-c)也是4个字节;但是sizeof(c)就一个字节
2.算术转换
如果某个操作符的各个操作数属于不同类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行。(运算的一瞬间进行了算术转换,操作数的本质类型还是不变的)下面的层次体系称为寻常算术转换:
long double
double
float
unsigned long int
long int
unsigned int
int
向上转换,都是大于等于4个字节
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另一个操作数的类型后执行运算。
注意:
算术转换要合理,要不然会有一些潜在的问题
float f=3.14f;
int mun=f; //发生了隐式转换,会有精度丢失
3.操作符的属性
复杂表达式的求值有三个影响的因素。
1.操作符的优先级
2.操作符的结合性
3.是否控制求值顺序。
操作符优先级
一些问题表达式:
表达式的求值部分由操作符的优先级决定
表达式1:
ab+cd+e*f
//代码1在计算的时候,由于 *比 + 的优先级高,只能保证, * 的计算是比 + 早, 但是优先级并不能决定第三个 * 比第一个 + 早执行。
所以表达式的计算机顺序就可能是:
ab
cd
ab+cd
ef
ab+cd+ef
或者
ab
cd
ef
ab+cd
ab+cd+ef
表达式2
c + --c
//操作符的优先级只能决定自减–的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。(无法确定唯一的计算路径)
表达式3
int a=2+3+5
//相邻操作符的优先级相同的情况下,结合性起作用;相邻操作符的优先级高的先算,低的后算。
注意:
函数的调用先后顺序无法通过操作符的优先级确定。