参考资料:
C语言中文网:http://c.biancheng.net/
《C语言编程魔法书基于C11标准》
视频教程:C语音深入剖析班(国嵌 唐老师主讲)
C语言加减乘除运算
加减乘除是常见的数学运算
加法 | 减法 | 乘法 | 除法 | 求余数(取余) | |
---|---|---|---|---|---|
数学 | + | - | × | ÷ | 无 |
C语言 | + | - | * | / | % |
C语言中的加号、减号与数学中的一样,乘号、除号不同;另外C语言还多了一个求余数的运算符,就是 %。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 12;
int b = 100;
float c = 8.5;
int m = a + b;
float n = b * c;
double p = a / c;
int q = b % a;
printf("m = %d,n = %f,p = %lf,q = %d\n", m, n, p, q);
system("pause");
return 0;
}
运行结果:
m = 112,n = 850.000000,p = 1.411765,q = 4
你也可以让数字直接参与运算:
#include <stdio.h>
int main()
{
int a = 12;
int b = 100;
float c = 8.9;
int m = a - b; // 变量参与运算
int n = a + 239; // 有变量也有数字
double p = 12.7 * 34.3; // 数字直接参与运算
printf("m=%d, n=%d, p=%lf\n", m, n, p);
printf("m*2=%d, 6/3=%d, m*n=%ld\n", m*2, 6/3, m*n);
return 0;
}
输出结果:
m=-88, n=251, p=435.610000
m2=-176, 6/3=2, mn=-22088
对除法的说明
C语言中的除法运算有点奇怪,不同类型的除数和被除数会导致不同类型的运算结果:
- 当除数和被除数都是整数时,运算结果也是整数;如果不能整除,那么就直接丢掉小数部分,只保留整数部分,这跟将小数赋值给整数类型是一个道理。
- 一旦除数和被除数中有一个是小数,那么运算结果也是小数,并且是 double 类型的小数。
请看下面的代码:
#include <stdio.h>
int main()
{
int a = 100;
int b = 12;
float c = 12.0;
double p = a / b;
double q = a / c;
printf("p=%lf, q=%lf\n", p, q);
return 0;
}
运行结果:
p=8.000000, q=8.333333
a 和 b 都是整数,a / b 的结果也是整数,所以赋值给 p 变量的也是一个整数,这个整数就是 8。
另外需要注意的一点是除数不能为 0,因为任何一个数字除以 0 都没有意义。
然而,编译器对这个错误一般无能为力,很多情况下,编译器在编译阶段根本无法计算出除数的值,不能进行有效预测,“除数为 0”这个错误只能等到程序运行后才能发现,而程序一旦在运行阶段出现任何错误,只能有一个结果,那就是崩溃,并被操作系统终止运行。
请看下面的代码:
#include <stdio.h>
int main()
{
int a, b;
scanf("%d %d", &a, &b);
//从控制台读取数据并分别赋值给a和b
printf("result=%d\n", a / b);
return 0;
}
程序开头定义了两个 int 类型的变量 a 和 b,程序运行后,从控制台读取用户输入的整数,并分别赋值给 a 和 b,这个时候才能知道 a 和 b 的具体值,才能知道除数 b 是不是 0。像这种情况,b 的值在程序运行期间会改变,跟用户输入的数据有关,编译器根本无法预测,所以就没法及时发现“除数为 0”这个错误。
对取余运算的说明
取余,也就是求余数,使用的运算符是 %。C语言中的取余运算只能针对整数,也就是说,% 的两边都必须是整数,不能出现小数,否则编译器会报错。
另外,余数可以是正数也可以是负数,由 % 左边的整数决定:
- 如果 % 左边是正数,那么余数也是正数;
- 如果 % 左边是负数,那么余数也是负数。
请看下面的例子:
#include <stdio.h>
int main()
{
printf(
"100%%12=%d \n100%%-12=%d \n-100%%12=%d \n-100%%-12=%d \n",
100%12, 100%-12, -100%12, -100%-12
);
return 0;
}
运行结果:
100%12=4
100%-12=4
-100%12=-4
-100%-12=-4
在 printf 中,% 是格式控制符的开头,是一个特殊的字符,不能直接输出;要想输出 %,必须在它的前面再加一个 %,这个时候 % 就变成了普通的字符,而不是用来表示格式控制符了。
加减乘除运算的简写
有时候我们希望对一个变量进行某种运算,然后再把运算结果赋值给变量本身(复合赋值操作符),请看下面的例子:
#include <stdio.h>
int main()
{
int a = 12;
int b = 10;
printf("a=%d\n", a);
a = a + 8;
printf("a=%d\n", a);
a = a * b;
printf("a=%d\n", a);
return 0;
}
输出结果:
a=12
a=20
a=200
a = a + 8
相当于用原来 a 的值(也即12)加上 8,再把运算结果(也即20)赋值给 a,此时 a 的值就变成了 20。
a = a * b
相当于用原来 a 的值(也即20)乘以 b 的值(也即10),再把运算结果(也即200)赋值给 a,此时 a 的值就变成了 200。
以上的操作,可以理解为对变量本身进行某种运算。
在C语言中,对变量本身进行运算可以有简写形式。假设用 # 来表示某种运算符,那么
a = a # b
可以简写为:
a #= b
# 表示 +、-、*、/、% 中的任何一种运算符。
上例中a = a + 8
可以简写为a += 8
,a = a * b
可以简写为a *= b
。
下面的简写形式也是正确的:
int a = 10, b = 20;
a += 10; //相当于 a = a + 10;
a *= (b-10); //相当于 a = a * (b-10);
a -= (a+20); //相当于 a = a - (a+20);
注意:a #= b 仅是一种简写形式,不会影响程序的执行效率。
自增(++)和自减(–)
一个整数类型的变量自身加 1 可以这样写:
a = a + 1;
或者
a += 1;
不过,C语言还支持另外一种更加简洁的写法,就是:
a++;
或者
++a;
这种写法叫做自加或自增,意思很明确,就是每次自身加 1。
相应的,也有a--
和--a
,它们叫做自减,表示自身减 1。++
和--
分别称为自增运算符和自减运算符,它们在循环结构中使用很频繁。
自增和自减的示例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 10, b = 20;
printf("a = %d,b = %d\n", a, b);
++a;
--b;
printf("a = %d,b = %d\n", a, b);
a++;
b--;
printf("a=%d,b=%d\n", a, b);
system("pause");
return 0;
}
运行结果:
a=10, b=20
a=11, b=19
a=12, b=18
自增自减完成后,会用新值替换旧值,将新值保存在当前变量中。
自增自减的结果必须得有变量来接收,所以自增自减只能针对变量,不能针对数字,例如
10++
就是错误的。
需要重点说明的是,++ 在变量前面和后面是有区别的:
- ++ 在前面叫做前自增(例如 ++a)。前自增先进行自增运算,再进行其他操作。
- ++ 在后面叫做后自增(例如 a++)。后自增先进行其他操作,再进行自增运算。
自减(–)也一样,有前自减
和后自减
之分。
下面的例子能更好地说明前自增(前自减)和后自增(后自减)的区别:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 10, b = 20, c = 30, d = 40;
int a1 = ++a, b1 = b++, c1 = --c, d1 = d--;
printf("a=%d,a1=%d\n", a, a1);
printf("b=%d,b1=%d\n", b, b1);
printf("c=%d,c1=%d\n", c, c1);
printf("d=%d,d1=%d\n", d, d1);
system("pause");
return 0;
}
输出结果:
a=11, a1=11
b=21, b1=20
c=29, c1=29
d=39, d1=40
a、b、c、d 的输出结果相信大家没有疑问,下面重点分析a1、b1、c1、d1:
-
对于
a1=++a
,先执行 ++a,结果为 11,再将 11 赋值给 a1,所以 a1 的最终值为11。而 a 经过自增,最终的值也为 11。 -
对于
b1=b++
,b 的值并不会立马加 1,而是先把 b 原来的值交给 b1,然后再加 1。b 原来的值为 20,所以 b1 的值也就为 20。而 b 经过自增,最终值为 21。 -
对于
c1=--c
,先执行 --c,结果为 29,再将 29 赋值给c1,所以 c1 的最终值为 29。而 c 经过自减,最终的值也为 29。 -
对于
d1=d--
,d 的值并不会立马减 1,而是先把 d 原来的值交给 d1,然后再减 1。d 原来的值为 40,所以 d1 的值也就为 40。而 d 经过自减,最终值为 39。
可以看出:a1=++a;
会先进行自增操作,再进行赋值操作;而b1=b++;
会先进行赋值操作,再进行自增操作。
c1=--c;
和d1=d--;
也是如此。
为了强化记忆,我们再来看一个自增自减的综合示例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 12, b = 1;
int c = a - (b--); // ①
int d = (++a) - (--b); // ②
printf("c=%d,d=%d\n", c, d);
system("pause");
return 0;
}
输出结果:
c=11, d=14
我们来分析一下:
-
执行语句①时,因为是后自减,会先进行
a-b
运算,结果是 11,然后 b 再自减,就变成了 0;最后再将a-b
的结果(也就是11)交给 c,所以 c 的值是 11。 -
执行语句②之前,b 的值已经变成 0。对于
d=(++a)-(--b)
,a 会先自增,变成 13,然后 b 再自减,变成 -1,最后再计算13-(-1)
,结果是 14,交给 d,所以 d 最终是 14。
贪心法
++ 、–表达式的阅读技巧
例如:a+++b
,这个表达式就要用到贪心法进行阅读
编译器处理的每个符号应该尽量可能多的包含字符,编译器以左向右的顺序一个一个尽可能多的读入字符,当即将读入的字符不可能和已读入的字符组成合法符号为止,也就是上面的a+++b
编译器会解释成a++ + b
错误实例
#include <stdio.h>
int main()
{
int i = 0;
++i++;
return 0;
}
进行编译的时候会报错,因为编译器阅读这个表达式的时候使用的是贪心法进行阅读,那么编译器先自增了一遍,也就变成了1++,这时因为1是常量,所以编译器会报错
注意
当编译器在使用贪心法进行匹配的时候,遇到空格和;
号的时候就不会再进行贪心匹配,所以我们编程的时候进行多使用空格,避免其他人阅读的时候看不懂代码。
关系运算符在使用时,它的的两边都会有一个表达式,比如变量、数值、加减乘除运算等,关系运算符的作用就是判明这两个表达式的大小关系。注意,是判明大小关系,不是其他关系。
gcc与VS不同的处理情况
++
与--
在不同编译器可能会出现不一样的情况
前置运算
#include <stdio.h>
int main()
{
int k = 2;
k = (++k) + (++k);
printf("%d\n", k);
return 0;
}
运算结果
VS:8
GCC:8
看汇编
VS中的汇编
int k = 2;
00CE1858 mov dword ptr [k],2 //把2的值放到k的地址中
k = (++k) + (++k);
00CE185F mov eax,dword ptr [k] //把k地址中的值放到eax寄存器中
00CE1862 add eax,1 //把eax中的值+1然后再保存到eax中,eax=3
00CE1865 mov dword ptr [k],eax //把eax中的值保存到k的地址中,k=3
00CE1868 mov ecx,dword ptr [k] //把k地址中的值赋值到ecx寄存器中,ecx=3
00CE186B add ecx,1 //把ecx中的值+1,然后保存到ecx中,ecx=4
00CE186E mov dword ptr [k],ecx //把ecx中的值保存到k地址中,k=4
00CE1871 mov edx,dword ptr [k] //把k地址中的值赋值到edx中
00CE1874 add edx,dword ptr [k] //edx与k中的值相加,也就是k+k=4+4=8,再把值保存到edx中
00CE1877 mov dword ptr [k],edx //把edx中的值8保存到k地址中,所以k=8
------------------------------------------------------------------------
gcc中的汇编
movl $2, -4(%rbp) //把2赋值到rbp - 4的地址中,所以相当于k = 2
addl $1, -4(%rbp) //把rbp - 4的地址中的值 + 1,再保存到rbp - 4地址中,相当于 k = k + 1 = 3
addl $1, -4(%rbp) //把rbp - 4的地址中的值 + 1,再保存到rbp - 4地址中,相当于 k = k + 1 = 4
sall -4(%rbp) //把rbp - 4的地址中的值左移1位,相当于0100 左移 成为1000也就是8,也就相当于 k = 4 + 4 = 8
从汇编代码可看出前置运算VS与gcc所实现的原理是一样的,都是先把前置运算的先运算完再计算
后置运算
#include <stdio.h>
int main()
{
int k = 2;
k = k++ + k++;
printf("%d\n", k);
return 0;
}
运算结果:
VS:6
gcc:5
看汇编
VS中的汇编
int k = 2;
00A81858 mov dword ptr [k],2 //把2的值放到k的地址中
k = k++ + k++;
00A8185F mov eax,dword ptr [k] //把k地址中的值赋值给eax寄存器中
00A81862 add eax,dword ptr [k] //把k中的值与eax中的值进行计算并保存到eax寄存器中,也就是eax = k + k = 2 + 2 = 4
00A81865 mov dword ptr [k],eax //把eax中的值赋值给k,那么k = 4
//到这里已经完成了 k = k + k
00A81868 mov ecx,dword ptr [k] //把k的值赋值给ecx
00A8186B add ecx,1 //把ecx中的值+1再赋值给ecx
00A8186E mov dword ptr [k],ecx //把ecx中的值赋值给k地址,那么k = 5,完成第一次k++
00A81871 mov edx,dword ptr [k] //把k的值赋值给edx
00A81874 add edx,1 //把edx中的值+1再赋值给edx
00A81877 mov dword ptr [k],edx //把edx中的值赋值到k的地址中,k = 6,这里完成了第二次k++
------------------------------------------------------------------------
gcc中的汇编
movl $2, -4(%rbp) //把2赋值给rbp - 4的地址处,也就相当于k = 2
movl -4(%rbp), %edx //把地址rbp - 4的地址处的数据赋值给edx寄存器,edx = 2
leal 1(%rdx), %eax //edx寄存器是32位寄存器,rdx寄存器是64位寄存器,那么rdx与edx寄存器中的值是相同的,把rdx 地址 + 1,也就是 2 + 1,然后把这个值当成地址传给eax,那么eax等于3
movl %eax, -4(%rbp) //把eax中的值赋值给rbp - 4地址处,也就是k = 3,这里完成了第一次的k++
movl -4(%rbp), %eax //把rbp - 4地址处的数据赋值给eax,eax = 3
leal 1(%rax), %ecx //eax寄存器是32位寄存器,rax寄存器是64位寄存器,那么rax与eax寄存器中的值是相同的,把rax 地址 + 1,也就是 3 + 1,然后把这个值当成地址传给ecx,那么ecx等于4
movl %ecx, -4(%rbp) //把ecx中的值赋值到rbp - 4地址处,也就是把k = 4,这里完成了第二次的k++
addl %edx, %eax //把edx中的值与eax中的值相加,并保存到eax中,这时edx=2,eax=3,相当于2 + 3,所以eax = 5
movl %eax, -4(%rbp) //把eax中的值赋值到rbp - 4处也就是k处,相当于k = eax = 5
从上面的汇编可得出过程
VS相当于
int k = 2;
k = k + k; // k = 2 + 2
k = k + 1; // k = 4 + 1
k = k + 1; // k = 5 + 1
GCC相当于
int k = 2,temp1,temp2;
temp1 = k; //temp1 = 2
k = k + 1; // k = 2 + 1
temp2 = k; //temp2 = 3
k = k + 1; //k = 3 + 1
k = temp1 + temp2; //k = 2 + 3
所以在后置运算中,gcc与VS是不一样的运算原理,gcc是先把原来的值保存到一个临时变量中保存起来,然后再把原来的值自增,在进行运算的时候把临时变量的值来进行运算,而VS是先把所有的值进行运算,然后再进行自增
关系运算符
关系运算符 | 含 义 | 数学中的表示 |
---|---|---|
< | 小于 | < |
<= | 小于或等于 | ≤ |
> | 大于 | > |
>= | 大于或等于 | ≥ |
== | 等于 | = |
!= | 不等于 | ≠ |
关系运算符都是双目运算符,其结合性均为左结合
。
关系运算符的优先级低于算术运算符,高于赋值运算符。
在六个关系运算符中,<、<=、>、>=的优先级相同,高于==
和!=
,==
和!=
的优先级相同。
在C语言中,有的运算符有两个操作数,例如 10+20,10和20都是操作数,+ 是运算符。我们将这样的运算符称为双目运算符。同理,将有一个操作数的运算符称为单目运算符,将有三个操作数的运算符称为三目运算符。常见的双目运算符有 +、-、*、/ 等,单目运算符有 ++、-- 等,三目运算符只有一个,就是 ? : 。
关系运算符的两边可以是变量、数据或表达式,例如:
-
a+b > c-d
-
x > 3/2
-
‘a’+1 < c
-
-i-5*j == k+1
关系运算符也可以嵌套使用,例如:
-
a > (b > c)
-
a != (c == d)
关系运算符的运算结果只有 0 或 1。当条件成立时结果为 1,条件不成立结果为 0。例如:
- 5>0 成立,其值为 1;
- 34-12>100 不成立,其值为 0;
- (a=3)>(b=5) 由于3>5不成立,故其值为 0。
我们将运算结果 1 称为“真”,表示条件成立,将 0 称为“假”,表示条件不成立。
下面的代码会将关系运算符的结果输出:
#include <stdio.h>
int main()
{
char c='k';
int i=1, j=2, k=3;
float x=3e+5, y=0.85;
int result_1 = 'a'+5<c, result_2 = x-5.25<=x+y;
printf( "%d, %d\n", result_1, -i-2*j>=k+1 );
printf( "%d, %d\n", 1<j<5, result_2 );
printf( "%d, %d\n", i+j+k==-2*j, k==j==i+5 );
return 0;
}
运行结果:
1, 0
1, 1
0, 0
对于含多个关系运算符的表达式,如 k==j==i+5
,根据运算符的左结合性,先计算k==j
,该式不成立,其值为0,再计算0==i+5
,也不成立,故表达式值为0。
需要提醒的是,==
才表示等于,而=
表示赋值,大家要注意区分,切勿混淆。
逻辑运算符
运算符 | 说明 | 结合性 | 举例 |
---|---|---|---|
&& | 与运算,双目,对应数学中的“且” | 左结合 | 1&&0、(9>3)&&(b>a) |
|| | 或运算,双目,对应数学中的“或” | 左结合 | 1||0、(9>3)||(b>a) |
! | 非运算,单目,对应数学中的“非” | 右结合 | !a、!(2<5) |
逻辑运算的结果
在编程中,我们一般将零值称为“假”,将非零值称为“真”。逻辑运算的结果也只有“真”和“假”,“真”对应的值为 1,“假”对应的值为 0。
1) 与运算(&&)
参与运算的两个表达式都为真时,结果才为真,否则为假。例如:
5&&0
5为真,0为假,相与的结果为假,也就是 0。
(5>0) && (4>2)
5>0 的结果是1,为真,4>2结果是1,也为真,所以相与的结果为真,也就是1。
2) 或运算(||)
参与运算的两个表达式只要有一个为真,结果就为真;两个表达式都为假时结果才为假。例如:
10 || 0
10为真,0为假,相或的结果为真,也就是 1。
(5>0) || (5>8)
5>0 的结果是1,为真,5>8 的结果是0,为假,所以相或的结果为真,也就是1。
3) 非运算(!)
参与运算的表达式为真时,结果为假;参与运算的表达式为假时,结果为真。例如:
!0
0 为假,非运算的结果为真,也就是 1。
!(5>0)
5>0 的结果是1,为真,非运算的结果为假,也就是 0。
输出逻辑运算的结果:
#include <stdio.h>
int main()
{
int a = 0, b = 10, c = -6;
int result_1 = a&&b, result_2 = c||0;
printf("%d, %d\n", result_1, !c);
printf("%d, %d\n", 9&&0, result_2);
printf("%d, %d\n", b||100, 0&&0);
return 0;
}
运行结果:
0, 0
0, 1
1, 0
优先级
逻辑运算符和其它运算符优先级从低到高依次为:
赋值运算符(=) < &&和|| < 关系运算符 < 算术运算符 < 非(!)c
&& 和 || 低于关系运算符,! 高于算术运算符。
按照运算符的优先顺序可以得出:
- a>b && c>d 等价于 (a>b)&&(c>d)
- !b==c||d<a 等价于 ((!b)==c)||(d<a)
- a+b>c&&x+y<b 等价于 ((a+b)>c)&&((x+y)<b)
另外,逻辑表达式也可以嵌套使用,例如a>b && b || 9>c
,a || c>d && !p
。
逻辑运算符举例:
#include <stdio.h>
int main()
{
char c='k';
int i=1,j=2,k=3;
float x=3e+5,y=0.85;
printf( "%d,%d\n", !x*!y, !!!x );
printf( "%d,%d\n", x||i&&j-3, i<j&&x<y );
printf( "%d,%d\n", i==5&&c&&(j=8), x+y||i+j+k );
return 0;
}
运行结果:
0,0
1,0
0,1
本例中!x和!y分别为0,!x*!y也为0,故其输出值为0。由于x为非0,故!!!x的逻辑值为0。对x|| i && j-3式,先计算j-3的值为非0,再求i && j-3的逻辑值为1,故x||i&&j-3的逻辑值为 1。对i<j&&x<y式,由于i<j的值为1,而x<y为0故表达式的值为1,0相与,最后为0,对i==5&&c&&(j=8)
式,由于i==5为假,即值为0,该表达式由两个与运算组成,所以整个表达式的值为0。对于式x+ y||i+j+k由于x+y的值为非0,故整个或表达式的值为1。
短路规则
||
从左向右开始计算,当遇到为真的条件时停止计算,整个表达式为真;所有条件为假时表达式才为假。
&&
从左向右开始计算,当遇到为假的条件时停止计算,整个表达式为假;所有条件为真时表达式才为真。
例子:
#include <stdio.h>
int main()
{
int a = 1,b = 1,c = 0,d;
d = a || b || (c = b + 3);
printf("b = %d,c = %d\n",b,c);
return 0;
}
结果:
b = 1,c = 0
例子:
#include <stdio.h>
int main()
{
int a = 0,b = 0,c = 0,d;
d = a || b || (c = b + 3);
printf("b = %d,c = %d\n",b,c);
return 0;
}
结果:
b = 0,c = 3
例子:
#include <stdio.h>
int main()
{
int a = 0,b = 0,c = 0,d;
d = a && b && (c = b + 3);
printf("b=%d,c = %d\n",b,c);
return 0;
}
结果:
b = 0,c = 0
例子:
#include <stdio.h>
int main()
{
int a = 1,b = 1,c = 0,d;
d = a && b && (c = b + 3);
printf("b=%d,c = %d\n",b,c);
return 0;
}
结果:
b=1,c = 4
逗号运算符与表达式
用逗号将多个表达式连接起来,又称为"顺序求值运算符"。整个表达式的值是最后那个逗号之后表达式的值,逗号运算符会从左往右逐个进行运算
#include <stdio.h>
#include <stdlib.h>
void main()
{
int a, b, c, d, e, f;
a = 3, b = 4, c = 5, d = 6, e = 7, f = (8, 9, 10);
printf("a = %d,b = %d,c = %d,d = %d,e = %d,f = %d", a, b, c, d, e, f);
system("pause");
}
结果:
a = 3,b = 4,c = 5,d = 6,e = 7,f = 10
逗号运算符会把整个表达式都运算一次,然后取最后一个值
例如:
#include <stdio.h>
int main()
{
int a,b,c,d,e,f;
f = (a = 1,b = 2,c = 3,d = 4,e = 5,6,7,8,9,10);
printf("a = %d,b = %d,c = %d,d = %d,e = %d,f = %d", a, b, c, d, e, f);
return 0;
}
结果
a = 1,b = 2,c = 3,d = 4,e = 5,f = 10
位运算
所谓位运算,就是对一个比特(Bit)位进行操作。
C语言提供了六种位运算符:
运算符 | & | | | ^ | ~ | << | >> |
---|---|---|---|---|---|---|
说明 | 按位与 | 按位或 | 按位异或 | 取反 | 左移 | 右移 |
按位与运算(&)
一个比特(Bit)位只有 0 和 1 两个取值,只有参与&
运算的两个位都为 1 时,结果才为 1,否则为 0。例如1&1
为 1,0&0
为 0,1&0
也为 0,这和逻辑运算符&&
非常类似。
C语言中不能直接使用二进制,&
两边的操作数可以是十进制、八进制、十六进制,它们在内存中最终都是以二进制形式存储,&
就是对这些内存中的二进制位进行运算。其他的位运算符也是相同的道理。
例如,9 & 5
可以转换成如下的运算:
0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)
& 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)
-----------------------------------------------------------------------------------
0000 0000 – 0000 0000 – 0000 0000 – 0000 0001 (1 在内存中的存储)
也就是说,按位与运算会对参与运算的两个数的所有二进制位进行&
运算,9 & 5
的结果为 1。
又如,-9 & 5
可以转换成如下的运算:
1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)
& 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)
-----------------------------------------------------------------------------------
0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)
-9 & 5
的结果是 5。
再强调一遍,&
是根据内存中的二进制位进行运算的,而不是数据的二进制形式;其他位运算符也一样。以-9&5
为例,-9 的在内存中的存储和 -9 的二进制形式截然不同:
1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)
-0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (-9 的二进制形式,前面多余的 0 可以抹掉)
按位与运算通常用来对某些位清 0,或者保留某些位。例如要把 n 的高 16 位清 0 ,保留低 16 位,可以进行n & 0XFFFF
运算(0XFFFF 在内存中的存储形式为 0000 0000 – 0000 0000 – 1111 1111 – 1111 1111)。
【实例】对上面的分析进行检验。
#include <stdio.h>
int main()
{
int n = 0X8FA6002D;
printf("%d, %d, %X\n", 9 & 5, -9 & 5, n & 0XFFFF);
return 0;
}
运行结果:
1, 5, 2D
按位或运算(|)
参与|
运算的两个二进制位有一个为 1 时,结果就为 1,两个都为 0 时结果才为 0。例如1|1
为1,0|0
为0,1|0
为1,这和逻辑运算中的||
非常类似。
例如,9 | 5
可以转换成如下的运算:
0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)
| 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)
-----------------------------------------------------------------------------------
0000 0000 – 0000 0000 – 0000 0000 – 0000 1101 (13 在内存中的存储)
9 | 5
的结果为 13。
又如,-9 | 5
可以转换成如下的运算:
1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)
| 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)
-----------------------------------------------------------------------------------
1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)
-9 | 5
的结果是 -9。
按位或运算可以用来将某些位置 1,或者保留某些位。例如要把 n 的高 16 位置 1,保留低 16 位,可以进行n | 0XFFFF0000
运算(0XFFFF0000 在内存中的存储形式为 1111 1111 – 1111 1111 – 0000 0000 – 0000 0000)。
【实例】对上面的分析进行校验。
#include <stdio.h>
int main()
{
int n = 0X2D;
printf("%d, %d, %X\n", 9 | 5, -9 | 5, n | 0XFFFF0000);
return 0;
}
运行结果:
13, -9, FFFF002D
按位异或运算(^)
参与^
运算两个二进制位不同时,结果为 1,相同时结果为 0。例如0^1
为1,0^0
为0,1^1
为0。
例如,9 ^ 5
可以转换成如下的运算:
0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)
^ 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)
-----------------------------------------------------------------------------------
0000 0000 – 0000 0000 – 0000 0000 – 0000 1100 (12 在内存中的存储)
9 ^ 5
的结果为 12。
又如,-9 ^ 5
可以转换成如下的运算:
1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)
^ 0000 0000 – 0000 0000 – 0000 0000 – 0000 0101 (5 在内存中的存储)
-----------------------------------------------------------------------------------
1111 1111 – 1111 1111 – 1111 1111 – 1111 0010 (-14 在内存中的存储)
-9 ^ 5
的结果是 -14。
按位异或运算可以用来将某些二进制位反转。例如要把 n 的高 16 位反转,保留低 16 位,可以进行n ^ 0XFFFF0000运算(0XFFFF0000 在内存中的存储形式为 1111 1111 – 1111 1111 – 0000 0000 – 0000 0000)。
【实例】对上面的分析进行校验。
#include <stdio.h>
int main()
{
unsigned n = 0X0A07002D;
printf("%d, %d, %X\n", 9 ^ 5, -9 ^ 5, n ^ 0XFFFF0000);
return 0;
}
运行结果:
12, -14, F5F8002D
取反运算(~)
取反运算符~
为单目运算符,右结合性,作用是对参与运算的二进制位取反。例如~1
为0,~0
为1,这和逻辑运算中的!
非常类似。。
-
例如,
-
0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)
-----------------------------------------------------------------------------------
1111 1111 – 1111 1111 – 1111 1111 – 1111 0110 (-10 在内存中的存储)
~9
可以转换为如下的运算:
所以~9
的结果为 -10。
-
例如,
-
1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)
-----------------------------------------------------------------------------------
0000 0000 – 0000 0000 – 0000 0000 – 0000 1000 (8 在内存中的存储)
~-9
可以转换为如下的运算:
所以~-9
的结果为 8。
【实例】对上面的分析进行校验。
#include <stdio.h>
int main()
{
printf("%d, %d\n", ~9, ~-9 );
return 0;
}
运行结果:
-10, 8
左移运算(<<)
左移运算符<<
用来把操作数的各个二进制位全部左移若干位,高位丢弃,低位补0。
例如,9<<3
可以转换为如下的运算:
<< 0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)
-----------------------------------------------------------------------------------
0000 0000 – 0000 0000 – 0000 0000 – 0100 1000 (72 在内存中的存储)
所以9<<3
的结果为 72。
又如,(-9)<<3
可以转换为如下的运算:
<< 1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)
-----------------------------------------------------------------------------------
1111 1111 – 1111 1111 – 1111 1111 – 1011 1000 (-72 在内存中的存储)
所以(-9)<<3的结果为 -72
如果数据较小,被丢弃的高位不包含 1,那么左移 n 位相当于乘以 2 的 n 次方。
【实例】对上面的结果进行校验。
#include <stdio.h>
int main()
{
printf("%d, %d\n", 9<<3, (-9)<<3 );
return 0;
}
运行结果:
72, -72
右移运算(>>)
右移运算符>>
用来把操作数的各个二进制位全部右移若干位,低位丢弃,高位补 0 或 1。如果数据的最高位是 0,那么就补 0;如果最高位是 1,那么就补 1。
例如,9>>3
可以转换为如下的运算:
>> 0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)
-----------------------------------------------------------------------------------
0000 0000 – 0000 0000 – 0000 0000 – 0000 0001 (1 在内存中的存储)
所以9>>3
的结果为 1。
又如,(-9)>>3
可以转换为如下的运算:
>> 1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)
-----------------------------------------------------------------------------------
1111 1111 – 1111 1111 – 1111 1111 – 1111 1110 (-2 在内存中的存储)
所以(-9)>>3
的结果为 -2
如果被丢弃的低位不包含 1,那么右移 n 位相当于除以 2 的 n 次方(但被移除的位中经常会包含 1)。
【实例】对上面的结果进行校验。
#include <stdio.h>
int main()
{
printf("%d, %d\n", 9>>3, (-9)>>3 );
return 0;
}
运行结果:
1, -2
左右移规则
左移运算符<<
将运算数的二进制位左移
规则:高位丢弃,低位补0
右移运算符>>
把运算数的二进制位右移
规则:高位补符号位,低位丢弃
左移n位相当于乘以2的n次方,但效率比数学运算符高
右移n位相当于除以2的n次方,但效率比数学运算符高
三目运算符
三目运算符定义:(a ?b : c) 当a的值为真时,返回b的值;否则返回c的值
三目运算符(a ?b : c) 返回类型:
1、通过隐试类型转换规则返回b和c中的较高类型
2、当b和c不能隐试转换到同一类型时将编译出错
例:通过隐试类型转换规则返回b和c中的较高类型
#include <stdio.h>
int main()
{
short a = 10;
short b = 20;
long long c = 30;
printf("num = %d,sizeof = %d\n",(a < b ? b : c),sizeof((a < b ? b : c)));
printf("num1 = %d,sizeof = %d\n",(a > b ? b : c),sizeof((a > b ? b : c)));
return 0;
}
结果:
num = 20,sizeof = 8
num1 = 30,sizeof = 8
例:当b和c不能隐试转换到同一类型时将编译出错
#include <stdio.h>
int main()
{
short a = 10;
short b = 20;
long long c = 30;
struct D {
int a1 = 10;
float f = 3.14;
} d; //结构体
printf("num = %d,sizeof = %d\n",(a < b ? b : c),sizeof((a < b ? b : c)));
printf("num1 = %d,sizeof = %d\n",(a > b ? b : c),sizeof((a > b ? b : c)));
printf("num2 = %d,sizeof = %d\n",(a > b ? b : d),sizeof((a > b ? b : d)));
return 0;
}
括号运算符
括号运算符,也是优先级最高的运算符,用于提升运算符的优先级,()
例如
#include <stdio.h>
int main()
{
int a = 3,b = 4,c = 5,d = 6,e;
e = (a + b) * (c + d);
return 0;
}
如果我们不加括号,那么就会变成e = a + b * c + d;
那么C语言的编译器就会算成29,就不是77了,所以我们添加括号修改优先级
优先级表
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
---|---|---|---|---|---|
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | |
() | 圆括号 | (表达式) 函数名(形参表) | |||
. | 成员选择(对象) | 对象.成员名 | |||
-> | 成员选择(指针) | 对象指针->成员名 | |||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
(类型) | 强制类型转换 | (数据类型)表达式 | |||
++ | 自增运算符 | ++变量名 变量名++ | 单目运算符 | ||
– | 自减运算符 | –变量名 变量名– | 单目运算符 | ||
* | 取值运算符 | *指针变量 | 单目运算符 | ||
& | 取地址运算符 | &变量名 | 单目运算符 | ||
! | 逻辑非运算符 | !表达式 | 单目运算符 | ||
~ | 按位取反运算符 | ~表达式 | 单目运算符 | ||
sizeof | 长度运算符 | sizeof(表达式) | |||
3 | / | 除 | 表达式 / 表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | 双目运算符 | ||
% | 余数(取模) | 整型表达式%整型表达式 | 双目运算符 | ||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | 双目运算符 | ||
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | 双目运算符 | ||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | 双目运算符 | ||
< | 小于 | 表达式<表达式 | 双目运算符 | ||
<= | 小于等于 | 表达式<=表达式 | 双目运算符 | ||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | 双目运算符 | ||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | |
/= | 除后赋值 | 变量/=表达式 | |||
*= | 乘后赋值 | 变量*=表达式 | |||
%= | 取模后赋值 | 变量%=表达式 | |||
+= | 加后赋值 | 变量+=表达式 | |||
-= | 减后赋值 | 变量-=表达式 | |||
<<= | 左移后赋值 | 变量<<=表达式 | |||
>>= | 右移后赋值 | 变量>>=表达式 | |||
&= | 按位与后赋值 | 变量&=表达式 | |||
^= | 按位异或后赋值 | 变量^=表达式 | |||
|= | 按位或后赋值 | 变量|=表达式 | |||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 |