0.前言
在C语言中,操作符是程序员最常用的一种元素之一,它们负责执行各种计算和操作。本篇博客将深入探讨C语言中常见的操作符,包括二进制和进制转换、原码、反码和补码、移位操作符、位操作符、单目操作符、逗号表达式、下标访问和函数调用操作符、结构成员访问操作符、以及操作符的属性等。
1.常见操作符分类
在C语言中,操作符是用于执行各种操作和计算的符号或关键字。这些操作符可以分为多个类别,每个类别都有特定的功能和用途,下面是常见的操作符汇总:
• 算术操作符: + 、- 、* 、/ 、%
• 移位操作符: << >>
• 位操作符: & | ^ `
• 赋值操作符: = 、+= 、 -= 、 *= 、 /= 、%= 、<<= 、>>= 、&= 、|= 、^=
• 单目操作符: !、++、--、&、*、+、-、~ 、sizeof、(类型)
• 关系操作符: > 、>= 、< 、<= 、 == 、 !=
• 逻辑操作符: && 、||
• 条件操作符(三目操作符): ? :
• 逗号表达式: ,
• 下标引用: [ ]
• 函数调用: ( )
• 结构成员访问: . 、->
2.二进制和进制转换
2.1二进制
Wikipedia中,对于二进制的定义是这样的:
二进制(英语:binary)在数学和数字电路中指以2为底数的记数系统,以2为基数代表系统是二进位制的。这一系统中,通常用两个不同的数字0和1来表示。数字电子电路中,逻辑门直接采用了二进制,因此现代的计算机和依赖计算机的设备里都用到二进制。每个数字称为一个比特(二进制位)或比特(Bit,Binary digit 的缩写)。
简单来说,二进制是一种用0和1的形式的数值,类比生活中常见的十进制规则:
①满10进1
②数字每一位都由0~9的数字组成
二进制中也有类似规则:
①满2进1
②数字每一位都由0~1的数字组成
我们举一个简单的例子:
十进制的17就是二进制的10001.
2.2进制转换
2.2.1二进制转十进制
二进制转十进制的过程是将每位的二进制数乘以2的相应次方并相加。例如,我们将二进制数转换为十进制:(注:数字下标表示进制数,无下标的数默认为10进制,下同)
因此,二进制数 转换为十进制是 13。
2.2.2 十进制转二进制
十进制转二进制的过程是反复除以2,将余数作为二进制的低位,直到商为0。例如,将十进制数 13 转换为二进制:
13/2=6余1
6/2=3余0
3/2=1余1
1/2=0余1
反向取余数(余数从下向上读):
因此,十进制数 13 转换为二进制是 。
2.2.3二进制转八进制
二进制转八进制是将每3位二进制数转换为一位八进制数(从右向左取3位,左边不足3位补0)。例如,将二进制数 转换为八进制:
其中二进制数的八进制数为
,二进制数
的八进制数为
,因此
对应的八进制数为
。
在C语言中,八进制数写为065.
2.2.4二进制转十六进制
二进制转十六进制是将每4位二进制数转换为一位十六进制数(从右向左取4位,左边不足4位补0)。例如将二进制数转化为十六进制,我们先在左边补一个0:
,其中二进制数
的十六进制数为
,二进制数
的十六进制数为
,二进制数
的十六进制数为
,因此
对应的16进制数是
。
在C语言中,十六进制数写为0x4b3;(十六进制中A~F大小写均可)
3.原码、反码和补码
在计算机中,整数的二进制表示有三种方式,分别是原码、反码和补码。这三种表示方法都包括符号位和数值位两部分,其中最高位的1位是符号位,用于表示正负。
3.1 符号位的表示
- 符号位为0表示“正”。
- 符号位为1表示“负”。
3.2 三种表示方法的生成方式
注意:3.2中示例均为8bit数字,对应C语言中char类型。
-
原码: 直接将数值按照正负数的形式翻译成二进制,最高位作为符号位。
- 例如,正整数3的原码为:0000 0011(原)。
- 负整数-3的原码为:1000 0011(原)。
-
反码: 将原码的符号位保持不变,其他位按位取反。
- 例如,正整数3的反码为:0000 0011(反)。
- 负整数-3的反码为:1111 1100(反)。
-
补码: 补码是反码加1,即将反码的每一位都取反后加1。
- 例如,正整数3的补码为:0000 0011(补)。
- 负整数-3的补码为:1111 1101(补)。
3.3 为什么使用补码?
在计算机系统中,数值一律用补码来表示和存储。这是因为使用补码能够统一处理符号位和数值域,同时加法和减法也可以统一处理,而且补码与原码相互转换的运算过程是相同的,无需额外的硬件电路。
3.4 补码的应用
整数的补码表示方式在计算机中被广泛应用,尤其是在处理加法和减法运算时更为方便。补码的设计使得计算机可以使用同一套逻辑电路来处理正数和负数,简化了硬件设计和运算逻辑。
4.移位操作符
移位操作符是C语言中用于对二进制数进行位移操作的一组操作符。主要有左移操作符(<<
)和右移操作符(>>
)两种,它们分别用于将二进制数向左或向右移动指定的位数。
4.1左移操作符
左移操作符(<<
)将一个二进制数向左移动指定的位数,右侧空出的位用零填充。左移的效果相当于将原数乘以 ,其中 n 为左移的位数。
int num = 5;
int result = num << 2; // 将二进制数5左移2位
- 初始值:0000 0101₂
- 左移2位后:0001 0100₂
- 结果:20
4.2右移操作符
右移操作符(>>
)分为算数右移和逻辑右移两种,它们在处理带符号整数时有不同的行为。
4.2.1 算数右移
算数右移是对带符号整数进行右移操作,保留符号位,并用符号位进行填充。这意味着如果原数是负数,右移后左侧将用1填充,如果原数是正数,右移后左侧将用0填充。
int num = -8;
int result = num >> 2; // 算数右移2位
- 初始值:[-0000 1000₂]
- 算数右移2位后:[-0000 0010₂]
- 结果:-2
4.2.2 逻辑右移
逻辑右移是对带符号整数进行右移操作,不保留符号位,左侧用0进行填充。这意味着无论原数是正数还是负数,右移后左侧都用0填充。
int num = -8;
int result = (unsigned int)num >> 2; // 逻辑右移2位,将数值强制转换为无符号整数
- 初始值:[-0000 1000₂]
- 逻辑右移2位后:[0011 1110₂]
- 结果:1073741822
在使用逻辑右移时,为了确保左侧用0填充,需要将带符号整数先转换为无符号整数,然后进行右移操作。
注意:编译器一般默认右移形式为算术右移。
5.位操作符 & | ^ ~
位操作符用于对二进制数的各个位进行操作,包括按位与(&
)、按位或(|
)、按位异或(^
)和按位取反(~
)。这些操作符通常用于底层的位级操作,可以在一些特定的编程场景中发挥重要作用。
5.1 按位与(&
)
按位与操作符(&
)对两个二进制数的每一位执行与操作,结果中的每一位都是两个操作数相应位的与运算结果。与运算法则如下:
- 1 & 1 = 1
- 1 & 0 = 0
- 0 & 1 = 0
- 0 & 0 = 0
例子:
int a = 5; // 二进制:0101
int b = 3; // 二进制:0011
int result = a & b; // 按位与操作
// 二进制按位与结果:0101 & 0011 = 0001
// 十进制结果:1
5.2 按位或(|
)
按位或操作符(|
)对两个二进制数的每一位执行或操作,结果中的每一位都是两个操作数相应位的或运算结果。或运算法则如下:
- 1 | 1 = 1
- 1 | 0 = 1
- 0 | 1 = 1
- 0 | 0 = 0
例子:
int a = 5; // 二进制:0101
int b = 3; // 二进制:0011
int result = a | b; // 按位或操作
// 二进制按位或结果:0101 | 0011 = 0111
// 十进制结果:7
5.3 按位异或(^
)
按位异或操作符(^
)对两个二进制数的每一位执行异或操作,结果中的每一位都是两个操作数相应位的异或运算结果。异或运算法则如下:
- 1 ^ 1 = 0
- 1 ^ 0 = 1
- 0 ^ 1 = 1
- 0 ^ 0 = 0
例子:
int a = 5; // 二进制:0101
int b = 3; // 二进制:0011
int result = a ^ b; // 按位异或操作
// 二进制按位异或结果:0101 ^ 0011 = 0110
// 十进制结果:6
5.4 按位取反(~
)
按位取反操作符(~
)对一个二进制数的每一位执行取反操作,即0变为1,1变为0。
例子:
int a = 5; // 二进制:0101
int result = ~a; // 按位取反操作
// 二进制按位取反结果:~0101 = 1010
// 十进制结果:-6(注意:取反后要考虑符号位)
6.单目操作符
单目操作符是C语言中用于对单个操作数进行操作的操作符,包括逻辑非(!
)、自增(++
)、自减(--
)、取地址(&
)、解引用(*
)、正号(+
)、负号(-
)、按位取反(~
)、sizeof
运算符和强制类型转换((类型)
)。
6.1 逻辑非(!
)
逻辑非操作符(!
)用于对一个条件进行逻辑非操作,如果条件为真,则返回假(0),如果条件为假,则返回真(1)。
int x = 5;
int result = !x; // 逻辑非操作
// result为0(因为x不为0,所以逻辑非后为假)
6.2 自增(++
)
自增操作符(++
)用于将变量的值增加1。可以前置使用(++x
)或后置使用(x++
)。
int x = 5;
int result = ++x; // 前置自增操作
// x的值变为6,result也为6
6.3 自减(--
)
自减操作符(--
)用于将变量的值减少1。可以前置使用(--x
)或后置使用(x--
)。
int x = 5;
int result = x--; // 后置自减操作
// x的值变为4,result为5(原始x的值)
6.4 取地址(&
)
取地址操作符(&
)用于获取变量的地址。
int x = 5;
int *ptr = &x; // 取地址操作
// ptr中存储了x的地址
6.5 解引用(*
)
解引用操作符(*
)用于通过指针获取存储在特定地址上的值。
int x = 5;
int *ptr = &x; // 取地址操作
int result = *ptr; // 解引用操作
// result的值为5(存储在x的地址上的值)
6.6 正号(+
)
正号操作符(+
)用于表示正数。在大多数情况下,正号并不会对操作数产生实际影响。
int x = 5;
int result = +x; // 正号操作
// result的值为5
6.7 负号(-
)
负号操作符(-
)用于表示负数。
int x = 5;
int result = -x; // 负号操作
// result的值为-5
6.8 按位取反(~
)
按位取反操作符(~
)对二进制数的每一位执行取反操作。(5.4已介绍)
6.9 sizeof
运算符
sizeof
运算符用于获取数据类型或变量的大小(以字节为单位)。
int arr[5];
int size = sizeof(arr); // 获取数组的大小
// size的值为20(5个整数,每个占4字节)
6.10 强制类型转换((类型)
)
强制类型转换操作符用于将一个数据类型转换为另一个数据类型。
double pi = 3.14159;
int approxPi = (int)pi; // 强制将double转换为int
// approxPi的值为3(小数部分被截断)
7.逗号表达式
逗号表达式是C语言中的一种特殊的表达式,它由多个表达式通过逗号(,
)连接而成。逗号表达式的求值顺序是从左到右,最终的值是逗号表达式中最右边的表达式的值。
7.1 语法
表达式1, 表达式2, ..., 表达式n
7.2 求值顺序
逗号表达式的求值顺序是从左到右,每个表达式按顺序执行,最终的值是最右边表达式的值。
int result = (x = 5, y = 10, x + y);
// x的值变为5,y的值变为10,最终result的值为15
7.3 应用场景
-
多个表达式在一个语句中执行: 逗号表达式允许在一个语句中使用多个表达式,用于紧凑地表示多个操作。
int a = 5, b = 10, c;
c = (a++, b++, a + b);
// a的值变为6,b的值变为11,c的值为17
-
在
for
循环中更新多个变量: 可以在for
循环中使用逗号表达式更新多个变量。
for (int i = 0, j = 10; i < 5; i++, j--)
{
// 循环体
}
7.4 注意事项
-
逗号表达式的值为最右边表达式的值: 即使逗号表达式中包含多个表达式,其整体的值为最右边表达式的值。
-
避免过度使用: 虽然逗号表达式提供了一种紧凑的语法,但过度使用可能使代码变得难以理解。在实际应用中,建议大家谨慎使用逗号表达式,确保代码的可读性。
8.下标访问、函数调用操作符
在C语言中,下标访问操作符([]
)和函数调用操作符(`()``)分别用于数组元素的访问和函数的调用。这两个操作符在程序中频繁使用,分别提供了对数组和函数的灵活访问和调用能力。
8.1下标访问操作符 [ ]
下标访问操作符用于通过索引访问数组或其他类似结构的元素。数组的索引从0开始,通过指定索引,可以获取数组中相应位置的元素。
int numbers[5] = {1, 2, 3, 4, 5};
int element = numbers[2]; // 获取索引为2的元素
// element的值为3
在上述例子中,numbers[2]
表示访问数组 numbers
中索引为2的元素,即数字3。
8.2函数调用操作符 ( )
函数调用操作符用于调用函数,并传递参数给函数。通过在函数名后加上括号,并在括号内传递参数,可以执行函数中的代码。
int add(int a, int b)
{
return a + b;
}
int result = add(3, 5); // 调用函数add
// result的值为8
在上述例子中,add(3, 5)
表示调用函数 add
并传递参数3和5,函数返回的结果被赋给 result
变量。
8.3 应用场景
数组遍历: 使用下标访问操作符可以方便地遍历数组中的元素。
int numbers[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++)
{
// 访问数组中的元素,例如 numbers[i]
}
多维数组: 对于多维数组,可以使用多个下标访问操作符来访问元素。
int matrix[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int element = matrix[1][2]; // 访问二维数组中的元素
表达式求值: 下标访问和函数调用操作符可以嵌套使用,用于构建复杂的表达式。
int matrix[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int sum = add(matrix[0][0], add(matrix[1][1], matrix[2][2]));
9.结构成员访问操作符
在C语言中,结构成员访问操作符(.
)用于访问结构体中的成员。结构体是一种用户自定义的数据类型,允许将多个不同类型的变量组织在一起。
9.1结构体
结构体是一种用于组织多个不同数据类型的变量的用户自定义数据类型。结构体定义了一组相关的数据,并为这些数据起了一个名字。
9.1.1结构的声明
结构的声明定义了结构体的名称以及结构体中包含的成员的数据类型和名称。
struct Person {
char name[50];
int age;
float height;
};
上述例子中,声明了一个名为 Person
的结构体,包含了名为 name
、age
和 height
的三个成员,分别为字符数组、整数和浮点数。
9.1.2结构体变量的定义和初始化
结构体变量的定义和初始化可以在声明结构体的同时进行。
struct Person {
char name[50];
int age;
float height;
};
struct Person person1 = {"John", 25, 1.75};
上述例子中,定义了一个名为 person1
的结构体变量,同时进行了初始化。
9.2结构成员访问操作符
结构成员访问操作符(.
)用于访问结构体中的成员。
9.2.1结构体成员的直接访问
可以直接使用结构成员访问操作符访问结构体中的成员。
struct Person {
char name[50];
int age;
float height;
};
struct Person person1 = {"John", 25, 1.75};
printf("Name: %s\n", person1.name);
printf("Age: %d\n", person1.age);
printf("Height: %.2f meters\n", person1.height);
上述例子中,通过结构成员访问操作符直接访问了结构体 person1
中的成员。
9.2.2结构体成员的间接访问
通过指针和结构成员访问操作符,可以间接访问结构体中的成员。
struct Person {
char name[50];
int age;
float height;
};
struct Person person1 = {"John", 25, 1.75};
struct Person *ptr = &person1;
printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);
printf("Height: %.2f meters\n", ptr->height);
上述例子中,通过指向结构体的指针 ptr
,使用 ->
操作符来间接访问结构体 person1
中的成员。
9.3 应用场景
-
数据组织: 结构体允许组织多个不同类型的变量,适用于表示实体对象或记录。
struct Point {
int x;
int y;
};
-
函数参数传递: 结构体可以作为函数的参数传递,方便地传递和处理多个相关的数据。
void printPerson(struct Person p)
{
printf("Name: %s, Age: %d, Height: %.2f meters\n", p.name, p.age, p.height);
}
-
动态内存分配: 结构体可以与指针结合,用于动态内存分配和释放。
struct Person *p = (struct Person *)malloc(sizeof(struct Person));
10.操作符的属性:优先级和结合性
在C语言中,操作符具有不同的优先级和结合性,这些属性决定了在表达式中多个操作符同时存在时的计算顺序。理解操作符的优先级和结合性对于正确理解和书写表达式至关重要。
10.1优先级
操作符的优先级决定了它们在表达式中的计算顺序。具有较高优先级的操作符会在具有较低优先级的操作符之前进行计算。
以下是一些常见操作符的优先级(从高到低):
()
:括号优先级最高,可以通过括号改变其他操作符的计算顺序。[]
:数组下标访问->
、.
:结构体成员访问++
、--
:自增和自减+
、-
:正负号!
、~
:逻辑非和按位取反*
、/
、%
:乘法、除法和取模+
、-
:加法和减法<<
、>>
:左移和右移<
、<=
、>
、>=
:关系运算符==
、!=
:相等性运算符&
:按位与^
:按位异或|
:按位或&&
:逻辑与||
:逻辑或? :
:条件运算符
10.2结合性
结合性指的是相邻的相同优先级的操作符在缺少括号的情况下是从左往右计算还是从右往左计算。C语言中大多数操作符是从左往右计算的,这意味着相邻的相同优先级的操作符将按照从左到右的顺序计算。
例如,赋值操作符 =
具有从右到左的结合性,所以表达式 a = b = 5
将被解释为 a = (b = 5)
。
结合性的理解对于正确解读表达式的含义和避免由于计算顺序不当引起的错误非常重要。
11.表达式求值
在C语言中,表达式的求值涉及到多个概念,其中包括整型提升和算数转换。这些概念影响了表达式中不同类型的操作数是如何相互作用的。
11.1整型提升
整型提升是为了确保在表达式中不同大小的整数类型能够以相同的大小进行计算。在C语言中,int
通常是计算的基本单位,因此,为了防止混合大小的操作数引起的问题,小于int
的整数类型在表达式中会被自动提升为int
。
整型提升遵循一些规则,主要包括以下几点:
- 所有的
char
和short
类型在表达式中会被自动转换为int
。 - 如果一个操作数的类型比
int
小,它会被提升为int
。 - 如果一个操作数的类型比
int
大,它会保持原样。
11.1.1. 有符号整数提升
如果有符号整数类型(如char
、short
)在表达式中参与运算,整形提升时会按照变量的数据类型的符号位来提升。具体而言,将符号位的值复制并填充到更高位。
例如,对于有符号的 char
类型,其符号位是最高位:
- 对于正数
char
(比如65
,二进制表示为01000001
),整形提升时高位会补0,即:00000000 00000000 00000000 01000001
。 - 对于负数
char
(比如-65
,二进制表示为10111111(补码)
),整形提升时高位会补1,即:11111111 11111111 11111111 10111111
。
11.1.2. 无符号整数提升
如果无符号整数类型在表达式中参与运算,整形提升时高位会补0。
例如,对于无符号的 char
类型,其整形提升时高位会补0:
- 无符号
char
(比如65
,二进制表示为01000001
),整形提升时高位会补0,即:00000000 00000000 00000000 01000001
。
总而言之,在整形提升时,有符号整数和无符号整数的符号位处理方式不同。有符号整数保持符号,而无符号整数高位补0。
11.2算数转换
在C语言中,如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换:
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上述列表中排名靠后,那么首先要转换为另外一个操作数的类型后才能执行运算。
例子
考虑以下表达式:
int a = 5;
float b = 2.5;
double result = a + b;
在这个例子中,a
是 int
类型,b
是 float
类型。由于 float
在寻常算术转换中排名更高,所以 a
会被自动转换为 float
类型,然后执行加法运算,最后结果被赋给 double
类型的变量 result
。
12.总结与展望
C语言的操作符构建了表达式和实现各种运算的基础,包括算术、位运算、逻辑运算等,操作符的优先级和结合性影响着表达式的计算顺序。整形提升和算数转换为不同类型的操作数提供了协同工作的基础,而寻常算术转换确保了不同类型的操作数在表达式中的合理转换。最后,祝大家在编程学习的旅途中更加顺利!感谢观看本篇博客。