前情回顾:C语言特别篇--数组与函数的实践 + 函数递归-CSDN博客
前面我们学习了C语言中函数与数组的大致内容,并且做了初步的实践,前面我们就学习了许多操作符,但是我们从来没有进行系统性的分类,这篇博客我会帮大家系统性总结操作符接下来我们将进入操作符的世界中,感受这个魔法符号的魅力。
一、操作符的分类
算术操作符:+、-、*、/、%
移位操作符:<<、>>
位操作符:&、|、^
赋值操作符:=、+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=
单目操作符:!、++、--、&、*、+、-、~、sizeof、(类型)
关系操作符:>、>=、<、<=、==、!=
逻辑操作符:&&、||
条件操作符:?、:
逗号表达式:,
下标引用:[ ]
函数调用:( )
结构成员访问:. 、->
上述操作符我们有许多见过的也有许多素未谋面,接下来我们将先讲解二进制的一些知识帮助大家更好的理解其中一些操作符。
二、二进制和进制转换
1、不同进制的介绍
我们学习编程应该经常可以听到2进制、8进制、10进制、16进制这样的说法,那他们到底是什么意思呢,其实上述四种进制不过是表示形式不同罢了。我们平常生活中都采用10进制,数值是0~9,接下来为大家科普其他三个:
二进制:0、1表示
八进制:0、1、2、3、4、5、6、7
十进制:0、1、2、3、4、5、6、7、8、9
十六进制:0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F(0~15)
接下来我们还是举个例子来看看,我们想用不同的进制来表示十进制的数字15:
15的二进制:1111
15的八进制:17
15的十六进制:F
接下来我们将详细介绍一下二进制,他与十进制其实也有许多类似的地方:
*10进制中满10进1
*10进制的数字每一位都是0~9的数字组成
其实二进制也是一样的
*2进制中满2进1
*2进制的数字每⼀位都是0~1的数字组成
2、进制转换
其实10进制的123表示的值是一百二十三,为什么是这个值呢?其实10进制的每一位是有权重的,10进制的数字从右向左是个位、十位、百位....,分别每⼀位的权重是10^0 ,10^1,10^2... 如下图所示:
2进制与10进制是类似的只不过权重不同,二进制从右向左依次是:2^0,2^1,2^2...
二进制的 1101该如何理解呢,如下图所示:
<1>、 十进制转二进制(也可以推广换算八进制或者十六进制)
这个方法我们用一幅图来展示:
这种方法我们也可以用来同时求10进制数字转换成8进制或16进制的数字。
总结口诀:进制取余、结果逆置
<2>、二进制转八进制
8进制的数字每一位是0~7的,0~7的数字,各自写成2进制,最多有3个2进制位就足够了,比如7的二进制是111,所以在2进制转8进制数的时候,从2进制序列中右边低位开始向左每3个2进制位会换算⼀ 个8进制位,剩余不够3个2进制位的直接换算。
如:2进制的01101011,换成8进制:0153,0开头的数字,会被当做8进制。不可以省略这个0
<3>、二进制转十六进制
16进制的数字每⼀位是0~9,a~f的,0~9,a~f的数字,各自写成2进制,最多有4个2进制位就足够了,比如f的二进制是1111,所以在2进制转16进制数的时候,从2进制序列中右边低位开始向左每4个2进制位会换算一个16进制位,剩余不够4个二进制位的直接换算。
如:2进制的01101011,换成16进制:0x6b,16进制表示的时候前面加0x
三、原码、反码、补码
整数的2进制表示方法有三种,即原码、反码和补码
有符号整数的三种表示方法均有符号位和数值位两部分,2进制序列中,最高位的1位是被当做符号 位,剩余的都是数值位。 符号位都是用0表示“正”,用1表示“负”。
接下来我还是以图片的方式向大家展示上述内容:
我们现在假设有int a=10,int b=-10,我们该如何写下这俩个数的原码、反码、补码呢?
我们可以看出其有32位,第一位是表示其是负数还是正数,后面31位是数值位。
原码: 直接将数值按照正负数的形式翻译成二进制得到的就是原码
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码
补码:反码+1就得到补码
补码得到原码也可以使用:取反,+1的操作
无符号整数的三种2进制表示相同,没有符号位,每一位都是数值位。
对于整型来说: 数据存放在内存中其实存放的是补码。
这里顺便解释一下why?
四、移位操作符
<<:左移操作符
>>:右移操作符
注意:移位操作符的操作数只能是整数,移动的是二进制位
1、左移操作符
移动规则:左边抛弃右边补零
接下来我们还是从代码的角度来理解左移操作符:
接下来我们再举个负数的例子来看看:
经过上述代码我们不难发现俩个结论:
1、打印时候打印的是补码,但是有时候我们需要转换成原码看看是多少
2、移位操作符移动的是补码
下面再给出一个具体的代码和图示希望可以帮助大家更好的理解:
#include <stdio.h>
int main()
{
int num = 10;
int n = num << 1;
printf("n = %d\n", n);
printf("num = %d\n", num);
return 0;
}
2、右移操作符
移动规则:首先右移运算分俩种:
1、逻辑右移:左边用0填充,右边丢弃
2、算术右移:左边用原该值的符号位填充,右边丢弃
(具体是哪种取决于编译器,大部分情况下是2)
下面我们举一个负数的例子来理解右移操作符(这里采用的是VS2022,所以是算数右移):
同样的下面再给出一个具体的代码和图示希望可以帮助大家更好的理解:
#include <stdio.h>
int main()
{
int num = 10;
int n = num >> 1;
printf("n = %d\n", n);
printf("num = %d\n", num);
return 0;
}
警告⚠:对于移位运算符,不要移动负数位,这个是标准未定义的。
五、位操作符的简单介绍:&、|、^、~
位操作符有:&、|、^、~
注意:他们的操作数必须是整数
& //按位与
| //按位或
^ //按位异或
~ //按位取反
1、& -- 按位与
运算规则:是对应二进制位进行与运算,只要有0则为0,俩个同时为1,才为1。
如下图所示:
2、| -- 按位或
运算规则:是对应二进制位进行与运算,只要有1就是1,俩个同时为0才为0。
如下图所示:
3、^ -- 按位异或
运算规则:是对应二进制位进行与运算,相同为0,相异为1。
如下图所示:
4、~ -- 按位取反
运算规则:是对应二进制位进行与运算,如果原来是0则变为1,若原来为1则变为0
如下图所示:
注意:上述操作符的操作数必须是整数
六、位操作符的应用
上面我们学习了四种位操作符但是我们肯定还是不明白学习这些位操作符有什么用呢,接下来我们便从实践中去感受。
1、整数交换(不创建第三个变量)
<1>、最常用的方法--创建第三个变量
#include<stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int c = 0;
printf("交换前:a=%d,b=%d\n", a, b);
c = a;
a = b;
b = c;
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
这个代码我们可以很简单的实现俩个数进行交换,也十分通俗易懂,但这不符合题目要求,这时候应该会有人想到另一个方法
<2>、方法二
#include<stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
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);
return 0;
}
我们同样也给出图片解释其内部逻辑:
这种方法虽然也能解决问题,但是他有数据溢出的风险,代码是有bug的,接下来就是我们今天的主角:方法三
<3>、使用位操作符
这时候我们要采用的是按位异或^,首先我们介绍一下 ^ 中我们不知道的一些定律:
a ^ a = 0;
a ^ 0 = a;
^ 具有交换律
我们还是从数据代码的角度来分析:
交换律大家可以自行去实践。
接下来有了上述知识的铺垫,我们便可以写出下面这样的代码:
#include<stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
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);
return 0;
}
大家看到这串代码肯定也会有疑惑这和方法二不就是符号不一样吗?大家不要着急,接下来我会为大家写出这个的逻辑:
这么一看是不是就变得逻辑很清晰了,这其实原来是一道科技公司的面试题,那主考官肯定希望看到的是第三种方法,但是论平常使用,我们肯定会采取第一种,但是我们还是可以多多了解,拓展自己的知识面。
2、编写代码实现,求一个整数存储在内存中的二进制中1的个数
<1>、方法一
我们首先想到的是十进制中求这是几位数我们会采用 /10 与 %10 的操作来实现,我们类比一下就会想到,那们二进制中我们便采用不断 /2 与 %2 的操作来实现即可,下面给出代码:
#include<stdio.h>
int main()
{
int num = 0;
scanf("%d", &num);
int count = 0;
while (num)
{
if (num % 2 == 1)
count++;
num = num / 2;
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
这段代码存在着明显的缺陷就是求不了负数,那我们可不可以对代码优化一下呢?
<2>、方法二
我们学习这种方法之前需要了解一个预备知识,我给大家写出来了,为大家展示:
明白了上述知识之后我们联想我们之前学过的移位操作符,我们求出来一位之后我们将其进行移位,采用循环的方式来实现。下面给出代码:
#include<stdio.h>
int count_bit1_of_n(int n)
{
int count = 0;
for (int i = 0; i < 32; i++)//一共有32位
{
if (((n >> i) & 1) == 1)
count++;
}
return count;
}
int main()
{
int n = 0;
scanf("%d", &n);
int r = count_bit1_of_n(n);
printf("%d\n", r);
return 0;
}
从上述代码我们不难看出这是一个很好的思路,但是他每次都要循环32次过于繁琐了,我们想一想有没有更加好用的方法呢?这么说当然是有的,接下来我们就来看看我们的方法三。
<3>、方法三
学习方法三之前我们依旧要学习一个预备知识:
从这幅图中我们可以了解到我们可以采用循环的方式来实现这个问题,代码如下:
#include<stdio.h>
int count_bit1_of_n(int n)
{
int count = 0;
while (n)
{
n = n & (n - 1);
count++;
}
return count;
}
int main()
{
int n = 0;
scanf("%d", &n);
int r = count_bit1_of_n(n);
printf("%d\n", r);
return 0;
}
这个循环中二进制有几个1就循环了几次这样就可以很快的解决这个问题了。这种方法很好,达到了优化的效果就是很难想到,这就需要我们在后面的学习中不断巩固了。
<4>、拓展
我们根据方法三我们可以拓展一下求n是或是2的次方数
我们知道:4 = 2 ^ 2 -- 00100 8 = 2 ^ 3 -- 01000……我们不难发现2的次方数转换为二进制里面只有1个1,我们便可以采用这样的方式来实现:
if(n & (n - 1)== 0)
{
//就是次方数
count++;
}
3、二进制位置0或者置1
编写代码将13二进制序列的第5位修改为1,然后再改回0
首先我们先写出大概效果:
13的2进制序列 :00000000000000000000000000001101
将第5位置为1后:00000000000000000000000000011101
将第5位再置为0:00000000000000000000000000001101
要将13的第n位改为1我们分俩步:
1、<<(n-1)
2、13 | (1<<(n-1))
我们细细拆开看:
下面我们给出完整的代码:
#include<stdio.h>
int main()
{
int a = 13;
a = a | (1 << (5 - 1));
printf("%d\n", a);
a = a & ~(1 << (5 - 1));
printf("%d\n", a);
return 0;
}
这样我们就是实现了操作,以上就是关于二进制我们拓展出来的一些内容,希望给大家带来了帮助,接下里我们继续讲解操作符。
七、单目操作符
单目操作符有这些: !、++、--、&、*、+、-、~ 、sizeof、(类型)
单目操作符的特点是只有⼀个操作数,在单目操作符中只有 & 和 * 没有介绍,这2个操作符,我会在后面指针的地方给大家讲解
八、逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。逗号表达式,从左向右依次执行。整个表达式的结果是最后⼀个表达式的结果。
例子1
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
c是多少?
我们还是以图片方式为大家展示:
我们可以清楚的看出前面的表达式会影响后面的表达式,但是最后一个表达式起的是关键性作用,他决定了结果。
例子2
if (a = b + 1, c = a / 2, d > 0)
对这段代码我们进行分析,从左往右分析,我们不难得出没有d,所以结果也是未知的含有随机性。
例子3
我们也可以使用逗号表达式对我们的代码进行优化。
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
//...
a = get_val();
count_val(a);
}
如果使⽤逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
这样也避免了出现句式杂糅,使我们的代码看起来更加清晰。
九、下标访问[ ]、函数调用( )
1、[ ]下标引用操作符
操作数:一个数组名+一个索引值(下标)
int arr[10];//创建数组
arr[9] = 10;//实⽤下标引⽤操作符。
[ ]的两个操作数是arr和9。
2、( )函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。最少有一个操作数。
#include <stdio.h>
void test1()
{
printf("hehe\n");
}
void test2(const char *str)
{
printf("%s\n", str);
}
int main()
{
test1(); //这⾥的()就是作为函数调⽤操作符。
test2("hello bit.");//这⾥的()就是函数调⽤操作符。
return 0;
}
十、结构成员访问操作符
1、结构体
C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类 型还是不够的,假设我想描述学生,描述一本书,这时单一的内置类型是不行的。 描述一个学生需要名字、年龄、学号、身高、体重等; 描述⼀本书需要书名、作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如: 标量、数组、指针,甚至是其他结构体。
<1>、结构的声明
struct tag
{
member-list;
}variable-list;
struct便是结构体的关键字。
tag是变量名我们可以根据自己的喜好或者题目要求去自行定义。
member-list:是我们自己去定义的变量
variable-list:变量列表的意思(可有可无)
e.g:我们想来描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
里面可以有一个或者多个成员,struct是关键字,Stu是我们自己定义的。
<2>、结构体变量的定义和初始化
我为大家用代码总结一下,如下图所示:
struct Stu s1就是按照顺序来进行初始化
struct Stu s2就不是按照顺序来初始化,所以要加上.age、.name、.score、.id这样就不需要按照顺序来初始化(一个点加名字)
同时是从上述代码我们不难发现85.5和95.5后加上了一个f,这是因为85.5和95.5默认是double类型的,要将其变成float类型则在末尾加上一个f
当然除了上述的情况之外还有结构体调用结构体:
结构体里套用结构体时候要注意的是我们要注意{ },否则则会出现初始值设定项值太多,这种类似的错误。如下图所示:
当然了我们再次用代码的方式为大家展示一个完整版:
//代码1:变量的定义
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//代码2:初始化。
struct Point p3 = {10, 20};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s1 = {"zhangsan", 20};//初始化
struct Stu s2 = {.age=20, .name="lisi"};//指定顺序初始化
//代码3
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
2、结构成员访问操作符
<1>、结构成员的直接访问
结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下所示:
使用方式:结构体变量 . 成员名
下面给出代码大家可以自行尝试:
#include <stdio.h>
struct Point
{
int x;
int y;
}p = {1,2};
int main()
{
printf("x: %d y: %d\n", p.x, p.y);
return 0;
}
<2>、结构体成员的间接访问
有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针。如下所示:
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3, 4};
struct Point *ptr = &p;
ptr->x = 10;
ptr->y = 20;
printf("x = %d y = %d\n", ptr->x, ptr->y);
return 0;
}
使用方式:结构体指针->成员名
<3>、综合举例
#include <stdio.h>
#include <string.h>
struct Stu
{
char name[15];//名字
int age; //年龄
};
void print_stu(struct Stu s)
{
printf("%s %d\n", s.name, s.age);
}
void set_stu(struct Stu* ps)
{
strcpy(ps->name, "李四");
ps->age = 28;
}
int main()
{
struct Stu s = { "张三", 20 };
print_stu(s);
set_stu(&s);
print_stu(s);
return 0;
}
更多的结构体知识我会在后面继续写一篇博客为大家详细介绍,先了解这些基本的即可。
十一、操作符的优先级与结合性
C语言的操作符有2个重要的属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。
1、优先级
优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不一样的。
2、结合性
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符( = )。
运算符的优先级顺序很多,下面是部分运算符的优先级顺序(按照优先级从高到低排列),建议大概记住这些操作符的优先级就行,其他操作符在使用的时候查看下面表格就可以了。
参考:https://zh.cppreference.com/w/c/language/operator_precedence
十二、表达式求值
1、整型提升
C语言中整型算术运算总是至少以缺省(默认)整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
后面l表示转换为long类型,以此类推,ll即是转换为long long类型。
下面我们给出一段有趣的代码:
这段代码的执行结构是-116是不是出乎了很多人的预料,这是为什么呢?
b和c的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后再存储于a中。
如何进行整型提升:
1. 有符号整数提升是按照变量的数据类型的符号位来提升的
2. 无符号整数提升,高位补0
#include<stdio.h>
int main()
{
//char只能储存一个字节即8个数字
char a = 20;//截断后存储到a
//00000000000000000000000000010100
//00010100 - a
char b = 120;
//00000000000000000000000001111000
//01111000 - b
char c = a + b;
//00010100 - a
//00000000000000000000000000010100
//01111000 - b
//00000000000000000000000001111000
//00000000000000000000000000010100
// +
//00000000000000000000000001111000
//00000000000000000000000010001100
//10001100 - c
printf("%d\n", c);
//%d - 以10进制的形式,打印一个有符号的整型(int)
//11111111111111111111111110001100 - 补码
//10000000000000000000000001110011 - 反码
//10000000000000000000000001110100 - 原码
//-116
return 0;
}
下面为大家分三种情况分别说明(负数、正数、无符号整数):
//负数的整形提升
char c1 = -1;
变量c1的⼆进制位(补码)中只有8个⽐特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的⼆进制位(补码)中只有8个⽐特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//⽆符号整形提升,⾼位补0
2、算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换:
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后执行运算。下面为大家展示图片说明:
十三、总结以及下下期预告
1、总结
在 C 语言的广袤天地中,操作符如同灵动且关键的基石,构建起程序逻辑的坚实大厦。从算术操作符的精确运算,到逻辑操作符的严密判断,再到赋值操作符的巧妙传递,它们各司其职,协同运作。希望通过这篇文章,大家对 C 语言操作符有了更深入的理解,能够在后续的编程实践中熟练运用这些操作符,将它们化作手中利器,解决实际问题,创造出功能强大的程序。愿你在 C 语言的学习道路上,继续探索,不断进步,收获更多的编程乐趣与成就。
2、下期预告
下一篇博客我将为大家介绍C语言的重要知识指针,同时上次说的函数栈帧的创建和销毁会在指针的博客发布完之后三天内发布,大家敬请期待,共勉之!