文章目录
谁都懂的道理,不如直接开干,干就完了!
下面我们介绍的是操作符这一板块的内容,这里简单介绍,如果需要深入理解可以点这个链接"c语言符号深度理解和再认识",这里有一些比较难懂的符号,我已经汇总在了一起。
1. 操作符分类
操作符有这么几类:
算术操作符、移位操作符、位操作符、赋值操作符、单目操作符、关系操作符、逻辑操作符、条件操作符、逗号表达式、下标引用、函数调用、结构成员。
下面对这些操作符一一细细道来。
2. 算术操作符
+ - * / %
这五个都是常见的算术操作符,但是有两个符号需要特别注意:/和%。
看图得结论:
1.除了%操作符,其他的四个操作符可以用于整数和浮点数。
2.对于/操作符如果两个操作数都是整数,执行整数除法。而只是有浮点数执行的就是浮点数除法。
3.%操作符的两个操作数必须是整数,返回的是整除之后的余数。
3. 移位操作符
了解移位操作符,首先必须了解一下权值是什么?原码、反码、补码又是什么,怎么得来的?下面一步步展开。
<< 左移操作符
>> 右移操作符
注意:移位操作符的操作数只能是整数。
3.1 初步了解权值是什么
举一个例子就明白:
一个数字10用二进制表示为1010,所表示的意思是:10=1* 23 +0* 22 +1* 21+ 0*20.这里的1010的第一个1的权值就是23。第二个0的权值就是22。用八进制表示10,就是10=12,这里12中1的权值就是81,2的权值是80。下面我们再来了解原反补。
3.2 初步了解原码、反码、补码
一个数分为正数和负数,计算机中是不能识别负号的,所以计算机用补码的形式存储正数和负数,规定最高符号位对数分为正数和负数,正数最高符号位为0,负数为1,这样区分正数和负数。正数的原码、反码、补码都是一样的,负数的原码、反码、补码要进行运算(其中补码=反码+1,反码=原码按位取反(除了最高符号位)。
正数的最高符号位是0,负数的最高符号位是1。
举个例子:
正数10(因为是int类型的,所以有4个字节,也就是32个比特位)
00000000000000000000000000001010 - 10的原码
00000000000000000000000000001010 -10的反码
00000000000000000000000000001010 - 10的补码
正数的原码反码补码都相同。
负数-10(32个比特位)
10000000000000000000000000001010 - -10的原码
11111111111111111111111111110101 - -10的反码
11111111111111111111111111110110 - -10的补码
负数的补码=反码+1,反码=原码按位取反(除了最高符号位不变)
3.3 左移操作符
移位规则:左边抛弃,右边补0。
先看现象,在分析:
分析:
10的补码(原码)是:
00000000000000000000000000001010
左移:
00000000000000000000000000010100 = 20(原码=补码)(因为是正数)
再来看一个奇怪的现象:
我们发现左移操作符可以对正数或者负数乘以2。
3.4 右移操作符
移位规则:右移运算分为两种:1.逻辑移位:左边用0填充,右边丢弃;2.算术移位:左边用原来该值的符号位填充,右边丢弃。其中编译器到底用哪一种呢?这是取决于编译器本身,绝大多数是用的算术移位。
还是一样先看现象再分析:
10的补码(原码)是:
00000000000000000000000000001010
右移:
00000000000000000000000000000101 = 5(原码=补码)
再来看一个现象:
我们发现右移操作符可以对正数或者负数除以2。其中是取整方式是向0取整,不懂向取整可以点击文章开始的链接。
注意:对于移位操作符,不要移动负数位,这个是标准为定义。
这段话什么意思?看这个:
4. 位操作符
& //按位与
| //按位或
^ //按位异或
注:他们的操作数必须是整数。
符号用法;
//&-按位与
//比特位同时为1时才为1
#include <stdio.h>
int main()
{
int a = 10;
//00000000000000000000000000001010(原反补相同)
int b = -3;
//10000000000000000000000000000011(原码)
//11111111111111111111111111111100(反码)
//11111111111111111111111111111101(补码)
int c = a & b;
//00000000000000000000000000001010(a)
//11111111111111111111111111111101(b)
//00000000000000000000000000001000=8(补码)(最高符号位是0,所以是正数,补码=原码)
//
printf("%d\n", c);
return 0;
}
//|-按位或
//比特位同时为0时才为0
#include <stdio.h>
int main()
{
int a = 5;
//00000000000000000000000000000101 (5的原码也是补码)
int b = -2;
//10000000000000000000000000000010 (-2的原码)
//11111111111111111111111111111101(-2的反码)
//11111111111111111111111111111110(-2的补码)
int c = a | b;
//-2的补码|5的补码
//11111111111111111111111111111111 (c的补码)
//11111111111111111111111111111110(反码)
//10000000000000000000000000000001=-1 (原码)
printf("%d\n", c);
return 0;
}
//按位异或
//比特位相同为0,不同为1
#include <stdio.h>
int main()
{
int a = -8;
//10000000000000000000000000001000 (-8的原码)
//11111111111111111111111111110111(-8的反码)
//11111111111111111111111111111000 (-8的补码)
int b = 6;
//00000000000000000000000000000110(6的原码,也是补码)
int c = a ^ b;
//-8的补码^6的补码
//11111111111111111111111111111110(c的补码)
//11111111111111111111111111111101(反码)
//10000000000000000000000000000010=-2(原码)
printf("%d\n", c);
return 0;
}
4.1 练习:不能创建临时变量(第三个变量),实现两个数的交换
方法一(用按位异或来解决):
#include <stdio.h>
int main()
{
int a = 10;
//00000000000000000000000000001010(原码=补码)
int b = 20;
//00000000000000000000000000010100(原码=补码)
a = a ^ b;
//a^b=00000000000000000000000000011110=30=a
b = a ^ b;
//b=0000000000000000000000011110(a)^b=00000000000000000000000000001010=10;
a = a ^ b;
//a=00000000000000000000000000011110(a)^00000000000000000000000000001010(b)=20(a)
return 0;
}
方法二(用加法):
int main()
{
int a = 3;
int b = 5;
int c = 0;
c =a + b;//c=8
a =c - a;//a=5
b =c - a;//b=3
return o;
}
4.2 练习:编写代码实现:求一个整数存储在内存中的二进制中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;
}
//方法二:
#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;
}
//方法三:
#include <stdio.h>
int main()
{
int num = 3;
int i = 0;
int count = 0;//计数
while (num)
{
count++;
num = num & (num - 1);
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
5. 赋值操作符
这里再提一次:初始化和赋值的区别:初始化是创建变量的同时给定一个值,赋值时创建变量后对其给定一个值。
如:
#include <stdio.h>
int main()
{
int a;
a = 20;//这是赋值
int b = 10;//这是初始化
return 0;
}
5.1 连续赋值问题
#include <stdio.h>
int main()
{
int a = 10;
int b = 5;
int c = 0;
c = a = b + 8;
printf("%d\n", c);
return 0;
}//最终输出13
#include <stdio.h>
int main()
{
int a = 10;
int b = 5;
int c = 0;
a = b + 8;
c = a;
printf("%d\n", c);
return 0;
}最终输出13
通过上述两段代码可以看出:
连续赋值的阅读体验极差,往往给人一种不想看的感觉。
5.2 复合赋值符
+=、-=、*=、/=、%=、>>=、<<=、&=、|=、^=
看一例子吧:
#include <stdio.h>
int main()
{
int c = 0;
c = c + 10;
//c += 10;(相当于c = c + 10)
return 0;
}
6. 单目操作符
6.1 单目操作符介绍
!(逻辑反操作)
-(负值)
+(正值)
&(取地址)
sizeof(操作数的类型长度(以字节为单位)
~(对一个数的二进制按位取反)
–(前置、后置–)
++(前置、后置++)
*(间接访问操作符(解引用操作符))
(类型)(强制类型转换)
这里对!(逻辑反操作)解释一下:
#include <stdio.h>
int main()
{
int i = 0;
if (!i)//这里的意思是明确的真假
{
printf("hehe\n");
}
if (i == 0)//而这里的i在判断条件中相当于一个数值
{
printf("haha\n");
}
return 0;
}
对&(取地址操作符)和*(解引用操作符)解析:
int arr[10];
&arr;//取的是整个数组的地址
//也可以&函数;
提一句:首元素不表示首元素地址的两种情况:
- sizeof(数组名),这里的数组名是整个数组的大小空间。
- &数组名,这里的数组名是整个数组的大小。
int a = 10;
int *p = &a;
*p = 20;
//最终a别改为20
通常解引用操作符于指针有关,后期在对其进行理解,这里不在对其过多理解。
对sizeof操作符进行解释(直接看现象):
这里在打印指针的时候不管是什么类型在32位平台下都是4个字节的大小,在64位平台下都是8个字节的大小。
再看一例;
这里错误的原因是sizeof的()应该输入表达式。
再看一例:
这里我们观察到sizeof(s = a + 2)=2;说明计算的是s的空间大小。
6.2 sizeof和数组
还是一样通过现象看本质:
再看一例:
函数传参,传数组名,这里的数组名就是地址,所以在32为平台下是4个字节的大小。
对~(对一个数的二进制按位取反)解析:
#include <stdio.h>
int main()
{
int a = 10;
//a=00000000000000000000000000001010
//~a=11111111111111111111111111110101
//最高符号位是1,所以是补码
//11111111111111111111111111110100(~a的反码)
//10000000000000000000000000001011=-11(~a的原码)
printf("%d\n", ~a);
return 0;
}
对~这个符号了解后再来看个实例:
#include <stdio.h>
//把一个数的第n二进制位改为1,在改为0.
int main()
{
int a = 10;
//00000000000000000000000000001010
//假设对第四二进制位修改
//就是用1对其进行左移操作并用按位或进行操作
int n = 0;
scanf("%d", &n);
///把一个数的第n二进制位改为1
a = a | (1 << (n - 1));
printf("%d\n", a);
//把一个数的第n二进制位改为0
a = a & ~(1 << (n - 1));
printf("%d\n", a);
return 0;
}
对前置++和后置++以及前置–和后置–解析:
int main()
{
int a = 3;
int b = ++a;
//b = ++a等价于a=a+1,b=a;(先自身++后使用))
printf("b=%d\n", b);//b=4
return 0;
}
int main()
{
int a = 3;
int c = a++;
//c = a++等价于c=a,a=a+1;(先使用后自身++)
printf("c=%d\n", c);//c=3
return 0;
}
int main()
{
int g = 2;
int d = --g;
//int d = --g;//等价于g=g-1,d=g;(先自身--后使用)
printf("d=%d\n", d);//d=1
}
int main()
{
int g = 2;
int e = g--;
//e = g--等价于e=g,g=g-1;(先使用后自身--)
printf("e=%d\n", e);//e=2
}
对(类型)强制转化类型符号解析:
#include <stdio.h>
int main()
{
float a = 3.14f;
int b = (int)a;//float类型强制转化为int类型
return 0;
}
7. 关系操作符
>,>=,<,<=,!=,==
注意:编译过程中==和=不小心写错,导致错误。
8. 逻辑操作符
&& 逻辑与
|| 逻辑或
逻辑操作符就是判断真假,非0为真,0为假。
看例题:
#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;
}//最终输出结果是1234
这里的逻辑与发生短路,逻辑与要从左到右依次执行,碰到0就不用在执行其后面的,这里a++使用的时候是0,在对其进行本身++操作,所以打印a=1,a++使用的时候是0,逻辑与操作在碰到假是时,其后面不再执行,所以后面b,c,d的值都没有变。
#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;
}最终输出结果是1334
这里也是短路问题,从左到右一次执行,逻辑或在碰到真时不在执行其后面表达式,所以a++使用时候是0,然后就要再看++b,而++b是非0为真,所以不在看d++表达式。
总结:逻辑与符号和逻辑或符号都是从左到右依次执行,而逻辑与操作符号是看表达式是否为假,为假就不再看其后的表达式,为真则继续执行,知道碰到为假的表达式。逻辑或符号是只要碰到为真,则不在执行。
9. 条件操作符
exp1 ? exp2 : exp3
有时if else语句不如条件操作符简洁。
if(a>1)
{
b=3;
}
else
{
b=1;
}
上面的这段if else语句其实可以用条件操作符,b=(a>1?3:1);
10. 逗号表达式
exp1,exp2,exp3,exp4…
逗号表达式从左到右依次执行,整个表达式结果是最后一个表达式结果。
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
printf("%d\n", c);
return 0;
}//最终输出13.
11. 下标引用、函数调用、结构成员
11.1下标引用操作符([])
int arr[10];//这个[]是创建数组
arr[9]=10;//这个[]是下标引用操作符,它的操作数是9和arr。
11.2函数调用操作符
#include <stdio.h>
void test1()
{
printf("hehe\n");
}
void test2(const char* str)
{
printf("%s\n", str);
}
int main()
{
test1(); //实用()作为函数调用操作符。
test2("hello bit.");//实用()作为函数调用操作符。
return 0;
}
11.3结构成员
12. 表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
12.1隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型
提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU(general-purposeCPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
也就是char类型和short类型在运算时首先都要转化为int类型的再进行计算。
无符号整型提升,高位直接补0,有符号高位补符号位。
例如:
#include <stdio.h>
int main()
{
char a = 5;
//00000000000000000000000000000101=5
//CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度
//所以是32位,但是a变量是char类型的所以发生截断
//00000101=a
char b = 126;
//00000000000000000000000001111110=126
//01111110=b(发生截断)
char c = a + b;
//整型提升
//(a的最高符号位是0,所以在补齐的时候补0)
//(b的最高符号位是0,所以在补齐的时候补0)
//a+b=00000000000000000000000000000101+00000000000000000000000001111110
//00000000000000000000000010000011=c
//10000011=c(发生截断)
printf("%d\n", c);
//%d是十进制的方式打印有符号整数
//c的最高符号位是1,所以在补齐的时候补1
//11111111111111111111111100000011(补码)
//11111111111111111111111100000010(反码)
//10000000000000000000000011111101=-125(原码)
return 0;
}
再看一例:
#include <stdio.h>
int main()
{
char a = 0xb6;
//10110110=a
//整型提升后
//11111111111111111111111110110110(a最高符号位是1,所以补1)
short b = 0xb600;
//1011011000000000=b
//整型提升后
//11111111111111111011011000000000
int c = 0xb6000000;
if (a == 0xb6)//会发生整型提升不会再等于0xb6
printf("a");
if (b == 0xb600)//会发生整型提升
printf("b");
if (c == 0xb6000000)//int类型不会发生整型提升
printf("c");
//最终输出c
return 0;
}
再看一例:
#include <stdio.h>
int main()
{
char c = 1;
printf("%zu\n", sizeof(c));
printf("%zu\n", sizeof(+c));
printf("%zu\n", sizeof(-c));
//printf("%zu\n",sizeof(c+1));//最终输出也是4.
return 0;
}
//只要short和char类型的运算,就会发生整型提升。
输出结果:
c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以sizeof(+c) 是4个字节.表达式 -c也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof© ,就是1个字节.
12.2算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
float f = 3.14;//由double类型转换为float
int num = f;//由float类型转换为int类型
由高精度类型向低精度类型转化会有精度丢失问题的出现。在这里注意只有short类型和char类型转换为int类型时才是整型提升(原因是CPU内整型运算器(ALU)的操作数的字节长度,一般就是int的字节长度,同时也是CPU的通用寄存器的长度),而int类型转化为double等高精度不叫做整型提升,运算时也不遵循整型提升的运算规则。
举一个int类型转换为double和float类型的例子:
可见只是变为了浮点数并补0.