写在前面
这里的内容虽然有些多,不过整体较为简单,我总结了一些相对有些难度的的知识点。
操作符
C语言的操作符有很种,这里我把常见的一些和大家进行分析一下.
- 算术操作符
- 移位操作符
- 位操作符
- 赋值操作符
算术操作符
所谓的算数操作符就是我们的加减乘除,没有什么可以谈的.
+ - * / %
我在这里就谈谈 % 这个操作符,这个操作符是我们取余数的操作,有点意思.
int main()
{
int num = 11;
int ret = num % 10;
printf("%d\n", ret);
return 0;
}
注意,只有整数可以取余.
int main()
{
double num = 11.0;
double ret = num % 10;
printf("%lf\n", ret);
return 0;
}
移位操作符
在谈下面几个操作符时,我先说一下数据在内存中的存储形式,我们知道数据可以用三种二进制方式进行表示,其中补码是计算机的存储方式,毕竟我们数据存在负数,关于原反补三个是如何计算的,我们这里不谈.移位操作符分为下面的两种.
- 左移
- 右移
左移
注意左移操作符不是向左移动,准确的说应该是向高位移动,只不过我们平常喜欢把高位放在左边,故我们称之为左移.那么请问当我们左移之后,要知道类型的比特位数是固定的,那么最右边的空位我们因该如何做?这空缺位补0,某种意义上移动一位相当于数据翻倍.
#include<stdio.h>
int main()
{
int a = 5;
int b = a << 1;
printf("%d\n", b);
printf("%d\n", a);
return 0;
}
右移
右移是向低位移动.右移分为逻辑右移和算术右移.
- 逻辑右移 二进制向右移动 空缺位补 0
- 算术右移 二进制向右移动 空缺位补 符号位
对于正数来说,右移时逻辑右移还是算术右移都一样,但是负数就不一样了,
大多数编译器支持算术右移,下面我们看看VS系列怎么样
#include<stdio.h>
int main()
{
int a = -5;
int b = a >> 1;
printf("%d\n", b);
printf("%d\n", a);
return 0;
}
位操作符
位操作符能够直接改变数字的二进制,下面我就以两个例题来总结位操作符的用法,注意,他们的操作数必须是整数。
- 按位与 &
- 按位或 |
- 按位异或 ^
- 按位取反 ~
如何判断一个数是不是2的n次幂,我们看看下面数的特点
你会发现,凡是2的n次幂的数 ,他的二进制一定只有一个数字为 1 (这里不考虑负数),那么这就带来一种思路,我们是否可以通过位操作符进行判断,
#include<stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
// n-1 一定会拿掉那个唯一的 1 的
if ((n & (n - 1)) == 0)
{
printf("YES\n");
}
return 0;
}
交换a,b,不出现第三个变量,这里直接就给思路了 我们发现 n ^ n ==0 , n ^ 0 == n 看下面代码
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("Before a = %d b = %d\n", a, b);
a = a ^ b;
b = a ^ b; //把 b 赋给 a
a = a ^ b; //把 a 赋给 b
printf("After a = %d b = %d\n", a, b);
return 0;
}
赋值操作符
这里的赋值操作符就是 = 比较简单,这里就提一下 允许连续赋值
#include<stdio.h>
int main()
{
int a = 0;
int b = 0;
a = b = 5; //我们不建议 代码风格不好
printf("%d %d", a, b);
return 0;
}
操作符优先级
注意,操作符的优先级是会影响我们的我们代码的,如果操作符的优先级记忆错了那么有可能会带来很大的影响.不过我们不谈,与其我们背诵操作符的优先级不如我们多多使用括号,这样逻辑还清晰,而且不容易出错.
取整方式
我们都知道 5 / 2 = 2,这是我们记住的,那么我们想过没有,为什么结果是2呢?有人可能这样想在数学中 5 / 2 = 2.5,结果是要求是整型,去尾,所以是2.那为什么不是四舍五入得到 3 呢?这是编译器不同的取整方式造成的.下面我们谈谈取整的几种方式.
零向取整
零向取整 , 本质是向0靠近,不一定是四舍五入,否则-2应该是-3.
#include<stdio.h>
int main()
{
int a = 2.9;
int b = -2.9;
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
这也解决了 为何5 / 2 结果是2的问题,C语言默认采用零向取整.这里有一个函数,支持零向取整.
#include<stdio.h>
#include<math.h>
int main()
{
printf("%d\n", (int)trunc(2.9));
printf("%d\n", (int)trunc(-2.9));
return 0;
}
地板取整
所谓的地板取整就是向负无穷取整.
如果我想要它像负无穷取整的话.这里也是通过一个函数来实现.
#include <stdio.h>
#include <math.h> //因为使用了floor函数,需要添加该头文件
int main()
{
//本质是向-∞取整,注意输出格式要不然看不到结果
printf("%.1f\n", floor(-2.9)); //-3
printf("%.1f\n", floor(-2.1)); //-3
printf("%.1f\n", floor(2.9)); //2
printf("%.1f\n", floor(2.1)); //2
return 0;
}
向+∞取整
这个和上面恰好相反.
相关的函数如下.
#include <stdio.h>
#include <math.h>
int main()
{
//本质是向+∞取整,注意输出格式要不然看不到结果
printf("%.1f\n", ceil(-2.9)); //-2
printf("%.1f\n", ceil(-2.1)); //-2
printf("%.1f\n", ceil(2.9)); //3
printf("%.1f\n", ceil(2.1)); //3
return 0;
}
四舍五入取整
我们来到了我们最熟悉的的取整方式了.这里是round函数.
#include <stdio.h>
#include <math.h>
int main()
{
//本质是四舍五入
printf("%.1f\n", round(2.1));
printf("%.1f\n", round(2.9));
printf("%.1f\n", round(-2.1));
printf("%.1f\n", round(-2.9));
return 0;
}
取模
下面我们说一下是什么是取模?
如果a和d是两个自然数,d非零,可以证明存在两个唯一的整数 q 和 r,满足 a = q*d + r 且0 ≤ r < d.其中,q被称为商,r 被称为余数.
正数取模
这里很简单,我们一眼就可以看出了,重点放在负数上面,他们的一些总结是可以用在这里的.
int main()
{
int a = 10;
int d = 3;
printf("%d\n", a % d);
return 0;
}
负数取模
这里才是我们的大头,我们试试不同的编译环境然后仔细分析一下我们该如何做.
#include <stdio.h>
#include <math.h>
int main()
{
int a = -10;
int d = 3;
//printf("%d\n", a/d); //C语言中是-3,很好理解
printf("%d\n", a % d);
return 0;
}
我们把这个代码在不同的环境下跑一遍,先看现象,再来和大家分析.
我们不是说余数大于0吗?是不是定义错了.实际上因为在C语言中,现在-10%3出现了负数,根据定义:满足 a = q\*d + r 且0 ≤ r < d,C语言中的余数,是不满足定义的,因为,r<0了.故,大家对取模有了一个修订版的定义:
如果a和d是两个自然数,d非零,可以证明存在两个唯一的整数 q 和 r,满足 a = q*d + r , q 为整数,且0 ≤ |r|< |d|.其中,q 被称为商,r 被称为余数.
那么在Python中呢?这里的结果是什么样的呢?
这里我们就疑惑,好像出现了矛盾,实际上不是的,这是我们不同语言默认的取整的方式不同, Python采用向-∞取整
这里我们给出一个结论,方式决定商,商决定余数
下面我们继续说一下,我们有的时候把取模也称之为取余,这里还是有一定的区别的.
- 取余:尽可能让商,进行向0取整.
- 取模:尽可能让商,向-∞方向取整
提升与截断
我们之前谈到变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。可是我们看一下下面的代码.
int main()
{
int a = 10;
char ch = 'a';
int x = ch; // 疑问1
ch = a; // 疑问2
return 0;
}
这里面我们有两个疑问,我们之前说了不同类型的变量的空间大小是不一样的,我们的int是4字节,char类型是1个字节,请问他们是如何可以相互赋值的?这里就不得谈两个话题,一个是整形提升,一个是截断问题.
数据存储
谈一个耳熟能详的知识点,在计算机中我们存储的数据是补码,这一点我暂时认为大家非常了解,当然如果不太熟悉,可以去看后面的博客.
整型提升
由于CPU以四个字节计算速度快,所以在小于四个字节的的整形数据进行计算时,计算机隐形的将数据提升为四个字节,也就是补足32个比特位,下面是补全的规则.
- 无符号前面补 0
- 有符号前面补符号位
下面我分别举一些例子和大家分享.
//输出什么
#include <stdio.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a=%d,b=%d,c=%d",a,b,c);
return 0;
}
截断问题
所谓的数据截断就是我们把大空间的赋值给小空间的,编译器会自动的截断,把相应空间的数据给赋值过去,看代码.
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}