在初始C语言中我们只了解了关于操作符的简单的知识,并且还有许多的操作符都没有进行解释,本篇带来的是关于操作符的详解
算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号表达式
下标引用、函数调用和结构成员
下面就对这些操作符进行解释与学习
+ - * / %
1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
<< 左移操作符
>> 右移操作符
注:移位操作符的操作数只能是整数。
这里移动的位指的是一个数转化为二进制移动的二进制位
在学习之前我们先来了解一下原码、反码、补码
原码:按照二进制位展开
反码:原码的符号位不变,其他位按位取反(最高位是符号位:正数的符号位为0,复数的符号位为1)
补码:反码加1
正数的原码、反码、补码都是一样的
整数在内存中的存储是以补码的形式存储的
移位规则:
左边抛弃、右边补0
#include
int main()
{
int a = 10;
printf("%d", a << 1); //左移一个二进制位
return 0;
}
移位规则:
右移操作分为两种:
1. 逻辑移位
左边用0填充,右边丢弃
2. 算术移位
左边用原该值的符号位填充,右边丢弃
#include
int main()
{
int a = -1;
printf("%d", a >> 1); //右移一个二进制位
return 0;
}
从补码转化为原码:
取反+1或者-1取反
右移时采用哪种方法取决于编译器
警告 ⚠:
对于移位运算符,不要移动负数位,这个是标准未定义的。
例如
int num = 10;
num>>-1;//error
& 按位于
| 按位或
^ 按位异或
注:它们的操作数必须都是整数
这里的位也是二进制位
& 都为1则为1,一个为0则为0
| 一个为1则为1,都为0则为0
^ 相同为0,相异为1
#include
int main()
{
int n = 2;
//补码:00000000000000000000000000000010
int m = 4;
//补码:00000000000000000000000000000100
printf("%d\n", n & m);
//00000000000000000000000000000010
// &
//00000000000000000000000000000100
//都为1则为1,一个为0则为0
// =
//00000000000000000000000000000000
//所以结果为0
printf("%d\n", n | m);
//00000000000000000000000000000010
// |
//00000000000000000000000000000100
//一个为1则为1,都为0则为0
// =
//00000000000000000000000000000110
//结果为1*2^2 + 1*2^1 + 0*2^0 = 6
printf("%d\n", n ^ m);
//00000000000000000000000000000010
// ^
//00000000000000000000000000000100
// 相同为0,相异为1
// =
//00000000000000000000000000000110
//结果也为6
return 0;
}
不能创建临时变量(第三个变量),实现两个数的交换
在之前我们讲到过,如果要交换两个数,得创建第三个变量来进行辅助交换,但是如果不使用第三个变量,那又该怎么样交换呢?
可以使用求和法,但是这样方法有可能会导致数据的溢出
代码演示:
//代码1:求和法
#include
int main()
{
int a = 20;
int b = 10;
printf("交换前:a = %d b = %d\n", a, b);
int c = a + b; //a+b有可能会造成数据的溢出
a = c - a;
b = c - b;
printf("交换后:a = %d b = %d\n", a, b);
return 0;
}
使用按位异或
0 ^ a = a
a ^ a = 0
按位异或支持交换律
代码演示:
//代码2:按位异或
#include
int main()
{
int a = 10;
int b = 20;
printf("交换前:a = %d b = %d\n", a, b);
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("交换后:a = %d b = %d\n", a, b);
return 0;
}
这里采用的思想就是0 ^ a = a、a ^ a = 0
a = a^b ,将a带入b = a^b中,就得到了b = a^b^b=a,将b带入a = a^b中,就得到了a=a^a^b= b,因此就可以将两个数实现交换,但是这样的做法不容易想到。
赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值
int Scores = 50; //可以对变量进行赋值初始化
Scores = 98 //也可以对变量进行赋值修改
//也可以进行连续赋值
int a = 10;
int x = 0;
int y = 20;
a = x = y+1;//连续赋值
//但是这样看起来比较混
//可以写成这样
x = y + 1;
a = x;
//这样赋值看起来就比较清晰明了
复合赋值操作符
+=
-=
*=
/=
%=
>>=
<<=
&=
|&
^=
这些运算符都可以写成复合的效果
例如:
int x = 10;
x = x + 10;
x += 10;//复合赋值
//其他运算符一样的道理。这样写更加简洁
单目指的就是操作符的两边只有一个操作数
! - + & sizeof ~ -- ++ |
逻辑反操作 负值 正值 取地址 操作数的类型长度(以字节为单位) 对一个数的二进制按位取反 前置、后置-- 前置、后置++ |
* (类型) |
间接访问操作符(解引用操作符) 强制类型转化 |
#include
int main()
{
int a = -10;
//逻辑反操作:!
int c = !2;
//2为真,在使用!之后就会将其变为假,也就是0
printf("c = %d\n", c);//0
//负值:-
a = -a;
printf("a = %d\n", a);//10
//取地址:&
int* p = &a;
//取出了a的地址
printf("&a = %p\n", p);
//解引用:*
//通过p找到a的地址,对其进行解引用然后修改
*p = 20;
//大小:sizeof
int size = sizeof(a);
int size = sizeof(int);
//计算的是a这个整形变量的大小
printf("sizeof(a) = %d\n", size);//4
//按位取反:~
a = 0;
printf("~a = %d\n", ~a);//-1
//0的补码00000000000000000000000000000000
//按位取反
// 11111111111111111111111111111111
//要注意这个是补码,要转化为原码
// 10000000000000000000000000000001
// -1
//强制类型转化
float a = 3.14f;
int b = (int)a;
//强制类型转化知识临时的一种状态
//并不会真正的改变a的类型
return 0;
}
++和--运算符
//前置++和--
#include
int main()
{
int a = 10;
int x = ++a;
//先对a进行自增,然后对使用a,也就是表达式的值是a自增之后的值。x为11。
int y = --a;
//先对a进行自减,然后对使用a,也就是表达式的值是a自减之后的值。y为10;
return 0;
}
//后置++和--
#include
int main()
{
int a = 10;
int x = a++;
//先对a先使用,再增加,这样x的值是10;之后a变成11;
int y = a--;
//先对a先使用,再自减,这样y的值是11;之后a变成10;
return 0;
}
关于sizeof在之前我们都已经见过了,在这里再来巩固一下
#include
void test1(int arr[])
{
printf("%d\n", sizeof(arr));//(2)
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));//(4)
}
int main()
{
int arr[10] = { 0 };
char ch[10] = { 0 };
printf("%d\n", sizeof(arr));//(1)
printf("%d\n", sizeof(ch));//(3)
test1(arr);
test2(ch);
return 0;
}
(1)(2)这两处的结果分别是什么?
(3)(4)这两处的结果分别是什么?
(1)和(3)这两个结果就不用多说,求出的是int数组的整个数组的大小和char数组的整个数组的大小,结果分别为40和10
(2)和(4)这两个表达式看似和(1)、(3)的表达式一样,但是要注意,数组在传参的时候穿的是数组名,数组名是首元素的地址,地址值的就是指针,虽然形参部分用也是数组,但是本质上是int类型的指针和char类型的指针,关于指针的大小不在于它的类型,而在于平台,在32位平台上指针的大小是4个字节,在64位平台上指针的大小是8个字节,因此,(2)、(4)的结果是4/8
关系操作符
>
>=
<
<=
== 用于测试相等
!= 用于测试不相等
这些操作符比较简单,这里就不再一一介绍了
注:在代码的时候要注意==和=,前者是判断相等,后者是赋值,要注意区分
逻辑操作符
&& 逻辑与
|| 逻辑或
区分逻辑与和按位与
区分逻辑或和按位或
1&2 ---->0
1&&2 ---->1
1|2 ---->3
1||2 ---->1
逻辑与和或的特点:
逻辑于和逻辑或操作符只关注真假,不进行计算,如果结果为真则为1,如果为假则为0
&& 逻辑与左边为假,右边的表达式将不再进行计算
|| 逻辑或左边为真,右边的表达式将不再进行计算
这是什么意思呢?下面用代码来演示一下:
//程序输出的结果是什么?
#include
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf("a = %d b = %d c = %d d = %d\n", a, b, c, d);
//i = a++||++b||d++;
//printf("a = %d b = %d c = %d d = %d\n", a, b, c, d);
return 0;
}
注:要注意前置++和后置++的区别以及如何使用
逻辑于、逻辑或的特点
exp1?exp2:exp3
exp1的结果成立则执行exp2,exp1不成立则执行exp3
在前面的学习中我们学到过使用条件操作符来求两个数中较大的一个,再演示一遍:
#include
int main()
{
int a = 10;
int b = 20;
//使用分支语句
if (a > b)
printf("max = %d\n", a);
else
printf("max = %d\n", b);
//条件操作符
printf("max = %d\n", a > b ? a : b);
return 0;
}
可以看到使用条件操作符可以优化代码
exp1,exp2,exp3...expN
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
#include
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
//c是多少?
// a=b+10=12, b=a+1=12+1=13
//因此c的值为13
return 0;
}
谨记一定要从左往右依次进行计算,不可贸然计算最后一个表达式的结果,因为前面的表达式很有可能改变后面表达式的结果
使用逗号表达式也可以达到优化代码的目的
if (a > b)
{
a = b + 1, c = a / 2;
}
//使用逗号表达式进行优化
if(a = b + 1, c = a / 2,a > b)
下标引用操作符: [ ]
操作数:一个数组名 + 一个索引值
int arr[10];//创建数组
arr[9] = 10;//实用下标引用操作符。
//找到下标为9的元素,将其重新赋值为10
//[ ]的两个操作数是arr和9
函数调用操作符:()
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数
#include
void test1()
{
printf("hehe\n");
}
void test2(const char* str)
{
printf("%s\n", str);
}
int main()
{
test1(); //实用()作为函数调用操作符。
test2("hello bit.");//实用()作为函数调用操作符。
return 0;
}
访问结构体成员操作符:
. 访问结构体变量:结构体.成员名
-> 访问结构体指针:结构体指针->成员名
#include
struct Stu
{
char name[10];
int age;
};
void set_age1(struct Stu stu)
{
stu.age = 18;//访问结构体变量
}
void set_age2(struct Stu* pStu)
{
pStu->age = 18;//访问结构体指针
}
int main()
{
struct Stu stu;
struct Stu* pStu = &stu;//取出结构体的地址
stu.age = 20;//访问结构体变量
set_age1(stu);
pStu->age = 20;//访问结构体指针
set_age2(pStu);
return 0;
}
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。
所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
//实例1
char a,b,c;
...
a = b + c;
b和c的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后再存储于a中。
如何进行整形提升呢?
整形提升是按照变量的数据类型的符号位来提升的
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
实例:
#include
int main()
{
char a = 3;
char b = 127;
char c = a + b;
//c的值是多少?
printf("c = %d\n", c); //200?
return 0;
}
如何理解这段代码呢?
以前见过的加减乘除都是int类型的整形来进行运算,直接运算就可以,但是如果是char类型的变量在进行加减乘除运算时该怎么运算呢?
首先我们得知道一个char类型的数据在内存中占2个字节的大小,也就是8个比特位,因此一个char类型所能容纳的数据在-2^7~2^7-1之间,也就是-128~127之间,因此在进行运算时结果在这个范围内就可以正常运算,如果超出了这个范围,就要注意了!
我们再来看这段代码:a和b都是char类型,它们的范围也都在char类型所包含的范围之内,c也是一个char类型的数据,但是c是a+b,a+b的结果刚好超出了char类型的范围,那该怎么办呢?
在32位平台下,一个数的二进制位有32位,但最终要取决于它们自身的类型
char a = 3;
3的二进制序列: 00000000000000000000000000000011
char类型的3的二进制序列就要截断,保留8位:
00000011
char b = 127;
127的二进制序列:00000000000000000000000001111111
char类型的127的二进制序列要截断,保留8位:
01111111
char c = a + b;
因为a和b都是char类型,要计算a+b就得进行整形提升
00000000000000000000000000000011 - a
00000000000000000000000001111111 - b
00000000000000000000000010000010 - c
再计算完之后又要发生截断然后存储到c中
c:100000010
接下来要进行打印,以%d的形式打印,因此又要进行整形提升,这里的最高位是1,那就表示负数,负数整形提升时高位补充符号位1:
11111111111111111111111110000010
提升之后最高位也表示符号位,1表示负数,因此这个二进制位是一个补码,需要转化为原码才可以进行打印,补码转化为原码:-1取反和取反+1都可以
10000000000000000000000001111110 -> -126
因此a+b的得到的c居然是一个负数,这就很神奇,居然发生了正数+正数还能得到负数的事情,所以C语言是一门很神奇的语言
再来看一段代码:
//代码结果?
//代码1
#include
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
这段代码有三个类型不同的变量,我们已经了解过了整型提升,因此char类型的a和short类型的b要进行整型提升,而int类型的c不用,在提升之后就会与原来的值不同,因此代码结构只会打印c
0x开头的数字是16进制的数字,b6的二进制位为10110110,在经过整型提升之后会变为负数,因此a和b的值会发生改变
//代码2
//代码结果
#include
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字节.
表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof(c) ,就是1个字节.
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
警告:
但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
复杂表达式的求值有三个影响的因素。
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符 |
描述 |
用法示例 |
结果类型 |
结合性 |
是否控制求值顺序 |
() |
聚组 |
(表达式) |
与表达 式同 |
N/A |
否 |
() |
函数调用 |
rexp(rexp,...,rexp) |
rexp |
L-R |
否 |
[ ] |
下标引用 |
rexp[rexp] |
lexp |
L-R |
否 |
. |
访问结构成员 |
lexp.member_name |
lexp |
L-R |
否 |
-> |
访问结构指针成员 |
rexp->member_name |
lexp |
L-R |
否 |
++ |
后缀自增 |
lexp ++ |
rexp |
L-R |
否 |
-- |
后缀自减 |
lexp -- |
rexp |
L-R |
否 |
! |
逻辑反 |
! rexp |
rexp |
R-L |
否 |
~ |
按位取反 |
~ rexp |
rexp |
R-L |
否 |
+ |
单目,表示正值 |
+ rexp |
rexp |
R-L |
否 |
- |
单目,表示负值 |
- rexp |
rexp |
R-L |
否 |
++ |
前缀自增 |
++ lexp |
rexp |
R-L |
否 |
-- |
前缀自减 |
-- lexp |
rexp |
R-L |
否 |
* |
间接访问 |
* rexp |
lexp |
R-L |
否 |
& |
取地址 |
& lexp |
rexp |
R-L |
否 |
sizeof |
取其长度,以字节 表示 |
sizeof rexp sizeof(类 型) |
rexp |
R-L |
否 |
(类 型) |
类型转换 |
(类型) rexp |
rexp |
R-L |
否 |
* |
乘法 |
rexp * rexp |
rexp |
L-R |
否 |
/ |
除法 |
rexp / rexp |
rexp |
L-R |
否 |
% |
整数取余 |
rexp % rexp |
rexp |
L-R |
否 |
+ |
加法 |
rexp + rexp |
rexp |
L-R |
否 |
- |
减法 |
rexp - rexp |
rexp |
L-R |
否 |
<< |
左移位 |
rexp << rexp |
rexp |
L-R |
否 |
>> |
右移位 |
rexp >> rexp |
rexp |
L-R |
否 |
> |
大于 |
rexp > rexp |
rexp |
L-R |
否 |
>= |
大于等于 |
rexp >= rexp |
rexp |
L-R |
否 |
< |
小于 |
rexp < rexp |
rexp |
L-R |
否 |
<= |
小于等于 |
rexp <= rexp |
rexp |
L-R |
否 |
操作符 |
描述 |
用法示例 |
结果类型 |
结合性 |
是否控制求值顺序 |
== |
等于 |
rexp == rexp |
rexp |
L-R |
否 |
!= |
不等于 |
rexp != rexp |
rexp |
L-R |
否 |
& |
位与 |
rexp & rexp |
rexp |
L-R |
否 |
^ |
位异或 |
rexp ^ rexp |
rexp |
L-R |
否 |
| |
位或 |
rexp | rexp |
rexp |
L-R |
否 |
&& |
逻辑与 |
rexp && rexp |
rexp |
L-R |
是 |
|| |
逻辑或 |
rexp || rexp |
rexp |
L-R |
是 |
? : |
条件操作符 |
rexp ? rexp : rexp |
rexp |
N/A |
是 |
= |
赋值 |
lexp = rexp |
rexp |
R-L |
否 |
+= |
以...加 |
lexp += rexp |
rexp |
R-L |
否 |
-= |
以...减 |
lexp -= rexp |
rexp |
R-L |
否 |
*= |
以...乘 |
lexp *= rexp |
rexp |
R-L |
否 |
/= |
以...除 |
lexp /= rexp |
rexp |
R-L |
否 |
%= |
以...取模 |
lexp %= rexp |
rexp |
R-L |
否 |
<<= |
以...左移 |
lexp <<= rexp |
rexp |
R-L |
否 |
>>= |
以...右移 |
lexp >>= rexp |
rexp |
R-L |
否 |
&= |
以...与 |
lexp &= rexp |
rexp |
R-L |
否 |
^= |
以...异或 |
lexp ^= rexp |
rexp |
R-L |
否 |
|= |
以...或 |
lexp |= rexp |
rexp |
R-L |
否 |
, |
逗号 |
rexp,rexp |
rexp |
L-R |
是 |
总结:
表达式的求值部分由操作符的优先级决定
操作符的优先级只能决定相邻两个操作符的执行顺序,并不能决定相隔两个操作符的执行顺序
例如表达式1: a*b + c*d + e*f
操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值
例如表达式2: c + --c
只能通过操作符的优先级得知:先算乘法,再算减法,但是函数的调用先后顺序无法通过操作符的优先级确定
例如表达式3: answer = fun() - fun() * fun()
我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的
本篇关于操作符的学习就到此结束,如果老铁们还有什么疑问可以打在评论区,感谢大家访问,感谢!