还记得之前对操作符进行了简单的了解,并且学习了一些常见操作符的用法,如今我们又对操作符这部分的知识进行了更深一步的学习,有了一些新的感悟和体会,这篇文章我希望能把关于操作符的方方面面,包括一些细微之处,都说到位。
话不多说,让我们开始吧!
目录
一.操作符分类
- 操作符有哪些?可以分为几类:
- 算术操作符
- 移位操作符
- 位操作符
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符
- 逗号表达式
- 下标引用、函数调用和结构成员
接下来我们对这些操作符一一进行阐述!
二.算术操作符
+ - * / %
关于算数操作符,比较简单,这里说一些需要注意的:
1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除 法。
3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
三.移位操作符
<< 左移操作符
>> 右移操作符
这是我们今天要重点介绍的操作符之一
注:他们的操作数必须是整数。
1.有关进制和原码、反码、补码
int a = 5;
int b = a << 2;
这里的 a<<2 是把a向左移动2位,实质是:把a在内存中存储的二进制位向左移动两位
这里补充一下进制的知识:
- 二进制:逢二进一
基数为2,数值部分用两个不同的数字0、1来表示。
- 十进制:逢十进一
基数为10,数值部分用0、1、2、3、4、5、6、7、8、9来表示.
- 十六进制:逢十六进一
基数是16,有十六种数字符号,除了在十进制中的0至9外,还另外用6个英文字母A、B、C、 D、E、F来表示十进制数的10至15。
下面用一张图来帮助我们理解进制
下面我们还需要了解一下原码 、反码 、补码 的知识 :
整数有3种二进制表示形式:
- 原码
- 反码
- 补码
(1).原码
原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 比如如果是8位二进制
[+1]原码 = 0000 0001
[-1] 原码 = 1000 0001
(2).反码
反码的表示方法是:
正数的反码是其本身
负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.
[+1] = [00000001]原码 = [00000001]反码
[- 1] = [10000001]原码 = [111111110]反码
(3).补码
补码的表示方法是:
正数的补码就是其本身
负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)
[+1] = [00000001]原 = [00000001]反码 = [00000001]补码
[-1] = [10000001]原 = [11111110] 反码 = [11111111]补码
经过上面的叙述,我们很容易发现;
- 正整数:原码、补码、反码相同
- 负整数:原码、补码、反码不同,需要计算
这里我们再举个例子看看:
而整数在内存中存储的是补码!!!
如图,-1的在内存中存储的是补码
而VS编译器是16进制展示
所以显示的内存是 f f f f f f f f
我们知道16进制的 f 就是10进制的15,所以这里对应的就是-1的补码
2.移位操作符
首先需要说明的是:
移位操作符操作的是补码
而打印或使用的时候,用的是补码
所以使用移位操作符会某个数的二进制的补码改变后,我们如果需要打印或使用这个数的二进制是,要先通过改变后的补码反推出改变后的原码
铺垫做的差不多了,下面正式介绍移位操作符 !
(1).左移操作符
移位规则:左边抛弃、右边补0
如图,通过左移操作符把num向左移了一位, 正数10的二进制的补码 左边抛弃、右边补0
变成了 00000000000000000000000000010100
计算结果就为 1*2^4+1*2^2=20 了
如果要打印
int num2=num<<1;
printf("%d",num2);
这里的打印用的是num2的原码的值
需要注意的是:
这时num的值还是10,没有改变,只是我们计算了一下 num<<1 的结果而已
(2).右移操作符
移位规则:
首先右移运算分两种:
- 算术移位 : 左边用原该值的符号位填充,右边丢弃
- 逻辑移位 :左边用0填充,右边丢弃
到底是哪种取决于编译器,我们常见的编译器下都是算数右移
如果是正数,这两种结果一样
我们通过一张图来展示一下右移操作符的作用效果
我们用一个负数 -5 来看一下
经过算术操作符,结果是 -3
经过逻辑操作符,结果是 3
警告⚠ :
对于移位运算符,不要移动负数位,这个是标准未定义的行为。
如
int b = a >> -2;
这是不行的!
四.位操作符
位操作符有:
& //按位与
| //按位或
^ //按位异或
注:他们的操作数必须是整数。
这里的”位“指的都是二进制位
& 按位与 (对应的二进制位有0则为0,全1才为1)
| 按位或 (有1为1,全0为0)
^ 按位异或 (相同为0,相异为1)
(1).& 按位与
我们举个例子
int a = 3 ;
int b = -5 ;
int c = a & b ;
我们用一张图表示运算的过程:
(2). | 按位或
int a = 3 ;
int b = -5 ;
int c = a | b ;
结果是 -5
(3). ^ 按位异或
int a = 3 ;
int b = -5 ;
int c = a ^ b ;
结果是 -8
下面我们看曾经一道变态的面试题
不能创建临时变量(第三个变量),实现两个数的交换。
首先我们应该明确:
a ^ a = 0
a ^ 0 = a
上代码!
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a^b;
b = a^b;
a = a^b;
printf("a = %d b = %d\n", a, b);
return 0;
}
一个练习:
编写代码实现:求一个整数存储在内存中的二进制中1的个数。
//方法1
#include <stdio.h>
int main()
{
int num = 10;
int count= 0;//计数
while(num)
{
if(num%2 == 1)
count++;
num = num/2;
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
//思考这样的实现方式有没有问题?
//方法2:
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
for(i=0; i<32; i++)
{
if( num & (1 << i) )
count++;
}
printf("二进制中1的个数 = %d\n",count);
return 0;
}
//思考还能不能更加优化,这里必须循环32次的。
//方法3:
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
while(num)
{
count++;
num = num&(num-1);
}
printf("二进制中1的个数 = %d\n",count);
return 0;
}
//这种方式是不是很好?达到了优化的效果,但是难以想到。
五.赋值操作符
赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
int weight = 120; //体重
weight = 89; //不满意就赋值
double salary = 10000.0;
salary = 20000.0; //使用赋值操作符赋值。
赋值操作符可以连续使用,比如:
int a = 10;
int x = 0;
int y = 20;
a = x = y+1;//连续赋值
a的结果是21,这样写是没错,但不建议这样写
复合操作符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
这些运算符都可以写成复合的效果。 比如:
int x = 10;
x = x+10;
x += 10;//复合赋值
//其他运算符一样的道理。这样写更加简洁。
六.单目操作符
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
(1).逻辑反操作符 !
如:
int flag = 0;
则 !flag = 1
若
int flag = 5;
则 !flag = 0
注:!flag的结果只能为1或0 (真或假)
(2). ++和--运算符
前置++:先++,后使用
后置++:先--,后使用
(3). * 解引用操作符
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;//pa里存放的是a的地址
*pa = 20;//解引用操作符
printf("%d\n", a);
return 0;
}
我们把a的地址存到 pa 中
再对它解引用,就能找到a
(4). ( )强制类型转换
int main()
{
int a = 3.14;
return 0;
}
这个代码,编译时会有一个警告
如果我们一定要把3.14放到整型a中,就要用到强制类型转换
int main()
{
int a = (int)3.14;
return 0;
}
在学习了这些单目操作符后,我们看一个演示代码
#include <stdio.h>
int main()
{
int a = -10;
int *p = NULL;
printf("%d\n", !2);
printf("%d\n", !0);
a = -a;
p = &a;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(int));
printf("%d\n", sizeof a);//这样写行不行?
printf("%d\n", sizeof int);//这样写行不行?
return 0;
}
首先看逻辑反操作符
接着看这组
sizeof后面跟数据类型(如int)不能不加括号,后边跟变量时可以不加括号
而sizeof计算的是类型或变量所占空间的大小,跟变量是多少无关,只与变量所属的类型有关
如int就是4个字节
下面我们再来看一下sizeof和数组的一个代码演示
#include <stdio.h>
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;
}
看一下这一步,这个sizeof是在主函数内,没有传参
sizeof()内部单独放一个数组名,表示整个数组,所以这里的sizeof计算的是整个数组的大小
所以第一个计算结果是 4*10=40
第二个是 1*10=10
下面看函数调用,这里就是传参了
数组传参如果传数组名,那么传的是首元素地址,这里的数组名本质上是一个地址,是一个指针
而前面我们介绍过指针的大小,在32位系统下指针的大小是4个字节,在64位是8个字节
这里以 32位系统为例
- 函数 test1 计算的结果是4
- 函数 test2 计算的结果也是4
它们计算的都是指针的大小,与类型无关
七.关系操作符
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。
警告: 在编程的过程中== 和=不小心写错,导致的错误
八.逻辑操作符
&& 逻辑与
|| 逻辑或
我们可以把 && 理解为并且,把 || 理解为或者
庸俗的讲,&&是有一个不成立就都不成立,||是有一个成立就都成立
区分逻辑与和按位与
区分逻辑或和按位或
1&2----->0
1&&2---->1
1|2----->3
1||2---->1
逻辑与和或的特点:
360笔试题
#include <stdio.h>
int main()
{
int i = 0,a=0,b=2,c =3,d=4;
i = a++ && ++b && d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
我们来分析一下
这里由于a=0为假,所以整个i=a++&&++b&&d++都判断为假,所以a++后面的++b和d++都没有执行
对比着看一下逻辑或 ||
int main()
{
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++||++b||d++;
printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
return 0;
}
分析:
这里的a=1所以为真,所以整个
i = a++||++b||d++ 都判断为真,后面的++b和d++没有执行
总结
&& 左操作数为假,右边不计算
| | 左操作数为真,右边不计算
九.条件操作符
exp1 ? exp2 : exp3
表达式1为真,则结果为表达式2的值
表达式1为假,则结果为表达式3的值
十.逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
这里的c等于3
//代码1
if (a =b + 1, c=a / 2, d > 0)
//代码2
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
a = get_val();
count_val(a);
}
//如果使用逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
十一.下标引用、函数调用和结构成员
1.[ ] 下标引用操作符 操作数:一个数组名 + 一个索引值
int arr[10];//创建数组
arr[9] = 10;//实用下标引用操作符。
[ ]的两个操作数是arr和9。
这里补充一点,
arr[7]--> *(arr+7) --> *(7+arr)--> 7[arr]
2.( ) 函数调用操作符 接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
3. 访问一个结构的成员 . 结构体.成员名 结构体指针->成员名
#include <stdio.h>
struct Stu
{
char name[10];
int age;
char sex[5];
double score;
};
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;
}
十二. 表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。 同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
1.隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。 为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度。 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算
int main()
{
char a = 5;
char b = 126;
char c = a + b;
printf("%d\n", c);
return 0;
}
原因: a是char 类型,但5是int类型,
char是1个字节,int是4个字节,放不下,这时就要将5的二进制序列的补码截断
同理,126
此后 要进行计算,计算时要整型提升
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是: 11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是: 00000000000000000000000000000001
//无符号整形提升,高位补0
实际操作:
我们要把计算好的结果放到c中,所以又要截断
现在我们要把c打印出来
打印的是%d,是整型,而c是char类型,所以这时候我们又要整型提升
整型提升 提升的是在内存中的补码,而打印用的是原码所以我们还需要反推出原码
这样,我们就可以把c打印出来了,这个原码对应的数是 -125
再看一个整型提升的例子:
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;
}
实例1中的a,b要进行整形提升,但是c不需要整形提升
a,b整形提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假,
但是c不发生整形提升,则表 达式 c==0xb6000000 的结果是真.
所以程序输出的结果是: c
这里那a的整型提升举例
示例2
//实例2
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
分析:实例2中的,c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字节, 表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof(c) ,就是1个字节.
注意:sizeof内部表达式的值是不会去真实计算的,只关心它的类型,计算类型的大小
2.算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
警告: 但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f;
//隐式转换,会有精度丢失
3.操作符的属性
复杂表达式的求值有三个影响的因素。
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符优先级
注:N/A表示无
L-R表示从左向右
一些问题表达式
//表达式的求值部分由操作符的优先级决定。
//表达式1
a * b + c * d + e * f
注释:代码1在计算的时候,由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并 不能决定第三个*比第一个+早执行。
所以表达式的计算机顺序就可能是:
a * b
c * d
a * b + c * d
e * f
a * b + c * d + e * f
或者:
a * b
c * d
e * f
a * b + c * d
a * b + c * d + e * f
2
//表达式2
c + --c;
注释:同上,操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得 知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义 的。
3
//代码3-非法表达式
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
表达式3在不同编译器中测试结果:非法表达式程序的结果
//代码4
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);//输出多少?
return 0;
}
这个代码有没有实际的问题?
有问题!
虽然在大多数的编译器上求得结果都是相同的。
但是上述代码 answer = fun() - fun() * fun(); 中
我们只能通过操作符的优先级得知:先算乘法, 再算减法。
函数的调用先后顺序无法通过操作符的优先级确定。
//代码5
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
//尝试在linux 环境gcc编译器,VS2013环境下都执行,看结果。
VS2017环境的结果:
看看同样的代码产生了不同的结果,这是为什么?
简单看一下汇编代码.就可以分析清楚.
这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,
因为依靠操作符的优先级 和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。