操作符超详解


前言

各种操作符的介绍
在C语言中,可以单独操控变量中的位。读者可能好奇,竟然有人想这么做。有时必须单独操控位,而且非常有用。例如,通常向硬件设备发送一两个字节来控制这些设备,其中每个位( bit )都有特定的含义。另外,与文件相关的操作系统信息经常被存储,通常使用特定位表明特定项。许多压缩和加密操作都是直接处理单独的位。高级语言一般不会处理这级别的细节,C在提供高级语言便利的同时,还能在为汇编语言所保留的级别上工作,这使其成为编写设备驱动和嵌入式代码的首选语言。


背景知识

-首先要介绍位、字节、二进制记数法和其他进制记数系统的一些背景知识。

  • 二进制数、位和字节
    我们通常都是基于数字10来书写数字。例如:2157
    2 x 103 + 1 x 102 + 5 x 101 + 7 x 100 = 2157
    从某种意义上看,计算机的位只有2根手指,因为它只能被设置为0或1,关闭或打开。因此,计算机适用基底为2的数制系统。它用2的幂而不是10的幂。以2为基底表示的数字被称为二进制数(binary number
    十进制的数据中,都是0~9的数字组成。
    二进制的数据中,都是0~1的数字组成。
  • 数据的二进制表示:
    计算机界通常使用八进制记数系统和十六进制记数系统。因为8和16都是2的幂,这些系统比十进制系统更接近计算机的二进制系统。
  • 八进制
    八进制(octal)是指八进制记数系统。该系统基于8的幂,用0~7表示数字,例如:八进制数字451(在C中写作0451)(第一个0表示这是用八进制表示)
    4 x 82 + 5 x 81 + 1 x 80 = 297(十进制)
    换算八进制的一个简单的方法是,每个八进制位对应3个二进制位
八进制位等价的二进制位
0000
1001
2010
3011
4100
5101
6110
7111
  • 十六进制
    十六进制(hexadecimal)是指十六进制记数系统。该系统基于16的幂,用0~15表示数字。但是,由于没有单独的数表示10至15,所以用A至F来表示。例如,十六进制数A3F(在C中写作0xA3F)(0x表示这是用十六进制来表示)
    10 x 162 + 3 x 101 + 15 x 100 = 2623(十进制)
    每个十六进制位都对应一个4位的二进制数(即4个二进制位),那么两个十六进制位恰好对应一个8位字节。因此,十六进制很适合表示字节值。
十六进制等价二进制
00000
10001
20010
30110
40100
50101
F1111
  • 通常,1字节包含8位。可以从左往右给这8位分别编号为7~0。在1字节中,编号是7的位被称为高阶位,编号是0的位被称为低阶位。每一位对应2的相应指数。
    在这里插入图片描述
    这里,128是2的7次幂,以此类推。该字节能表示的最大数字是把所有位都设置为1,这个二进制数的值是:
    128 + 64 + 32 +16 + 8 + 4 + 2 + 1 = 255
    因此,1字节可存储0~255范围内的数字,总共256个值。通常,unsigned char 用1字节表示的范围是0 – 255,而signed char 用1字节表示的范围是-128 – +127
  • 整数是存放在整型变量中的,一个整型变量是4个字节,32比特位
  • 最高位的一位表示符号位,0表示正数,1表示负数
  • 整数的二进制表示形式:
    原码:把一个数按照正负直接翻译成二进制
    反码:原码的符号位不变,其它位按位取反
    补码:反码+1
  • 正整数的原码、反码和补码是一样的
    负整数的原码、反码和补码要进行计算。
  • 整数在内存中存储的是补码
  • 下面举一个实例来看看:-5
    在这里插入图片描述
    在这里插入图片描述

一、算术操作符

+ - * / %(取余操作符)

  • 1、除了%操作符之外,其他的几个操作符可以作用于整数和浮点数。
  • 2、对于/ 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。(执行浮点数除法时,打印也要记得相应地改变)(%f 、%lf)
  • 3、%操作符的两个操作数必须为整数,返回的是整除之后的余数
#include<stdio.h>
int main()
{
	int a = 10;
	printf("%d\n", a / 3);           // 3
	printf("%f\n", a / 3.0);         // 3.333333
	printf("%d\n", a % 3);           // 1
	return 0;
}

二、移位操作符

下面介绍C的移位操作符,移位操作符向左或向右移动位。

  • 1 左移 <<
    移位规则:左边抛弃,右边补0
    在这里插入图片描述
    该操作产生了一个新的位值,但是不改变其运算对象。例如,假设stonk为1,那么stonk << 2为4,但是stonk本身不变,仍为1。可以用左移赋值运算符(<<=)来更改变量的值。
  • 2 右移 >>
    移位规则:
    1、逻辑移位:左边用0填充,右边丢弃
    2、算术移位:左边用原该值的符号位填充,右边丢弃
    在这里插入图片描述
    警告:
    对于移位操作符,不要移动负数位,这个是标准未定义的。
  • 用法
    移位操作符针对2的幂提供快速有效的乘法和除法

在这里插入图片描述


三、位操作符

  • 位操作符有:
    & 按位与 ------> 对应的二进制位,有0为0,都为1则为1
    (从真/假方面看,只有当两个位都为真时,结果才为真。)
    | 按位或 ------> 有1为1,都为0则为0
    (从真/假方面看,如果两个运算对象中相应的一个位为真或两个位都为真,那么结果为真。)
    ^ 按位异或 ---->相同为0,相异为1
    (从真?假方面看,如果两个运算对象中相应的一个位为真且不是两个同为真,那么结果为真)
    它们的操作数必须是整数
  • 用法:
  • 1、掩码
    按位与运算符常用于掩码(mask)。所谓掩码指的是一些设置为开(1)或关(0)的位组合。例如,假设定义符号常量 MASK 为2(二进制形式为00000010),只有1号位是1,其他位都是0。
flags = flags & MASK;

把 flags 中除1号位以外的所有位都设置为0,因为使用 & 任何位与0组合都得0。
1号位的值不变(如果1号位是1,1 & 1得1;如果是0,0 & 1 还是得0)。
这个过程叫做“使用掩码”,因为掩码中的0隐藏了 flags 中相应的位

  • 2、检查位的值
if((flags & MASK) == MASK)
puts("nice!");

这里,检查 flags 中的1号位是否被设置为1,由于按位与运算符的优先级比==低,所以要加上圆括号。

  • 3、打开位(设置位)
    有时,需要打开一个值中的特定位,同时保持其他位不变。例如,一台 IBM PC 通过向端口发送值来控制硬件。例如,为了打开内置扬声器,必须打开1号位,同时保持其他位不变。这种情况可以使用按位或运算符( | )
flags = flags | MASK;

把 flags 的1号位设置为1,且其他位不变
因为使用 | 运算符,任何位与0组合,结果都是本身;任何位与1组合,结果都为1

  • 4、关闭位(清空位)
    和打开特定的位类似,有时也需要在不影响其他位的情况下关闭指定的位。假设要关闭变量flags 中的1号位。同样,MASK 只有1号位为1(即,打开的状态)。可以这样做:
flags = flags & ~MASK;

由于MASK 除1号位为1以外,其他位全为0,所以~MASK 将1号位变成0,其他位全都变成1。
使用& ,任何位与1组合都得本身,所以这条语句保持除1号位以外的其他位不变
使用&,任何位与0组合都得0,所以无论1号位的初始值是什么,都将其设置为0
MASK中为1的位在结果中都被设置(清空)为0.

  • 5、切换位
    切换位指的是打开已关闭的位,或关闭已打开的位。可以使用按位异或运算符( ^ )。
    因为0 ^ a = a, 1 ^ b = 1;
flags = flags ^ MASK;

如果使用^ 组合一个值和一个掩码,将切换该值与MASK 为1的位相对应的位,该值与MASK为0的位相对应的位不变。
flags 中与MASK为1的位相对应的位都被切换了,MASK为0的位相对应的位不变。

  • 一道变态的面试题:
    不能创建临时变量(第三个变量),实现两个数的交换。
#include<stdio.h>
int main()
{
	int a = 3;
	int b = 5;
	int tmp = a;
	a = b;
	b = tmp;
	printf("%d %d\n", a, b);
	return 0;
}

交换两个数,一般,我们会创建一个临时变量充当中间变量来进行交换,但是现在不允许这么操作。
于是,我们想到了 ^

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	a = a ^ b;
	b = a ^ b;   //b = a ^ b ^ b;
	a = a ^ b;
	printf("%d %d\n", a, b);
	return 0;
}

