目录
- 一、操作符的分类
- 二、算数操作符
- 1. 除法操作符
- 2. 取余操作符
- 三、位移操作符
- 1. 进制
- 2. 原码、反码和补码
- 3. 左移操作符(<<)和右移操作符(>>)
- 四、位操作符
- 1. 按位与 &
- 2. 按位或 |
- 3. 按位异或 ^
- 4. 按位取反 ~
- 五、单目操作符
- 1. 逻辑非(!)
- 2. 自增操作符(++)
- 3. 自减操作符(--)
- 4. 取地址操作符(&)
- 5. 解引用操作符(\*)
- 6. 正号(+)和负号(-)
- 7. sizeof 操作符
- 8. 强制类型转换操作符——(类型)
- 六、逗号表达式
- 七、下标引用操作符([])和函数调用操作符(())
- 八、结构成员访问操作符(.)
- 1. 结构体简介
- 2. 使用结构体成员访问操作符对结构体变量进行访问
- 九、操作符的优先级和结合性
- 十、表达式求值
- 1. 整型提升
- 2. 算数转换
- 3. 问题表达式解析
一、操作符的分类
• 算术操作符: + 、- 、* 、/ 、%
• 移位操作符: << 、>>
• 位操作符: & 、| 、^
• 赋值操作符:= 、+= 、 -= 、 *= 、 /= 、%= 、<<= 、>>= 、&= 、|= 、^=
• 单⽬操作符: !、++、–、&、*、+、-、~ 、sizeof、(类型)
• 关系操作符: > 、>= 、< 、<= 、 == 、 !=
• 逻辑操作符: && 、||
• 条件操作符: ? :
• 逗号表达式: ,
• 下标引⽤: []
• 函数调⽤: ()
以上操作符有些初始 C 语言的时候已经介绍过了,这次复习一下。
二、算数操作符
加法、减法和乘法操作符就不过多叙述,这里主要介绍除法和取余操作符。
1. 除法操作符
当两个操作对象有一个是浮点数时,执行浮点数的除法,也就是带小数的。当两个操作对象都是整数时,结果为商舍弃余数。如:5 / 3 = 1,余数 2 舍弃;而 5.0 / 3 = 1.6666。如下代码:
2. 取余操作符
首先,取余操作符的两个操作对象必须为整数,然后其运算结果为第一个操作对象除以第二个操作对象的余数。如:5 % 3 = 2,本来是商 1 余 2,这里只取余数。如下代码:
三、位移操作符
位移操作符是作用于整数的二进制位数的。在对操作符进行说明之前,需要补充一下进制和原码、反码、补码的相关知识。
1. 进制
我们日常生活中使用的数都是采用十进制,如:10、20、99等。但是在计算机中,数据都是采用二进制进行存储的。其实不同的进制之间除了表达方式不同,其实质并没有差异。如:二进制 1111 和十进制 15,都表示数值 15,就是表达形式不同。
(1)其他进制转十进制
其他进制转 10 进制,只要每位乘以相应的权重即可,如:二进制 1111 转十进制
(2)十进制转其他进制
除以相应进制数取余,然后逆序输出。如:十进制 15 转二进制,
15 / 2 商 7 余 1
7 / 2 商 3 余 1
3 / 2 商 1 余 1
1 / 2 商 0 余1
然后倒着输出余数,就是二进制 1111。如下是一个函数,接受一个整数输出其二进制数:
// 输出整数的二进制
// 采用递归的方法
void binary(int n)
{
if (n > 0)
{
int remain = n % 2;
binary(n / 2);
printf("%d", remain);
}
}
采用尾递归的方法更加方便逆序输出。
2. 原码、反码和补码
计算机中数据的存储都是采用二进制的形式,而二进制又分为原码,反码和补码。而正整数的这三种形式相同,如:int a = 10
原码:00000000000000000000000000001010
反码:00000000000000000000000000001010
补码:00000000000000000000000000001010
而负整数的反码和补码需要计算,反码是原码符号位不变,其余位取反;而补码是反码加 1,如:int b = -10;
原码:10000000000000000000000000001010
反码:111111111111111111111111111111110101
补码:111111111111111111111111111111110110
从上述 10 和 -10 的原码,不难得出最高位为符号位,且 1 表示负数,0 表示非负数。当然这是有符号整数,无符号整数的最高位仍参与计算,因为无符号整数没有负数。现在的 int 类型大多为 32 位,所需上述使用的是 32 为二进制数。
3. 左移操作符(<<)和右移操作符(>>)
顾名思义,左移操作符把被操作对象的二进制数左移,右移操作符把被操作对象的二进制数右移。且左移和右移操作符均针对整数的二进制补码进行操作。
(1)左移操作符(<<)
表达式 5<<1 的意思是把整数 5 的二进制数左移一位,如下:
左边超出的位数去掉,右边缺少的位数补 0,则左移一位后的结果为:
00000000000000000000000000001010
结果为十进制的 10,相当于原来的两倍。其实也很好理解,如:十进制 10 向左移动一位结果为 100,是原来的 10 倍。所以得出结论,当该正整数左移 n 位结果不超过该类型的范围时,其结果为原来的 2 的 n 次方倍。
左移负整数时,移动的是该负整数的补码,但是所得结果是该负整数的原码,这时就要进行计算。如:-5<<1
原码:10000000000000000000000000000101
反码:111111111111111111111111111111111010
补码:111111111111111111111111111111111011
左移一位:11111111111111111111111111110110
上述左移一位的结果仍是补码,这里要通过补码计算出原码,有两种方法:
(1)补码取反加 1
(2)补码减 1 取反
得出结果的原码:10000000000000000000000000001010,也就是 10 进制的 -10。
结论:当一个正整数左移 n 位时,其结果若不超出该类型的范围,那么结果是原数的 2 的 n 次方倍。但是对于负整数来说不一定。
(2)右移操作符(>>)
右移操作符和左移操作符类似,但是右移操作符分为:算数右移和逻辑右移。算数右移右边丢弃,左边补符号位;而逻辑右移左边补 0。但是大多情况下编译器使用的都是算数右移。
如:5>>1
补码:00000000000000000000000000000101
右移:00000000000000000000000000000010
结果:2
如:-5>>1
原码:10000000000000000000000000000101
反码:111111111111111111111111111111111010
补码:111111111111111111111111111111111011
右移:111111111111111111111111111111111101
原码:10000000000000000000000000000011
结果:-3
结论:对于正整数来说,右移 n 位,相当于除以 2 的 n 次方(取整数部分)。对于负整数来说不一定。
四、位操作符
位操作符有:
(1)按位与 &
(2)按位或 |
(3)按位异或 ^
(4)按位取反 ~
它们也是作用于整数的二进制数。
1. 按位与 &
如表达式 5 & 8 就是把两个数的 32 为二进制展开,一一对应,对应位均为 1 则为 1,否则为 0。如:
8:00000000000000000000000000001000
5:00000000000000000000000000000101
00000000000000000000000000000000
结果为 0。
2. 按位或 |
和按位与类似,但是对应位上只要有 1 则为 1,否则为 0。如: 5 | 8
8:00000000000000000000000000001000
5:00000000000000000000000000000101
00000000000000000000000000001101
结果为 13。
3. 按位异或 ^
按位异或是对应位上相同为 0,相异为 1。如:5 ^ 8
8:00000000000000000000000000001000
5:00000000000000000000000000000101
00000000000000000000000000001101
结果为 13。
4. 按位取反 ~
按位取反是把操作数的每一个二进制数都取反,包括符号位。如:~5
补码:00000000000000000000000000000101
取反:11111111111111111111111111111010
去反后得到的是补码,计算结果需要原码,所以进行计算:
原码:10000000000000000000000000000110,结果为 -6
五、单目操作符
单⽬操作符有这些:
!、++、–、&、*、+、-、~ 、sizeof、(类型)
1. 逻辑非(!)
把操作对象的逻辑取反,真变假,假变真。如:
2. 自增操作符(++)
自增操作符分为前置和后置。前置自增操作符先对操作数加 1,再使用。而后置自增操作符先使用操作数,然后再对操作数假 1。如:
前置:
int a = 5;
int b = ++a;
相当于
int a = 5;
a = a + 1;
int b = a;
后置:
int a = 5;
int b = a++;
相当于
int a = 5;
int b = a;
a = a + 1;
3. 自减操作符(–)
和自增操作符类似。前置自减操作符先对操作数减 1,然后使用。后置自减操作符,先使用操作数,然后再让操作数加 1。
4. 取地址操作符(&)
取出操作对象的地址,使用格式 %p 进行输出。如:
当然也可以使用指针变量。
5. 解引用操作符(*)
解引用操作符作用于指针变量,对该指针进行解引用找到其指向的对象。被解引用的指针必须是有效的,否则可能导致程序崩溃。
6. 正号(+)和负号(-)
正号几乎不使用,没啥用。符号就是把操作数取负,正数变负数,负数变正数。
7. sizeof 操作符
sizeof 操作符计算操作对象的大小,单位字节。当计算对象是类型时,必须加括号,当计算类型是一个变量时,括号可加可不加。如下:
对变量和对该变量的类型使用 sizeof 操作符的结果是一样的。
8. 强制类型转换操作符——(类型)
强制类型转换操作符把操作对象强制转换为需要的类型,如:int a = (int)3.14,该表达式把 double 值 3.14 强制类型转换为 int 值。在编写程序的过程中应该尽量不要使用强制类型转换。
六、逗号表达式
逗号表达式是用逗号隔开的多个表达式:
expr1, expr2, expr3, …, exprn
逗号表达式从左向右求值依次对每个表达式进行求值,但逗号表达式的最终结果为最右边表达式的值。如下代码:
int a = 2, b = 3;
int c = (a++, ++b);
由于赋值运算符的优先级要高于逗号表达式,所以在使用时需要加上括号。先计算a++,a 的值为 3,然后计算++b,b 的值为 4,所以逗号表达式的值为 4,赋值给 c。
七、下标引用操作符([])和函数调用操作符(())
下标引用操作符主要是给数组使用的,其有两个操作对象 —— 数组名和下标。作用为:获取该数组的该下标处的元素。
函数调用操作的操作对象至少有一个,即函数名和任意参数。其作用为调用该函数。
八、结构成员访问操作符(.)
1. 结构体简介
结构体是一种自定义类型,它可以包含多种不同类型。比如我们想要记录一个学生的信息:姓名、性别、年龄。就可以声明如下结构类型:
// 学生结构体声明
struct Stu {
char name[20]; // 姓名
char sex[5]; // 性别
int age; // 年龄
};
上述代码只是一个类型的声明,就是告诉编译器有这么一个类型,它里面包含什么信息。然后我们就可以像创建 int 变量一样创建该类型的变量。
// 创建 stuct Stu 变量并初始化
struct Stu Lihua = { "李华", "男", 18 };
前面的 struct 不能省略。如果声明放在一个函数中,那么该结构体就是局部结构体,只能在该函数中创建该结构体变量。如果在所有函数之外声明该结构体,那么该结构体就是全局结构体,可以在全局创建该结构体变量。
2. 使用结构体成员访问操作符对结构体变量进行访问
我们可以直接使用成员访问运算符来访问上述创建的 Lihua 变量的每个成员,如:
Lihua.name
Lihua.sex
Lihua.age
使用它们就和使用原来的类型一样,下面使用 printf() 函数显示其信息:
也可以定义一个打印函数,这样每次需要打印信息的时候调用一下该函数就好了,参数传递有两种形式,值传递和址传递。如下:
// 打印结构体 struct Stu
void PrintStu1(struct Stu s); // 值传递
void PrintStu2(struct Stu* ps); // 址传递
值传递:被调函数中的形参是主调函数中实参的副本(也就是重新创建了一个结构体变量,把实参的值拷贝过来了而已)。其缺点是,当结构体所占空间较大时,会造成时间和空间的损失;优点是不会修改实参。
址传递:把结构体的地址传递给了被调函数,被调函数通过该指针使用指向结构体成员运算符(->)来对主调函数的实参进行访问。这样只传递了一个指针,大大提高了运行效率。缺点是:可以修改实参(可以加上 const 防止被修改)。
下面是两个函数的函数定义:
// 打印结构体 struct Stu
void PrintStu1(struct Stu s) // 值传递
{
printf("姓名:%s\n", s.name);
printf("性别:%s\n", s.sex);
printf("年龄:%d\n", s.age);
}
void PrintStu2(struct Stu* ps) // 址传递
{
printf("姓名:%s\n", ps->name);
printf("性别:%s\n", ps->sex);
printf("年龄:%d\n", ps->age);
}
九、操作符的优先级和结合性
众所周知先乘除后加减,这是因为乘除的优先级比加减高,所以在同一个表达式中出现不同的操作符时,根据优先级进行先后计算。但是当优先级相同时,就需要根据操作符的结合型进行计算。如:3+4*5+10+6
上述表达式先计算 4*5,因为乘法的优先级高于加法,然后计算 3 + 4*5,因为加法操作符的结合性是从左到右,然后 + 10,再 + 6。
下面是常用操作符的优先级表:
上面这张图是比特 C 语言课程的课件哈,有兴趣的小伙伴可以了解一下比特,作者并没有打广告,个人觉得比特 C 语言的课程讲的很好。
十、表达式求值
1. 整型提升
当 char、short等短整型进行算数运算时,它们的值就会被提升为 int 类型,然后参与算数运算,该行为被称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执⾏,CPU内整型运算器(ALU)的操作数的字节⻓度⼀般就是int的字节⻓度,同时也是CPU的通⽤寄存器的⻓度。因此,即使两个char类型的相加,在CPU执⾏时实际上也要先转换为CPU内整型操作数的标准⻓度。通⽤CPU(general-purpose CPU)是难以直接实现两个8⽐特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种⻓度可能⼩于int⻓度的整型值,都必须先转换为int或unsigned int,然后才能送⼊CPU去执⾏运算。
整型提升的规则:有符号整型提升补符号位,无符号整型提升补 0。
如:
char a = -1;
short b = 2;
a + b;
进行 a + b 时,先对 a 和 b 进行整形提升
a:11111111
提升:111111111111111111111111111111111 结果为 -1
b:0000000000000010
提升:00000000000000000000000000000010 结果为 2
提升后相加:1
2. 算数转换
当操作符来两边出现不同类型的对象时,那么其中一个操作数就需要转换为另一个操作数的类型,否则无法进行计算。一般按照下面的顺序由低向高转换:
(1)long double
(2)double
(3)float
(4)unsigned long
(5)long
(6)unsigned
(7)int
3. 问题表达式解析
虽然我们已经学习了以上种种与表达式求值有关的知识,但是仍然有不少表达式我们是无法求出确切的值,而且我们在编写代码的时候也尽量不要编写这些代码。
(1)类型1
int i = 1;
(i++) + (i++) + (i++);
该表达式只能确定后置递增运算符在加法之前,但是不能确定先算计算几个递增运算符,我可以先计算三个 i++,然后 i 的值为 4,然后相加得出表达式的结果为 12,;我也可以先计算前面两个 i++ 然后 i 的值为 3,前面两个 i 相加得 6,再加最后一个 i++,表达式结果为 9,i 的值最终为 4。
所以尽量不要编写上述类型的代码。
(2)类型2
#include <stdio.h>
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);//输出多少?
return 0;
}
虽然大多数编译器上面求得该表达式的结果相同,但是我们只能确定乘法在加法之前,但是却不能得出三个函数的调用顺序,我可以先调用第一个函数,也可以先调用第二个。