1.算术操作符:+ - * / %
一.
’/‘ 两端都是整数的话,执行的都是整数除法
所以float a=3/2,结果也是1
必须写成float a=3/2.0,才是1.5
必须保证/两边要有一个是浮点数,才会计算小数部分
二.
’%‘取模,得到的是余数。
取模操作符无论是左边还是右边的操作数都必须是整形
当9%10,他的余数的范围就是0-9,因为余数是不可能大于被除数的。
整数的二进制表示方式有3种形式:原码,反码,补码
在有符号的整数下,最高位是符号位,0为正数,1为负数
正数的原码,反码,补码相同
负数的反码是:符号位不变,其他的按位取反
负数的补码是:反码+1
计算机在内存中,是以补码储存的,使用时会自己转成原码。
原码和补码直接如何快速转换?
法一:直接补码-1,然后符号位不变,其他按位取反
法二:补码再次取反,然后+1,得到原码
2.移位操作符
<< 左移操作符
>> 右移操作符
操作的是补码,且,移位操作符的操作数只能是整数。
一.左移操作符
左移操作符,直接整体左移,多出去的直接丢掉,右边剩下的补0
当数是负数时,需要转成补码进行操作,输出时要变回原码进行输出
观察可得左移操作符有一种*2的效果。
二.右移操作符
算数右移:右边丢弃,左边空出的补原符号位
逻辑右移:右边丢弃,左边一律补0
对于正数而言,左边补啥都无所谓,负数稍微有点区别。
左边一般来说都是补符号位,当然不排除补0的,主要取决于编译器。
对于移位运算符,不要移动负数位,这个是标准未定义的,例如:num>>-1; 就是错的
位操作符
| & | ^ | //按(二进制)位与:只要对应的二进制位,只要有0,那么与的结果就是0。两个同时为1的时候,才为1 //按(二进制)位或;只要对应的二进制位,只要有1,那么就是1。只有两个同时为0,那么才是0 //按(二进制)位异或:只要对应的二进制位,相同为0,相异为1 |
注:他们的操作数必须是整数。
注意:
&a 是取地址a,是单目操作符
a&b是 a按位与b,是双目操作符
1.&
int main()
{
int a = 3;
int b = -5;
int c = a & b;//只有对应的二进制位,只有有0,那么与的结果就是0。两个同时为1的时候,才为1,
// 00000000000000000000000000000011 3的补码
// 10000000000000000000000000000101 -5的原码
// 11111111111111111111111111111010 -5的反码
// 11111111111111111111111111111011 -5的补码
// 00000000000000000000000000000011 3的补码
// 11111111111111111111111111111011 -5的补码
// 00000000000000000000000000000011 -a&b的结果(补码)
//最高位是0,0为正数,所以源码和补码一致,结果就是3
return 0;
}
2.|
int main()
{
int a = 3;
int b = -5;
int c = a | b;//只要对应的二进制位,只要有1,那么就是1。只有两个同时为0,那么才是0
// 00000000000000000000000000000011 3的补码
// 10000000000000000000000000000101 -5的原码
// 11111111111111111111111111111010 -5的反码
// 11111111111111111111111111111011 -5的补码
// 00000000000000000000000000000011 3的补码
// 11111111111111111111111111111011 -5的补码
// 11111111111111111111111111111011 -a|b的结果(补码)
//所以结果是-5
return 0;
}
3.^ 异或操作符
int main()
{
int a = 3;
int b = -5;
int c = a ^ b;//只要对应的二进制位,相同为0,相异为1
// 00000000000000000000000000000011 3的补码
// 10000000000000000000000000000101 -5的原码
// 11111111111111111111111111111010 -5的反码
// 11111111111111111111111111111011 -5的补码
// 00000000000000000000000000000011 3的补码
// 11111111111111111111111111111011 -5的补码
// 11111111111111111111111111111000 -a^b的结果(补码)
// 11111111111111111111111111110111
// 10000000000000000000000000001000 原码 答案是-8
return 0;
}
a^a 是什么?对应的二进制数完全相同,所以a^a=0
0^a的结果是什么?其实就是它本身,所以0^a=a
比如说在我一堆成对出现的数字,让我找出只出现一个数字的题的情况下,我就可以用^操作符
例如1 1 3 3 2 2 5 4 4 6 6:我只要把这些数字全部^在一起, 只出现一次的数就找到了
例2:运用^实现两个数的交换
a=a^b相当于一把钥匙, a^b再^b,我就拿到了a。a^b^a,我就拿到了b。
但是这种算法,只能用于整数,小数就不行了。所以推荐使用第一种
2.求一个整数存储在内存中的二进制中1的个数
int main()
{
int input = 0;
scanf("%d", &input);
int i = 0;
int count = 0;
for (i = 0; i < 32; i++)
{
if (((input >> i) & 1)== 1)
{
count++;
}
}
printf("%d", count);
return 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;//连续赋值
他是先执行y+1,把值赋给x,再把x的值赋给a;他的可读性是非常低的,不建议这样写。最好能拆就拆了写
x=y+1; a=x;
复合赋值符
类似于 a += 2; →a=a+2
a=a>>1;→ a>>=1;
a=a&4;→ a&=4
…………
单目操作符
| ! * (类型) | 逻辑反操作 间接访问操作符(解引用操作符) 强制类型转换 |
!逻辑反操作符:真变成假,假变成真
0表示假,非0表示真
把一个假的事情,当作真的,要做什么事情
C语言中0表示假,非0表示真
int num = 10;
if (num)
{
}
if (!num)//num为假,做事情
{
}
在C99的标准中加入了布尔类型
C语言中C99之前没有表示真假的类型 C99中引用了布尔类型 #includeint main() { _Bool flag1 = false; bool flag2 = true; if (flag2) { printf("hehen"); }
int main()
{
//& 取地址操作符
//* 解引用操作符(间接访问操作符)
int a = 10;
int* pa = &a;
*pa = 20;//* - 解引用操作符
//
//*&a ==> a;
//
int arr[10] = {0};
&arr;//取出数组的地址,数组的地址应该放到【数组指针】中去
struct S s = {0};
struct S* ps = &s;
return 0;
}
sizeof操作符,是计算类型创建的变量所占的大小,单位是字节
sizeof是一个操作符,他不是函数。
printf("%dn", sizeof a);//括号是可以去掉的,所以不是函数
printf("%dn", sizeof(a));
printf("%dn", sizeof(int)); //当是类型时,括号不能去掉
接下来看这道题
int main()
{
int a = 10;
short s = 0;
printf("%dn", sizeof(s = a + 2));
printf("%dn", s);
return 0;
}
结果是 2和0,为什么不是2和12呢
因为sizeof括号里的表达式是不参与计算的,sizeof在编译期间,就已经把表达式给处理掉了
sizeof与数组
#includevoid test1(int arr[]) { printf("%dn", sizeof(arr)); } void test2(char ch[]) { printf("%dn", sizeof(ch)); } int main() { int arr[10] = {0}; char ch[10] = {0}; printf("%dn", sizeof(arr));//40 printf("%dn", sizeof(ch));//10 test1(arr); test2(ch); return 0; } 问: (1)、(2)两个地方分别输出多少? (3)、(4)两个地方分别输出多少?
我传过去的test 1 test 2 的结果是什么呢?
4和1吗?错
其实都是4/8 和4/8
我数组传参传的都是地址,只要是地址,那么大小就是4/8,可不能看贬了
~按位取反操作符
int main()
{
int a = 0;
a = ~a;
printf("%d", a);
return 0;
}
结果是什么? 是-1
00000000000000000000000000000000 11111111111111111111111111111111 - 补码 11111111111111111111111111111110 10000000000000000000000000000001 -> -1
按位或 | 操作符;只要对应的二进制位,只要有1,那么就是1。只有两个同时为0,那么才是0
我们来看这道题:
int a=11 11的二进制数是:00000000000000000000000001011 我如何把倒数第三位 0 变成1呢? ↑ 我直接在 这个位上或一个1,就是1了,其他位全部或0 就相当于按位或上个000000000000000000000000100 怎么得到这个数呢? 直接1 向左移动两位其实就是了,然后再或上去 a |=(1<<2) //结果是15 如何改回来呢? 在这个位与上一个0就可以了,其他位必须与1 所以与上11111111111111111111111111111011这样的数字 如何得到呢? 就是刚刚那数取反就可以了 a &=(~(1<<2)) //结果是11
为什么while(~scanf("%d“,&a))可以终止循环?
因为scanf在读取失败时,返回EOF,EOF其实就是-1,对-1进行按位取反时,就是0
"++" "--"操作符
int a=3; int b=++a //前置++ →先++,后使用 → a=a+1 b=a int b=a++ //后置++ →先使用,后+ →b=a,a=a+1 -- 如出一辙
强制类型转换()
eg:int a=(int)3.14;
关系操作符:
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
注意 ==不能比较字符串。语法写成if(“abcde”==“aee”)是没有问题,但是比较的其实是两个字符串首个字符的地址。地址绝对不可能相等的,所以没啥用。要想比较两个字符串得用strcmp
strcmp是比较两个字符串的大小,对应位置上字符(ASCII)的大小,不是比较长度
如果写成 char arr[ ]="abcdef" →是拿这样的常量字符串去初始化了我这个数组,这个常量字符串实际上是不存在的。
但是写成 char*p=”abcdef“ →这样的话这个常量字符串是存在的,我这个p变量是指向了他
逻辑操作符
&&逻辑与 ||逻辑或
我们来看这道题:
#includeint main() { int i = 0,a=0,b=2,c =3,d=4; i = a++ && ++b && d++; //i = a++||++b||d++; printf("a = %dn b = %dn c = %dnd = %dn", a, b, c, d); return 0; } //程序输出的结果是什么? &&→结果是1 2 3 4 ||→结果是
对于&&操作符而言,左边如果是假,我右边就不用算了
所以,a++是后置++,我先使用再++,a本来是0,i=a++的结果就是0,所以左边是假,右边不用算了。所以后面都没有动,唯一只有a用完了后自增了一下,所以结果是1234
对于||操作符也一样,如果左边是真,那么右边就不重要了
倘若把a变成1,&&的结果就是2 3 3 5
如果是||操作符,||的结果就是2234
如果a又变成0呢?结果就是1334
条件操作符:表达式1?表达式2:表达式3
eg: a>b?a:b
逗号表达式:用逗号隔开的多个表达式,从左到右依次执行,整个表达式的结果是最后一个表达式的结果
我们来看这道题:
int a = 1; int b = 2; int c = (a>b, a=b+10, a, b=a+1);//逗号表达式 c是多少
结果是13
这道题呢
if (a =b + 1, c=a / 2, d > 0)
其实最后只不过是拿d>0这个表达式进行判断
用逗号表达式我也可以给代码进行一些优化
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
a = get_val();
count_val(a);
}
如果使用逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
这样写就不会太冗余
1. [ ] 下标引用操作符
arr和[ ]里面的数就是两个操作数而已,[ ]就是一个操作符,类似于+-这样的操作符
所以arr[4]其实也可以写成 4[arr],完全没有任何问题
编译器碰到arr[4]也会编译成→*(arr+4)
2. ( ) 函数调用操作符
Add(a,b),操作数是Add,a,b,但是这里的ab和add就不会换位置。函数调用操作符的操作数至少是一个。
3. 访问一个结构的成员
| . -> | 结构体.成员名 结构体指针->成员名 |
#includestruct Stu { char name[20]; int age; float score; }; void print1(struct Stu ss) { printf("%s %d %fn", ss.name, ss.age, ss.score); } //结构体变量.成员名 void print2(struct Stu* ps) { //printf("%s %d %fn", (*ps).name, (*ps).age, (*ps).score); printf("%s %d %fn", ps->name, ps->age, ps->score); } //结构体指针->成员名 int main() { struct Stu s = {"张三", 20, 90.5f}; strcpy(s.name, "张三丰"); //scanf("%s", s.name); // *(s.name) = "张三丰"// err 只能访问一个字符 因为他的类型是char* // // //print1(s); print2(&s); return 0; }
表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型
12.1 隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度
一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长
度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转
换为int或unsigned int,然后才能送入CPU去执行运算。
如果进行整型提升?整形提升是按照变量的数据类型的符号位来提升的
//负数的整形提升 char c1 = -1; 变量c1的二进制位(补码)中只有8个比特位: 1111111 因为 char 为有符号的 char 所以整形提升的时候,高位补充符号位,即为1 提升之后的结果是: 11111111111111111111111111111111 //正数的整形提升 char c2 = 1; 变量c2的二进制位(补码)中只有8个比特位: 00000001 因为 char 为有符号的 char 所以整形提升的时候,高位补充符号位,即为0 提升之后的结果是: 00000000000000000000000000000001 //无符号整形提升,高位补0
来看这道题:
int main()
{
char c1 = 3;
char c2 = 127;
char c3 = c1 + c2;
printf("%d",c3);
return 0;
}
结果是什么?
3→00000000000000000000000000000011
放到char c1里就要截断:00000011
127→00000000000000000000000001111111
放到char c2里就要截断:01111111
当c1+c2时,他们是char类型,但是cpu难以直接实现两个8比特字节直接相加运算,所以必须整型提升为int类型
00000000000000000000000000000011
00000000000000000000000001111111
结果→00000000000000000000000010000010
c3里存着就是10000010这个值。
最后打印又是以%d的形式打印,所以又要进行整型提升
11111111111111111111111110000010→补码
10000000000000000000000001111110→源码
所以结果最后是-126
由此也可以看出整型提升是存在的,只要他参与表达式计算,就会整形提升:
算术转换
当类型>=Int时,就会进行算术转换
long double ↑
double ↑
float ↑
unsigned long ↑
long ↑
unsigned int ↑
int ↑
int见到unsigned int类型进行计算时,int会变成unsigned int,这样向上转换的(隐式)
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。但是算术转换要合理,要不然会有一些潜在的问题。
例如:
float f = 3.14;
int num = f; //隐式转换,会有精度丢失
操作符的属性
复杂表达式的求值有三个影响的因素。
1. 操作符的优先级:相邻的两个操作符才有优先级
2. 操作符的结合性:相邻的两个操作符优先级一样,先算哪个?
3. 是否控制求值顺序:例如&&操作符,左边为假,右边不算了
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
优先级由高到低增长,结合性L-R→从左到右结合,R-L同理
| 操作 符 | 描述 | 用法示例 | 结果类 型 | 结合 性 | 是否控制求值 顺序 |
| () | 聚组 | (表达式) | 与表达 式同 | N/A | 否 |
| () | 函数调用 | rexp(rexp,...,rexp) | rexp | L-R | 否 |
| [ ] | 下标引用 | rexp[rexp] | lexp | L-R | 否 |
| . | 访问结构成员 | lexp.member_name | lexp | L-R | 否 |
| -> | 访问结构指针成员 | rexp->member_name | lexp | L-R | 否 |
| ++ | 后缀自增 | lexp ++ | rexp | L-R | 否 |
| -- | 后缀自减 | lexp -- | rexp | L-R | 否 |
| ! | 逻辑反 | ! rexp | rexp | R-L | 否 |
| ~ | 按位取反 | ~ rexp | rexp | R-L | 否 |
| + | 单目,表示正值 | + rexp | rexp | R-L | 否 |
| - | 单目,表示负值 | - rexp | rexp | R-L | 否 |
| ++ | 前缀自增 | ++ lexp | rexp | R-L | 否 |
| -- | 前缀自减 | -- lexp | rexp | R-L | 否 |
| * | 间接访问 | * rexp | lexp | R-L | 否 |
| & | 取地址 | & lexp | rexp | R-L | 否 |
| sizeof | 取其长度,以字节 表示 | sizeof rexp sizeof(类 型) | rexp | R-L | 否 |
| (类 型) | 类型转换 | (类型) rexp | rexp | R-L | 否 |
| * | 乘法 | rexp * rexp | rexp | L-R | 否 |
| / | 除法 | rexp / rexp | rexp | L-R | 否 |
| % | 整数取余 | rexp % rexp | rexp | L-R | 否 |
| + | 加法 | rexp + rexp | rexp | L-R | 否 |
| - | 减法 | rexp - rexp | rexp | L-R | 否 |
| << | 左移位 | rexp << rexp | rexp | L-R | 否 |
| >> | 右移位 | rexp >> rexp | rexp | L-R | 否 |
| > | 大于 | rexp > rexp | rexp | L-R | 否 |
| >= | 大于等于 | rexp >= rexp | rexp | L-R | 否 |
| < | 小于 | rexp < rexp | rexp | L-R | 否 |
| <= | 小于等于 | rexp <= rexp | rexp | L-R | 否 |
| 操作 符 | 描述 | 用法示例 | 结果类 型 | 结合 性 | 是否控制求值 顺序 |
| == | 等于 | rexp == rexp | rexp | L-R | 否 |
| != | 不等于 | rexp != rexp | rexp | L-R | 否 |
| & | 位与 | rexp & rexp | rexp | L-R | 否 |
| ^ | 位异或 | rexp ^ rexp | rexp | L-R | 否 |
| | | 位或 | rexp | rexp | rexp | L-R | 否 |
| && | 逻辑与 | rexp && rexp | rexp | L-R | 是 |
| || | 逻辑或 | rexp || rexp | rexp | L-R | 是 |
| ? : | 条件操作符 | rexp ? rexp : rexp | rexp | N/A | 是 |
| = | 赋值 | lexp = rexp | rexp | R-L | 否 |
| += | 以...加 | lexp += rexp | rexp | R-L | 否 |
| -= | 以...减 | lexp -= rexp | rexp | R-L | 否 |
| *= | 以...乘 | lexp *= rexp | rexp | R-L | 否 |
| /= | 以...除 | lexp /= rexp | rexp | R-L | 否 |
| %= | 以...取模 | lexp %= rexp | rexp | R-L | 否 |
| <<= | 以...左移 | lexp <<= rexp | rexp | R-L | 否 |
| >>= | 以...右移 | lexp >>= rexp | rexp | R-L | 否 |
| &= | 以...与 | lexp &= rexp | rexp | R-L | 否 |
| ^= | 以...异或 | lexp ^= rexp | rexp | R-L | 否 |
| |= | 以...或 | lexp |= rexp | rexp | R-L | 否 |
| , | 逗号 | rexp,rexp | rexp | L-R | 是 |
计算机最重要的是调用顺序,而不是计算顺序
我们来看这个式子,有什么问题?
1.a*b + c*d + e*f
我们唯一能确定的是先乘后加,但是我们并不能确定,先a*b,再c*d,然后两个相加,再算e*f,
还是我们三个都先乘,然后再一起加,这个顺序我们无法确定
倘若我们只是计算值,这些顺序对于我没什么影响
要是这里的abcdef,分别代表的是表达式的话,那我的执行顺序,调用顺序,就会对我最后的结果造成非常大的影响。
我们再来看这个式子
2. c + --c
这个式子我们从优先级判断肯定可以看出是先--,然后再+
但是我们无法确定寄存器先储存的是什么,+号前面的c什么时候准备好的,我们无法确定。如果c=2,寄存器先处理了c,再处理--c,再相加,结果就是2+1=3
如果我先处理--c,再处理c,再相加,结果就是1+1=2。我们无法确定计算机先调用什么,所以这种代码就是问题代码
3.我们再来看这个式子:
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %dn", 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 | 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;
}
倘若我先算后面乘法,再算前面,就是4-2*3=-2
要是我先准备前面的,再准备后面,就是2-3*4=-10
我们唯一能确定的是先乘后减,但是我们根本无法确定我们先调用哪一个函数。所以这样的代码都是错误的,调用顺序是最重要的,而不是计算顺序。在Vs2019的编译器运行的代码,结果就是-10
所以我们要避免写出这样的代码
看看这道题什么问题?
#includeint main() { int i = 1; int ret = (++i) + (++i) + (++i); printf("%dn", ret); printf("%dn", i); return 0; }
可知,在两个编译器底下的结果截然不同。在Vs底下,是先算i++,三个i++过后,等于i=4,再后3个4相加等于12
但在VC底下,是先算两个++,i=3,相加=6,再算最后一个++,再相加,结果=10
所以这是个错误代码
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。



