8. C语言之操作符详解
文章目录
- 一、算术操作符
- 二、移位操作符
- 1、 原码、反码、补码
- 2、左移操作符
- 3、右移操作符
- 三、位操作符
- 1、按位与【&】
- 2、按位或【|】
- 3、按位异或【^】
- 4、按位取反【~】
- 5、两道面试题
- 6、进制定位
- 将变量a的第n位置为1
- 将变量a的第n位置为0
- 四、赋值操作符
- 1、复合赋值符
- 五、单目操作符
- 1、单目操作符介绍
- 2、【!】逻辑反操作
- 3、【&】和【*】
- 4、【-】和【+】
- 5、sizeof
- 6、【++】和【- -】
- 7、强制类型转换
- 六、关系操作符
- 七、逻辑操作符
- 一道笔试题~~
- 八、条件操作符
- 九、逗号表达式
- 十、下标引用、函数调用和结构成员
- 1、下标引用操作符 [ ]
- 2、函数调用操作符 ( )
- 3、结构成员调用操作符 . 和 ->
- 十一、表达式求值
- 1、隐式类型转换【整型提升】
- 整型提升的意义
- 2、算术转换
- 3、操作符的属性【附优先级列表】
- 4、问题表达式
- 问题表达式1
- 问题表达式2
- 问题表达式3
- 问题表达式4
- ⑤ 问题表达式5
一、算术操作符
-
除了 【%】 操作符之外,其他的几个操作符可以作用于整数和浮点数。
-
对于【/】 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
-
【%】 操作符的两个操作数必须为整数。返回的是整除之后的余数。
-
整数除法:
- 浮点数除法:
- 取余操作符
二、移位操作符
1、 原码、反码、补码
- 其实我们经常能听到2进制、8进制、10进制、16进制这样的讲法,那是什么意思呢?其实2进制、8进制、10进制、16进制是数值的不同表示形式而已。
- 比如:数值15的各种进制的表示形式:
15的2进制:1111 15的8进制:17 15的10进制:15 15的16进制:F
-
对于一个整数来说,在内存中的二进制表示形式有 【原码】【反码】【补码】 三种,写成二进制位的形式也就是32位二进制,例如整数4,它的原码即为0 0000000000000000000000000000100。最高位对于32个二进制位来说,叫做符号位
- 符号位是0,表示正整数
- 符号位是1,表示负整数
-
而对于一个正数来说,它的原码、反码、补码都是相同的
-
对于一个负数来说:
- 原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。
- 反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
- 补码:反码+1就得到补码。
- 对于整形来说:数据存放内存中其实存放的是补码。
- 为什么呢?
在计算机系统中,数值⼀律⽤补码来表⽰和存储。原因在于,使⽤补码,可以将符号位和数值域统⼀处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
- 在计算机中都是使用二进制数的补码进行运算的,但是在计算完之后输出的结果都要再转化为原码的形式
2、左移操作符
【移位规则】:左边抛弃、右边补0
- 下面计算一个数字4左移1位后的,将其运算后的结果放到b里面去。
- 在内存中进行计算都是使用补码的形式,因为依次写出4的原、反、补码,因为它是正数,所以均是相同的
int main() { int a = 4; //0 0000000000000000000000000000100 - 4的原码 //0 0000000000000000000000000000100 - 4的反码 //0 0000000000000000000000000000100 - 4的补码 int b = a int a = -4; //1 0000000000000000000000000000100 - -4的原码 //1 1111111111111111111111111111011 - -4的反码 //1 1111111111111111111111111111100 - -4的补码 int b = a -1;//error
三、位操作符
1、按位与【&】
- 【规则】:全1为1,有0为0
//按位与 - 全1为1,有0为0 int main() { int a = 3; int b = -5; int c = a & b; printf("c = %d\n", c); //00000000000000000000000000000011 - 3的原码、反码、补码 //10000000000000000000000000000101 - -5的原码 //11111111111111111111111111111010 - -5的反码 //11111111111111111111111111111011 - -5的补码 //00000000000000000000000000000011 //11111111111111111111111111111011 //00000000000000000000000000000011 - 3【补码即为原码】 return 0; }
- 根据按位与的运算规则,我们就可以得出最后的结果为3
2、按位或【|】
- 【规则】:有1为1,全0为0
//按位或 - 有1为1,全0为0 int main() { int a = 3; int b = -5; int c = a | b; printf("c = %d\n", c); //00000000000000000000000000000011 - 3的原码、反码、补码 //10000000000000000000000000000101 - -5的原码 //11111111111111111111111111111010 - -5的反码 //11111111111111111111111111111011 - -5的补码 //00000000000000000000000000000011 //11111111111111111111111111111011 // -------------------------------------- //11111111111111111111111111111011 | //11111111111111111111111111111010 | //10000000000000000000000000000101 | - 5 return 0; }
- 根据按位或的运算规则,我们就可以得出最后的结果为-5
3、按位异或【^】
- 【规则】:相同为0,相异为1
//按位异或 - 相同为0,相异为1 int main() { int a = 3; int b = -5; int c = a ^ b; printf("c = %d\n", c); //00000000000000000000000000000011 - 3的原码、反码、补码 //10000000000000000000000000000101 - -5的原码 //11111111111111111111111111111010 - -5的反码 //11111111111111111111111111111011 - -5的补码 //00000000000000000000000000000011 //11111111111111111111111111111011 // -------------------------------------- //11111111111111111111111111111000 //11111111111111111111111111110111 //10000000000000000000000000001000 -> -8 return 0; }
- 根据按位异或的运算规则,我们就可以得出最后的结果为-8
注意:
- 两个相同的数异或为0【a ^ a = 0】
- 任何数和0异或均为那个数本身【a ^ 0 = a】
4、按位取反【~】
【规则】:1变0, 0变1
- 取反直接就可以将1变成0,0变成1
int main() { int a = 0; int b = ~a; printf("b = %d\n", b); //00000000000000000000000000000000 //11111111111111111111111111111111 按位取反【补码】 //11111111111111111111111111111110 【反码】 //10000000000000000000000000000001 -> -1【原码】 return 0; }
- 那么0按位取反之后就变成了-1
5、两道面试题
两数交换
-
我们之前学的是使用第三方临时变量做一个存放,进行交换
-
还有以一种方法就是通过加减的方式来交换一下这两个数
int main() { int a = 3; int b = 5; 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; }
- 首先将两个数相加放到a中,再将a-b的值放到b中,此时a中右a和b的值,b中有a的值,最后再将a-b就是b的值
- 上面这种方法有一点不太好,就是当两个很大的数的时候,会出现数据溢出的情况,所以我们还需要另外一种方法:使用异或
- 首先a = a ^ b将a和b异或后的结果暂存到a里面去,然后再去异或b的话就相当于是a ^ b ^ b,根据规则便可以得出结果为a,将其放入b中
- 然后a = a ^ b,就相当于是a ^ b ^ a,那么结果就是b,将其放入a中
int main() { int a = 3; int b = 5; printf("a = %d, b = %d\n", a, b); a = a ^ b; b = a ^ b; //a ^ b ^ b = a ^ 0 = a a = a ^ b; //a ^ b ^ a = b ^ 0 = b printf("a = %d, b = %d\n", a, b); return 0; }
- 最后打印出来就是交换之后的结果
6、进制定位
将变量a的第n位置为1
思路分析:
- 首先就是要使用到我们上面学习过的按位或 | 运算,将第三位按位或上一个1,那么这一位就变成了1,但是又不想让其他位置发生变化,那此时就让其他位按位或上一个0即可,若是那个位上为0,那么就是0,若是那个位上为1,那也为1,那也就是00000000000000000000000000000100,但是要如何去获取到这个二进制数呢,此时就又需要使用到我们上面讲到过的一个操作符叫做左移
int a = 10;
int n = 0;
scanf("%d", &n);
a = a | 1
int a = 10;
int n = 0;
while (scanf("%d", &n)!=EOF)
{
a = a | 1
int flag = 0;
if (!flag)
{
printf("haha\n");
}
return 0;
}
short s = 10;
int a = 2;
printf("%d\n", sizeof(s = a + 2));
printf("%d\n", s);
return 0;
}
printf("%d\n", sizeof(arr));
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));
}
int main()
{
int arr[10] = {0};
char ch[10] = {0};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(ch));
test1(arr);
test2(ch);
return 0;
}
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);
printf("------\n");
printf("i = %d\n", i);
return 0;
}
int a = 5;
int b = 0;
if (5 == a)
b = 3;
else
b = -3;
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}
//业务处理
a = get_val();
count_val(a);
}
//业务处理
}
int arr[] = { 1,2,3,4,5 };
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i price);
- 可以看到对于这三种形式都是可以访问到这个结构体变量的成员
十一、表达式求值
1、隐式类型转换【整型提升】
- C的整型算术运算总是至少以缺省整型类型的精度来进行的
为了获得这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整型,这种转换称为[整型提升]
整型提升的意义
- 表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
- 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
- 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算
如何进行整型提升
- 整形提升是按照变量的数据类型的符号位来提升的
正数的整形提升
char c1 = 1;
- 正数的原反补码是相同的
- 再将一个整数1赋值给一个char类型的变量c1,此时就会发生截断,字符类型在内存中只占一个字节(8个比特)
- 接下来c1会进行整形提升,在会提升成00000000000000000000000000000001达到整型4个字节32个比特位在内存中的要求
负数的整型提升
char c2 = -1;
-
负数在内存中存放的是11111111111111111111111111111111,将其给到一个整型的变量之后就会发生截断即为11111111
-
那这个时候再进行一个整型提升就和正数不一样了,因为负数的符号位为1,而整型提升在高位是要补充符号位,所以会在前头加上24个1,那其实也就变回了和之前-1的补码一般的样子,为32个1
2、算术转换
- 如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行
- 如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。【排名从上面最高,下面最低】
- 例如int和unsigned int一起进行算术运算的时候这个前者就要转换为后者的类型
- 例如long int和long double一起进行算术运算的时候这个前者就要转换为后者的类型
- 那其实可以看出,在char和short面前称霸的int如今沦落为了小弟
【注意】:
但是算术转换要合理,要不然会有一些潜在的问题
int main() { float f = 3.14; int num = f;//隐式转换,会有精度丢失 printf("%d\n", num); return 0; }
3、操作符的属性【附优先级列表】
复杂表达式的求值有三个影响的因素
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
- 两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性
下面有一张关于操作符优先级的列表~~
4、问题表达式
表达式的求值部分由操作符的优先级决定,优先级只能决定先算谁,但是哪个表达式先调用要取决于编译器
- 对于有些表达式而言,其实在不同的编译器上所呈现的结果是不同的,我们将其称作为【问题表达式】
问题表达式1
a*b + c*d + e*f
- 来看上面这段代码,通过上面的优先级列表可以看出[*]的优先级一定是比[+]要来得高,因此可以保证[*]两端的数字先进行运算,但是却不能保证第三个*比第一个+早执行
问题表达式2
- 继续来看下一个问题表达式
//表达式2 c + --c; 12
- 虽然对于这个[--]操作符来说比[+]操作符的优先级来得高,但是呢我们却不知道在编译器运算的时候这个【c】是什么时候准备好
问题表达式3
- 对于下面这段代码,也是存在很大的争议,特别是对于++和--混搭的这种表达式尤其严重,你可以去不同的编译器上运行看看,结果都是不一样的~~
//代码3-非法表达式 int main() { int i = 10; i = i-- - --i * ( i = -3 ) * i++ + ++i; printf("i = %d\n", i); return 0; }
- 下面是在不同的编译器上运行出来的结果,可见这个表达式问题有多大!!!
值 编译器 - 128 Tandy 6000 Xenix 3.2 - 95 Think C 5.02(Macintosh) - 86 IBM PowerPC AIX 3.2.5 - 85 Sun Sparc cc(K&C编译器) - 63 gcc,HP_UX 9.0,Power C 2.0.0 4 Sun Sparc acc(K&C编译器) 21 Turbo C/C++ 4.5 22 FreeBSD 2.1 R 30 Dec Alpha OSF1 2.0 36 TDec VAX/VMS 42 Microsoft C 5.1 问题表达式4
- 看到main函数中的函数调用表达式answer = fun() - fun() * fun();其实也是存在一个歧义的,因为你完全不知道编译器先调用的是哪个fun()
//代码4 int fun() { static int count = 1; return ++count; } int main() { int answer; answer = fun() - fun() * fun(); printf( "%d\n", answer);//输出多少? return 0; }
-
可以看到,若是前面的fun()先执行的话,最后的结果就是-10,若是后面的fun()先执行的话,最后的结果就是-2
-
正常来说大家应该都认为是第二个表达式符合我们的运算规则,因为先乘除后加减,可是呢我们最常用的VS出来的结果都不是我们想要的
我们可以到不同编译器上面去观察一下
-
- 看到main函数中的函数调用表达式answer = fun() - fun() * fun();其实也是存在一个歧义的,因为你完全不知道编译器先调用的是哪个fun()
- 下面是在不同的编译器上运行出来的结果,可见这个表达式问题有多大!!!
- 对于下面这段代码,也是存在很大的争议,特别是对于++和--混搭的这种表达式尤其严重,你可以去不同的编译器上运行看看,结果都是不一样的~~
- 虽然对于这个[--]操作符来说比[+]操作符的优先级来得高,但是呢我们却不知道在编译器运算的时候这个【c】是什么时候准备好
- 继续来看下一个问题表达式
- 来看上面这段代码,通过上面的优先级列表可以看出[*]的优先级一定是比[+]要来得高,因此可以保证[*]两端的数字先进行运算,但是却不能保证第三个*比第一个+早执行
- 对于有些表达式而言,其实在不同的编译器上所呈现的结果是不同的,我们将其称作为【问题表达式】
- 如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行
-
- 整形提升是按照变量的数据类型的符号位来提升的
- C的整型算术运算总是至少以缺省整型类型的精度来进行的
- 可以看到对于这三种形式都是可以访问到这个结构体变量的成员
- 首先就是要使用到我们上面学习过的按位或 | 运算,将第三位按位或上一个1,那么这一位就变成了1,但是又不想让其他位置发生变化,那此时就让其他位按位或上一个0即可,若是那个位上为0,那么就是0,若是那个位上为1,那也为1,那也就是00000000000000000000000000000100,但是要如何去获取到这个二进制数呢,此时就又需要使用到我们上面讲到过的一个操作符叫做左移
int a = 10;
int n = 0;
scanf("%d", &n);
a = a | 1
int a = 10;
int n = 0;
while (scanf("%d", &n)!=EOF)
{
a = a | 1
int flag = 0;
if (!flag)
{
printf("haha\n");
}
return 0;
}
short s = 10;
int a = 2;
printf("%d\n", sizeof(s = a + 2));
printf("%d\n", s);
return 0;
}
printf("%d\n", sizeof(arr));
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));
}
int main()
{
int arr[10] = {0};
char ch[10] = {0};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(ch));
test1(arr);
test2(ch);
return 0;
}
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);
printf("------\n");
printf("i = %d\n", i);
return 0;
}
int a = 5;
int b = 0;
if (5 == a)
b = 3;
else
b = -3;
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}
//业务处理
a = get_val();
count_val(a);
}
//业务处理
}
int arr[] = { 1,2,3,4,5 };
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i price);
- 最后打印出来就是交换之后的结果
- 上面这种方法有一点不太好,就是当两个很大的数的时候,会出现数据溢出的情况,所以我们还需要另外一种方法:使用异或
- 首先将两个数相加放到a中,再将a-b的值放到b中,此时a中右a和b的值,b中有a的值,最后再将a-b就是b的值
-
- 那么0按位取反之后就变成了-1
- 取反直接就可以将1变成0,0变成1
- 根据按位异或的运算规则,我们就可以得出最后的结果为-8
- 【规则】:相同为0,相异为1
- 根据按位或的运算规则,我们就可以得出最后的结果为-5
- 【规则】:有1为1,全0为0
- 根据按位与的运算规则,我们就可以得出最后的结果为3
- 【规则】:全1为1,有0为0
- 在计算机中都是使用二进制数的补码进行运算的,但是在计算完之后输出的结果都要再转化为原码的形式
-
- 取余操作符
- 浮点数除法:
-