文章目录
前言
各种操作符的介绍
在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个二进制位。
八进制位 | 等价的二进制位 |
---|---|
0 | 000 |
1 | 001 |
2 | 010 |
3 | 011 |
4 | 100 |
5 | 101 |
6 | 110 |
7 | 111 |
- 十六进制
十六进制(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位字节。因此,十六进制很适合表示字节值。
十六进制 | 等价二进制 |
---|---|
0 | 0000 |
1 | 0001 |
2 | 0010 |
3 | 0110 |
4 | 0100 |
5 | 0101 |
… | … |
F | 1111 |
- 通常,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( );中我们只能通过操作符的优先级得知:
先算乘法,再算减法。
函数的调用先后顺序无法通过操作符的优先级确定。 -
我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
总结
这一章,我们学习了各种操作符的用法和注意事项
二进制的表示和存储,
以及整型算术中的整型提升和隐式类型转换
位操作符的特点和运用场景
操作符的优先级和结合性对表达式的影响