4.运算符

参考资料:
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 += 8a = 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:

  1. 对于a1=++a,先执行 ++a,结果为 11,再将 11 赋值给 a1,所以 a1 的最终值为11。而 a 经过自增,最终的值也为 11。

  2. 对于b1=b++,b 的值并不会立马加 1,而是先把 b 原来的值交给 b1,然后再加 1。b 原来的值为 20,所以 b1 的值也就为 20。而 b 经过自增,最终值为 21。

  3. 对于c1=--c,先执行 --c,结果为 29,再将 29 赋值给c1,所以 c1 的最终值为 29。而 c 经过自减,最终的值也为 29。

  4. 对于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

我们来分析一下:

  1. 执行语句①时,因为是后自减,会先进行a-b运算,结果是 11,然后 b 再自减,就变成了 0;最后再将a-b的结果(也就是11)交给 c,所以 c 的值是 11。

  2. 执行语句②之前,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都是操作数,+ 是运算符。我们将这样的运算符称为双目运算符。同理,将有一个操作数的运算符称为单目运算符,将有三个操作数的运算符称为三目运算符。常见的双目运算符有 +、-、*、/ 等,单目运算符有 ++、-- 等,三目运算符只有一个,就是 ? : 。

关系运算符的两边可以是变量、数据或表达式,例如:

  1. a+b > c-d

  2. x > 3/2

  3. ‘a’+1 < c

  4. -i-5*j == k+1

关系运算符也可以嵌套使用,例如:

  1. a > (b > c)

  2. 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>ca || 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,这和逻辑运算中的!非常类似。。

例如, ~9可以转换为如下的运算:
0000 0000 – 0000 0000 – 0000 0000 – 0000 1001 (9 在内存中的存储)
-----------------------------------------------------------------------------------
1111 1111 – 1111 1111 – 1111 1111 – 1111 0110 (-10 在内存中的存储)

所以~9的结果为 -10。

例如, ~-9可以转换为如下的运算:
1111 1111 – 1111 1111 – 1111 1111 – 1111 0111 (-9 在内存中的存储)
-----------------------------------------------------------------------------------
0000 0000 – 0000 0000 – 0000 0000 – 0000 1000 (8 在内存中的存储)

所以~-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;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0i4Zfkv6-1580915084814)(img\1.png)]

括号运算符

括号运算符,也是优先级最高的运算符,用于提升运算符的优先级,()

例如

#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,逗号运算符表达式,表达式,…左到右
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值