如果你从未接触过C语言, 那么我建议你先阅读前面的文章:
操作符
在认识条件分支和循环语句的文章↗中, 介绍了逻辑运算符&& || !
和关系运算符== != >= <= > <
这两类运算符是C语言操作符的一部分
什么是操作符
操作符(Operator) 是C语言中用于对数据(变量、常量等)执行特定操作的符号或关键字
简单来说, C语言中的操作符就是用来操作数据的, 特定的操作符可以对数据实现特定的操作
操作符介绍
C语言存在许多操作符, 本篇文章只做简单介绍, 某些操作符的具体使用方法, 在之后的文章中会涉及
算术运算符
C语言提供的算术运算符有:
运算符 | 功能 | 示例 |
---|---|---|
+ | 加, 实现两数相加 | 1 + 1 => 2 |
- | 减, 实现两数相减 | 1 - 1 => 0 |
* | 乘, 实现两数相乘 | 2 * 2 => 4 |
/ | 除, 实现两数相除 | 2 / 2 => 1 |
% | 模, 取模, 即对整数求余数 | 5 % 2 => 1 |
移位运算符
移位运算符, 顾名思义是操作在数据的二进制层面的, 用于对数据二进制位进行左移和右移
运算符 | 功能 | 示例 |
---|---|---|
<< | 左移位操作符, 实现将数据二进制位向左移动 | 1 << 2 => 4 |
>> | 右移位操作符, 实现将数据二进制位向右移动 | 8 >> 2 => 2 |
移位操作符, 就是将数据 在二进制位的层面上 向左或向右移动n
位
移位操作符可能有一些抽象, 如果对二进制不是很了解, 可能不容易理解
不过也可以通过例子简单理解一下:
/*
* 以 4(int) 为例
* 4 的 二进制是 00000000 00000000 00000000 00000100 (32位)
* 4 << 5, 就是 二进制位左移5位, 得:
* 00000000 00000000 00000000 00000000 10000000, 128
*
* 4 >> 2, 就是 二进制位右移2位, 得:
* 00000000 00000000 00000000 00000000 00000001, 1
*/
有关移位操作还有一些细节与数据类型有关, 暂不做更多分析
但要注意, <<
左移时 移动的位数只能小于数据类型大小, 否则就是C语言标准未定义的行为, 会出现未知的问题
// 左移的错误例子
#include <stdio.h>
int main() {
int age = 2;
printf("%u\n", age << 32);
return 0;
}
如果左移位数不小于数据类型的大小, 编译时也会提醒
位运算符
位操作符, 也是操作在数据的二进制层面的, 但并不是移动:
运算符 | 功能 | 示例 |
---|---|---|
& | 按位与, 对两数二进制位执行与操作, 对应位置均为1, 结果位才为1 | 2 & 3 => 2 |
| | 按位或, 对两数二进制位执行或操作, 对应位置均为0, 结果位才为0 | 2 | 4 => 6 |
~ | 按位取反, 对数据二进制位执行取反操作, 对数据每位进行取反操作, 即 1变0, 0变1 | ~1 => -2 (有符号) |
^ | 按位异或, 对两数二进制数据执行异或操作, 对应位置不同, 结果位才为1 | 2 ^ 3 => 1 |
位操作符, 比移位操作符要更加抽象一些, 不过依旧可以通过例子进行理解:
/*
* 以 4(int) 和 7(int) 为例
* 4: 00000000 00000000 00000000 00000100
* 7: 00000000 00000000 00000000 00000111
*
* 4 & 7 (与):
* 00000000 00000000 00000000 00000100
* 00000000 00000000 00000000 00000111
* -----------------------------------
* 00000000 00000000 00000000 00000100 (对应位均为1, 结果位才为1)
*
* 4 | 7 (或):
* 00000000 00000000 00000000 00000100
* 00000000 00000000 00000000 00000111
* -----------------------------------
* 00000000 00000000 00000000 00000111 (对应位均为0, 结果位才为0)
*
* 4 ^ 7 (异或):
* 00000000 00000000 00000000 00000100
* 00000000 00000000 00000000 00000111
* -----------------------------------
* 00000000 00000000 00000000 00000011 (对应位不同, 结果位才为1)
*
* ~4 (取反);
* 00000000 00000000 00000000 00000100
* -----------------------------------
* 11111111 11111111 11111111 11111011 (对应位取反)
*/
仔细观察一下, 比较容易理解的
赋值运算符
C语言提供了赋值运算符, 可以给变量赋值: =
用法很简单:
#include <stdio.h>
int main() {
int age;
age = 20;
printf("%d\n", age);
return 0;
}
这段代码, age = 20;
给age
变量赋值20
除此之外, C语言还提供了一些赋值运算符的扩展, 被称为复合赋值运算符:
运算符 | 功能 | 示例 | 等价形式 |
---|---|---|---|
= | 赋值, 将右侧的值 赋给左侧变量 | a = 5; | a = 5; |
+= | 加后赋值, 左侧变量 加 右侧值 再赋给左侧 | a += 3; | a = a + 3; |
-= | 减后赋值, 左侧变量 减 右侧值再赋给左侧 | a -= 2; | a = a - 2; |
*= | 乘后赋值, 左侧变量 乘 右侧值 再赋给左侧 | a *= 4; | a = a * 4; |
/= | 除后赋值, 左侧变量 除以 右侧值 再赋给左侧 | a /= 2; | a = a / 2; |
%= | 取模后赋值, 左侧变量 对 右侧值取模 再赋给左侧 | a %= 3; | a = a % 3; |
&= | 按位与后赋值, 左侧变量 与右侧值按位与 再赋给左侧 | a &= b; | a = a & b; |
` | =` | 按位或后赋值, 左侧变量 与右侧值按位或 再赋给左侧 | `a |
^= | 按位异或后赋值, 左侧变量 与右侧值按位异或 再赋给左侧 | a ^= b; | a = a ^ b; |
<<= | 左移后赋值, 左侧变量 左移 右侧值位 再赋给左侧 | a <<= 2; | a = a << 2; |
>>= | 右移后赋值, 左侧变量 右移 右侧值位 再赋给左侧 | a >>= 1; | a = a >> 1; |
在不考虑编译器额外优化的情况下, 使用复合赋值运算符要比正常的计算后赋值, 运行效率要高一点
单目操作符
单目操作符, 从名字来看 看不出是什么意思
事实上, 单目操作符就是操作单个对象的操作符
什么是操作单个对象呢?
以之前用过的计算变量大小的操作符为例: sizeof
sizeof
的用法是这样的:
sizeof(int); // 操作类型
sizeof(1.23f); // 操作数据
long long age = 10;
sizeof(age); // 操作变量
可以看到, 操作的对象只有一个
上面已经介绍过的算术、移位、位、赋值运算符都是双目操作符, 他们操作的都是两个对象:
var1 + var1;
var1 - 1;
var1 << 2;
var1 & var2;
var1 += 1;
...
C语言最常用的单目操作符有:
操作符 | 功能 | 示例 |
---|---|---|
++ | 自增, 即 自增1. 但 区分前置和后置 | var++ 、++var |
-- | 自减, 即 自减1. 但 区分前置和后置 | var-- 、--var |
sizeof | 求变量、类型等大小 | sizeof(int) 、sizeof int |
& | 取地址, 可以取到变量空间在内存中地址值 | &var |
+ | 正值 | +var |
- | 负值 | -var |
! | 逻辑取反, 属于逻辑运算符 | !var |
~ | 按位取反, 属于位运算符 | ~var |
* | 解引用, 可以解引用指针, 按特定类型访问指针所指向空间的值 指针定义声明符, 用于声明 定义指针变量 | *addr (解引用)char* (声明指针) |
(type) | 强制类型转换, 可以将变量、常量值强制转换为目标类型 | (int)23.33 |
单目操作符中, 有几个需要特别介绍一下:
++
和--
自增和自减操作符
如果不区分前置和后置, 从功能上很容易理解, 就是自增1, 它与
+= 1
是等价的但自增和自减操作符是区分前置和后置的, 且在使用上有很大的区别, 即
i++
和++i
是存在区别的可以直接通过代码来介绍:
#include <stdio.h> int main() { int index = 0; index++; printf("index: %d\n", index); ++index; printf("index: %d\n", index); return 0; }
这段代码的运行结果很明显, 只是
index
变量两次自增1从结果来看好像没有区别
实际上, 要观察区别, 应该这样观察:
#include <stdio.h> int main() { int index = 0; int res1 = index++; int res2 = index; int res3 = ++index; printf("res1: %d\n", res1); printf("res2: %d\n", res2); printf("res3: %d\n", res3); return 0; }
这段代码的运行结果是什么呢?
自增的执行顺序还是 先
index++
后++index
但从结果可以看到
res1 = 0; (index++)
res2 = 1; (index)
res3 = 2; (++index)
为什么
res1 = 0
? 不是自增了吗?从
res = 1
可以看出来index
确实自增了为什么
res3 = 2
? 按照res1
的结果推测,res3
不应该是1
吗?造成这个结果的原因就是 前置
++
和后置++
存在的区别了++
前后置调用的区别是:前置
++
, 先执行自增1的操作, 然后将自增后的结果返回后置
++
, 先保存原值, 再执行自增1的操作, 然后返回原值所以, 从没有优化的角度来看, 前置
++
要比后置++
性能稍微好一点点--
自减操作符, 与++
自增操作符相同, 同样区分前置使用和后置使用&
和*
(简单介绍)C语言中,
&
取地址操作符 和*
解引用操作符 是两个密切关联的操作符&
取地址操作符, 作用于变量, 作用是 取变量在内存中占用空间的地址*
解引用操作符, 作用于指针, 作用是 按类型访问指针所指向地址的空间这两个操作符的使用通常是相关的:
#include <stdio.h> int main() { int index = 10; int* pIndex = &index; printf("index在内存中的地址: %p, index的值: %d\n", pIndex, *pIndex); return 0; }
简单介绍一下这段代码具体做了什么操作:
int index = 10;
, 定义一个整型变量index
并初始化为10
int* pIndex = &index;
, 定义一个int*
类型变量pIndex
并初始化为 变量index
的地址&
取地址操作符, 作用于变量, 可以取出这个变量在内存中的地址上面列表中介绍了
*
可以用来声明定义指针变量,int*
就是一个int
类型的指针变量指针(pointer)变量是用来存储地址的一种变量, 在此例中
pIndex
这个变量中存储的就是index
的地址访问
pIndex
pIndex
中存储的是index
这个变量的地址, 可以看作pIndex
指针变脸实际指向index
的地址空间那么, 访问
pIndex
就能获取到index
的地址访问
*pIndex
*
解引用操作符, 始终作用于指针, 用于按指针类型访问指针所指向的空间pIndex
是一个int
类型的指针, 存储的是变量index
的地址那么
*pIndex
就会按照int
类型去访问pIndex
的存储的地址, 实际访问到的就是变量index
的空间所以
*pIndex
就能访问到index
的值
这两个操作符的使用细节 以及 指针的使用细节
将在之后具体介绍指针的文章中涉及
(类型)
C语言可以对数据进行强制类型转换, 即 将非目标类型的数据强制转换为目标类型
使用上也非常简单:
#include <stdio.h> int main() { float varF = 12.333; printf("%d %d\n", (int)varF, varF); return 0; }
这段代码的执行结果:
同样用
%d
打印整型数据的格式化字符,(int)varF
正确打印出了12
, 而varF
却是0
在C语言标准中, 使用特定类型的格式化字符 尝试去 格式化特定类型的行为, 是没有被定义的, 即 是一种未定义行为
所以使用
%d
去格式化浮点数据, 是一种未定义行为, 结果通常是未知的但可以通过
(int)floatData
将浮点类型数据强制转换为整型数据, 这样再使用%d
进行格式化, 就不是未定义行为了
条件操作符
C语言标准中定义了一个唯一一个三目操作符, 同时也被称作条件操作符: condition ? result1 : result2
condition
表示条件表达式, result1
和result2
表示两个结果
条件操作符的用法为: 条件满足时, 执行并返回result1
, 否则 执行并返回result2
以一个简单的例子做介绍:
(a > b) ? (a - b) : (b - a)
例子的意思为:
如果a > b
成立, 则执行a - b
并返回结果, b - a
不会被执行
如果a > b
不成立, 则执行b - a
并返回其结果, a - b
不会被执行
可以将? :
当作一个简写的if-else
条件语句, 逻辑上是完全等价的
逗号表达式
C语言标准中提供了一个特殊的操作符,
用法是这样的:
expression1, expression2, expression3, expression4, expression5, expression6, expression7, expression8, ...
可以通过,
组成不限制长度的逗号表达式
逗号表达式的规则为: 从左到右按顺序执行所有的表达式, 但整个逗号表达式的结果为最后一个表达式的执行结果
以简单的代码为例:
#include <stdio.h>
int main() {
int index = 0;
index = ++index, ++index, ++index, ++index, ++index;
printf("index: %d\n", index);
return 0;
}
执行结果为:
可以看到, index
的最终结果为5
, 即为最后一次执行++index
的结果
特殊操作符
除了上述的操作符, C语言标准中还存在几个特殊的操作符, 具体功能需要结合具体的情况进行使用:
[]
数组下标访问操作符()
函数调用操作符.
结构体成员访问操作符->
结构体成员访问操作符
[]
和()
没有什么需要特别注意的
.
和->
在介绍结构体时再进行介绍
操作符的运算优先级
C语言操作符的使用, 也是有优先级的
就像小学就学过的”先乘除后加减”一样, C语言也一样
不过, C语言中这么多的操作符, 运算优先级也都不尽相同:
优先级 | 操作符 | 描述 | 结合性 |
---|---|---|---|
1 | () [] -> . | 函数调用、数组下标、结构体成员访问 | 左到右 |
2 | ++ -- | 后缀自增、后缀自减 | 左到右 |
3 | ++ -- + - ! ~ (type) * & sizeof | 前缀自增、前缀自减、正号、负号、逻辑非、按位取反、类型转换、解引用、取地址、求字节大小 | 右到左 |
4 | * / % | 乘法、除法、取模 | 左到右 |
5 | + - | 加法、减法 | 左到右 |
6 | << >> | 左移、右移 | 左到右 |
7 | < <= > >= | 关系比较 | 左到右 |
8 | == != | 相等比较 | 左到右 |
9 | & | 按位与 | 左到右 |
10 | ^ | 按位异或 | 左到右 |
11 | ` | ` | 按位或 |
12 | && | 逻辑与 | 左到右 |
13 | ` | ` | |
14 | ?: | 条件操作符(三目运算符) | 右到左 |
15 | = += -= *= /= %= <<= >>= &= ^= |= | 赋值操作符 | 右到左 |
16 | , | 逗号表达式 | 左到右 |
C语言操作符的优先级是需要牢记的, 序号越小 运算优先级越高
当然, 如果你在使用操作符时不确实不确定哪个优先级高, 你可以用()
进行包裹, 被()
包裹的表达式总是优先计算