1、操作符分类
算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号操作符
下标引用、函数调用和结构成员。
2、算数操作符
+ - * / %
// 7 / 2 ----->3
// 7 % 2 ----->1
//那如果想要打印出小数呢?只需要两端有一个浮点数就会执行浮点数得出发
#include <stdio.h>
int main()
{
float a = 7 / 2.0;
printf("%.1f\n",a); //.1就是保留小数点后一位
return 0;
}
3、移位操作符(二进制)
<< 左移位符 就是左边丢弃,右边补0。
>> 右移位符
注:移位操作符的操作数只能是整数
移位操作符,移动的是二进制位。
警告:对于移位运算符,不要移动负位数,这个是标准定义的。
3.1、左移位
3.1.1、原码、反码、补码介绍
整数的二进制表示有3种:
- 原码
- 反码
- 补码
正的整数的原码、反码、补码相同。
负的整数的原码、反码、补码是要计算的。
1、比如7的原码、反码、补码:
因为7是正值所以,原码 = 反码 = 补码
7是int类型的,占4个字节,一个字节=8bit,所以:
原码 = __0__0000000000000000000000000000111
注意:最高位(就是最左边的哪一位)0代表是正数,1代表是负数。
2、-7的原码、反码、补码
- 原码:10000000000000000000000000000111
- 反码:11111111111111111111111111111000(按照原码的最高位不变,其它的取反)
- 补码:11111111111111111111111111111001(反码+1=补码)
整数在内存中存储的是补码。
3.1.2、求a=7,a<<1的值
那么接下来言归正传,既然移位操作符,移动的是二进制,那移动的肯定是存储在内存中的补码。
比如:7,补码是:__0__0000000000000000000000000000111。
那如果进行b= 7 <<1;向左移位,就是左边丢弃,右边补0。
那得到的补码结果就是: 00000000000000000000000000001110
那么此时补码的结果转为十进制就是:8+4+2+0=14
看代码效果:
#include <stdio.h>
int main()
{
int a = 7;
int b = a << 1;
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
输出:
可见a左移一位,a本身没有变化。
3.1.3、求a=-7,a<<1的值
-7的补码:11111111111111111111111111111001,那根据左移位:左边丢弃,右边补0的原则来推算,可以得到新的补码为:11111111111111111111111111110010。那这时注意,如果想要得到a<<1的值,需要-7的原码,然后转为十进制即可。
所以现在需要反推,新的补码:11111111111111111111111111110010,那么补码-1就是反码,那求出反码(最高位不取反)为:11111111111111111111111111110001,然后在取反得到原码:10000000000000000000000000001110
这个原码转为十进制是:8+4+2+0=14,又因为是a=-7,所以a<<1=-14。
代码验证:
#include <stdio.h>
int main()
{
int a = -7;
int b = a << 1;
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
输出:
3.1.4、得出左移位的结论
a=7,那a<<1=14
a=-7,那a<<1=-14
所以左移位有原数*2的效果。
3.2、右移操作符
右移操作符分为两类:
- 算术移位:右边丢弃、左边补原符号位。
- 逻辑移位:右边丢弃,左边补0。
具体的算数移位和逻辑移位是取决于编译器的。UP🐖见过的都是算数移位。
3.2.1、求a=7,a>>1的值
算术移位:右边丢弃、左边补原符号位。
补原符号位就是a<<1,a如果是正数,就补0,a如果是负数就补1。
比如:7的补码:__0__0000000000000000000000000000111,按照:右边丢弃,左边补原符号位。那得到的新补码就是:__0__000000000000000000000000000011,所以此结果是:2+1=3。
代码验证:
#include <stdio.h>
int main()
{
int a = 7;
int b = a >> 1;
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
输出:
3.2.2、求a=-7,a<<1的值
7的补码:11111111111111111111111111111001,那根据:右边丢弃,左边补原符号位。那得到的新补码就是:1111111111111111111111111111100(算数移位)或者0111111111111111111111111111100(逻辑移位),那具体是那个移位呢?
VS编译器是算数移位。
分析:-7的新补码:1111111111111111111111111111100,新补码-1=反码=1111111111111111111111111111011,然后取反(最高位不取反)然后得到原码:1000000000000000000000000000100
然后转为十进制结果为:-4。
代码验证:
#include <stdio.h>
int main()
{
int a = -7;
int b = a >> 1;
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
输出:
4、位操作符
位操作符有:
- & -> 按(2进制)位与
- | -> 按(2进制)位或
- ^ -> 按(2进制)位异或
4.1、&按位与
下面先来看那段代码:
#include <stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a & b;
printf("c=%d\n", c);
return 0;
}
输出:
分析:既然内存中放的是补码,所以我们将3和-5的补码写出来:
//3的原码:__0__0000000000000000000000000000011,因为正数的原码=反码=补码。所以:
//3的补码:__0__0000000000000000000000000000011
//-5的原码:__1__0000000000000000000000000000101,取反:
//-5的反码:__1__1111111111111111111111111111010,根据:反码+1=补码,所以:
//-5的补码:__1__1111111111111111111111111111011
那么现在将3和-5的补码进行按位与。
按位与的规则:只要有0,按位与的结果就是0(两个0按位与也是0),两个全部是1,按位与的结果就是1。
3的补码:__0__0000000000000000000000000000011
-5的补码:__1__1111111111111111111111111111011
按位与后的补码:__0__0000000000000000000000000000011
由于按位与后的补码符号位是0,是正数,正数的补码=反码=原码,所以原码结果为:2+1=3,所以3 & 5 = 3,所以上述代码正确。
4.2、| 按位或
先来看段代码:
#include <stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a | b;
printf("c=%d\n", c);
return 0;
}
输出:
按位或的规则:两个数只有1结果为1,两个数全部是0,结果为0。
下面还是需要补码。
分析:
3的补码: __0__0000000000000000000000000000011
-5的补码: __1__1111111111111111111111111111011
得到按位或的补码:__1__1111111111111111111111111111011
由于得到按位或的补码符号位是1,为负数,所以需要-1为反码,然后在取反为原码,然后转为十进制,取值。
按位或的补码-1=反码:__1__1111111111111111111111111111010,
然后反码取反=原码:10000000000000000000000000000101,转化为是十进制为5,又因为符号位为1,所以为负数,所以最终结果为-5。
4.3、^ 按位异或
先来看段代码:
#include <stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a ^ b;
printf("c=%d\n", c);
return 0;
}
输出:
按位异或的规则:相同为0(两个都是0结果为0,两个都是1结果为1),相异为1
分析:
3的补码: __0__0000000000000000000000000000011
-5的补码: __1__1111111111111111111111111111011
按位异或后的补码:__1__1111111111111111111111111111000
补码-1=反码:__1__1111111111111111111111111110111
反码取反=原码:__1__0000000000000000000000000001000
转为十进制为8,因为符号位为1,所以结果为负,所以结果是-8。
4.3.1、记着结论:
- a ^ a = 0
- 0 ^ a = a
- a ^ a ^ b = a ^ b ^ a(说明按位异或支持交换律)。
4.3.2、不创建临时变量,交换两个整数的值
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
printf("交换前a=%d b=%d\n", a, b);
a = a ^ b; //a=3^5
b = a ^ b; //b=3^5^5 ----> b=3
a = a ^ b; //a=3^5^3 ----> a=5
printf("交换前a=%d b=%d\n", a, b);
return 0;
}
输出:
5、赋值操作符
int weight = 100; //初始化
weight = 200; //赋值
a = x = y+1;
6、单目操作符
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置--、后置--
++ 前置++、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
在C语言中0代表假,非0代表为真
6.1、!的介绍
#include <stdio.h>
int main()
{
int flag = 0; //flag初始化为0,所以flag此时为假
if (!flag) //那既然flag为假,那么!flag就为真
printf("现在为真");
return 0;
}
6.2、sizeof的使用介绍
#include <stdio.h>
int main()
{
int a = 10;
printf("%d\n", sizeof(a)); //4
printf("%d\n", sizeof(int)); //4
printf("%d\n", sizeof a); //因为size是个操作符,所以不带()也可以使用 输出:4
return 0;
}
当然sizeof也可以统计数组的类型大小
#include <stdio.h>
int main()
{
int arr[10] = {0};
printf("%d\n", sizeof(arr)); //40
printf("%d\n", sizeof(arr[0])); //4,
printf("%d\n", sizeof(arr) / sizeof(arr[0])); //计算数组中元素的个数的方法
return 0;
}
6.3、–、++的使用
这里只说明一个++即可,–的使用同理
1、先说后置++,eg:a++,后置++遵循一个原则:先使用,后++
#include <stdio.h>
int main()
{
int a = 10;
int b = a++; //先使用,后++,也就是先 int b = a,然后在a = a+1
printf("%d\n", b);
printf("%d\n", a);
return 0;
}
输出:
10
11
2、前置++,eg:++a。前置++遵循一个原则:先++,后使用
#include <stdio.h>
int main()
{
int a = 10;
int b = ++a; //先++,后使用,也就是先 a = a+1,然后在int b = a
printf("%d\n", b);
printf("%d\n", a);
return 0;
}
输出:
11
11
常规的操作就不说了,下面来看这个代码:
#include <stdio.h>
int main()
{
int a = 10;
printf("%d\n", a--); //a--是个表达式,先使用,后--,先使用那表达式结果就为10.
printf("%d\n", a); //后--,a=9
return 0;
}
输出:
还有这个代码也是一样的道理:
#include <stdio.h>
int main()
{
int a = 10;
test(a--); //这里传参的值也是10
return 0;
}
在C语言中,for循环中i++,++i效果都是一样的。但在C++中就不一样了。
6.4、强制类型转换
#include <stdio.h>
int main()
{
int a = (int)3.14;
printf("%d\n", a);
return 0;
}
输出:
3
6.5、~ 二进制按位取反
先来看段代码:
#include <stdio.h>
int main()
{
int a = 3;
printf("%d\n", ~a);
return 0;
}
输出:
~是按二进制取反,那肯定是对一个数的补码进行取反。
3是个整数,所以:原码=反码=补码:__0__0000000000000000000000000000011
那么~a就是对补码取反得到新补码:__1__1111111111111111111111111111100
注意:这里取反之后,符号位就变成1了,也就意味着是负数了,
然后在求~a的值,新补码-1=反码:__1__1111111111111111111111111111011,
反码取反=原码:__1__000000000000000000000000000100
转为十进制就是4,又因为符号位位1,所以最终结果为-4。
6.6、* 解引用操作符
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;
*p = 20; //*对p解引用,通过p里面存储的地址,找到它所指向的对象。
printf("%d\n", a);
return 0;
}
7、关系操作符
> >= < <= !=
7.1、关于字符串比较
#include <stdio.h>
int main()
{
if ("abc" == "abcdef") //这样比较不是字符串内容,而是字符串的首字符地址。
{
;
}
return 0;
}
//字符串比较内容需要使用库函数:strcmp()
8、逻辑操作符
&& 逻辑与(并且)
|| 逻辑或(或者)
8.1、这里有个重要的规则,如下:
如果:
- a && b,如果a为假,则b不在执行。
- a || b,如果a为真,则b也不在执行。
这个用于判断真假。a && b为真结果为1,a && b为假结果为0。如下:
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
int c = a && b; //c=1,1为真
printf("%d\n", c);
return 0;
}
输出:
8.2、一道360面试题
如果:表达式1 && 表达式2,如果表达式1 为假,那么表达式2无论为真还是为假,表达式2就不会在算了。
如下代码:
#include <stdio.h>
int main()
{
int i = 0, a = 0, 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=0,a++之后,先使用,后++,所以a++表达式的结果为0,然后a后++所以最终a的值为1,既然a++表达式结果为0,所以&&前面的为假,所以后面的++b就不再运行了,所以b的值不变,b=2。由于a++为0,那么在第二个&&之前都是假,因此后面的d++也不在执行,所以d的值也没有发生改变,d=4,所以最终结果为1 2 3 4。(c=3从头就每改变)。
8.2.2、题型改编(1),提问:如果修改为a=1?
#include <stdio.h>
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;
}
输出:
8.2.3、题型改编(2):吧&&变为||,结果为?
#include <stdio.h>
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 || b,a为真在,则b在不执行。a++这个表达式的结果为1,为真,然后后++,所以a=2,因为 a++这个表达式的结果为1所以第一个||后面的++b,不在执行,所以b=2。所以整个a++ || ++b为真,所以d++不在执行,所以d=4。最终结果为:2 2 3 4。
9、条件操作符
exp1?exp2:exp3
exp1 ? exp2 : exp3
真 算 不算 那么整个表达式的结果就是exp2表达式的结果
假 不算 算 那么整个表达式的结果就是exp3表达式的结果
代码示例:
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int r = a > b ? a : b; //比较最大值
printf("%d\n", r);
return 0;
}
输出:
20
10、逗号表达式
exp1,exp2,exp3,...expN
逗号表达式就是用逗号隔开的一串表达式。
逗号表达式的特点是:从左向右依次计算,整个表达式的结果是最后一个表达式的结果。
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 0;
int d =(c = a - 2, a = b + c, c - 3); //最后一个表达式c-3=5,所以最终输出结果5
printf("%d\n", d);
return 0;
}
输出:
5
11、下标引用、函数调用和结构体调用
11.1、下标引用
arr[9],[]是操作符,arr 和 9是操作数。也就是说[]它的两个操作数是arr 和 9。
其实在写数组引用时,我们即可以这样写:arr[7],又可以这样写7[arr]。所以这就更说明了[]是个操作符。
其实arr[7]就是 *(arr+7)的解引用:
其实arr[7] —> *(arr+7)—> *(7+arr)—>7[arr]。
arr是数组首元素地址
arr+7就是跳过7个元素,指向第8个元素。
*(arr+7)就是第八个元素。
11.2、函数调用操作符—>()
Add(a,b):
- ()是操作符
- 操作数:Add,a,b。
11.3、结构体调用—> . ->
代码如下:
#include <stdio.h>
struct Stu
{
char name [20];
int age;
char sex[5];
char id[15];
};
void print(struct Stu* ps)
{
//形式:结构体对象.结构体成员变量
printf("%s %d %s %d\n", (*ps).name, (*ps).age, (*ps).sex, (*ps->id));
printf("%s %d %s %d\n", ps->name, ps->age, ps->sex, ps->id);
//形式:结构体指针->结构体成员
//这两者所产生的效果一样。
}
int main()
{
struct Stu S =
{
"zhangsan",17,"nan","123456789"
};
print(&S);
return 0;
}
就类比前面说到的指针变量那一节,道理一样。
11.4、关于结构体的简单总结
当使用此结构体时:有两种方法,这两种方法是等效的
- 结构体对象.结构体成员变量,eg:(*ps).name
- 结构体指针->结构体成员,eg:ps->name
ps->name等价于(*ps).name。
12、表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。同样,有些表达式的操作数在求值的过程中可能需要转换为其它类型。
12.1、隐式类型转换
所谓的隐式类型转换,就是偷偷的类型转换,我们并不会感知到这个转换的动作,就已经转换完毕了。
注意:上面我们说了一句话:有些表达式的操作数在求值的过程中可能需要转换为其它类型。
怎么理解这句话呢?
这里引入一个概念:整型提升:
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个比特位:
11111111
因为char为有符号的char,(有符号的char可以理解为是char类型的整数变量具有正负)
所以整型提升的时候,高位补充符号位,即为1。
所以提升后的结果是:11111111111111111111111111111111
//正数的整型提升
char c2 = 1;
变量c1的二进制位(补码)中只有8个比特位:
00000001
因为char为有符号的char,(有符号的char可以理解为是char类型的整数变量具有正负)
所以整型提升的时候,高位补充符号位,即为0。
所以提升后的结果是:00000000000000000000000000000001
详解一下这个负数整型提升,char c1 = -1;首先c1是char类型的,应该先整型提升,整型int是4个字节,1字节=8bit,所以4个字节=32bit,所以整型提升后的原码是:1000000000000000000000001,之后取反得到反码:11111111111111111111111111111110,之后+1,得到补码:11111111111111111111111111111111。
然后截断:11111111。
举个例子,比如:
#include <stdio.h>
int main()
{
char a = 5;
char b = 126;
char c = a + b;
printf("%d\n", c);
return 0;
}
输出:
思考一下:为什么这里的结果是-125呢?其实这就和整型提升有关!!!
分析:
首先正常的5的int类型的十进制补码为:00000000000000000000000000000101,但由于5现在是char类型的,所以,我们应该截断char是一个字节=8bit,所以直接后8位:__0__0000101。注意:这的最左边的一位仍是符号位,0代表正数,1代表负数。这里5是正数,所以为:__0__0000101。
同理,正常的126的int类型的十进制补码为:00000000000000000000000001111110,然后截断为:__0__1111110。
然后此时程序来到:char c = a + b;
这里我们可以发现:a+b,都是char类型的数据,是达不到int类型的,所以需要整型提升。
那就按照上面说到的规则进行整型提升:char类型的需要提升到int类型的,且char是有符号char,需要补24个bit,这补的24个bit,是补0还是补1呢?这需要看符号位。比如:char类型5的补码为:__0__0000101,符号位是0,所以前面的24bit位全部补0,所以整型提升后就是:__0__0000000000000000000000000000101。
(如果是无符号char,就不用考虑了,直接补0即可。)
同理:char类型的126的补码为:__0__1111110,整型提升后就为:__0__0000000000000000000000001111110。
好了,至此为止,整型提升结束,
现在使用它们开始运算:
- __0__0000000000000000000000000000101 —> 5
- __0__0000000000000000000000001111110 —> 126
- __0__0000000000000000000000010000011 —> 相加的到后的结果
同理上面相加得到的结果是int类型的c的大小,但是现在c是char类型的,所以需要再次截断,直接后8bit位:得到:
- 10000011 —>得到char类型的c,
然后走到打印printf("%d\n", c);
这一步,由于是以%d
的形式打印,所以c还需要整型提升,由于c的符号位是1,所以c、整型提升后的结果为:__1__1111111111111111111111110000011。但是这是还不能出结果,因为这里存储的只是补码,由于符号位是1,为负数。所以我们需要 补码-1=反码:__1__1111111111111111111111110000010,然后在取反=原码:1000000000000000000000001111101,最后在转为十进制,值为:125,又因为符号位是1,所以是负数,最终结果就是:-125。
这就是一个非int类型的数据,运算过程。
12.2、算术转换
ok。上面我们我们介绍了,小于整型类型(int)的数据在进行运算时,需要进行整形提升。那我们也许会问了,那要是数据类型超过了整型呢?那如何工作呢?这个时候就需要提到算数转换了。
算数转换针对的是整型及整形以上的类型。
算术转换:如果某个操作符的各个操作数属于不同类型,那么除非其中一个操作数转换化另一个操作数的类型,否则操作就无法进行,下面的层次体系称为__寻常算数转换。__
什么意思呢?简单来说就是:如果是两个不同类型的数据进行运算,其中一个类型要转换为另一个类型,比如:long 和int的数据进行运算,需要int类型的转为long类型,才能运算。
而究竟是int转换未long,还是long转换为int呢?这里有个规则:由低转高。也就是说long和int在一起,需要int转换为long类型的。
long double
double
long int
unsigned int int //这里unsigned int的范围高,所以进行算数转换需要将int转为unsigned int。
12.3、操作符的属性
复杂表达式的求值有三个影响的因素。
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
两个相邻的操作符先执行那个?取决于它们的优先级,如果两者的优先级相同,取决于他们的结合性。
比如:
#include <stdio.h>
int main()
{
2 + 3 * 4 + 5;
return 0;
}
比如:2+3*4+5,肯定是*优先级高,所以先算乘法,然后算加法,但是有两个加法,这时候优先级一样就需要看符号的结合性。加法是从左向右的,所以先算2+…,再算+5…。
但是就算确定了优先级,结合性,依然在运算中会出现问题,我们要做的就是吧自己的代码书写简单,不要太复杂。
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。