算术操作符
+ - * / %
移位操作符>> <<
位操作符| & ^
赋值操作符= += -= *= /=...
单目操作符! sizeof ~ - + & *
关系操作符> < >= <= == !=
逻辑操作符&& ||
条件操作符? :
逗号表达式,
下标引用,函数调用和结构成员[] () . ->
+ - * / %
%
操作符之外,其他的几个操作符都可以作用于整数和浮点数.如果%
操作数出现-
,计算时按照没有-
计算,如果被取模操作数即第一个操作数有-
,在结果加入-
.#include <stdio.h>int main(void)
{printf("%dn", 5 % (-3));printf("%dn", (-5) % 2);printf("%dn", (-5) % (-2));return 0;
}
结果为:
/
操作符,如果两个操作数都为整数,执行整数除法.只要有浮点数,执行的就是浮点数除法.同时除数不能为0
.#include <stdio.h>int main(void)
{int a = 7 / 2;printf("%dn", a); //3double b = 7 / 2;printf("%.2lfn", b); //3.00double c = 7 / 2.0;printf("%.2lfn", c); //3.50return 0;
}
结果为:
<< 左移操作符
>> 右移操作符
注: 移位操作符的操作数只能是整数
整数有三种二进制的表示形式:
C语言中int类型占4个字节,32位,这里讨论int类型下的整数,只是位数不同,其余一致.
[15]原 :00000000000000000000000000000001
[-15]原:10000000000000000000000000000001
[15]反 :00000000000000000000000000000001
[-15]反:11111111111111111111111111111110
[15]反 :00000000000000000000000000000001
[-15]反:11111111111111111111111111111111
移位规则:
左边抛弃,右边补0
#include <stdio.h>int main(void)
{int num = 10;//0000 0000 0000 0000 0000 0000 0000 1010num = num << 2;//0000 0000 0000 0000 0000 0000 0010 1000 == 40printf("%dn", num);return 0;
}
结果是:
左移一位可以认为将该数乘以2
移位规则:
首先右移运算分为两种:
- 逻辑移位:左边用0补充,右边丢弃
- 算数移位:左边用符号位补充,右边丢弃
#include <stdio.h>int main(void)
{int num = -15;//1111 1111 1111 1111 1111 1111 1111 0001 --补码 num = num >> 2;//1111 1111 1111 1111 1111 1111 1111 1100 --补码 真值为-4printf("%dn", num);return 0;
}
结果是:
C语言没有明确规定移位规则.可以看出,在visual studio
编译器中,右移操作符实际上是算数右移.
右移一位可以认为将该数乘以2
#include <stdio.h>int main(void)
{int a = 5;int b = a >> -2; //标准未定义行为return 0;
}
& //按位与
| //按位或
^ //按位异或
注:它们的操作数必须是整数
将两个操作数进行按位运算
#include <stdio.h>int main(void)
{int a = 3;//0000 0000 0000 0000 0000 0000 0000 0011 --补码int b = -5;//1111 1111 1111 1111 1111 1111 1111 1011 --补码int c = a & b; //有0出0,全1出1//0000 0000 0000 0000 0000 0000 0000 0011 --补码 真值为3int d = a | b; //有1出1,全0出0//1111 1111 1111 1111 1111 1111 1111 1011 --补码 真值为-5int e = a ^ b; //相同出0,相异出1//1111 1111 1111 1111 1111 1111 1111 1000 --补码 真值为-8printf("%dn", c); //3printf("%dn", d); //-5printf("%dn", e); //-8return 0;
}
结果是:
#include <stdio.h>int main(void)
{int a = 5;int b = 3;a = a + b; //8b = a - b; //8 - 3 = 5a = a - b; //8 - (8 - 3) = 3;printf("%dn", a);printf("%dn", b);return 0;
}
但这个解法,如果a和b足够大,可能产生溢出
用位操作符也可以求解出来:
a ^ a = 0;
a ^ 0 = a;
a ^ a ^ b = b;
#include <stdio.h>int main(void)
{int a = 5;//0000 0000 0000 0000 0000 0000 0000 0101int b = 3;//0000 0000 0000 0000 0000 0000 0000 0011a = a ^ b;//0000 0000 0000 0000 0000 0000 0000 0110b = a ^ b; // b = a ^ (b ^ b) -> b = a//0000 0000 0000 0000 0000 0000 0000 0101a = a ^ b; // a = a ^ b ^ a ^ b ^ b -> a = b//0000 0000 0000 0000 0000 0000 0000 0101printf("%dn", a);printf("%dn", b);return 0;
}
这样也是可以将两个数进行交换的,且不会产生溢出,用到了异或的交换律.
第一种:按照我们由十进制转换为二进制的方法做:除二有1放1 有0放0 最后翻转即该数的二进制数,这边我们只需要得到二进制中有1的个数,就不需要翻转了
#include <stdio.h>int main(void)
{int num = 15;int count = 0;while (num){if (num % 2 == 1){count++;}num /= 2;}printf("%dn", count); //4return 0;
}
第二种:如果该位是1的话,那么它&1应该为1,将每一位&1得到结果
#include <stdio.h>int main(void)
{int num = 15;int i = 0;int count = 0;for (i = 0; i < 32; i++){//1.如果该位是1,则结果不为0//2.如果该位是0,则结果为0if (num & (1 << i)){count++;}}printf("%dn", count); //4return 0;
}
第三种:每次n&
n-1,直到n为0
#include <stdio.h>int main(void)
{int num = 15;int i = 0;int count = 0;while (num){num = num & (num - 1);count++;}printf("%dn", count); //4return 0;
}
减一的时候,会向位置最低的1借位,两数与后,该位变为0.直至所有的位都为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;//连续赋值
//这样的代码感觉怎么样?
//那同样的语义,你看看:
x = y+1;
a = x;
//这样的写法是不是更加清晰爽朗而且易于调试。
复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
这些运算符都可以写成复合的效果
比如:
int x = 10;
x = x + 10;
x += 10; 复合赋值
//其他运算符也是一样的道理,这样写更加简洁
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置,后置--
++ 前置,后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
!
//! 逻辑反操作
#include <stdio.h>int main(void)
{int flag = 0;if (!flag){printf("hehen"); //判断为真,打印hehe}return 0;
}
非真取反为真,真取反为非真
-
+
//- 负值
//+ 正值
#include <stdio.h>int main(void)
{int a = 5;int b = 6;a = -a;b = +b;printf("%dn", a); //-5printf("%dn", b); //6return 0;
}
同时,在printf()
函数中,+
表示强制在结果前面加上正号或负号(+ 或 -) ,即使对于正数也是如此。默认情况下,只有负数前面有一个符号.
-
表示给定字段宽度内的左对齐; 默认为右对齐(参见宽度子说明符).
&
*
//& 取地址
//* 间接访问操作符(解引用操作符)
#include <stdio.h>int main(void)
{int a = 10;int* p = &a; //取a的地址赋值给指针变量p*p = 20; //解引用指针变量p修改p指向空间的值printf("%dn", a); //20return 0;
}
sizeof
//sizeof 操作数的类型长度(以字节为单位)
#include <stdio.h>int main(void)
{int a = 0;int arr[10] = { 0, };printf("%dn", sizeof(a)); //4printf("%dn", sizeof(int)); //4printf("%dn", sizeof a); //4printf("%dn", sizeof(arr)); //40printf("%dn", sizeof(int [10])); //40return 0;
}
如果操作数是变量名可以不加括号,同时也说明了sizeof
不是函数,是操作符.
~
//~ 对一个数的二进制按位取反
#include<stdio.h>int main(void)
{int num = -15;//1111 1111 1111 1111 1111 1111 1111 0001 --补码 num = ~num;//0000 0000 0000 0000 0000 0000 0000 1110 --补码 真值-14printf("%dn", num);return 0;
}
结果是:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JlDayItV-1684333358705)(image/操作符详解/1684303584911.png)]
还有一个情况使用~
while(~scanf("%d", &a))
用于判断是否输入结束.
sizeof
和数组#include<stdio.h>void test1(int arr[])
{printf("%dn", sizeof(arr)); //(2) 8
}
void test2(char ch[])
{printf("%dn", sizeof(ch)); //(4) 8}
int main()
{int arr[10] = { 0, };char ch[10] = { 0, };printf("%dn", sizeof(arr)); //(1) 40printf("%dn", sizeof(ch)); //(3) 10test1(arr);test2(ch);return 0;
}
在主函数中,(1)和(3)的数组名代表整个数组,使用sizeof
得到的是整个数组的长度.而通过函数传递的数组名,代表的其实是一个指向数组首元素的指针,在函数内使用sizeof
得到的是指针变量的大小,在64位机中,指针变量的大小为8
;32位则为4
.
//-- 前置, 后置--
//++ 前置, 后置++
#include <stdio.h>//前置++ --
int main(void)
{int a = 10;int x = ++a;//先对a进行自增,然后将自增后的a赋值给x int y = --a;//先对a进行自减,然后将自减后的a赋值给yreturn 0;
}//后置++ --
#include <stdio.h>int main(void)
{int a = 10;int x = a++;//先将a的值赋值给x,再对a进行自增 a = 11int y = a--;//先将a的值赋值给y,再对a进行自减 a = 10return 0;
}
>
<
>=
<=
== 用于测试相等
!= 用于测试不相等
这些关系操作符比较简单,需要注意的就是使用==
注意不要误写成=
,只能用在合适的类型上面.
&& 逻辑与
|| 逻辑或
与需要两个条件同时为真才为真
或则要一个条件为真就为真
和& |
的区别是有短路:即&&
前一个条件为假直接判断假,||
前一个条件为真,直接判断真.
1&2------->0
1&&2------>11|2------->3
1||2------>1
#include<stdio.h>
int main()
{int i = 0, a = 0, b = 2, c = 3, d = 4;i = a++ && ++b && d++;//当a++时,先使用a=0,为假,则后面不予考虑printf("a=%dnb=%dnc=%dnd=%dn", a, b, c, d); // 1 2 3 4return 0;
}
#include<stdio.h>
int main()
{int i = 0, a = 1, b = 2, c = 3, d = 4;i = a++ && ++b && d++;//先判断a(1),为真,判断下一个条件同时 a自增至2//先b自增至3,判断b(3),为真,判断下一个条件//先判断d(4),为真,再d自增至5, i = 1printf("a=%dnb=%dnc=%dnd=%dn", a, b, c, d); // 2 3 3 5return 0;
}
#include<stdio.h>
int main()
{int i = 0, a = 1, b = 2, c = 3, d = 4;i = a++ || ++b || d++;//先判断a(1),为真,随后条件不进行判断 a自增至2printf("a=%dnb=%dnc=%dnd=%dn", a, b, c, d); // 2 2 3 4return 0;
}
#include<stdio.h>
int main()
{int i = 0, a = 0, b = 2, c = 3, d = 4;i = a++ || ++b || d++;//先判断a(0),为假,进行下一判断同时 a自增至1//先b自增至3,判断b(3),为真, 逻辑判断结束printf("a=%dnb=%dnc=%dnd=%dn", a, b, c, d); // 1 3 3 4return 0;
}
exp1 ? exp2 : exp3
这是一个三目操作符,需要三个操作数
如果表达式1为真,则执行表达式2,表达式3不执行
如果表达式1为假,则执行表达式3,表达式2不执行
由此可见,三目运算符和
int main(void)
{int a = 0;int b = 0;//使用 if ... else 判断if (a > 5)b = 3;elseb = -3;printf("%dn", b);//使用三目运算符也能达到同样的效果b = a > 5 ? 3 : -3;printf("%dn", b);return 0;
}
结果b都为-3:
exp1, exp2, exp3, ...expN
逗号表达式,就是用逗号隔开的多个表达式.
逗号表达式,从左向右依次执行.整个表达式的结果是最后一个表达式的结果.
#include <stdio.h>
int main(void)
{int a = 1;int b = 2;int c = (a > b, a = b + 10, a, b = a + 1); //逗号表达式printf("%dn", c);return 0;
}
第一个逗号, a > b为假 没有任何效果.
第二个逗号, a变为12.
第三个逗号, a(12)为真 没有任何效果.
第四个逗号, b = 13 同时因为是最后一个表达式,将b的值赋值给c
最终c为13
代码2:
if (a = b + 1, c = a / 2, d > 0)
这里先将a和c进行赋值,其实最后if
是否执行是看d>0
这个表达式的真或假.
代码3:
a = get_val();
count_val(a);
while(a > 0)
{//业务处理a = get_val();count_val(a);
}//如果使用逗号表达式,改写:
while(a = get_val(), count_val(a), a > 0)
{//业务处理
}
适当的使用逗号表达式可以使代码变得更为简洁.
[] 下标引用操作符
int arr[10];//创建数组
arr[9] = 10;//使用下标引用操作符
[] 的两个操作数是arr和9
() 函数调用操作符
#include <stdio.h>int Add(int a, int b)
{return a + b;
}int main(void)
{int a = 1;int b = 1;Add(a, b); //使用() 作为函数调用操作符printf("%d", a);//使用() 作为函数调用操作符return 0;
}
函数操作符()
至少有一个操作数,就是函数名.因为有可能函数是无参函数,但是函数名却绝对不可以缺少.
. 结构体.成员名
-> 结构体指针->成员名
结构体是C语言中特别重要的知识点,结构体使得C语言有能力描述复杂类型,将多个类型的变量组合抽象成一个结构体类型.
比如描述学生,学生包含:名字+年龄+性别+学号这几项信息.
就只能用结构体类型来描述了.
#include <stdio.h>struct Stu
{char name[20]; //姓名 一般一个汉字是两个字符即两个字节int age; //年龄char sex[5]; //性别char id[5]; //学号
};void print1(struct Stu* ps)
{//结构体指针通过->访问成员变量printf("name = %s age = %d sex = %s id = %dn", ps->name, ps->age, ps->sex, ps->id);return;
}
int main()
{//结构体初始化struct Stu s = {"张三", 20, "男", "1001"};//.为结构体变量访问成员符号printf("name = %s age = %d sex = %s id = %dn", s.name, s.age, s.sex, s.id);//定义结构体指针变量struct Stu *ps = &s;//调用打印函数print1(ps);return 0;
}
表达式求值的顺序一部分是由操作符的优先级和结合性决定.
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型.
转换类型有两种:
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个比特位:
1111 1111
因为char
为有符号的char
所以整型提升的时候,高位补充符号位,即为1
提升后的结果为:
1111 1111 1111 1111 1111 1111 1111 1111
正数的整型提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
0000 0001
因为char
为有符号的char
所以整型提升的时候,高位补充符号位,即为0
提升后的结果为:
0000 0000 0000 0000 0000 0000 0000 0001
例如1:
#include <stdio.h>int main(void)
{char c1 = 5;//0000 0101 - c1//整型提升为 0000 0000 0000 0000 0000 0000 0000 0101 char c2 = 127;//0111 1111 - c2//整型提升为 0000 0000 0000 0000 0000 0000 0111 1111char c3 = c1 + c2;//0000 0000 0000 0000 0000 0000 1000 0100 //截断为 1000 0100 //整型提升为 1111 1111 1111 1111 1000 0100//取反 1111 1111 1111 1111 1111 1011//加1 1111 1111 1111 1111 1111 1100 //结果为-124//%d - 10进制打印有符号整数printf("%dn", c3); // -124
}
0000 0101
,整型提升后为0000 0000 0000 0000 0000 0000 0000 0101
0111 1111
,整型提升后为0000 0000 0000 0000 0000 0000 0111 1111
0000 0000 0000 0000 0000 0000 1000 0100
,由于要存放到8bit位的char
类型的c3中,截断为1000 0100
%d
表示要打印10进制数有符号整数.要对char
类型计算,先将该数进行整型提升,提升为1111 1111 1111 1111 1000 0100
,该二进制补码的真值就为-124
#include <stdio.h>int main(void)
{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;
}
最终结果:
只有c被判断一致.
这边a的二进制数为1011 0110
因为不满四个字节,整型提升后为1111 1111 1111 1111 1011 0110
显然与原来的数并不一致
同样b也是因为整型提升后,导致判断结果不一致.
c因为本来就占四个字节,不会发生整型提升,最终只有c被判断一致.
#include <stdio.h>int main(void)
{char c = 1;printf("%un", sizeof(c));printf("%un", sizeof(-c));printf("%un", sizeof(+c));return 0;
}
最终结果:
这里的-
和+
都是单目运算符,表示对c取正和取负,只要参加运算,就需要将未满4个字节的数进行整型提升,所以sizeof(+c) 和 sizeof(-c)
结果最终都为4.
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行.下面的层次体系称为寻常算数转换.
由低到高向上转换
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算.
但是算数转换要合理,要不然会有一些潜在的问题.
float f = 3.14;
int num = f;//隐式转换,会有精度丢失;
复杂表达式的求值有三个影响的因素.
两个相邻的操作符先执行哪个?取决于他们的优先级.如果两者的优先级相同,取决于他们的结合性.
最后有一些问题表达式:
//表达式的求值部分由操作符的优先级决定
//代码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
c + --c
同上,操作符的优先级只能保证
+
在--
的后面,但是我们并没有办法得知,+
操作符的左操作数的获取是在右操作数之前即--
计算前获取还是之后获取,所以结果是不可预测的,是由歧义的.
//代码3-非法表达式
int main()
{int i = 10;i = i-- - --i * (i = -3) * i++ + ++i;printf("%dn", i);return 0;
}
表达式3在不同编译器中测试结果:非法表达式程序的结果
值 | 编译器 |
---|---|
—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 | Dec VAX/VMS |
42 | Microsoft C 5.1 |
//代码4
int fun()
{static int count = 1;return ++count;
}int main()
{int answer;answer = fun() - fun() * fun();printf(" %dn", answer); //输出多少?return 0;
}
这段代码有实际的问题.
虽然在大多数的编译器求得结果都是相同的.
但是上述代码answer = fun() - fun() * fun();
中我们只能通过操作符的优先级得知:先算乘法,再算减法.
函数的调用先后顺序无法通过操作符的优先级确定.
//代码5
#include <stdio.h>
int main(void)
{int i = 1;int ret = (++i) + (++i) + (++i);printf("%dn", ret);printf("%dn", i);return 0;
}
Linux环境的结果
visual studio环境的结果
简单看一下vs中的汇编代码
可以看到,在vs编译器中是先将i三次自增后,再进行对ret的计算的.
但在linux是不一样的.
因为依靠运算符的优先级是无法决定第一个++
和第三个++
的先后顺序的.
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的.
本章完
本文发布于:2024-01-28 10:54:21,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/17064104666910.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |