目录
前言
一、操作符分类
二、算术操作符
三、移位操作符
1.左移操作符
2.右移操作符
?四、位操作符
1.&按位与
2. | 按位或?
3.^按位异或
?4.位操作符的练习题
五、赋值操作符
1.复合赋值符
六、单目操作符
1.单目操作符介绍
2.!逻辑反操作符
3.sizeof操作符
3.1 sizeof操作符的注意事项
?3.2 sizeof操作符和数组
4.~按位取反
5.自加自减举例
6.&取地址操作符和*解引用操作符
七、关系操作符
八、逻辑操作符
九、条件操作符
十、逗号表达式
十一、下标引用、函数调用和结构成员
1.[ ]下标引用操作符
2.( )函数调用操作符
3.访问一个结构体成员
十二、表达式求值
1.隐式类型转换
2.算术转换
3.操作符的属性
4.一些问题表达式
前言
本文章详细地和各位小伙伴展示了我对C语言操作符的理解,欢迎大家一起讨论!
一、操作符分类
- 算术操作符
- 移位操作符
- 位操作符
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符
- 逗号表达式
- 下标引用、函数调用和结构成员调用
二、算术操作符
+ ????????- ????????*? ? ? ? /? ? ? ? %
1.除了%操作符之外,其他的几个操作符可以作用于整数和浮点数。
2.对于/操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行就是浮点数除法。?
#include<stdio.h>
void main() {
//下面两个表达式都是以整数为操作数
int a = 10 / 3;
double b = 10 / 3;
//一个操作数为小数形式
double c = 10.0 / 3;
//两个操作数都为小数形式
double d = 10.0 / 3.0;
printf("a=%d,b=%f,c=%f,d=%f", a, b, c, d);
}
?注意:不管最终接收的数据是int型,还是float型,计算结果的值都是由操作数本身的数据类型来控制的。
3.%操作符的两个操作数必须为整数。返回的是整数之后的余数。
int main()
{
int a = 7 % 3;//余1
printf("%d\n", a);
return 0;
}
三、移位操作符
<<? 左移操作符
>>? 右移操作符
注意:移位操作符的操作数只能是整数(因为是转化成二进制进行移位操作)
1.左移操作符
#include<stdio.h>
void main() {
int a = 2;
int b = a << 1;
printf("a=%d,b=%d", a, b);
}
?想要了解此操作符的意义,那我们就必须将十进制2转化为对应的int的二进制数
①十进制数2对应的二进制数:00000000000000000000000000000010(32位)
②a<<1表示将此二进制数的所有位都向移动一位,左边丢弃,右边补0。也就得到
00000000000000000000000000000100(32位)也就是提升了此数的一个权位,来到了2^2部分,所以左移操作符相当于该数乘以2。
注意:虽然说a是进行了左移,但不意味着a的值有变化,只是将a进行左移后的值赋值给b,他本身没有发生变化。
③还有如下代码证明
2.右移操作符
移位规则:首先右移运算分为两种
①逻辑移位:左边用0填充,右边丢弃。
②算术移位:左边用原该值的符号位填充,右边丢弃。
例子1:以正整数为例
?首先由于这个4是正整数,所以无论是进行逻辑移位,还是算术移位,都是在往左边补0
①十进制正整数4的二进制数为:00000000000000000000000000000100(32位)
②右移之后就变成了:00000000000000000000000000000010(32位)
③从上面的例子不难总结右移操作符就是在原来的数除以2。
例子2:以负整数为例
?首先我们得知道负数是以补码的形式进行存储的,那么怎么将一个数从原码转化成补码呢?转换顺序为:原码->反码->补码,如下所示:
(1)原码:直接根据数值写出的二进制序列就是原码。
(2)反码:原码的符号位不变,其他位按位取反就是反码。
(3)补码:反码+1,就是补码。
注意:正整数的补码就是他的原码
我们知道了原码怎么转变为补码后,就来推推这个列子
①十进制数的-1的二进制原码是:10000000000000000000000000000001(第一位的数值表示正负号,0表示正,1表示负)。
②对应的反码:11111111111111111111111111111110。
③对应的补码:11111111111111111111111111111111。
④对11111111111111111111111111111111进行右移一位(算术右移)得到的的二进制数为:11111111111111111111111111111111,那么该值也是为-1的补码。
注意:内存里是以补码形式存储,但是结果是以原码形式展现的。
警告:?对于移位运算符,不要移动负数位,这个是标准为定义的,例如:
?四、位操作符
位操作符有:
&? 按位与
|? ?按位或
^? 按位异或
注意:他们的操作数都必须是整数
1.&按位与
按位与规则:只有两个对应位置的数都为1,结果才为1;如果有是0对1或者0对0,结果都为0,包括符号位也要变化。(有点像逻辑操作符的&&)
例子1:以正整数和正整数为例
①我们得知道?3,5所对应的补码,如上所示,
②然后按照对应的规则进行按位与&
?例子2:以负整数和正整数为例
#include<stdio.h>
void main() {
int a = -2;
int b = 3;
int c = a & b;
//-2原码:10000000000000000000000000000010
//-2反码:11111111111111111111111111111101
//-2补码 11111111111111111111111111111110
//3的补码 00000000000000000000000000000011
//-2 & 3 为:00000000000000000000000000000010(结果为2)
printf("c=%d", c);
}
①还是一样得知道-2的补码,3的补码。
②然后按照对应的规则进行按位与&。
例子3:以负整数和负整数为例
#include<stdio.h>
void main() {
int a = -1;
int b = -2;
int c = a & b;
//-1的补码:11111111111111111111111111111111
//-2的补码:11111111111111111111111111111110
//-1&-2:11111111111111111111111111111110(记为x)
//x的反码:11111111111111111111111111111101
//x的原码:10000000000000000000000000000010(为-2)
printf("c=%d", c);
}
①首先得知道-2,-1的补码
②?按照对应的规则进行按位与&,结果为-2
2. | 按位或?
按位或规则:只要对应的位置有一个1,此位置生成的数也是1,如0对1或者1对1,结果都为1;如果对应的是0对0,那么结果就是0,包括符号位也要变化。(有点像逻辑操作符的||)
例子1:以正整数和正整数为例
①知道5,3的补码
②按照按位或的规则进行运算,得出结果为7?
例子2:以负整数和正整数为例
#include<stdio.h>
void main() {
int a = -2;
int b = 3;
int c = a | b;
//-2原码:10000000000000000000000000000010
//-2反码:11111111111111111111111111111101
//-2补码 11111111111111111111111111111110
//3的补码 00000000000000000000000000000011
//-2 | 3 为:11111111111111111111111111111111(记为x)
//x的原码为:
//11111111111111111111111111111110(反码)
//10000000000000000000000000000001(原码)结果为-1
printf("c=%d", c);
}
①首先得知道-2的补码和3的补码
②然后按照按位或的规则进行运算,得出结果为-1
例子3:以负整数和负整数例子
#include<stdio.h>
void main() {
int a = -1;
int b = -2;
int c = a | b;
//-1的补码:11111111111111111111111111111111
//-2的补码:11111111111111111111111111111110
//-1|-2:11111111111111111111111111111111(记为x)
//x的反码:11111111111111111111111111111110
//x的原码:10000000000000000000000000000001(为-1)
printf("c=%d", c);
}
①的指导-1和-2的补码
②然后按照按位或的规则进行运算,得出结果为-1
3.^按位异或
按位异或规则:按(2进制)位异或,对应的二进制位进行异或,相同为0,相异为1,包括符号位也要变化。
例子1:以正整数和正整数为例
例子2:以正整数和负整数为例
?例子3:以负整数和负整数为例?
?4.位操作符的练习题
例1:不能创建临时变量(第三个变量),实现两个数的交换
#include<stdio.h>
void main() {
int a = 5;
int b = 3;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("a=%d,b=%d", a, b);
}
?其实我们还可以从另一个角度去理解:首先我们的指导0^任何数的值都为该数的值;一个数去^自己本身的值为0,例子如下:
?那么a ^ b ^ b可以理解为:b ^ b == 0,a ^ 0 = 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 = a - b;
printf("a = %d b = %d\n", a, b);
int c = 0;//空瓶
printf("a = %d b = %d\n", a, b);
c = a;
a = b;
b = c;
printf("a = %d b = %d\n", a, b);
return 0;
}
例2:编写代码实现求一个整数存储在内存中的二进制中1的个数
方法一:运用了位操作符和移位操作符
思路如下图:?
?代码如下:
void main() {
int a = 5;
int count = 0;
int i;
for (i = 0;i < 32;i++) {
if (a & (1 << i)) {
count++;
}
}
printf("%d", count);//结果为2
}
方法二:利用十进制转二进制的思路,用余数来记录1的个数
void main() {
int a = 5;
int count = 0;
int i;
while (a) {
if (a % 2 == 1) {
count++;
}
a /= 2;
}
printf("%d", count);//结果为2
}
方法三:采用相邻的两个数据进行按位与运算
#include<stdio.h>
void main() {
int a = 5;
int count = 0;
while (a) {
a = a & (a - 1);
count++;
}
printf("%d", count);//结果为2
}
五、赋值操作符
赋值操作符是一个很好用的操作符,他可以让你重新对一个变量进行赋值。(赋值操作符为“=”)
void main() {
int weight = 120;//体重
weight = 100;//使用赋值操作符重新赋值
double salary = 10000.0;//money
salary = 200000.0;//使用赋值操作符重新赋值
}
int a = 10;
int x = 0;
int y = 20;
//连续赋值,一般不会这么用,影响了可读性
a = x = y + 1;
1.复合赋值符
- +=:a += b,a与b的和赋值给a
- -=:a -= b,a与b的差赋值给a
- *=:a *= b,a与b的积赋值给a
- /=:a /= b,a与b的商赋值给a
- %=:a %= b,a与b的余数赋值给a
- >>=:a >>= b,a右移b个位后的值赋值给a
- <<=:a <<= b,a左移b个位后的值赋值给a
- &=:a &= b,a与b按位与后的值赋值给a
- |=:a |= b,a与b按位或后的值赋值给a
- ^=:a ^= b,a与b按位异或后的值赋值给a
这些操作符都可以达到复合的效果!
int a = 10;
a = a + 10;
a += 10;//复合赋值
//其他运算符一样的道理,这样写更加简洁
六、单目操作符
1.单目操作符介绍
- !? ? ? ? 逻辑反操作
- -? ? ? ? ? 负值
- +? ? ? ? ?正值
- &? ? ? ? ?取地址
- sizeof? 操作数的类型长度(以字节为单位)
- ~? ? ? ? ?对一个数的二进制按位取反
- --? ? ? ? ?前置、后置--
- ++? ? ? ?前置、后置++
- *? ? ? ? ? 间接访问操作符(解引用操作符)
- (类型)? 气质类型转换
2.!逻辑反操作符
int main()
{
int flag = 0;
printf("%d\n", !flag);
//flag为真,打印hehe
if (flag)
{
printf("hehe\n");
}
//flag为假,打印haha
if (!flag)
{
printf("haha\n");
}
return 0;
}
3.sizeof操作符
3.1 sizeof操作符的注意事项
sizeof可以用来求变量(类型)所占的空间大小
#include<stdio.h>
void main() {
short s = 5;
int a = 10;
printf("%d\n", sizeof(s = a + 2));
printf("%d\n", s);
/*
sizeof的执行是发生在编译时期,而a + 2的运算是发生在运行时期。
所以这里的sizeof是发生在a + 2之前的,所以不会有类型提升的说法,
同时不管你类型是否提升,最终都是放到short型里,所以都是short型。
sizeof在编译期就结束了,sizeof只想知道你的类型是什么,它不管你
是否有进行了运算,就直接结束了。
*/
int arr[10] = { 0 };
printf("%d\n", sizeof(arr));//单位是字节
printf("%d\n", sizeof(int[10]));//40 - int [10]是arr数组的类型
printf("%d\n", sizeof(a));//计算a所占空间的大小,单位是字节
printf("%d\n", sizeof(int));
printf("%d\n", sizeof a);//说明了sizeof是一个操作符而不是一个函数
}
编译结果如下:
?3.2 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;
}
问:(1)(2)(3)(4)分别是输出什么?答案如下:
位置(1):sizeof arr就是输出int arr[10]的数组长度,也就是40个字节。
位置(2):sizeof(ch)就是输出char ch[10]的数组长度,也就是10个字节。
位置(3):首先我们得知道,传到函数test1()里面的实参虽然是arr,但是它的本质其实是一个指针,也就是int* arr,所以test1()函数里面的sizeof(arr)就是输出int的大小,也就是4个字节。
位置(4):同理于位置(3),也就是输出指针char* ch的大小,也就是4个字节。
注意:无论是什么类型的,他的地址长度是看操作位数决定的32位就是4个字节,64位就是8个字节。
4.~按位取反
int main()
{
int a = -1;
//10000000000000000000000000000001 - 原码
//11111111111111111111111111111110 - 反码
//11111111111111111111111111111111 - 补码
//~ 按位取反
//11111111111111111111111111111111
//00000000000000000000000000000000
//
int b = ~a;
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
按位取反操作符是连同符号位也会进行取反。
5.自加自减举例
#include <stdio.h>
void main() {
int a = 10;
printf("%d\n", a--);//10
printf("%d\n", a);//9
}
注意:后置++或者后置--,都是等到整条语句结束的时候才进行自加或自减。
6.&取地址操作符和*解引用操作符
int main()
{
int a = 10;
printf("%p\n", &a);//& - 取地址操作符
int * pa = &a;//pa是用来存放地址的 - pa就是一个指针变量
*pa = 20;//* - 解引用操作符 - 间接访问操作符
printf("%d\n", a);//20
return 0;
}
七、关系操作符
- >
- >=
- <
- <=
- !=? ? ? ? 用于测试“不相等”
- ==? ? ? ?用于测试“相等”
这些关系运算符比较简单,没什么可细述,注意别在编程中把==和=混淆就行。
八、逻辑操作符
逻辑操作符有哪些:
- &&? ? ? ? 逻辑与
- ||? ? ? ? ? ?逻辑或
注意:区分逻辑与和按位与;区分逻辑或和按位或。
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++;
? ?//i = a++||++b||d++;
? ?printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
? ?return 0; }
//程序输出的结果是什么?
逻辑与和逻辑或都有“短路”的效果。
当逻辑与的左边第一个为假,那么右边就不会执行了,直接结束
当逻辑或的左边第一个为真,那么右边就不会执行了,直接结束
按照上面的思路:结果为
a = 1
b = 2
c = 3
d = 4
九、条件操作符
条件操作符也称为三目运算符
结构:exp1 ? exp2 : exp3
int main()
{
int a = 3;
int b = 0;
if (a > 5)
b = 1;
else
b = -1;
//三目操作符
b = (a > 5 ? 1 : -1);
return 0;
}
十、逗号表达式
结构:exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果
int main()
{
int a = 3;
int b = 5;
int c = 0;
//逗号表达式 - 要从做向右依次计算,但是整个表达式的结果是最后一个表达式的结果
int d = (c = 1, a = c + 3, b = a - 4, c += b);
//c=10 a=8 b=4
printf("%d\n", d);//结果为10,也就是c的值
return 0;
}
十一、下标引用、函数调用和结构成员
1.[ ]下标引用操作符
操作数:一个数组名+一个索引值
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
// 0 1 4
printf("%d\n", arr[4]);//[] - 就是下标引用操作符
//[] 的操作数是2个:arr , 4
return 0;
}
2.( )函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的实参。
//函数的定义
int Add(int x, int y)
{
return x + y;
}
void test()
{}
int main()
{
int a = 10;
int b = 20;
//函数调用
int ret = Add(a, b);//() - 函数调用操作符
test();
return 0;
}
3.访问一个结构体成员
.? ? ? ? ?结构体.成员名
->? ? ? ?结构体指针->成员名
struct Book
{
//结构体的成员(变量)
char name[20];
char id[20];
int price;
};
int main()
{
//int num = 10;
//结构体变量名.成员名
struct Book b = {"C语言", "C20210509", 55};
struct Book * pb = &b;
//结构体指针->成员名
printf("书名:%s\n", pb->name);
printf("书号:%s\n", pb->id);
printf("定价:%d\n", pb->price);
//利用*解引用知道结构体变量b,在用点操作符去访问结构体成员
printf("书名:%s\n", (*pb).name);
printf("书号:%s\n", (*pb).id);
printf("定价:%d\n", (*pb).price);
//直接用结构体变量b加点操作符去访问结构体成员
printf("书名:%s\n", b.name);
printf("书号:%s\n", b.id);
printf("定价:%d\n", b.price);
return 0;
}
十二、表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样有些表达式操作符在求值的过程中可能需要转换为其他类型。
1.隐式类型转换
c的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要自CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特位直接相加运算(虽然机器指令中可能有这种字节的相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行。
例子1:
int main()
{
char a = 3;
//00000000000000000000000000000011
//00000011 - a
char b = 127;
//00000000000000000000000001111111
//01111111 - b
char c = a + b;
//00000000000000000000000000000011
//00000000000000000000000001111111
//00000000000000000000000010000010
//10000010 - c,此数就是将原来的值截断
//%d是按int类型输出,所以这里又是类型提升
printf("%d\n", c); //-126
//11111111111111111111111110000010 - 补码
//11111111111111111111111110000001 - 反码
//10000000000000000000000001111110 - 原码
//-126
//发现a和b都是char类型的,都没有达到一个int的大小
//这里就会发生整形提升
return 0;
}
这里因为%d要按照int类型输出,所以就发生了隐式类型提升,符号位是什么就往左补符号位相对应的数。也就是整型提升是按照变量的数据类型的符号位来提升。
例子2:
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
例子3:
//实例1
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; }
例子3中的a,b要进行整型提升,但是c不需要整型提升
a,b整型提升之后,变成了负数,所以表达式a==0xb6,b==0x600的结果为假,但是c不发生整型提升,则表达式c==0x6000000的结果为正
结果输出c
例子4:
(一)
?(二)
首先我们得知道一个运算表达式他是具有两个属性,分别是值属性和类型属性。
值属性:是通过运算后得到的值,可以理解为是在运行时期发生。
类型属性:是通过推断后得到的,可以理解为在编译时期就发生了。
(一)c本身没进行运算,所以结果为1;第2、第3和第4条输出语句中都有运算表达式,所以发生了类型属性推断,也就是隐式类型转换。(在gcc环境中,第四条语句的结果为4,vs的编译环境不太对!所以为1)
(二)首先我们知道了sizeof的类型判断和运算表达式的类型推断都是发生在编译期,所以我们就可以分为两部分来看
①先看s = a + 3,不管它在运算时怎么类型转换,最终都是short型,那么此运算表达式的类型推断为short型。
②sizeof得知了s = a + 3的类型推断为short型,那么它的值也就是2。
2.算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系成为寻常算术转换。
- long double
- double
- float
- unsigned long int
- long int
- unsigned int
- int?
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。但是算术转换要合理,要不然会有一些潜在的问题。
3.操作符的属性
复杂表达式的求值有三个影响的因素
(1)操作符的优先级
(2)操作符的结合性
(3)是否控制求值顺序
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符的优先级如下:
操作符的优先级
操作符 | 描述 | 结合性 | 是否控制求值顺序 |
---|
() | 聚组 | N/A | 否 | () | 函数调用 | L-R | 否 | [ ] | 下标引用 | L-R | 否 | . | 访问结构体成员 | L-R | 否 | -> | 访问结构指针成员 | L-R | 否 | ++ | 后缀自增 | L-R | 否 | -- | 后缀自减 | L-R | 否 | ! | 逻辑反 | L-R | 否 | ~ | 按位取反 | R-L | 否 | + | 单目,表示正值 | R-L | 否 | - | 单目,表示负值 | R-L | 否 | ++ | 前缀自增 | R-L | 否 | -- | 前缀自减 | R-L | 否 | * | 间接访问(解引用) | R-L | 否 | & | 取地址 | R-L | 否 | sizeof | 取其长度,以字节表示 | R-L | 否 | (类型) | 类型转换 | R-L | 否 | * | 乘法 | L-R | 否 | / | 除法 | L-R | 否 | % | 整数取余 | L-R | 否 | + | 加法 | L-R | 否 | - | 减法 | L-R | 否 | << | 左移位 | L-R | 否 | >> | 右移位 | L-R | 否 | > | 大于 | L-R | 否 | >= | 大于等于 | L-R | 否 | < | 小于 | L-R | 否 | <= | 小于等于 | L-R | 否 | == | 等于 | L-R | 否 | != | 不等于 | L-R | 否 | & | 按位与 | L-R | 否 | ^ | 按位异或 | L-R | 否 | | | 按位或 | L-R | 否 | && | 逻辑与 | L-R | 是 | || | 逻辑或 | L-R | 是 | ?: | 条件操作符 | N/A | 否 | = | 赋值 | R-L | 否 | += | 加后赋值 | R-L | 否 | -= | 减后赋值 | R-L | 否 | *= | 乘后赋值 | R-L | 否 | /= | 除后赋值 | R-L | 否 | %= | 取余后赋值 | R-L | 否 | <<= | 左移后赋值 | R-L | 否 | >>= | 右移后赋值 | R-L | 否 | &= | 按位与后赋值 | R-L | 否 | ^= | 按位异或后赋值 | R-L | 否 | |= | 按位或后赋值 | R-L | 否 | , | 逗号 | L-R | 是 |
总结:
(1)单(目)>算(术)>关(系)>逻(辑)>赋(值)
(2)单目运算符和赋值运算符:自右至左结合
(3)其它的:自左到右
4.一些问题表达式
例子1:
//表达式的求值部分由操作符的优先级决定。
//表达式1
a*b + c*d + e*f
代码在计算的时候,由于*比+优先级高,只能保证*的计算比+早,但是优先级并不能决定第三个*比第一个+早执行
所以表达式在计算机顺序就可能是:
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在不同的编译器中测试结果:非法表达式程序的结果
值 | 编译器 |
---|
-128 | Tandy 6000 Xenix3.2 | -95 | Think C 5.02(Macintosh) | -86 | IBM PowerPC AIX 3.2.5 | -85 | Sun Sparc cc(K&C编译器) | -63 | gcc | 4 | Sun Sparc acc(K&C编译器) | 21 | Turbo C/C++ 4.5 | 42 | Microsoft C 5.1 |
例子4:
//代码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:
//代码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编译器,VS2019环境下都执行,看结果。
VS2013的环境下:
有的小伙伴就感觉奇怪了,为什么不是2+3+4=9呢?而是4+4+4=12呢?
(1)这就需要我们去了解一下汇编语言,首先得知道计算机有许多寄存器(读取效率:寄存器>内存>磁盘)(寄存器有:eax、ebx、ecx、edx、ebp、esp等等)。
(2)用vs2019进行代码调试,在ret变量那里打开反汇编,就可以看到此例子第2张图片的界面,我们可以发现i的值是寄存到ecx寄存器中的,由寄存器去帮我们进行运算,最终得到一个值,也就是说他是执行了3次++i后才进行的算术运算,所以结果就为12。
(3)可以理解为变量i是从寄存器ecx中取值的步骤,而ecx只有一个且是统一执行到结束,里面存放的结果就是4,所以所有的i都为4。
?在调试的状态下,对变量ret右键,就可以看到有转到反汇编的指令,点击就行了。
呼!12340个字,终于写完了,重新对操作符进行学习,加深了我对C语言操作符的认识,感觉还挺有趣的!
点个赞呗!
|