寒假时在b站上挑了个C语言的课程,花了一个多月看完了,也算是入门C语言了吧。
在看那个课时,讲师很多次强调,一定要多写博客,来记录自己的学习。
显然,以我的自制力,课程看完了,博客是一篇没写。
现在C语言算是入门了,但还是缺乏巩固。
于是乎,便打算写几篇C语言学习的总复盘,用于巩固昔日所学。
后面我会花几天,尽力去复盘学到的知识结合自己为数不多的做题经验,写成博客,并坚持更新下去。
这篇是第二章
上一篇很幸运的被选为“每天最佳新人”了,还是很惊喜的。
书接上回,这次主要复盘运算符
运算符
运算符分为以下几类
算数运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、指针运算符、其它运算符
就像日常使用的加减乘除那些数学运算一样,这些运算符也都是有优先级的,优先级高的运算符会优先计算。
以下是C语言中运算符优先级的一个简要排序,从高到低:
优先级
-
圆括号
()
(包括函数调用和强制类型转换) -
后缀自增/自减运算符
++
、--
-
一元运算符
+
、-
(作为负号时)、!
(逻辑非)、~
(按位取反)、&
(取地址) -
乘法
*
、除法/
、求余%
-
加法
+
、减法-
-
左移
<<
、右移>>
-
关系运算符
<
、>
、<=
、>=
、==
、!=
-
按位与
&
-
按位异或
^
-
按位或
|
-
逻辑与
&&
-
逻辑或
||
-
条件运算符(三目运算符)
? :
-
赋值运算符
=
以及扩展赋值运算符+=
、-=
、*=
、/=
、%=
,<<=
、>>=
、&=
、^=
、|=
-
逗号
,
(逗号表达式)
注意,虽然优先级规定了运算符的计算顺序,但在有相同优先级的运算符出现时,还需要考虑运算符的结合性。大多数二元运算符都是从左到右结合,但赋值运算符和逻辑或运算符是从右到左结合的。圆括号可以用来改变运算的优先级和结合性,确保某个子表达式的计算顺序。
算术运算符
+和-我想没有什么讲的必要了,我们着重讲解下除(/)和取余(%)
/除
当是两个整数进行/运算时
int a=10;
int b=3;
int result1;
result1=a/b;//得到10/3,但由于result是整数,故实际的到的是3
如果希望得到小数结果,那么a、b至少有一个应该为浮点数类型
float a=10.0;
float b=3.0;
float result2;
result2=a/b;//得到10/3,由于result2为浮点数类型,故实际得到的是3.33333
但是如果是在直接进行实数运算时
float a=2;
float b=a+3/2;//b的值为3.000000
float c=a+3.0/2;//c的值为3.50000
即,直接进行实数运算,如果希望保留小数位,那么运算时要加上“.0”
%取余
取余。就是得到除后的余数。
即,10➗3=3...1 —— 10/3=3,10%3=1
int x=10;
int y=3;
int x/y;//得到 3
int x%y;//得到 1
取余常用于判断一个数是否为偶数
if(a%2==0)
{printf("偶数");}
//即,如果一个数对2取余,得到0,即这个数除以二没有余数,那么就说明这个数是偶数
因此更准确说,取余最大作用就是判断一个数是不是另一个数的倍数
a%n==0//就是说a为n的倍数
c==a/n;//c得到a是n的几倍
那么根据这个思路,就可以通过取余,每隔几位数进行一次操作。
取余也常用于单独拿出一个数的个位/十位/百位。这个在单片机的数码管、显示屏上常用。
int a=5614;
int gewei=a%10;//4
int shiwei=a%100-gewei;//14-4=10
int baiwei=a%1000-shiwei-gewei;//614-10-4=600
因此如果将取余(%)和除(/)结合在一起,就能通过循环提取一个数的每一位
int a=114514;
while(a)
{
//将每一位放到数组中
arr[i++]=a%10;
//每一位相加
sum=sum+a%10;
a/=10;
}
通过这个,我们就可以将数据分块处理。
++/--自增/自减
自增/自减,一般默认是自增1/自减1,即 i++ 等价于 i=i+1。
自增自减有前置和后置的区别,即 i++ 和 ++i 是存在区别的。
i++:后缀自增,先返回当前变量的值,然后再增加变量的值。
++i:前缀自增,先增加变量的值,再返回增加后变量的值。
int i=1;
while(--i);
printf("%d",i);//输出0
int j=1;
while(j--);
printf("%d",j);//输出-1
关系运算符
就是很常见的大小比较。但有一些细节需要注意
例如在for循环中使用<还是<=
for(int i=0;i<10;i++)//方式一
for(int i=1;i<=10;i++)//方式二
这两种都是进行了10次循环,差别是对i的初始化,是0还是1。
方式一更适合数组,因为数组下标是从0开始,即arr[i];
方式二更能直观地看到循环了几次,从1到10十次。
还有==
相信大家在这个上面没少犯错。在条件判断中,==经常被不小心写成=
if(a=0)//由于=是赋值,无论a为何值因此a必定被赋值为0,()中返回值仍为1,满足循环条件
if(a==0)//这才是把a和0做比较,只有当a==0时,才满足条件
但当写上a=0时,编译器往往不会报错。因此在处理a==0时,建议把0写到左边,即0==a,这样如果不小心写成0=a,那么编译器会报错,就能及时发现处理。
逻辑运算符
&&逻辑与
A&&B,当A和B同为真时结果才为真
短路逻辑:
A&&B&&C&&D,从左到右依次判断真假,若C为假,则不再判断D,即不再执行D内的语句
int a = 0;
int b = 1;
if (a != 0 && b --) {
// 如果 a 为0,那么 b 是否为0并不重要,因为整个表达式注定为假
// 所以在 a 为0的情况下,sth() 不会被调用
}
||逻辑或
A||B,当A和B任意为真时,结果为真;两者均为假,结果才为假。
短路逻辑:
A||B||C||D,从左到右以此判断,若A、B为假,C为真,则不再判断D
int x = -1;
int y = sth();
if (x > 0 || y) {
// 如果 x > 0 为真,那么无需检查 y 的值,因为整个表达式已确定为真
// 因此,在 x > 0 的情况下,sth() 不会被调用
}
!逻辑非
在嵌入式中经常会看到这样的语句:
while(!Sth_Init());
就是反复初始化一个外设,直到外设初始化成功,Init函数返回1,再由!1成为0。如果初始化不成功,则是返回0,再由!0转为1,循环继续。
^逻辑异或
A^B,在A和B只有一个为真时,结果为真;若同时为真或同时为假,则结果为假。
位运算符
位运算符是我在初次学习时最不重视的东西,但当做到一个个有趣的题时,我才意识到位运算符的魅力。
位运算符是在二进制的基础上进行运算的,即,如果你输入了一个十进制的数,编译器会把先转化为二进制,再进行计算。
所以这里先扯一下二进制
二进制
二进制就是逢二进一,只有0和1这两个数字。
二进制位其实就是比特(bit)位,比如int类型占2个字节,也就是16个bit位,因此最大的数为2^16。也就是16个空间,每个空间有0或1这两个数字。
如果要在程序中表示二进制,直接int a=00001000是不行的,常常要在数值前加上0b,即int a=0b00001000。同样对于八进制(加0)、十六进制(加0x)也有同样的规则
(C语言无法直接表示二进制!!!即0b00001001这样的不能直接写到程序里。这个要记住,0b只是为了在演示时声明)
请注意,0b00001000 形式的二进制字面量虽然在一些现代C编译器(如gcc和clang)中可用,但并不属于ANSI C标准的一部分。
在标准C中,推荐使用十六进制来间接表示二进制数,因为每四位十六进制数正好对应一个字节的二进制数,
转换关系直观且简单。如:
一个字节有8个bit位,即 0000 0000,这也就是一个字节的二进制数
如果一个十六进制为0x1A,也就是十进制的26,对应二进制 0001 1010。每四位一组是因为四位二进制最大值正好为15,到16进1
0001-1010
1----A
正好和十六进制的两位对应
其实如果这样对应八进制数字也可以,那么就是三位一组 00 000 000。26对应八进制032
00-011-010
0---3---2
上面这些其实在嵌入式中应用的更多。
所以为了展现二进制的这种层次,下面对于二进制的表示都会通过每四位一组分开
二进制特点:
若为偶数,则尾项为0,若为奇数,则尾项为1。因此可以通过与1按位与来判断其奇偶
if(y&1)//y为偶数,则不执行,为奇数则执行
//1实质上就是 0000 0001
//如 0010 1101 & 0000 0001----0000 0001,为奇数
//如 0010 1100 & 0000 0001----0000 0000,为偶数
if(y%2)//类似于与2取余,为偶数,则不执行,为奇数则执行
&按位与
1&1==1,1&0==0,0&0==0
按位与有以下这些常用功能:
-
提取特定位:
//掩码操作 int value = 0b11111010; // 假设value是我们感兴趣的特定几位数据混合在一起 int mask = 0b00000111; // 创建一个掩码,只想保留最后三位 int extracted_bits = value & mask; // 提取最后三位,结果为0b00000010
-
清除特定位:
int flags = 0b00100101; // 初始化标志变量 // 若要清除第3位 flags &= ~(1 << 2); // flags现在为0,因为在掩码中第3位变成了0,与操作后该位变为0 //1<<2是将1左移2位,即0000 0001<<2---0000 0100,在通过取反得到 1111 1011
-
n=n&(n-1)可以不断将后面的1依次变为0,
即在每次循环时 0101 1001 0101 1000 0101 0000 0100 0000 0000 0000
这样就可以通过循环次数,计算一个二进制数中1的个数
/*
输入一个整数 n ,输出该数32位二进制表示中1的个数。其中负数用补码表示。
*/
class Solution {
public:
int NumberOf1(int n) {
int res = 0;
//当n为0时停止比较
while(n){
n &= n - 1;
res++;
}
return res;
}
};
|按位或
1|1==1,1|0==1,0|0==0
按位或有以下常用功能:
-
设置特定位:
int flags = 0; // 初始化标志变量0b00000000 // 若要设置第3位(从0开始计数) flags |= (1 << 2); // flags现在为0b00001000
-
旋转位:
int rotate_left(int n, int shift) { return ((n << shift) | (n >> (sizeof(n) * CHAR_BIT - shift))) & ((1 << (sizeof(n) * CHAR_BIT)) - 1); } int rotate_right(int n, int shift) { return ((n >> shift) | (n << (sizeof(n) * CHAR_BIT - shift))) & ((1 << (sizeof(n) * CHAR_BIT)) - 1); }
^按位异或
0^1==1,1^1==0,0^0==0
异或操作符有一些很妙的性质:
-
自反性:任何数与自身的异或结果总是0。即
a ^ a = 0
。
而任何数^0,结果为原数。
-
交换律:异或运算符满足交换律,即
a ^ b = b ^ a
。 -
结合律:异或运算符满足结合律,即
(a ^ b) ^ c = a ^ (b ^ c)
。 -
异号得1,同号得0:如同上面的真值表所示,异或操作实际上是检查两个位是否不同。
-
异或运算可以用来交换两个变量的值而不需额外的临时变量(称为无临时变量交换):
a ^= b; b ^= a; a ^= b;
这三个表达式执行完毕后,a
和b
的值将会互换。
通过以上性质就可以实现下面这些有用的功能:
-
检测一个数的奇偶性:如果一个数与1进行异或运算,结果为1,则该数为奇数,否则为偶数,因为二进制下最后一个比特位(最低位)为1代表奇数,为0代表偶数。
if(x^1)//x为偶数,即最低位为0,则执行语句
-
翻转特定位:如果想要翻转一个整数的特定位(例如第n位),可以对该整数与
1 << n
进行异或运算,其中1 << n
是一个仅第n位为1的数。int flags = 0b00100101; // 初始化标志变量 // 若要清除第3位 flags ^= (1 << 2); //flags变为0b00100001,第三位被翻转
-
查找仅出现一次的数字:在一个数组中,如果只有一个数字出现奇数次,其余数字都出现偶数次,可以对数组中的所有元素进行异或操作,结果将是那个唯一出现奇数次的数字。如果有两个数字各出现一次,通过异或操作可以得到这两个数字的异或结果,然后通过位操作分离出这两个数字。
/* 用C语言函数指针实现以下内容: 一个数组中只有两个数字是出现一次,其他所有数字都出现了两次。 编写一个函数找出这两个只出现一次的数字。 例如: 有数组的元素是:1,2,3,4,5,1,2,3,4,6 只有5和6只出现1次,要找出5和6. */ #include <stdio.h> // 定义异或操作的函数 int xor_operation(int a, int b) { return a ^ b; } // 函数指针类型声明 typedef int (*XorFunc)(int, int); // 主要逻辑函数:找出数组中只出现一次的两个数字 void find_single_numbers(int arr[], int size, XorFunc xor_func, int *num1, int *num2) { int single_num = 0; for (int i = 0; i < size; ++i) { single_num ^= arr[i]; } // 找出最低位为1的比特位 int bit_mask = 1; while ((single_num & bit_mask) == 0) { bit_mask <<= 1; } // 分离出两个只出现一次的数字 int num1_val = 0, num2_val = 0; for (int i = 0; i < size; ++i) { if ((arr[i] & bit_mask) == 0) { num1_val ^= arr[i]; } else { num2_val ^= arr[i]; } } // 输出结果 *num1 = num1_val; *num2 = num2_val; } int main() { int arr[] = {1, 2, 3, 4, 5, 1, 2, 3, 4, 6}; int size = sizeof(arr) / sizeof(arr[0]); int num1, num2; XorFunc xor_func_ptr = xor_operation; find_single_numbers(arr, size, xor_func_ptr, &num1, &num2); printf("只出现一次的数字是:%d 和 %d\n", num1, num2); return 0; }
>>右移
如果要让一个数不断自减直至为0,最常见的就是让它在每次循环时减一。
如果二进制数末尾为0,是偶数,>>1就相当于÷2。
如果二进制数末尾为1,为奇数,>>1就相当于-1再÷2。
if(x){
x>>=1;//不断向右移位,直到为0。
}
<<左移
在讲&和|时都有在代码中看到<<。这就是<<最常用的功能,就是把1移向某个特定的位
1<<3是向左移动3位,由00000001变为00001000
>>左移和<<右移都有一个需要注意的点,就是你会发现在进行移位时,会导致某一边的数据缺失。这个就涉及到补位的知识了,在后面数据类型转换会讲到。
OK,写不少了,这一节也就到这里吧。如有错误,感谢指正。