目录
前言
作者写本篇文章旨在复习自己所学知识,并把我在这个过程中遇到的困难的将解决方法和心得分享给大家。由于作者本人还是一个刚入门的菜鸟,不可避免的会出现一些错误和观点片面的地方,非常感谢读者指正!希望大家能一同进步,成为大牛,拿到好offer。
本系列(初识C语言操作符),是为了与大家分享自己学习经验和所遇到的困难,同大家一起进步。
日志
不记得首发了
2024.5.12首发
1.操作符分类
- 算术操作符
- 移位操作符
- 位操作符
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符
- 逗号表达式
- 下标引用、函数调用和结构成员
2.算术操作符
+ - * / %
前三者和数学上的一样,就不赘述了。
2.1 C语言的两种除法
- 整数除法(除号两边都是整数)
这种除法的结果为商,小数部分丢弃
- 浮点除(除法两端至少有一个浮点数)
这种除法完全是数学上的除法,小数部分不会舍弃
- 除法中,除数不可以是0
2.2%取模(取余)
-
%取模(取余),得到的是整除后的余数
-
不能对0取模
-
%的范围
对于%来说,取的是余数
即%n的范围是在[0, n-1] -
%的2个操作数必须是整数
3.移位操作符
<<和>>移动的都是二进制位,而且位操作符的操作数只能是整数
3.1数据在计算机中以二进制存储
计算机是一个通电的硬件,只能产生高低电平的电信号。电信号正负转化为1/0的数字信号。所有的数据都在存储的时候,都会转化成二进制。也就是说其实,计算机能处理的只是二进制的信息
因此我们看起来的十进制,在计算机中实际上是用二进制来储存和运算的
3.2原码、反码、补码
3.2.1整数的三码存储
正负整数和0则则是通过原码、反码、补码在计算机中表示、存储、运算
- 正整数和0的原码、反码、补码是相同的
- 负整数的原码、反码、补码需要计算
3.2.2 整数的二进制表达形式有三种(整数才有)
-
原码:直接通过二进制形式写出来的就是原码
比如说int类型的整数3,它的大小有32个二进制位。而正整数和0,三码都是一样的。 -
反码:对原码符号位不变,其他为按位取反
-
补码:反码+1得到补码。
在计算机中整数是以补码进行存储和运算的
3.2>>右移操作符
在整数的二进制位上向右移动
-
>>操作符移动的具体过程
-
C语言的>>有两种,本身没有规定。但大多编译器选择算术右移
算术右移:右边丢弃,左边补原来的符号位
通过上面,也可以看出来在我的VS2022也是算术右移
逻辑右移:右边丢弃,左边直接补0
如果刚刚的-3>>1是逻辑右移,结果会非常大,不可能是-2
3.3<<左移操作符
<<左移值有一种规则:左边丢弃,右边补0
3.4<<和>>的范围
-
<<和>>可移动有效范围内的2进制,来到你想要的的位置上
int a = 1;
int类型就可以移动的范围是0~32,可以把二进制序列的某一位通过移位操作符来移动到你想要的位置上 -
不要移动负数位
这是标准未定义行为,C语言也不支持这种写法。可能能算,但怎么算取决于编译器。
比如说能不能<<-1来实现右移1呢?
能算但是会报警告,而且具体怎么算,谁也不知道。所以为了良好的代码风格,想左移就<<,右移就>>,不要乱来。
4.位操作符
也是在二进制位上进行操作的,位操作符有
& 按位与
| 按位或
^ 按位异或注意:它们的操作数必须是整数
4.1&按位与
有0则0,全1则1
#include <stdio.h>
int main()
{
int a = 3;
//0011 - 三码
int b = 5;
//0101 - 三码
int c = a & b;
//0001 - 三码
printf("%d\n", c);//打印1
return 0;
}
4.2|按位或
有1则1,全0则0
int a = 3;
//0011 - 三码
int b = 5;
//0101 - 三码
int c = a | b;
//0111 - 三码
printf("%d\n", c);//打印7
4.3^按位异或
相同为0,相异为1
int a = 3;
//0011 - 三码
int b = 5;
//0101 - 三码
int c = a ^ b;
//0110 - 三码
printf("%d\n", c);//打印6
4.4交换两个整数,不能创建临时变量(变量)
- 我们一般写的交换两个变量是通过创建tmp来进行的
int a = 3;
int b = 5;
printf("交换前:a=%d b=%d\n", a, b);
//交换
int tmp = a;
a = b;
b = tmp;
printf("交换后:a=%d b=%d\n", a, b);
但是人家说不能创建临时变量
- 利用ab的和来交换
int a = 3;
int b = 5;
printf("交换前:a=%d b=%d\n", a, b);
//交换
a = a + b;
b = a - b;
a = a - b;
printf("交换后:a=%d b=%d\n", a, b);
但是这种方法有一个缺陷,那就是当a特别大但是没有超过int范围,b也特别大,也没有超过int范围。a+b就会超过int范围,超过的部分存不下,被丢失了。因此会出先截断问题,精度丢失
- 终极解法
int a = 3;
int b = 5;
printf("交换前:a=%d b=%d\n", a, b);
//交换
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("交换后:a=%d b=%d\n", a, b);
首先要明白^操作符的一些定理
^支持交换律
现在在回来看这道题
这个方法是这道题的终极解,它只是在二进制进行^,不会产生进入操作,因此不会发生溢出。
- 各自优缺点
虽然说第三种方法是终极解,但是还是建议使用第一种方法
第一种:可读性高,效率高
第二种:可读性不高,还存在溢出风险
第三种:不会溢出,但可读性不高
4.6求一个整数存储在内存中的二进制位的1的个数
-
我们做题首先就要有这个思路
-
我们可以这样获取一个整数的32个bit位。对1个数&1,就能得到这个二进制最低位的数
-
当把一位通过>>,使得它来到最后一位。再&1,得到1说明该二进制位是1,得到0说明该位是0。就可以用得到的结果进行判断,来让计数器++。如此>>1或得一位,&1,判断,>>1…这样的过程进行32次,就能解决这道题了
-
前面说可以通过&1的结果来判断该位是1还是0之外,还有一种判断方式。因为在二进制表达的形式下,如果最低位是1,那么它一定是奇数。所以是奇数说明最低位是1,计数器++也可以。
-
版本1
int main()
{
int count = 0;
int n = 0;
scanf("%d", &n);
int i = 0;
for (i = 0; i < 32; i++)
{
//if ((n >> i) % 2 != 1)
if (((n >> i) & 1) == 1)
{
count++;
}
}
printf("count=%d\n", count);
return 0;
}
这种方法的缺陷是,不论什么数据都会计算32次。
版本2
int main()
{
int n = 0;
scanf("%d", &n);
int count = 0;
while (n)
{
if (n % 2 == 1)
count++;
n = n / 2;
}
return count;
}
这个版本优点是当数字很小的时候,不用循环32次。缺点是进行了大量的%,%运算的效率本身就低。而且如果用a/2来达到获得最低位的效果的话,因为除法的效率也很低,那就更低了。
- 版本3
采用相邻两个数进行按位与运算。每次&运算都会是n不断减少
最后,n的二进制位有多少个1,就进行多少次循环。而且采用的是&运算,效率提高
int NumberOf1(int n)
{
int count = 0;
while (n)
{
n = n & (n - 1);
count++;
}
return count;
}
5.赋值操作符
=、+=、-=、*=、/=、%=、>>=、<<=、&=、|=、^=
后面都是复合赋值符,没什么好讲的。主要讲=
- 最好不要连续赋值
int main()
{
int a = 0;
int x = 2;
int y = 1;
//a = x = y + 1;
//可读性不高(监视也看不到这一步的过程),容易出问题,不建议使用
//a = x = y + 1;完全等价下面的
x = y + 1;
a = x;
//写法明确且易于调试
return 0;
}
- =和==不要搞混
n等于3,应该不打印的。可是缺因为少写一个=,使得变成了赋值。导致5赋值给n,5为真,所以进入if。
因此当我们在比较一个数字和一个变量时候,可以尝试把数值写在左边。
这样编译器会逼着你去改。
6.单目操作符
6.1单目操作符分类
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
-、+不讲了
6.2!逻辑反操作
对真假进行转换
看一下它的值
在C语言中,&&、||、!这些操作符只关注真假。默认是,0为假,非0为真。计算出表达式的结果为真时,则默认用1表示。
6.3&、sizeof
&取地址操作符:把地址取出来
sizeof操作符,计算变量或类型的大小,单位字节。且sizeof返回的是无符号的整型
sizeof是计算变量或类型大小的,上面a的类型显然是int,所以计算变量a的大小实际上也就是计算int类型的大小。那么数组的类型是什么?实际上数组的类型就是创建数组的格式去掉数组名。
6.4sizeof与数组
#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;
}
需要清楚的是数组名通常就是数组首元素地址,但有两个例外。
sizeof(数组名)计算的是整个数组的大小
&(数组名)取出的是整个数组的的地址
因为传的是首元素地址,地址又是指针。所以当进入函数后,再使用sizeof计算,计算的就是一个指针或者说地址的大小。指针的大小只与平台有关,32/64平台上大小是4/8。
当数组作为实参传进函数里头时,传的实际上是首元素地址,所以此时在函数内部不可能求得数组元素个数(行和列)。所以函数内部要使用到sz、row、col这些数据时候,必须在外面就准备好,从而作为实参传入函数。
6.5++和–
分为前置,和后置。
–同理不赘述了。
6.6sizeof和strlen
- sizeof是操作符,strlen是函数
- sizeof是计算变量和类型所占有的大小,单位字节,不关注里面的内容
- strlen是求字符串长的,只能针对字符串。计算的是字符串中’\0’出现的字符个数
像ch1这样创建的字符数组,里面会多一个’\0’作为字符串内容,所以sizeof计算它的长度时,会多一个字节。而正是因为ch2是一个个字符放进去的,所以没有’\0’,所以什么时候遇到’\0’是随机的,因此打印随机值。
6.7*和.操作符
-
*解引用操作符
*用在指针中。对指针变量解引用,找到指针所指向的空间
-
.和->
二者用在访问结构体成员中
struct book
{
char name[30];//成员(书名)
char author[20];//成员(作者)
double price;//成员(价格)
};
void test(struct book* p)
{
printf("%s %s %.1lf\n", (*p).name, (*p).author, (*p).price);//结构体.成员
printf("%s %s %.1lf\n", p->name, p->author, p->price);//结构体指针->成员
}
int main()
{
struct book b1 = { "活着", "余华", 66.6 };
struct book b2 = { "高质量的C/C++编程", "林锐", 50.0 };
printf("%s %s %.1lf\n", b1.name, b1.author, b1.price);
printf("%s %s %.1lf\n", b2.name, b2.author, b2.price);
printf("\n");
test(&b1);
test(&b2);
return 0;
}
结构体.成员
结构体->指针
6.8~按位取反
- ~对一个数的二进制位按位取反
- 有了逻辑操作符之后,就丰富了我们多组输入的写法
int n = 0;
//scanf读取失败返回EOF(-1)
//while (scanf("%d", &n) == 1);
//while (scanf("%d", &n) != EOF);
while (~scanf("%d", &n));
//-1
//10000000000000000000000000000001 - 原码
//11111111111111111111111111111110 - 反码
//11111111111111111111111111111111 - 补码
//~-1
//00000000000000000000000000000000 - 原码
//结果为0,说明读取失败,while的判断0假,不执行
6.9()强制类型转换
把一个类型强制转换为其他类型
对于浮点数来说,强制转换为整数,会丢掉小数点后的数据。强制转换少用,容易出错
7.关系操作符
=
<
<=
!= 用于测试“不相等”
== 用于测试“相等
注意:小心=和==写错,导致错误
8.逻辑操作符
看的是真假。而且计算结果为真用1表示,为假用0表示。
8.1&&逻辑与
一假则假,全真则真。数学上并且的意思
8.2||逻辑或
一真则真,全假则假。数学上或者的意思
8.3&&和||会控制表达式求值顺序
8.3.1&&左假,右边不算
因为&&是两个都为真,结果才为真。只要有一个假,那整个表达式结果为0假。因此只要左边为0了,右边就不会就算。
8.3.2||左真,右边不算
对于||来说,一真则真,全假真假。所以只要左边为真,右边是没有机会计算的。
8.3.3陷阱
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
//i = a++||++b||d++;
printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
return 0;
}
来猜猜看,这段代码的结果是什么?
再来一个||的
9.条件操作符
exp1 ? exp2 : exp3;条件表达式,也叫三目操作符
3不大于5所以不执行++a,执行++b。所以a还是3,++b,b变成6
10.逗号表达式
- exp1, exp2, exp3, …, expn
从左向右依次计算,整个条件表达式的结果是expn。前往不要以为逗号表达式的结果是expn,前面的就不算了。因为前面的表达式,可能会影响expn的值。
假设这里你以为逗号表达式只需要算最后一个的话,就会漏掉a = b + 10;就会影响后面b的结果。所以一定要从左向右计算 - 逗号表达式的应用
两种代码是等价的
11.下标引用、函数调用和结构成员
11.1[]下标引用
下标引用操作符的操作数是:数组名[索引的下标]
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//创建数组的[]不是下标引用,只是个形式
arr[0] = 100;//通过[]用下标找到第一个元素
//[]的操作数:arr和0
printf("%d\n", arr[0]);//打印100
11.2()函数调用
- ()的操作数:函数名和参数
//()的操作数:函数名和参数
int len = strlen("abc");//操作数:strlen和"abc"
printf("%d\n", len);//操作数:printf和len
因此()的操作数最少是1个,即函数无参,只有函数名作为参数的时候。
- 可变参数列表
像printf这种在手册里有(…)的,这个东西叫做可变参数列表
他的参数是可以变化的,不固定。
11.3.结构体成员
结构体.成员
struct book
{
char name[30];//成员(书名)
char author[20];//成员(作者)
double price;//成员(价格)
};
int main()
{
struct book b1 = { "活着", "余华", 66.6 };
printf("%s %s %.1lf\n", b1.name, b1.author, b1.price);
return 0;
}
给我们的感觉就像是在访问一个叫b1的文件,(.)能打开这个文件,访问里面的内容。
12.表达式求值
表达式求值的顺序(路径)是一部分是有操作符的优先级和结合性决定的。但是,有些表达式的操作数,在求值的过程中可能需要转换为其他类型。
12.1隐式类型转换
C的整型算术运算总是至少以缺省(默认)整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前会被转换为普通整型,这种转换称为整型提升。
- 整型提升的意义
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长
度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转
换为int或unsigned int,然后才能送入CPU去执行运算。
是不是很懵逼?我学的时候也这么觉得,大概可以这么理解。
- 整型提升的具体过程
整型提升是按照变量的数据类型的符号位来提升的
int main()
{
//5是一个整数int,char的c1会放不下,会发生截断。127也是一样
char c1 = 5;
//00000000000000000000000000000101 - 5的原码
//00000101 - 截断 保留的是最低位的那8位,保留最高位没有意义
char c2 = 127;
//00000000000000000000000001111111 - 127的原码
//01111111 - 截断
char c3 = c1 + c2;
//5和127,发生了整型运算,而c1和c2不是整型,所以要发生整型提升
//00000000000000000000000000000101 - 5
//00000000000000000000000001111111 - 127
//00000000000000000000000010000100 - 5+127
//c3放不下,所以又要发生截断
//10000100 - 补码 最终c3放的值
//在以%d的形式打印的时候要发生整型提升
//%d - 以10进制的形式打印有符号的整数
//c3此时是char类型,人家要打印的是整型,所以c3就要发生整型提升才能打印
//整型提升是按照变量的数据类型的符号位来提升的,所以这里提升1
//11111111111111111111111110000100 - 补码
//11111111111111111111111110000011 - 反码
//10000000000000000000000001111100 - 原码
printf("%d\n", c3);//-124
return 0;
}
而整型提升这个过程是悄悄发生的,我们看不见,所以叫做隐式类型转换。
- 整型提升真的存在吗?
这种0x形式的数字,都是无符号。无符号数直接用0来提升
虽然它们的二进制序列一样,但发生整型提升时,a高位补1,0xb600高位补0。所以最后它们肯定不等,下面b也是如此。
因为c本来就是整型,不需要发生整型提升,所以值打印了c
//%u - 以16进制的形式打印无符号的整数
int main()
{
char c = 1;
printf("%u\n", sizeof(c));//打印1
printf("%u\n", sizeof(+c));//打印4
printf("%u\n", sizeof(-c));//打印4
return 0;
}
本来sizeof计算char类型的c,就是1。当给上±号之后,发生了整型算术运算。而c又是char类型,所以发生了整型提升。
12.2算术转换
- 如果某个操作符的操作数不属于相同的类型,那么除非将其中一个操作数的型转换为另一个操作数的类型,否则表达式求值操作就无法进行。因此在计算的时候会发生算术转换。下面的层次体系称为寻常算术转换。
比如说有float类型和int类型发生运算,那么int类型就要转换为float类型
float f = 3.14f;
int n = 10;
f + n;//n必须转换为float类型,才能与f相加
注意:转换都是临时的,参与运算的瞬间提升,n本身回来之后是不变的。包括上面的整型提升也是如此。
12.3操作符的属性
复杂表示求值顺序有三个影响因素
1.操作符的优先级
2.操作符的结合性
3.是否控制求值顺序
两个相邻操作符先执行哪个?取决于他们的优先级,如果优先级相同,取决于它们的结合性。最后是否控制求值顺序也会影响表达式求值的计算路径。
操作符 | 描述 | 用法示例 | 结果类型 | 结合性 | 是否控制求值顺序 |
---|---|---|---|---|---|
() | 聚组 | (表达式) | 与表达同 | N/A | 否 |
() | 函数调用 | rexp (rexp, …,rexp) | rexp | L-R | 否 |
[ ] | 下标引用 | rexp[rexp] | lexp | L-R | 否 |
. | 访问结构成员 | lexp.member_name | rexp | L-R | 否 |
-> | 访问结构指针成员 | rexp->member_nam | lexp | L-R | 否 |
++ | 后缀自增 | lexp ++ | rexp | L-R | 否 |
– | 后缀自减 | lexp – | rexp | L-R | 否 |
! | 逻辑反 | ! rexp | rexp | R-L | 否 |
~ | 按位取反 | ~ rexp | rexp | R-L | 否 |
+ | 单目,表示正值 | + rexp | rexp | R-L | 否 |
- | 单目,表示负值 | - rexp | rexp | R-L | 否 |
12.3.1操作符的优先级
- 相邻操作符优先级高的先算,低的后算
一看就知道先算哪个
-
相邻操作符的优先级相同的情况下,结合性起作用
-
是否控制求值顺序
像&&、||、条件表达式、逗号表达式等都会控制求值顺序
&&左假,右边不算了
||左真,有边不算了
条件表达式,表达式1为真,表达式2计算,表示式3不算。表示1为假,表示2不计算,表达式3计算
,逗号表达式,从左向右依次计算,只有最后一个表达式的结果才是整个表达式的结果
所以像这种能控制求值顺序的操作符在一定程度上,也会影响表达式求值。
12.4无法确定唯一计算路径
- 就算影响控制顺序的三个条件都知道了,表达式求值顺序也可能不唯一
这种代码是有问题的。给上括号,能很好避免这种问题。 - 表达式求值顺序唯一,结果依旧可能不一样
所以就算你知道了求值顺序,但仍然因为操作符左边的值什么时候准备好,也有可以会发生不一样的结果。这种也是问题代码。
因此我们写代码的时候,怕写出这种代码就可以适当加上括号或者写得足够简单。 - 通过上面,我们知道即使你完全了解操作符的优先级和结合性等等,也依旧可能写出问题代码。
比如C和指针作者对下面代码做了一个测试
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
结果他在各个编译器上测完的结果各不相同。所以说这种代码是有问题的,可能他的语法没有错看,但是无法确定唯一的就算路径,是垃圾代码。不要写这种代码。
- 再来一个问题代码
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int ret;
ret = fun() - fun() * fun();
printf("%d\n", ret);//输出多少?
return 0;
}
通过这段代码ret = fun() - fun() * fun();
我们可以知道先算*,再算-。但是这些函数什么时候调用?因为函数什么时候调用,也会导致没有唯一的计算路径。
所以真的真的不要这样写,或者适当加括号。
13.总结
还没想好