一个数与其自身按位取反就得到了0
0与任何数按位取反不会改变

  • 下面,是运用位操作符来实现的来显示二进制
//binbit.c --- 使用位操作显示二进制

#include<stdio.h>
#include<limits.h>        //提供 CHAR_BIT 的定义,CHAR_BIT 表示每字节的位数
 
char* itobs(int, char*);
void show_bstr(const char *);

int main()
{
	char bin_str[CHAR_BIT * sizeof(int) + 1] = { 0 };
	int number = 0;

	puts("Enter integers and see them in binary.");
	puts("Non-numeric input terminates program.");
	while (scanf("%d", &number) == 1)
	{
		itobs(number, bin_str);
		printf("%d is ", number);
		show_bstr(bin_str);
		putchar('\n');
	}
	puts("Bye!");
	return 0;
}

char* itobs(int n, char* ps)
{
	int i;
	const static int size = CHAR_BIT * sizeof(int);

	for (i = size - 1; i >= 0; i--, n >>= 1)
	{
		ps[i] = (01 & n) + '0';       //这一步可以把n的最后一位表示出来
		ps[size] = '\0';
	}

	return ps;
}

void show_bstr(const char* str)
{
	int i = 0;

	while (str[i])
	{
		putchar(str[i]);
		if (++i % 4 == 0 && str[i])
		{
			putchar(' ');
		}
	}
}

