目录
- 1.算术操作符
- 2.移位操作符
- 2.1左移操作符<<
- 2.2右移操作符>>
- 3.位操作符
- 4.赋值操作符
- 5.单目操作符
- 5.1单目操作符介绍
- 5.2sizeof 和 数组
- 6.关系操作符
- 7.逻辑操作符
- 8.条件操作符(三目操作符)
- 9.逗号表达式
- 10.下标引用、函数调用和结构体
- 11.表达式传值
- 11.1 隐式类型转换
- 11.2 算术转换
- 12.3 操作符的属性
- 12.优先级
1.算术操作符
操作符 | 含义 | 范例 | 结果 |
---|---|---|---|
+ | 加法 | x+y | x和y的和 |
– | 减法 | x–y | x和y的差 |
* | 乘法 | x * y | x和y的积 |
/ | 除法 | x / y | x和y的商 |
% | 求余 | x%y | x除以y的余数 |
注意:
(1)运算符%要求左右两个运算数据必须为整型数据,如5%2的值为3。
(2)相除时,结果为整数,小数部分舍去。但若除数或被除数中有一个为负值,则舍入的方向是不固定的。如5/3=1,但–5/3在有的计算机得到的结果是–1,而有的计算机上的结果是–2。C99规定采取 “向零取整” 的方法,即取整后向零取整。
(3)字符型数据可以和数值型数据进行运算,因为字符型数据在计算机中是用一个字节的整型数(ASCII码)表示的。如 ‘A’+1在进行运算时会把A的ASCII码65与1相加,最后得出结果就是66。
2.移位操作符
移的是二进制(补码)。
整数的二进制有三种表示法:
- 原码
- 补码
- 反码
浮点数还有一种移码,移码通常用于表示浮点数的阶码。(计算机组成原理会讲,咱们这里先做了解)
整数在内存中存储的是补码。
编码 | 正数 | 负数 |
---|---|---|
真值 | +(二进制) | –(二进制) |
原码 | 高位补0,符号位补0 | 高位补0,符号位补1 |
反码 | 与原码相同 | 原码的符号位不变,其他位按位取反(0→1,1→0) |
补码 | 与原码相同 | 原码取反+1(反码+1) |
移码 | 符号位变1,其他位与补码相同 | 符号位变0,其他位与补码相同 |
例:演示只用8位,最高一位为符号位(已加粗)
编码 | 5 | –5 |
---|---|---|
真值 | +0101 | –0101 |
原码 | 00000101 | 10000101 |
反码 | 00000101 | 11111010 |
补码 | 00000101 | 11111011 |
移码 | 10000101 | 01111011 |
移位操作符的操作数只能是整数。
2.1左移操作符<<
规则:左边抛弃,右边补0
例1:5<<1
5的补码变为了00001010,原码为00001010,其二进制数为1010,转换为十进制:10。所以5左移一位后变成了10。
例2:–5<<1
–5的补码变为了11110100,反码为11110011,原码为10001100,其二进制数为–1100,转换为十进制:–12。所以–5左移一位后变成了–12。
2.2右移操作符>>
规则:
首先右移运算分两种:
- 逻辑移位
左边用0填充,右边丢弃- 算术移位
左边用原该值的符号位填充,右边丢弃
警告⚠ :对于移位运算符,不要移动负数位,这个是标准未定义的。
例:
int num = 10;
num>>-1;//错误
3.位操作符
位操作符 | 含义 | 范例 |
---|---|---|
& | 按位与 | x & y |
∣ | 按位或 | x ∣ y |
^ | 按位异或 | x ^ y |
注:他们的操作数必须是整数。
#include <stdio.h>
int main()
{
int num1 = 1;
int num2 = 2;
num1 & num2;
num1 | num2;
num1 ^ num2;
return 0;
}
4.赋值操作符
(1)简单赋值运算
“ = ”的作用是将赋值符号右边的对象的值赋值给左边的对象。例:x = 9;
把值9赋给x,实际意义是将10存储到x的存储单元中。
此处“ = ”是赋值符,而非等于号。等于号是“ == ”,例如:if(x==10),意思是如果x等于10。
注:①10 = x;是无效语句。赋值运算符的左侧必须是一个运算对象,此对象的值可以改变。10是整数常量,不能改变其值,不能给常量赋值。
②若参与赋值运算的运算对象的数据类型不同,则右边对象的数据类型会被转换成左侧对象的类型。
③可以连续赋值:x = y = 10;
(2)复合赋值运算
在赋值运算符前加上其他运算符,例如在“ = ” 前加上“ + ”,运算符就变成了复合运算符“ += ”。
例:x += 10;
相当于 x = x+10;
其他算术运算符也可以与赋值运算符组合成复合赋值运算符。
5.单目操作符
5.1单目操作符介绍
单目操作符 | 含义 | 范例 |
---|---|---|
! | 逻辑反操作 | !a |
+ | 正号 | +x |
– | 负号 | –x |
& | 取地址 | &a |
sizeof | 操作数的类型长度(以字节为单位) | sizeof(arr) |
~ | 对一个数的二进制按位取反 | ~a |
++ | 前置、后置++ | ++a 或 a++ |
– – | 前置、后置 – – | – – a 或 a – – |
* | 间接访问操作符(解引用操作符) | *p |
(数据类型) | 强制类型转换 | (int)a |
#include <stdio.h>
int main()
{
int a = -10;
int *p = NULL;
printf("%d\n", !2);
printf("%d\n", !0);
a = -a;
p = &a;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(int));
printf("%d\n", sizeof a);//这样写行不行?
printf("%d\n", sizeof int);//这样写行不行?
return 0;
}
自增运算符++
当 i 变量每次都要加1时,可写为 i++,或++i
相当于 i = i +1;
自减运算符 – –
当 i 变量每次都要减1时,可写为 i – – 或 – – i
相当于 i = i – 1;
自增、自减运算符的结合性是“ 从右到左 ”。x * y++ 相当于x * (y++),而并非是(x * y)++。
//++和--运算符
//前置++和--
#include <stdio.h>
int main()
{
int a = 10;
int x = ++a;
//先对a进行自增,然后对使用a,也就是表达式的值是a自增之后的值。x为11。
int y = --a;
//先对a进行自减,然后对使用a,也就是表达式的值是a自减之后的值。y为10;
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;
}
5.2sizeof 和 数组
一般形式:sizeof(运算对象)
圆括号可有可无,但是当运算对象是数据类型是,必须加上圆括号。
例如:sizeof(int), sizeof x, sizeof(x), sizeof 34, sizeof(34)
sizeof的结合性是“ 从右到左 ”。
如果运算对象是表达式,那表达式将不会被执行,只会输出此表达式的数据类型所占的字节数。
#include<stdio.h>
int main()
{
int a = 12,b = 1;
printf("%zd\n",sizeof(a = a+b);
printf("a = %d\n",a);
return 0;
}
运算结果:
4
a = 12
sizeof运算的结果是一个无符号整数类型。C99新增了转换说明%zd用于输出 sizeof 运算结果的值。如果编译器不支持使用%zd,可以使用 %u 或 %lu 代替 %zd。
#include <stdio.h>
void test1(int arr[])
{
printf("%d\n", sizeof(arr));//(2)
}
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));//(1)
printf("%d\n", sizeof(ch));//(3)
test1(arr);
test2(ch);
return 0;
}
问:
(1)、(2)两个地方分别输出多少?
(3)、(4)两个地方分别输出多少?
答:
(1)、(2)两个地方分别输出多少?——40、4/8
(3)、(4)两个地方分别输出多少?——10、4/8
(1)、(3)= 数组大小 × 数据类型所占字节
(2)、(4)= 指针大小(4/8字节)
(2)、(4)为什么是指针大小?
test1(arr) 和 test2(ch) 其实传的都是地址,又可以写成test1(&arr) 和 test2(&ch),那函数接收地址则是用的指针,指针大小为4byte或8byte(取决于编译器)。
具体的我们下一篇指针部分详细讲解。
6.关系操作符
关系操作符 | 含义 | 范例 |
---|---|---|
< | 小于 | x<y |
> | 大于 | x>y |
<= | 小于或等于 | x<=y |
>= | 大于或等于 | x>=y |
== | 等于 | x==y |
!= | 不等于 | x!=y |
关系运算符的结合性是“ 从左到右 ”。a < b > c <= d 与( (a < b) > c) <= d 相同。
注意:浮点数之间进行比较时,尽量只使用 “ < ” 和 “ > ”。因为浮点数使用近似值表示的,这会导致逻辑上相等的两数却不相等。
7.逻辑操作符
逻辑运算符 | 含义 | 范例 |
---|---|---|
&& | 逻辑与 | x && y |
∣∣ | 逻辑或 | x ∣∣ y |
! | 逻辑非 | !x |
在逻辑运算中,如果运算对象的值为非0,则认为是逻辑真,否则认为是逻辑假。真用 1 表示,假用 0 表示。
逻辑非
例:int a=5;
!a的结果为0,因为a不等于0。
a | b | !a | !b | a&&b | a∣∣b |
---|---|---|---|---|---|
真 | 真 | 假 | 假 | 真 | 真 |
真 | 假 | 假 | 真 | 假 | 真 |
假 | 真 | 真 | 假 | 假 | 真 |
假 | 假 | 真 | 真 | 假 | 假 |
运算符 ! 的结合性是“ 从右到左 ”,而&&和 || 的结合性是“ 从左到右 ”。
在多个&&(或 ||)相连时,因结合性是从左到右,故当左边的运算对象为假(或真),停止运算。
8.条件操作符(三目操作符)
条件运算符的一般形式为:
表达式1 ?表达式2 :表达式3
条件运算符 ?:的功能是:在计算表达式1后,如果值为真(非0),则计算表达式2,并将表达式2的值作为整个表达式的结果;如果表达式1的值为假(0),则计算表达式3,并将表达式3的结果作为整个表达式的值。
x=10;
if(x > 8)
y=20;
else
y=10;
那我们可以用条件表达式来处理:
y = (x > 8) ? 20 : 10;
因x=10,比8大。故y的值为20,如果x所赋的值小于等于8,y的值赋为10。
9.逗号表达式
逗号运算符(,)用于将两个表达式连接时。如:
a+b , a+c
一般形式:表达式1 , 表达式2
求解过程:先求表达式1,再求表达式2。整个逗号表达式的值是表达式2的值。
一个逗号表达式又可以与另一个表达式组成一个新的逗号表达式。所以逗号表达式的一般形式又可拓展为:
表达式1, 表达式2, 表达式3, … , 表达式 n
注意:逗号也用作分隔符。下面代码中的逗号都是分隔符,而非逗号运算符。
int a,b;
printf("%d %d",a,b);
10.下标引用、函数调用和结构体
[]
下标引用操作符
操作数:一个数组名 + 一个索引值
int arr[10];//创建数组
arr[9] = 10;//实用下标引用操作符。
[ ]的两个操作数是arr和9。
()
函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include <stdio.h>
void test1()
{
printf("hehe\n");
}
void test2(const char *str)
{
printf("%s\n", str);
}
int main()
{
test1(); //实用()作为函数调用操作符。
test2("hello bit.");//实用()作为函数调用操作符。
return 0;
}
- 访问结构体成员变量
.
:结构体 . 成员名->
:结构体指针 -> 成员名
#include <stdio.h>
struct Stu
{
char name[10];
int age;
char sex[5];
double score;
};
void set_age1(struct Stu stu)
{
stu.age = 18;
}
void set_age2(struct Stu* pStu)
{
pStu->age = 18;//结构成员访问
}
int main()
{
struct Stu stu;
struct Stu* pStu = &stu;//结构成员访问
stu.age = 20;//结构成员访问
set_age1(stu);
pStu->age = 20;//结构成员访问
set_age2(pStu);
return 0;
}
11.表达式传值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
11.1 隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
-
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purposeCPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int
或 unsigned int,然后才能送入CPU去执行运算。
//实例1
char a,b,c;
...
a = b + c;
b和c的值被提升为普通整型,然后再执行加法运算。加法运算完成之后,结果将被截断,然后再存储于a中。
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的
- 负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111- 正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001- 无符号整形提升,高位补0
11.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
警告:
但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
12.3 操作符的属性
复杂表达式的求值有三个影响的因素。
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
12.优先级
运算符优先级图源:https://www.wendangwang.com/doc/75d2728bbb843fed98536860