操作符详解
操作符详解-从认识到深入
1.操作符分类
操作符名称 | 表示 |
---|---|
算术操作 | + 、- 、* 、/ 、% |
移位操作符 | << >> |
位操作符 | & | ^ |
赋值操作符 | = 、+= 、 -= 、 *= 、 /= 、%= 、<<= 、>>= 、&= 、 |
单⽬操作符 | !、++、–、&、*、+、-、~ 、sizeof、(类型) |
关系操作符 | > 、>= 、< 、<= 、 == 、 != |
逻辑操作符 | && 、|| |
条件操作符 | ? |
逗号表达 | , |
引⽤ | [ ] |
函数调⽤ | () |
结构成员访问 | . 、-> |
2. 原码,反码,补码
- 整数的二进制表示方式有三种,即 原码,反码,补码。
- 有符号整数的表示由符号位和数值位两部分组成,二进制中,最高位的1位通常被认为是符号位,其它的都是数值位;
- 符号位都是用0表示“正”,用1表示“负”
- 正数的原码,反码,补码相同
- 负数的原码,反码,补码表示方法有所区别:
-
原码:将符号位设为1,其余位表示该数的绝对值(二进制数)。例如,-7的原码表示为:10000111(最高位是符号位,后面的位表示7的二进制形式)。
-
反码:原码的基础上,符号位保持不变(仍为1),其余各位取反(0变为1,1变为0)。继续以-7为例,其反码为:11111000。
-
补码:反码的基础上+1。对于-7,其补码为:11111000(反码)+ 1 = 11111001
-
另外:补码得到原码可以使用:补码取反,+1;
3.移位操作符
<<
左移操作符>>
右移操作符
ps:移位操作符的操作数只能是整数;
3.1左移操作符
移位规则:左边抛弃,右边补0;
例如:
#include <stdio.h>
int main()
{
int n = 6;
int z = n << 1;
printf("n = %d\n",n);
printf("z = %d\n", z);
return 0;
}
图解:
注意:n
在先左移动了一位之后,n
的值是不变的,要与自增自减的符号区别;
3.2 右移操作符
右移操作符的移位准确来说分两种运算方式:
- 注意:在现实生活中,具体是运用那一个规则进行右移,取决于编译器的实现,但常见的编译器都是运用的算数右移
例如:
#include <stdio.h>
int main()
{
int n = 6;
int z = n >> 1;
printf("n = %d\n", n);
printf("z = %d\n", z);
return 0;
}
图解:
注意:对于移位操作符,不要移动负数位,这是未定义的
4.位操作符
& | 按位与 |
---|---|
| | 按位或 |
^ | 按位异或 |
~ | 按位取反 |
注意:它们的操作数也必须是整数;
#include <stdio.h>
int main()
{
int n = -5;
int m = -6;
printf("n & m = %d\n", n & m);
printf("n | m = %d\n", n | m);
printf("n ^ m = %d\n", n ^ m);
printf("~ 0 = %d\n", ~ 0);
return 0;
}
下面是n & m的计算过程图解:
- 不创建临时变量实现两个数的交换:
#include <stdio.h>
int main()
{
int a = 1314;
int b = 520;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("a = %d b = %d\n",a,b);
return 0;
}
- 求一个整数储存在内存中的二进制数中1的个数
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int count = 0;
while (n) {
n = n & (n - 1);
count++;
}
printf("二进制中1的个数:%d\n", count);
return 0;
}
- 将13的二进制序列的第五位改为1,然后再改回0:
#include <stdio.h>
int main()
{
int n = 13;
n = n | (1 << 4);
printf("a = %d\n",n);
n = n & (~(1 << 4));
printf("a = %d\n",n);
return 0;
}
5.单目操作符
!,++,--,&,*,+(正号),-(负号),~,sizeof,(类型转换)
- 特点:只有一个操作数;
6.逗号表达式
表示方式
(exp1,exp2,exp3,exp4......expN)
- 逗号表达式就是用逗号隔开的多个表达式;
- 逗号表达式,从左到右依次执行,整个表达式的结果是最后一个子表达式的结果;
1.简单的逗号表达式:
int a = 1, b = 2, c;
c = (a++, b--, a + b);
7.下标引用操作符[ ],函数调用()
7.1 []
下标引用操作符
- 操作数:一个数组名 + 一个索引值
int arr[100];//创建数组
arr[9] = 10;//使用下标引用操作符调用数组元素
//[ ] 的两个操作数分别是:arr和9;
7.2 函数调用操作符
- 可以接收一个或多个操作数:第一个操作数是函数名,剩余的操作数是传递给函数的参数
- 函数调用操作符,至少有一个操作数——函数名;
- 函数调用操作符用于执行一个函数,并传递给它任何必需的参数。
- 数调用操作符的基本语法如下:
return_type function_name(parameter1, parameter2, ...);
// 函数调用
function_name(argument1, argument2, ...);
function_name
是你希望调用的函数的名称,
parameter1, parameter2, ...
是函数定义中声明的参数列表,
argument1, argument2, ...
是你在调用函数时传递的实际参数。
#include <stdio.h>
// 函数声明
int add(int a, int b);
int main() {
int sum;
// 函数调用,传递两个整数参数
sum = add(5, 3); //这里的()就是函数调用操作符
// 输出结果
printf("The sum is: %d\n", sum);
return 0;
}
// 函数定义
int add(int a, int b) {
return a + b;
}
8.结构体成员访问操作符
C语言中已经内置了许多类型:int,char,short,char,float,double,long
等,但是只有这些类型但在实际运用中是完全不够的。如果我i们想要描述学生,描述一本书,描述一个人的基本信息,只有这些单一的内置类型是不足以描述清楚的。例如描述一个人,需要知道他的性命,身高,体重等。所以C语言中我们可以运用结构体这种自定义的数据类型来实现;
- 结构体是一些值的集合,这些值我们称为成员变量。结构体的每个成员可以是不同类型的变量,如标量,数组,指针,甚至可以是其他结构体类型;
struct 结构体类型名 {
数据类型 成员名1;
数据类型 成员名2;
...
数据类型 成员名N;
};
例如描述一个学生:
struct Student {
char name[50]; 姓名
int age; 年龄
float score; 分数
};//分号不能掉
8.1结构体变量的定义和初始化
#include <stdio.h>
// 代码1:定义了一个结构体Point,用于表示一个点,包含两个整数成员a和b。
struct Point
{
int a;
int b;
} p1; // 声明了一个Point类型的变量p1
struct Point p3; // 又声明了一个Point类型的变量p3
// 代码2:定义了一个结构体Stu,用于表示一个学生,包含字符串数组name和整数age。
// 然后声明并初始化了两个Stu类型的变量s1和s2。
struct Stu
{
char name[15];
int age;
};
struct Stu s1 = { "lisi", 20 }; // 初始化s1,name为"lisi",age为20
struct Stu s2 = { .age = 20, .name = "wangwu" }; // 使用点操作符.初始化s2,先初始化age再初始化name
// 代码3:定义了一个结构体Node,用于表示链表的一个节点,包含整数data、一个Point类型的成员p和一个指向下一个Node的指针next。
// 然后声明并初始化了两个Node类型的变量n1和n2。
struct Node
{
int data;
struct Point p; // Node结构体包含一个Point类型的成员
struct Node* next; // 指向下一个Node的指针
} n1 = { 10, {4,5}, NULL }; // 初始化n1,data为10,p为{4,5},next为NULL
struct Node n2 = { 20, {5,6}, NULL }; // 初始化n2,data为20,p为{5,6},next为NULL
8.2 结构成员访问操作符
8.2.1 结构体成员的直接访问
- 结构体成员的直接访问是直接通过
.
点操作符进行访问的,使用点操作符可以访问或修改该结构体的成员; - 点操作符的直接访问需要接收两个操作数:
结构体变量.成员名
#include <stdio.h>
struct Point {
int x;
int y;
};
int main() {
struct Point p;
// 使用点操作符设置结构体成员的值
p.x = 10;
p.y = 20;
// 使用点操作符访问结构体成员的值,并打印它们
printf("Point p: (%d, %d)\n", p.x, p.y);
return 0;
}
8.2.2 结构体成员的间接访问
- 又是我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针
- 结构体指针->成员名:
#include <stdio.h>
// 定义一个名为Point的结构体,它有两个整型成员:x和y
struct Point
{
int x;
int y;
};
int main()
{
// 声明一个Point类型的变量p,并初始化为{ 3, 4 },即x为3,y为4
struct Point p = { 3,4 };
// 声明一个指向Point类型的指针ptr,并将其初始化为变量p的地址
struct Point* ptr = &p ;
// 使用指针ptr访问结构体成员x,并将其值设置为10
ptr->x = 10;
// 使用指针ptr访问结构体成员y,并将其值设置为20
ptr->y = 20;
// 打印通过指针ptr访问到的结构体成员x和y的值
printf("x = %d y = %d\n",ptr->x,ptr->y);
return 0;
}
9.操作符优先级:优先性,结合性
- C语言中操作符的优先性和结合性决定了表达式求值的计算顺序
9.1 优先性
- 优先性是指如果一个表达式包含多个运算符,那么哪个运算符先执行就是优先性。就像我们计算算术题时,会运用先乘除后加减的规则一样;
1314 + 520 * 520
在这个式子中有+
有*
,那么根据C语言中优先性的规则,会先计算*
再计算+
9.2 结合性
- 如果式子中运算符的优先级相同,那么根据优先性就无法判断先计算那个了,这时候就要看结合性。大部分运算符是左结合(从左往右),少数运算符是右结合(从右往左)(比如赋值操作符);
1314 * 520 / 1314
这个式子*
和/
的优先级相同,它们都是左结合运算符,所以从左往右依次计算。
- 其中圆括号
()
的优先级最高,可以改变其他运算符的优先级;
10.表达式求值
10.1整型提升
- 在C语言中,整型提升(Integer Promotion)是一个自动进行的类型转换过程,它通常发生在算术表达式中涉及多种整数类型的操作数时。
- 整型提升的意义:
- 表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度⼀般就是int的字节长度,同时也是CPU的通用寄存器的长度。
- 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
- 通用CPU(general-purpose CPU)是难以直接实现两个8特字比节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
10.1.1 整型提升的方法
有符号的整型提升:
//负数的整型提升:-1;
char c1 = -1;
//变量的二进制(补码)只有8个比特位:
11111111
//因为char为有符号的char
//所以整型提升时,高位补充符号位,即补充1;
//所以提升后的结果为:
1111111111111111111111111111111
//正数的整型提升:1
char c2 = 1;
//变量的二进制位(补码)只有8个比特位:
00000001
//因为char是有符号的char
//所以整型提升后,高位补符号位 ,即0;
//所以提升后的结果为:
00000000000000000000000000000001
- 对于无符号整型提升,高位补0即可;
补充:绝大多数编译器对于char
类型的定义是signed char
,是有符号类型;
10.2 算术转换
- 在C语言中,当参与运算的操作数具有不同的整型或浮点型时,为了保证运算结果的精确性和正确性,编译器会自动将它们转换成具有相同类型的更大或最通用的类型,这个过程称为算术转换(Arithmetic Conversions)。算术转换涉及的操作数类型和规则主要取决于这些类型的范围和大小。
规则如下:
-
整型提升:首先,如果操作数是较小的整数类型(如char,short等),则它们会被提升至更大的整数类型,通常是int类型(或者如果int类型无法表示它们的全部范围,那么可能会提升至unsigned int或更大的类型)。这个步骤是整型提升,它确保操作数至少具有int的大小。
-
操作数类型匹配:如果两个操作数都是整数类型,且它们的类型不同,则它们的类型将被转换为二者中能够表示更大范围值的类型。具体转换规则如下:
- 如果一个操作数是有符号的,另一个是无符号的,且它们的类型大小相同,则无符号操作数将被转换为有符号类型(如果它表示的值在有符号类型的范围内)。否则,有符号操作数将被转换为无符号类型。
- 如果两个操作数都是有符号的,则它们会被转换为能够表示二者中较大范围的类型。
- 如果两个操作数都是无符号的,则它们会被转换为能够表示二者中较大范围的类型。
-
浮点型与整型之间的转换:如果操作数包括整型和浮点型,则整型操作数会被转换为浮点型。
-
不同浮点类型之间的转换:如果操作数包括不同的浮点类型(如float,double,long double),则较小的浮点类型会被转换为较大的浮点类型。
-
复合类型:如果操作数包括结构体、联合体或指针类型,则这些类型通常不参与算术转换。不过,它们可以与整数0或空指针常量进行比较或进行减法运算。
下面的层次体系称为寻常算数转换:
1.long double
2.double
3.float
4.unsigned long int
5.long int
6.unsigned int
7.int
从上到下转换级别依次减小,如果一个式子中操作数类型靠后则需要转换到另一个排名靠前的类型之后再进行运算;