四、赋值操作符

赋值操作符是一个很棒的操作符,它可以让你得到一个你之前不满意的值,也就是你可以给自己重新符赋值。

int a = 10;
int x = 0;
int y = 20;
a = x = y+1;    //连续赋值

x = y+1;
a = x;
同样的语义,这样的写法更加清晰爽朗且易于调试

注意:赋值操作符是自右向左来运算的。

  • 复合赋值符:
    += 、-=、 *= 、 /= 、 %= 、 >>= 、 <<= 、 &= 、 |= 、 ^=
    写出来的效果更加简洁。

五、单目操作符

逻辑反操作
-负值
+正值
&取地址
sizeof操作数的类型长度(以字节为单位)
~对一个数的二进制按位取反
- -前置、后置- -
++前置、后置++
*间接访问操作符(解引用操作符)
(类型)强制类型转换
  • 逻辑反操作
    正数的逻辑反操作就是将其变为0
int flag = 3;
if(flag)        //flag 为真做什么
;
if(!flag)       //flag为假做什么
;
  • sizeof 和数组
#include<stdio.h>

void test1(int arr[])
{
	printf("%d\n", sizeof(arr));   // 4
}

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));   // 40
	printf("%d\n", sizeof(ch));    //  10
	test1(arr);   
	test2(ch);
	return 0;
}
 

sizeof 后面的括号里单独放数组名,表示整个数组,所以两个数组的大小分别是40和10
当把数组名作为参数传给函数时,传过去的是地址,也就是指针变量,指针变量是4个字节或8个字节

  • 按位取反
    一元运算符~把1变为0,把0变为1
while(~scanf("%d",&n))
{
	;
}

当scanf 读取失败时为EOF,EOF = 1
对1进行按位取反变为 0
while(0)表示循环终止

  • ++和- - 运算符
#include<stdio.h>
int main()
{
	int a = 10;
	int x = ++a;
	printf("%d\n", x);
	//先对a进行自增,然后再使用a,x为11
	int y = --a;
	//先对a进行自减,然后再使用a,y为10
	printf("%d\n", y);
	return 0;
}
#include<stdio.h>
int main()
{
	int a = 10;
	int x = a++;
	//对a先使用,再自增,这样x的值是10,之后a变成11
	int y = a--;
	//对a先使用,再自减,这样y的值是11,之后a变成10
	return 0;
}

上面就是减减和加加的用法,前置后用,后置先用


六、关系操作符

>>=
<<=
!===
  • 警告:
    在编程的过程中 == 和 = 不小心写错,导致的错误。

七、逻辑操作符

  • 逻辑操作符有哪些:
    && ----->逻辑与
    || -------> 逻辑或
    逻辑操作符只关注真假,用来判断真假,真为1,假为0
#include<stdio.h>
int main()
{
	int i = 0, a = 0, b = 2, c = 3, d = 4;
	i = a++ && ++b && d++;
	printf("a = %d b = %d c = %d d = %d\n", a, b, c, d);
	//1  2  3  4 


	i = a++ || ++b || d++;
	printf("a = %d b = %d c = %d d = %d\n", a, b, c, d);
	// 1  3  3  4
	return 0;
}

要想读懂上面这两段代码,就要知道逻辑与和逻辑或的一些规则
&&操作符,左边为假,右边不再计算
||操作符,左边为真,右边不再计算


八、条件操作符

exp1 ?exp2:exp3

使用规则: 当exp1为真,执行exp2;当exp1为假,执行exp3

if(a > 5)
	b = 1;
else
	b = -1;

可以将其转换成条件表达式
b = ((a > 5) ? 1 : -1);
  • 条件表达式的使用使得代码更加清晰和简洁

九、逗号表达式

  • 逗号表达式,就是用逗号隔开的多个表达式
  • 逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果
int main()
{
	int a = 1;
	int b = 2;
	int c = (a > b, a = b + 10, a, b = a + 1);
	printf("%d\n", c);
	return 0;
}

像上面这个例子,从左往右进行运算,但最终表达式的结果是括号中最后一个表达式的结果


十、下标引用、函数调用和结构成员

  • 1、下标引用操作符:[ ]
    操作数:一个数组名 + 一个索引值
int arr[10];    //创建数组
arr[9] = 10;    //使用下标引用操作符
  • 2、函数调用操作符:( )
    接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
void test1()
{
	printf("hehe\n");
}

