C语言——操作符详解
前言:
操作符又称为运算符:作为运算对象的变量或者常量称为操作数。
操作符左侧的操作数称为左操作数,操作符右边的操作数称为右操作数。
操作符同时对两个操作数进行运算的称为“双目操作符”,操作符只对一个操作数进行运算的称为“单目操作符”。
一、目录
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
//相邻操作符的优先级相同的情况下,结合性起作用;相邻操作符的优先级高的先算,低的后算。
注意:
函数的调用先后顺序无法通过操作符的优先级确定。