void test2(const char *str)
{
	printf("%s\n", str);
}

int main()
{
	test1();              //使用()作为函数调用操作符
	test2("hello world.");    //使用()作为函数调用操作符
	return 0;
}
  • 3、访问一个结构的成员
.结构体 . 成员名
->结构体指针 -> 成员名

下面就简单演示了下怎么样使用结构成员访问

struct Stu
{
	char name[10];
	int age;
	char sex[5];
	double score;
};

void set_age1(struct Stu stu)
{
	stu.age = 18;    //访问结构体中的 age 成员
}

void set_age2(struct Stu* pStu)
{
	pStu->age = 19;
}

int main()
{
	struct Stu stu;
	struct Stu* pStu = &stu;
	
	stu.age = 21;
	set_age1(stu);

	pStu->age = 20;
	set_age2(pStu);
	printf("%d", stu.age);
	return 0;
}

12、表达式求值

  • 表达式求值的顺序一部分是由操作符的优先级和结合性决定
  • 同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。也就是int类型

12.1、隐式类型转换

  • C的整型算术运算总是至少以缺省整形类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升
  • 整型提升的意义
    表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
    因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
  • 如何进行整型提升呢?
    整型提升是按照变量的数据类型的符号位来提升的
    整型提升的时候,高位补充符号位
int main()
{
	char a = 3;
	//00000011                            - 先进行阶段,char只有8个比特位
	//00000000000000000000000000000011        -进行整形提升
	
	char b = 127; 
	//01111111
	//00000000000000000000000001111111

	char c = a + b;
	//10000010
	//11111111111111111111111110000010     -整型提升的时候,高位补充符号位

	printf("%d\n", c );          //  -126
	return 0;
}
  • 下面的代码就很好地验证了会发生整型提升,代码中的a和b要进行整型提升,但是c不需要整型提升,因为其本身就是int类型。a,b进行整形提升之后,变成了负数。
int main()
{
	char a = 0xb6;
	short b = 0xb600;
	int c = 0xb6000000;
	if (a == 0xb6)
		printf("a");
	if (b == 0x600)
		printf("b");
	if (c == 0xb6000000)
		printf("c");
	return 0;
}
  • 下面的代码中表明C只要参与表达式运算,就会发生整型提升,表达式+c,就会发生提升,所以sizeof(+c)是4个字节。表达式 -c 也会发生整型提升,所以sizeof(-c)是4个字节。
int main()
{
	char c = 1;
	printf("%u\n", sizeof(c));    //1
	printf("%u\n", sizeof(+c));   //4
	printf("%u\n", sizeof(-c));   //4
	return 0;
}

12.2、算术转换

  • 如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
    在这里插入图片描述
    两个不同类型的操作数,排名低的操作数类型要转换成另一个操作数类型高后才执行运算
12.3、操作符的属性
  • 复杂表达式的求值有三个影响的因素:
    1、操作符的优先级
    2、操作符的结合性
    3、是否控制求值顺序
  • 两个相邻的操作符先执行哪个?
    取决于它们的优先级。如果两者的优先级相同,那么久取决于它们的结合性
  • 下面这个表是各个操作符的优先级排序:
操作符描述结合性是否控制求值顺序
()聚组N/A
()函数调用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
&取地址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按位或L-R
&&逻辑与L-R
ll逻辑或L-R
? :条件操作符N/A
=赋值R-L
+=加等R-L
-=减等R-L
*=乘等R-L
/=除等R-L
%=取模等R-L
<<=左移等R-L
>>=右移等R-L
&=按位与等R-L
^=按位异或等R-L
l=按位或等R-L
逗号L-R
  • 一些问题表达式
//代码1
a*b + c*d + e*f

在这里插入图片描述
上面代码在计算的时候,由于 * 比 + 的优先级高,所以只能保证 * 的计算比 + 早,但是优先级并不能决定第三个 * 比第一个 + 早执行。,所以,这是一个非法的表达式

//代码2
c + --c;

同上,操作符的优先级只能决定自减 - - 的运算在 + 的运算的前面,但是我们并没有办法得知,+ 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。

//代码3
int fun()
{
	static int count = 1;
	return ++count;
}

int main()
{
	int answer;
	answer = fun() - fun() * fun();
	printf("%d\n", answer);
	return 0;                          //  -10
}
  • 虽然在大多数的编译器上求结果都是相同的。
    但是上述代码 answer = fun( ) - fun( ) * fun( );中我们只能通过操作符的优先级得知:
    先算乘法,再算减法。
    函数的调用先后顺序无法通过操作符的优先级确定。

  • 我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。


总结

这一章,我们学习了各种操作符的用法和注意事项
二进制的表示和存储,
以及整型算术中的整型提升和隐式类型转换
位操作符的特点和运用场景
操作符的优先级和结合性对表达式的影响

  • 8
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值