一、C语言
C语言是一种面向过程的计算机程序设计语言,于1972年由美国贝尔实验室的Dennis Ritchie所开发。C语言广泛应用于操作系统、编译器、网络通信等方面,也是很多高级语言的底层实现基础。
C语言具有简洁、高效、可移植性好等特点,以及丰富的库函数,可以方便地进行底层的操作和系统编程。同时,C语言也是很多计算机科学专业的必修课程,深受程序员和计算机专业人员的喜爱。
C语言也是后来C++、Java、Python等高级编程语言的基础,有助于程序员更好地理解和掌握这些语言的特性和基础知识。
初学阶段看再多这种文字都是苍白无力的,此时的要求只是会写简单的程序就行了,如果是刚刚学到这里,那么现在你只需要会写个helloworld即可,在后来的学习中,随着书写的代码越来越多,你会慢慢对C的特性开始有了解,再回首看上面的这一段介绍才刚刚好,那时候你会认识到C是一门底层的语言,很稳定,再后来,当你学完C开始学C++或者Java等面向对象的时候,你才会对所谓的面向过程有一定理解,但也很有限,叫你说你也说不出个所以然哈哈哈
1、C语言程序helloworld
作为任何一种编程语言的NPC,helloworld绝对是所有程序员最熟悉的一个程序,没有之一。
值得说的一点是,任何一种语言都是不断发展的,虽然高校的教材十几年都没变,这导致我们书写的helloworld形式可能在直观上有很大不同,但我个人在编程的角度上是相信优胜劣汰的说法的,所以选择当下时兴的写法才是正解,可我们在编程上也有着风格一说,简单一点理解就是写的都没错,但某一批人觉得那样写更好些更好看些,这就是那一批人的风格,至于喜欢啥样的写法风格,这就见仁见智了。
// helloworld# include <stdio.h>int main(void)
{printf("hello world!\n");return 0;
}
2、解析helloworld程序
hello world
——>注释
// hello world
是行注释,
/*hello world*/
为区域注释
#include <stdio.h>
——>声明头文件
stdio.h
指的是标准输入输出函数,只有提前像上面这样对他声明(#include <stdio.h>
)才能调用标准输入输出函数
<>
通常代表系统预设的头文件,有他自己的储存位置和检索顺序,如果是""
,通常代表自己定义的头文件,通常存在于当前的工作目录,关于存储位置不用过多纠结,只需要知道二者分别是预设的和自己定义的即可。另外,不要去纠结
include
代表啥,以及声明头文件为什么要这么写:#include <stdio.h>
,不知道include啥意思自己去查查英汉词典去,现在是在写C语言程序,你现在学习的正是怎么写,也就是说正在学习规则。在C语言的规则下,声明头文件通常就是这样写的,记住就行
int main(void)
——>主函数
一个程序中只能有一个主函数,即 main 函数
主函数的写法有多种:
main()
,main(void)
,main(int argc,char **argv)
……没必要纠结写成啥样,除非老师或公司犯强迫症了要你写成啥样
int
是main函数返回值的类型,由于示范函数中返回的是int类型的数值 0,所以这里填int括号内的参数,叫做主函数传参:
argc
代表当前命令传入参数的个数,argv
当前传入主函数的参数内容,至于那两个*
就没必要纠结了,涉及到指针和地址暂时没必要了解
printf("hello world!\n");
——>这是main函数的内容,printf
函数为输出函数,这行代码的意思是在终端上输出hello world
\n
代表换行符,你可以不加\n
然后运行,看看会发生啥,你就理解他有啥用了
return 0;
——>返回值
用来判断函数是否正常结束,也可以返回运算结果,在此处,return 0 表示的是正常结束,
不要问为什么一定要加,当然对于很多程序包括这个,不加也能跑出来,但你尽可以试试在以后的所有程序不加,我保证我一定看不见你的bug
3、学习C语言的一些参考
(1)、个人的一些体会
学习一门编程语言最好的方法当然是手搓,但手搓只是一种行为,熟能生巧耳,写多了自然就知道该咋写,就好比如幼儿园小朋友认字,他不需要知道字是什么意思,只需要每天看每天写,看多了写多了自然就觉得是那么回事,编程语言也是,天天看代码写代码,没有不熟的道理。
但我们不能死写,编程是需要创造性的,这样才能满足实际的需求,所以说真正懂还是很有必要的,只有懂了某个东西才能举一反三,而不至于局限在死记硬背上面,虽然CV大法很好用我也很喜欢,但我写博客的一部分目的也正是为了尽可能多的搞懂一些知识,这就可以看出懂了的重要性了。
上面的两段话是大方向上的,因为编程是一门周期很长的东西,活到老学到老,所以需要端正态度,要做到懂,要多写
下面讲一些遇到的坑,有则改之,无则加勉,暂时只想到了几个,随缘加
首先要吐槽一下高校的编程教学,就拿我的学校来说,我不知道是不是因为我本专业不属于计科院的缘故,学习C语言的时候很是潦草,教材极其老旧,里面的代码风格好像还停留在上个世纪,老师的教学也是不行,全程PPT,老师在上课的时候敲出来的代码极少,直接导致的后果就是考试啥都不会,几乎全班都在靠手段过,但就我自己的经历来说,起码二本学校的非计科院的编程语言课真的可以直接自学了。
然后就是要提到学习时候遇到的一些不懂的地方了,首先要明确,编程上遇到问题首先自己上网搜,看看有没有类似的,因为即使你问了老师,结果大部分时候会失望,当然,科技在进步,现在有了ChatGPT,只要你的问问题的方向与措辞对了,它可以回答出你的几乎一切关于编程方面的简单问题,而且,问ChatGPT的效率是要大于搜索的,没办法,互联网环境就那样。
第三个想说的就是关于bug了,写代码就绕不开这玩意,我想说的是,自己实在改不出来就不要在当时继续浪费时间,果断给其他人看看,或者问问ChatGPT叫它帮你改改看,或者先放放,过一段时间再回来看看这个bug。
(2)、一些学习的工具
ChatGPT
就不解释是啥了,没听说过的应该也不是会学编程的,需要知道的是,它几乎能解决你现阶段在编程方面的一切疑问,但很遗憾,需要翻墙使用,而且需要用国外手机号接收验证码才能注册,但网上教程一大堆,实际操作也不算难,强烈建议试试
这里也做一个关于C语言视频课程的整理:
讲师 | 课程所属高校或机构或个人 | 课程名称 | 链接 |
---|---|---|---|
翁凯 | 浙江大学 | C语言课程设计 | 来自B站的链接 |
笔者能力所限,难以整理很多精品课程,大家可以在评论区补充,我看到后会去验证,验证OK就添加到博客中
编程语言的学习需要有系统性,那么,一本好的参考书是再好不过了
书名 | 作者 | 获取链接 | 推荐理由 |
---|---|---|---|
C Primer Plus(第五版)中文版 | Stephen Prata | 来自夸克网盘的文件 | 最了解C语言的人写的C语言参考书 |
二、编程规范
1、什么是坏代码
编号 | 坏代码的问题 |
---|---|
1 | 逻辑过于复杂,方法过长 |
2 | 过于复杂的API接口,说详细一点就是: 函数接口做了太多事情 包含过多的实例变量和方法 函数的命名不足以描述所做的事情 |
3 | 逻辑分散: 发散式变化:某个函数常因为不同的变化在不同的方向上发生变化 散弹式变化:发生某种变化时,需要在多个函数中做修改 |
4 | 数据泥团/基本类型偏执: 一个函数中包含相同的字段或参数 应该使用函数但使用基本类型,比如表示数值和币种的Money函数、起始值和结束值的range函数 |
5 | 过多的条件判断 |
6 | 过长的参数数列 |
7 | 临时变量过多 |
8 | 令人迷惑的暂时字段: 某个实例变量仅为某种特定情况而设置 |
9 | 不恰当的命名 命名无法准确描述所做的事情 命名不符合约定俗成的惯例 |
10 | 无注释或注释过少 |
2、坏代码的问题
- 难以复用:系统关联性过多,导致很难分离可重用部分
- 难以变化:一处变化导致其他很多部分的修改,不利于系统稳定
- 难以理解:命名复杂,结构混乱,难以阅读和理解
- 难以测试:分支、依赖较多,难以覆盖全面
3、代码的书写格式规范
关于这个其实没啥定数,毕竟是写给人看的,能看懂就行,所以才说是规范,而不是标准,但是代码写的规范点虽然不一定有好处,你老板可能不会多给你一块钱,但代码写得不规范却有代价,这就像是一个野指针,或者说是一个隐藏起来的bug,你不知道他啥时候会爆炸
关于代码书写规范本身,这就有意思了,因为规范很多,就比如我们老师给我们参考的就是华为的:
https://pan.quark.cn/s/eed70f459aa4
我的这部分就主要参考的这个,只是节选了一部分,太多了,等学习得更深入才有能力继续摘抄
(1)、变量名
首字母大写,其他的小写
可 | 可 |
---|---|
username | UserName |
password | PassWord |
(2)、常量名
全部大写,多个单词应该用_分割
CACHE_EXPIRED_TIME
(3)、项目名
全部小写,多个单词用-
分割
spring-cloud
(4)、API接口名(函数名)
单词首字母大写
Add()
Head_Init()
(5)、关键字和左括号之间使用一个空格
for (a = 0; a < 5; ++a) //OK
for(a = 0; a < 5; ++a) //WRONG
(6)、在函数名和左括号之间不要用单空格
int32_t a = sum(4, 3); // OK
int32_t a = sum (4, 3); // WRONG
(7)、每个逗号后面使用单空格
func_name(5, 4) // OK
func_name(4,3) // WRONG
(8)、在比较操作符和赋值操作符之前和之后使用单个空格
a = 3 + 4; //OK
a=3+4; //WRONG
a = 3+4; //WRONGfor (a = 0; a < 5; ++a) //OK
for (a=0;a<5;++a) //WRONG
4、什么是好代码
- 可读性
- 可维护性
- 可拓展性
目前为止,个人认为好代码主要体现在:好的逻辑,好的结构,规范书写。
三、变量
1、变量/常量
(1)、二者本质上的区别
在编程语言中要知道,两个玩意的区别往往是在内存上的,所以我们要养成一个习惯,就是在内存的角度上去看。
首先需要明白,二者都是有对应的内存空间的,换句话说,就是我们存储常量和变量的地方不在一个地方。局部变量通常存储在堆栈中,而全局变量通常存储在静态数据区。而看常量的字面意思都知道,常量是一个固定的值,他在程序执行过程中不能被修改,在C语言中,常量可以是字面量,也可以是通过#define
宏定义的符号常量,字面量通常直接存储在指令区(代码段)中,而符号常量通常存储在静态数据区。
(2)、变量
变量本质是一个内存地址,其储存数据的空间大小,是根据所储存的数据类型决定的,所以说定义一个变量,实际上就是定义了一个内存地址,这个内存地址本身无法改变,但这个内存地址的目的是用来储存数据的,也没有对数据做限制,所以,这个内存地址下的数据可变,由此,变量被称作变量。
就比如int a;
,我定义了一个int
类型的变量a
(3)、常量
即确定的数字,其本身并不能改变,比如常量1
, 2
, 3
, 4
,pi
……
(4)、变量命名原则
注意,此处说的是原则,必须遵守,要区分于上文中“大哥相信我,我一定会遵守(手动狗头)”
的规范
- 必须是由字母(区分大小写),数字,下划线组合而成
- 所有的变量名第一位不能为数字
- 不能使用关键字
- 见名知意,又足够简洁
错误书写 | 错误原因 | 正确书写(选填) |
---|---|---|
int ,float ,char | 变量命名不能使用关键字 | |
+aaa | 变量要定义首字母是符号的话,只能是下划线 | aaa+ |
2、C语言中三大基本数据类型
不同操作系统位数的情况下有些类型的字节数不一样,现在用的通常是64位,所以很多时候都默认是64位的情况
(1)、整形
英文写法 | 中文名称 | 字节数 | 格式控制符 |
---|---|---|---|
short | 短整型 | 2字节 | %hd 或 %hi |
int | 整形 | 4字节 | %d |
long | 长整形 | 8字节 | %ld 或 %li |
long long | 长长整形 | 8字节 | %lld 或 %lli |
– | – | – | – |
– | – | – | – |
unsigned int | 无符号整形 | 4字节 | %u |
unsigned short | 无符号短整型 | 2字节 | %hu |
unsigned long | 无符号长整形 | 8字节 | %lu |
unsigned long long | 无符号长长整形 | 8字节 | %llu |
– | – | – | – |
– | – | – | – |
unsigned int | 无符号整形 | 4字节 | %d 或 %i :以十进制形式输出带符号整数 |
unsigned int | 无符号整形 | 4字节 | %u :以十进制形式输出无符号整数 |
unsigned int | 无符号整形 | 4字节 | %o :以八进制形式输出无符号整数 |
unsigned int | 无符号整形 | 4字节 | %x 或 %X :以十六进制形式输出无符号整数,%x输出小写字母,%X输出大写字母 |
需要注意的是,对于有符号整形数据,应该使用%d
作为格式控制符,对于无符号整形数据,应该使用%u
作为格式控制符。
而%d
和%i
都是int
类型的格式控制符。它们可以用来输入或输出带符号的十进制整数。两者的区别在于,%d
默认情况下会忽略前导的0,而%i
会将前导的0视为八进制数的前缀,可以输入或输出八进制数。
在 C/C++ 中,signed 和 unsigned 都是修饰整型的关键字,其区别在于:signed 表示该整型变量是有符号类型,可以表示正数、负数和0;而unsigned 表示该整型变量是无符号类型,只能表示非负数,即0和正数。
需要注意的是,signed 和 unsigned 对于表示相同大小的整数,其二进制位数相同,但表示的范围不同。例如,signed int 和 unsigned int 都占用4个字节(32位),但前者表示的范围是 -2^31 ~ 2^31-1,后者表示的范围是 0 ~ 2^32-1。
另外,对于带符号类型的整数,最高位表示符号位,0 表示正数,1 表示负数;对于无符号类型的整数,所有位都表示数值,没有符号位。
(2)、浮点型
浮点数只能保留6位小数,第7位四舍五入
英文写法 | 中文写法 | 字节数 | 格式控制符 |
---|---|---|---|
float | 单精度浮点型 | 4字节 | %f |
double | 双精度浮点型 | 8字节 | %lf |
long double | 长双精度 | 8字节 | %Lf |
float, double 类型默认保留6位小数,若要打印出两位小数,则:printf("%.2f\n", num);printf("%.2lf\n", num);
,就比如下面的例子:
#include <stdio.h>int main()
{float a;scanf("%f", &a);printf("former:\t%f\n", a);printf("now:\t%.2f\n", a);return 0;
}
(3)、字符型
在定义单个字符类型数据的时候,一定要用单引号进行赋值,如果单引号里有多个字符,此时只会输出末尾的那个字符
英文写法 | 中文写法 | 字节数 | 格式控制符 |
---|---|---|---|
char | 字符型 | 1字节 | %c |
signed char | 有符号字符 | 1字节 | %c |
unsigned char | 无符号字符 | 1字节 | %c |
– | – | – | – |
– | – | – | – |
character string | 字符串 | 随字符串的长度而变化 | %s |
我们可以通过sizeof函数来计算char、short、int、long、float、double等等数据类型分别占用多少个字节。
# include <stdio.h>int main(void)
{printf("char:%ld\n", sizeof(char));printf("int:%ld\n", sizeof(int));printf("short:%ld\n", sizeof(short));printf("long:%ld\n", sizeof(long));printf("float:%ld\n", sizeof(float));printf("double:%ld\n", sizeof(double));return 0;}
我们通常默认的是signed char,也就是说我们见到的char指的都是signed char,当使用unsigned char的时候,我们会专门写出来的
(4)、布尔类型
布尔类型只有TRUE/FALSE两种情况,我们如果要使用布尔类型,需要加上头文件:#include<stdbool.h>
#include <stdio.h>int main()
{if (2 > 1) printf("True\n");else printf("False\n");return 0;
}
布尔类型显然是用在有判断的地方,而有些地方其实是隐含了一个布尔类型的,这时候就不需要带上头文件,也举个例子:
#include <stdio.h>int main()
{int b = 1;if (b) printf("True\n");else printf("False\n");return 0;
}
英文名称 | 中文名称 | 字节数 | 格式控制符 |
---|---|---|---|
bool | 布尔类型 | 1字节 | %d |
C语言中并没有专门用于输出布尔类型的格式控制符,但是,布尔类型在内存中实际上被表示为0或1,因此可以用%d格式控制符输出。在C++中,可以使用boolalpha操纵符来输出布尔类型的字符串值。
(5)各数据类型的取值范围
一个数据类型的取值范围取决于他所占的字节数,以及他是unsigned或是signed,举一个简单的例子:
名称 | 取值范围 |
---|---|
short | -32768~32767 |
unsigned short | 0~65535 |
char | -128~127 |
unsigned char | 0~225 |
short占2个字节,而一个字节有8个位,所以,short的可能取值有256x256=65536个,由于需要区分正负,也要考虑到0这个数字,则为-32768~32767
对于单个字节,已知它有8个位,所以有2^8=256个可能取值,同理,若要区分正负,也要考虑0,则为:-128~127
(6)、变量的定义方式
数据类型 变量名
例如:
int a; //定义变量a
a = 10; //初始化(在C语言中,变量的第一次赋值,被称为变量的初始化)
也可以这样:
int a = 10; //定义变量a,并初始化
(7)、数据类型的格式控制符
前面顺带给了除了不少格式控制符,这里汇总一下,不详细,但实用
数据类型 | 格式控制符 |
---|---|
字符型 | %c |
短整型 | %hd |
整形 | %d |
长整形 | %ld |
长长整形 | %lld |
单精度浮点型 | %f |
双精度浮点型 | %lf |
无符号长整形 | %lu |
无符号整形 | %u |
打印地址 | %p |
打印字符串 | %s |
输出十六进制数据 | %x |
输出八进制数据 | %o |
输入百分号 | %% |
最后一个的百分号没见到过,这里介绍一下:若是这样写,显示的就是百分数了:printf("100%%")
对于十六进制的表示,这里有好几种写法:
#include "head.h"int main(int argc, char const *argv[])
{int i = 10;printf("%x\n", i);printf("%X\n", i);printf("%#x\n", i);printf("%#X\n", i);return 0;
}
3、char
(1)、signed/unsigned char
关于signed char 与 unsigned char,我们在前面其实已经做了一个简单的说明,但在这里还是啰嗦一下,二者的区别主要在数据的范围与有无符号位两个方面。
举一个简单的例子:C语言中,char变量占1个字节,也就是有8个位,所以就有2^8=256个可能的数,即-128~127,在计算机中,正负数都可以视作用补码表示,通常默认signed,-128表示为 1000 0000
,127表示为 0111 1111
,若是unsigned,范围则为:0000 0000~1111 1111
char
、signed char
和 unsigned char
是三种不同的类型。在C语言中,char类型可以被实现为有符号或无符号,具体取决于编译器的实现。在一些平台上,char默认被视为有符号类型,而在另一些平台上,char默认被视为无符号类型。为了保证代码的可移植性,建议在使用时显式地指定signed char或unsigned char类型。
// 用来判断编辑器是默认signed还是unsigned的一段程序#include <stdio.h>void main(void)
{char a = 0xff; //十六进制数,化为十进制等于255signed char sa = 0xff;unsigned char ua = 0xff;printf("a = %d, a = %c\n", a, a);printf("sa = %d, ua = %d\n", sa, ua);}
这里还是要解释一下的,关于char a =255,%d 格式下的输出却是-1的原因:由于默认signed,所以十六进制的0xff,即十进制的255,都是有符号位的,而在计算机中,正负数的储存都是补码形式存在,正数255的补码:1111 1111,对于这串补码,由于是signed,所以首个数位为符号位,故而此时计算机认为,补码1111 1111的符号位为1,即这个数为负,然后将补码1111 1111,即负数补码1111 1111化为真值,就得出了-1
(2)、关于ASCII码
ASCII码是一种将字符映射为数字的编码方式。ASCII是American Standard Code for Information Interchange(美国信息交换标准代码)的缩写,它使用7位二进制数表示128种字符(包括字母、数字、标点符号和控制字符),其中包括95个可显示字符和33个控制字符。例如,大写字母’A’的ASCII码是65,小写字母’a’的ASCII码是97,数字’0’的ASCII码是48,空格的ASCII码是32。
由于ASCII码只使用了7位二进制数,因此可以使用一个字节(8位二进制数)来存储一个ASCII码字符。ASCII码是计算机领域中最常用的编码方式之一,常被用于网络传输、文件存储等场合
获知ASCII码并不需要去浏览器上搜垃圾,直接在terminal中输入 man ascii
命令即可:
对于这一大张表,我们也无须死记,一点意义都无,需要用的时候我们通常的做法还是查,,,但test的时候就不好查了,所以我们只需要记住几个标志值即可:
%c(char) | %d(Dec) |
---|---|
‘0’ | 48 |
‘a’ | 97 |
‘A’ | 65 |
'\n' (空格) | 32 |
MULL(\0 ) | 0 |
可以直接用ASCII码来代替对应的字符,这样就不用带单引号了
#include <stdio.h>int main(int argc, char const *argv[])
{int a = 97;printf("%d, %c\n", a+1, a+1);printf("%ld\n", sizeof('a')); // 这里其实算的是ASCII,所以计算的是int数据所占空间的大小return 0;
}
(3)、关于字符型变量与ASCII的一些例程
用了一段时间的ASCII码了,暂时觉得ASCII码的主要作用是用来表示字符,让字符也能进行很清楚的进行计算,问了下ChatGPT关于ASCII存在的意义,他的回答:
ASCII码的存在使得计算机可以对文本进行处理和表示,这是非常重要的,因为文本是人们交流和储存信息的重要形式。在计算机的早期发展阶段,人们意识到需要一种标准来表示字符,以便不同计算机之间能够相互交流。ASCII码应运而生,它通过将每个字符映射到唯一的整数值来表示字符,并且这些整数值可以在计算机之间进行交换和存储。随着计算机技术的发展,ASCII码仍然是计算机领域中最重要的字符编码之一,因为它被广泛应用于计算机系统和应用程序中。
下面我记录了一下曾经碰见过的关于ASCII码应用的情况,简单,但是建议逐个手搓一遍:
例程1:字母的大小写转换
/*
如果变量c里的数据是大写字母,则将其转换成对应的小写,否则c的值不改变一些翻译:
uppercase---大写字母
lowercase---小写字母
capital---大写的
letter---字,信件
convert---转换为
*/#include <stdio.h>
int main(void)
{char c;printf("Please enter uppercase or lowercase letters:\n");scanf("%c", &c);if (c>='A'&& c<='Z'){printf("The variable c is a capital letter\n");printf("Convert to lowercase to:%c\n",c=c+32);}else{printf("The value remains unchanged, still:%c\n",c);}return 0;
}
例程2:ASCII与字符相互转换
// 编写一个程序,实现如下功能:用户输入一个 ASCII 码值(如 66),程序输出相应的字符# include <stdio.h>int main(void)
{int i = 0;printf("请输入一个 ASCII 码值:");scanf("%d", &i);printf("该 ASCII 码对应的字符为:%c\n", i);return 0;}
在这个例子上稍微延伸一下:
// 编写一个程序,实现如下功能:
// 用户输入一个 ASCII 码值(如 66),程序输出相应的字符# include <stdio.h>int main(void)
{char n = 9;// 字符型变量也可以直接赋值给整型printf("第1种1类:\t%c\n", n);printf("第1种2类:\t%d\n", n);// 看得出,字符型变量直接赋值给整型后,性质与操作上都属于整形char i = '9';// 字符量的正常转化printf("第2种1类:\t%c\n", i);printf("第2种2类:\t%d\n", i);int j = 9;// 整形变量的转化(尝试)printf("第3种1类:\t%c\n", j);printf("第3种2类:\t%d\n", j);int a = 'a'; // 顺着第2种下面新写的注释,想不到还可以这样赋值吧,只是看看,长长见识printf("第4种1类:\t%c\n", a);printf("第4种2类:\t%d\n", a);return 0;}
这里还要注意一下字符型赋值给整形时候越界的情况,这个情况我请教了一下ChatGPT:
字符型变量直接赋值给整型的时候,如果字符的编码值大于等于0x80(十进制128),也就是对应的是负数的补码形式,那么会出现越界的情况。因为char类型是有符号类型,它的取值范围是-128到127,而把它直接赋值给int类型时,int类型是无符号的,如果char类型的值是负数,转换后就会变成大于127的正数,这样就会越界。例如,将字符变量’€’(Unicode编码为0x20AC)赋值给int类型的变量会出现越界。
例程3:将字符转换为十进制数据
// 将字符 1 转化为数字 1# include <stdio.h>int main(void)
{char ch = '1';int b = ch - 48;printf("%d\n", b);return 0;}
4、进制转换
(1)、进制的使用
计算机中,储存数据用的是二进制,也就是说,计算机只能识别0和1,我们在电脑上看见的各种数据,很少有二进制,但要知道,那是给我们用户看的,若是电脑想要对其进行操作,则需要先将它转换为二进制的数据,才能进行后续的操作。
我们在Windows系统有一个很好用的工具,就是系统自带的计算器,我们将他转换为程序员模式,我们需要用到进制转换的时候就别傻傻的在草稿纸上列式子了,程序员之所以看起来聪明,我看很大一部分原因是因为习惯于使用各种工具:
- 十进制:DEC
- 二进制:BIN
- 八进制:OCT
- 十六进制:HEX
(2)十进制转二进制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZT37ufS-1680965950799)(https://fhzy886.oss-cn-hangzhou.aliyuncs.com/images/3a69f52fef8708b592e5ae1b82573ba.jpg)]
(3)、n进制转二进制
思路是用十进制当中间量,即先把某进制转化为十进制,然后用十进制转化为2进制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BA3i5ZyQ-1680965950800)(https://fhzy886.oss-cn-hangzhou.aliyuncs.com/images/2f19bef92e337a274bc28d86f909b19.jpg)]
四、原码补码反码
1、三种表示方法是什么
在计算机中,我们需要将数值转换为二进制表示,以便计算机进行运算。在二进制数中,最高位一般用来表示数值的符号,0表示正数,1表示负数。因此,针对有符号数,需要一种方法来表示负数的二进制表示。在计算机中,有三种表示负数的方法,即原码、反码和补码。
-
原码:最高位为符号位,0表示正数,1表示负数,其余位表示数值大小。例如,8位有符号整数中,00000101表示正数5,10000101表示负数-5。
-
反码:正数的反码就是原码本身,负数的反码就是对原码按位取反。例如,8位有符号整数中,00000101的反码为00000101,10000101的反码为11111010。
-
补码:正数的补码就是原码本身,负数的补码是其反码加1。例如,8位有符号整数中,00000101的补码为00000101,10000101的补码为11111011。
原码、反码和补码的主要区别在于对负数的表示方式。原码最直观,但对于减法操作则较为繁琐,因为需要将减数、被减数和差分别考虑符号位和数值位。反码解决了这个问题,但在运算中需要注意,同样是繁琐的。补码则是最常用的表示负数的方法,因为它可以用同一种方式进行加减法运算,而且最高位不再是特殊的符号位。
需要注意的是,计算机中使用的都是补码表示法。
2、负数求补码
我们知道,正数的补码是他本身,若是负数,则将其绝对值的二进制表示取反,然后加1。
以-128
为例,在8位二进制表示中,-128的绝对值为128,其二进制表示为1000 0000
。将其取反得到0111 1111
,然后加1得到1000 0000
,即为-128的补码。
3、关于原码
原码这玩意我们在这里也会见到别人说是真值,但二者不是一个东西,原码是一个有符号整数在计算机中存储时所采用的一种二进制表示方法,它的最高位表示符号,0表示正数,1表示负数;其余各位表示数值部分,即原数的绝对值的二进制表示。而真值是指在计算机中实际表示的数值,它可能等于原码,也可能不等于原码,具体取决于所使用的是何种编码方式,如原码、反码、补码等。
我们有时候会遇到这种情况,就是用到原码的时候,当一个unsigned数值大于255,8个位是不够用来存储数据的,此时我们会用12位来表示,例如256,我们就把他表示为 0001 0000 0000
关于如何求一个负数的原码,例如-127,我们首先观察这是一个负数,所以原码的符号位是1,然后其他7个位来表示负数的数据部分,即用这7个位来表示这个负数的绝对值,127的二进制是111 1111
,结合一下,所以-127的原码是 1111 1111
4、反码的范围
8位二进制可以表示256个不同的数,其中第1位表示符号位,因此可以表示数值的只有7个位,所以反码范围为-127至+127,即[-2^7+1, 2^7-1]
。
对于-128
,这个数值很是特殊,我们已经知道反码范围是不包括他的,事实上也是如此,他只有原码1000 0000
,反码和补码都不存在,因为它的绝对值大于可表示的最大值(在8位二进制中,最大值为127)。在计算机中,超出了数据类型能表示的范围,就会发生溢出现象。因此,在使用计算机进行数值计算时,需要注意数据类型的范围。
5、三码的转换
原码与反码:符号位不变,直接按位取反即可
原码转补码:通常我们拿到的是一个十进制数,对于这个数,如果它是正数,则它的原码、反码和补码都相同;如果它是负数,则先将它的符号位不变,绝对值转换为二进制数,此时也就求出了这个负数的原码,若想求补码,取反加1即可
这里还介绍一下有些人的做法,也是可行的:-127的补码可以通过以下步骤得到:
- 先将127转换为二进制数:0111 1111
- 将上一步得到的二进制数按位取反:1000 0000
- 将上一步得到的结果加1:1000 0001
因此,-127的补码是1000 0001。
补码转原码:对于一个数,如果它的符号位为0,则它的原码、反码和补码都相同;如果它的符号位为1,则先将它的补码形式减1得到它的反码,然后对反码的数据位取反得到它的原码。
这里,我们尝试将-127的补码转换为原码:
根据补码转原码的方法:
- 如果补码的最高位是1,那么该数为负数;
- 将补码除符号位外的每一位取反,得到反码;
- 反码加1,得到原码。
因此,对于补码1000 0001,可以按以下步骤转换为原码:
- 最高位是1,因此该数为负数;
- 将除符号位外的每一位取反,得到1111 1110为反码;
- 反码加1,得到1111 1111为原码。
因此,-127的原码为1111 1111,上面介绍的补转反我前后介绍了两个方式,都是可行的
补码与反码:对于一个数,如果它的符号位为0,则它的原码、反码和补码都相同;如果它的符号位为1,则反码加1等于补码。
例如,-127的补码是 1000 0001
,则反码为 1000 0000
6、原码反码补码的范围
这里不太理解原因,只简单记录下结论,参考了博客:http://t.csdn.cn/jLRxF
对于8位二进制数:
- 原码:
-127
~+127
- 反码:
-127
~+127
- 补码:
-128
~+127
实际上,将负数用补码表示,实际上是实现了一种从[-128, 127]
到[0, 255]
的映射
五、输入输出
1、printf
功能 | 格式化字符串输出 |
---|---|
原型 | int printf(const char *format, ...); |
头文件 | #include <stdio.h> |
备注 | printf会将输出写入标准输出流 |
// helloworld# include <stdio.h>int main(void)
{printf("hello world!\n");return 0;
}
2、scanf
功能 | 格式化字符串输入 |
---|---|
原型 | int scanf(const char *format, ...); |
头文件 | #include <stdio.h> |
备注 | scanf会将输入写入标准输入流 |
# include <stdio.h>int main(void)
{char arr[10];printf("请输入一个你喜欢的歌手:\t");scanf("%s", arr);printf("p:\t%s\n", arr);return 0;
}
连续输入的时候,用空格隔开格式控制符,除此之外,在双引号中不要有其他的内容,就连换行符\n
也不用加,然后在运行的时候输入,输入下一个的时用空格或者换行隔开都行,还需要注意的是,在scanf输入int类型与char类型的时候,不要忘记在语句中加上&
scanf("%d %d %d", &a, &b, &c);
使用空格连续输入的情况:
使用回车连续输入的情况:
六、运算符
1、运算符的优先级
以下是常见的运算符按照优先级从高到低的顺序:
需要注意的是,此优先级表是基于 C 语言的,不同的编程语言可能存在细微的差异,所以在编程时需要查阅对应编程语言的运算符优先级表。此外,在实际编程时也要注意运算符优先级可能会因为使用圆括号等符号而发生改变。
2、运算符的结合性
当一个表达式中存在多个运算符时,运算符之间的优先级和结合性会影响运算的顺序。虽然我们通常面临的书写不会太变态,毕竟我们总是习惯以简单的写法来解决问题,顶多用用优先级,但是有些变态的人总会恶意刁难你,来显示自己NB,会在代码中写一个带结合性的运算符计算,我们不能排除这种情况,所以认识一下结合性也是必要的。
结合性的应用场景在某一个区域存在多个具有相同优先级的运算符,此时才需要考虑它们的结合性,也就是说我们会首先去考虑优先级。运算符的优先级决定了在表达式中运算符的处理顺序。优先级高的运算符会先被计算,优先级低的运算符则在后面被计算。例如,在表达式 3 + 5 * 2
中,乘法运算符的优先级高于加法运算符,因此会先计算 5 * 2
的结果为 10,然后再将结果加上 3,得到最终结果为 13。
如果存在多个具有相同优先级的运算符,例如 + 和 -,则需要考虑它们的结合性。结合性有两种类型:左结合和右结合。左结合表示从左到右计算,而右结合则表示从右到左计算。
例如,在表达式 6 - 3 + 2
中,减法和加法的优先级相同,但是减法是左结合的,因此会先计算 6 - 3
的结果为 3,然后再将结果加上 2,得到最终结果为 5。
另一个例子是赋值运算符 =,它是右结合的。在表达式 a = b = 5
中,右侧的赋值运算符 b = 5
会先被计算,将 b 赋值为 5,然后将 5 赋值给 a,最终 a 和 b 的值都为 5。
#include <stdio.h>int main()
{int a = 1;int b = 2;int c = a++>b++ || b++<5;printf("a = %d\tb = %d\tc = %d\n", a, b, c);return 0;}
对于int c = a++>b++ || b++<5;
,我们尝试把a++>b++
改为a++<b++
, 直接导致的后果就是:使逻辑或||
左边的式子为真了,
而逻辑或||
的运算机制有点小坑,当他左边为真的时候结果此时一定为真,所以他就直接不管后面的了,这种情况叫做运算符的短路,在某些情况下不必计算整个表达式的值,而是可以根据前面的部分已经得出最终结果,从而节省计算资源的过程。
3、单目运算符与双目运算符的区别
单目运算符,双目运算符,三目运算符的主要区别在于他们需要操作数的数量,具体来说,单目运算符只需要一个数就能使用,例如 a++
,其他两种同理。
对于双目运算符,我们还需要留意一种情况,举个例子:a > b
,这个表达式是一个判断,是一个判断就只有 true 或者 false,也就是 0 或者 1,所以此时这个式子也就要当做 0 或者 1 对待。
4、a++与++a
对于a++和++a,他们都是C语言中的自增运算符,但它们的行为略有不同:a++表示先使用a的值进行操作,然后再将a的值加1。它的返回值是a的原始值。举个例子:
int a = 1;
int b = a++;
// a现在的值为2,b的值为1
在这个例子中,a++会首先把a的原始值1赋给b,然后再将a的值增加到2。
相反,++a表示先将a的值加1,然后再使用新的值进行操作。它的返回值是a的新值。举个例子:
int a = 1;
int b = ++a;
// a现在的值为2,b的值也为2
在这个例子中,++a会首先将a的值增加到2,然后把新的值2赋给b。
总之,a++和++a的差异在于它们对a的值增加的时机不同,也就是说,它们的副作用不同。在实际应用中,需要根据具体情况选择使用哪个运算符。
5、三目运算符
常见的三目运算符只有一个,即条件运算符 “?:”。它根据条件的真假选择不同的结果。它的语法为:
condition ? expr1 : expr2
如果 condition 为真,则返回 expr1,否则返回 expr2。
6、复合运算符
符号 | 拆分 |
---|---|
a += 1 | a = a + 1 |
a -= 1 | a = a - 1 |
a *= 1 | a = a * 1 |
a /= 1 | a = a / 1 |
除却这四个关于四则运算的符号,还有关于位运算的:
a |= b
即a=a|b
读作a按位或ba &= b
即a=a&b
读作a按位与ba ^= b
即a=a^b
读作a按位异或b
7、逻辑运算符
&&
逻辑与||
逻辑或!
逻辑非
8、位运算
|
或&
与~
取反^
异或(二者都真则为0,二者都假则为1)
#include "head.h"int main()
{unsigned int var = 0x12345678;unsigned int var_high6_8 = var%0x100;unsigned int var_high4_6 = var%0x10000/0x100;unsigned int var_high2_4 = var%0x1000000/0x10000;unsigned int var_high0_2 = var/0x1000000;var_high0_2 = var_high0_2; var_high2_4 = var_high2_4 << 8;var_high4_6 = var_high4_6 << 16;var_high6_8 = var_high6_8 << 24;var = var_high0_2 | var_high2_4 | var_high4_6 | var_high6_8;printf("%x\n", var);return 0;}
#include "head.h"int main()
{unsigned int var = 0x12345678;unsigned int var_high_1 = var/0x10000000;unsigned int var_high_2 = var%0x10000000/0x1000000;unsigned int var_high_3 = var%0x1000000/0x100000;unsigned int var_high_4 = var%0x100000/0x10000;unsigned int var_high_5 = var%0x10000/0x1000;unsigned int var_high_6 = var%0x1000/0x100;unsigned int var_high_7 = var%0x100/0x10;unsigned int var_high_8 = var%0x10;var_high_1 = var_high_1;var_high_2 = var_high_2 << 4;var_high_3 = var_high_3 << 8;var_high_4 = var_high_4 << 12;var_high_5 = var_high_5 << 16;var_high_6 = var_high_6 << 20;var_high_7 = var_high_7 << 24;var_high_8 = var_high_8 << 28;var = var_high_1 | var_high_2 | var_high_3 | var_high_4 | var_high_5 | var_high_6 | var_high_7 | var_high_8;printf("%x\n", var);return 0;}
9、移位运算符
左移操作符(<<)和右移操作符(>>)是用来进行二进制位移的操作符。在这里我们分别讨论左移和右移在正数和负数情况下的使用方法和注意点。
(1)、左移操作符(<<)
正数情况下:左移操作符将一个数的所有二进制位向左移动指定的位数,右侧空出的位用0填充,相当于原数乘以2的指定位数次方。
假设有一个无符号整数变量a = 0b0101(即5),将其左移两位,得到0b010100,即20。在这种情况下,左移会在低位补0,因此结果是2的n次方倍(n为移位位数)。
负数情况下:左移操作符在处理负数时需要注意,因为负数的补码是其原码取反加1,左移后可能导致符号位被改变,所以需要进行符号扩展。具体来说,可以在左移前将负数的符号位及其左侧所有二进制位均变为1,再进行左移,右侧空出的位仍用0填充,最后再将符号位及其左侧所有二进制位均变为1,这样得到的就是正确的结果。
假设有一个有符号整数变量a = -3,即0b11111101。将其左移两位,得到0b11110100,即-12。在这种情况下,左移仍然是在低位补0,但是由于a是有符号整数,因此可能会导致符号位改变,即由负变正。
(2)、右移操作符(>>)
正数情况下:右移操作符将一个数的所有二进制位向右移动指定的位数,左侧空出的位用0填充,相当于原数除以2的指定位数次方。
假设有一个无符号整数变量a = 0b1010(即10),将其右移两位,得到0b0010,即2。在这种情况下,右移会在高位补0,因此结果是原数除以2的n次方(n为移位位数)
负数情况下:右移操作符在处理负数时需要注意,因为负数的补码是其原码取反加1,右移后可能导致符号位被改变,所以需要进行符号扩展。具体来说,可以在右移前将负数的符号位及其左侧所有二进制位均变为1,再进行右移,左侧空出的位用1填充,最后再将符号位及其左侧所有二进制位均变为1,这样得到的就是正确的结果。
假设有一个有符号整数变量a = -10,即0b11110110。将其右移两位,得到0b11111101,即-3。在这种情况下,右移仍然是在高位补0,
总之,在使用移位操作符时需要注意溢出问题以及负数的符号扩展问题,以确保得到正确的结果。
七、类型转换
在C语言中,类型转换是指将一种数据类型的值转换为另一种数据类型的值的过程。C语言中有两种类型转换,隐式类型转换和显式类型转换。
1、隐式转换
隐式类型转换是在表达式中自动发生的,它是由编译器根据表达式中出现的数据类型来自动转换的。例如,当我们将一个整型变量的值赋给一个浮点型变量时,编译器会自动将整型转换为浮点型。
#include <stdio.h>int main() {int a = 10;float b = 3.14;double c = 2.718;int result = a + b + c; // float和double会转换成intprintf("Result: %d\n", result);return 0;
}
2、显示转换
显式类型转换则是在代码中显式地指定类型转换的过程,也称为强制类型转换。它使用一种特殊的语法来告诉编译器将某个值转换为特定的数据类型。这种转换可以通过使用类型转换运算符来实现,即在要转换的值前加上用括号括起来的目标类型。
例如,下面是将一个整型变量转换为浮点型变量的示例:
int a = 5;
float b = (float) a;
在这个例子中,我们使用了类型转换运算符将整型变量a转换为浮点型,然后将转换后的值赋给了浮点型变量b。
需要注意的是,类型转换可能会导致精度损失或者溢出等问题,因此在进行类型转换时,我们需要确保转换的结果是正确的,避免因为类型转换而引发错误。
#include <stdio.h>int main() {int a = 100;float b = 3.14;float result = (float)a / b;printf("result = %.2f\n", result);return 0;
}
八、缓冲区
1、什么是缓冲区
在C语言中,缓冲区(Buffer)指的是一个临时存储区域,用于临时存放程序处理的数据。在输入/输出(I/O)操作中,缓冲区用来存储待写入或待读取的数据,以减少对外部设备的直接访问次数,提高程序的效率。C语言标准库中提供了多个函数来实现缓冲区的操作,包括stdio.h头文件中的fopen()、fclose()、fread()、fwrite()、fgetc()、fgets()、fputc()、fputs()、fprintf()、fscanf()等函数。
在文件I/O中,文件通常以块的形式进行读取和写入。当我们使用fread()或fwrite()函数读写文件时,数据并不是逐个字节进行读写的,而是以块的形式,一次读写一定数量的字节数据。这些数据会被暂存在内存中的缓冲区中,待缓冲区填满后才会将整个缓冲区写入文件或从文件中读取到缓冲区中。
在标准输入/输出中,缓冲区同样扮演着重要的角色。例如,当我们使用printf()函数输出字符串时,实际上并不是直接将字符串写入标准输出设备(如控制台)中,而是先将其存入缓冲区中,待缓冲区填满或遇到换行符时再将其一次性写入标准输出设备中。同样的,使用scanf()函数从标准输入设备(如键盘)中读取数据时,实际上也是先将输入数据暂存在缓冲区中,待用户输入完毕后再一次性读取到程序中。
需要注意的是,在使用缓冲区时,一定要注意清空缓冲区,避免因为未清空导致程序出现问题。
2、缓冲区被清空的情况
#include <stdio.h>int main() {char c1, c2;printf("请输入两个字符:");scanf("%c%c", &c1, &c2);printf("输入的字符为:%c%c\n", c1, c2);return 0;
}
在上面的程序中,用户输入的两个字符会被存储在输入缓冲区中,直到程序调用scanf()函数从缓冲区中读取这两个字符。在scanf()函数中,%c格式符用来读取单个字符,因此需要调用两次%c格式符来读取两个字符。当用户输入两个字符后,按下回车键时,缓冲区中的内容会被清空,scanf()函数才会从缓冲区中读取这两个字符。
常见的清空缓冲区的情况包括:
-
输入函数读取不完整的数据:比如scanf函数读取整型数据时输入了一个字符,这个字符就会留在输入缓冲区中,下次读取时可能会对程序造成影响,需要清空输入缓冲区。
-
输出函数的缓冲区未满而被换行符清空:比如printf函数在输出内容时如果遇到了换行符,缓冲区会被清空,如果此时缓冲区中还有未输出的内容,就需要清空输出缓冲区。
-
程序需要清空缓冲区中的内容:比如在程序中进行交互式输入时,需要清空输入缓冲区,以免影响后续的输入操作;或者程序中需要在某个时刻清空输出缓冲区,以便将已经缓存的数据输出。
-
其他特殊情况:比如在进行网络通信时,需要清空网络缓冲区等。
在标准输入流中,缓冲区会在以下情况被清空:
-
输入了换行符(
\n
):当我们使用scanf、fgets、gets等函数读取用户输入时,当用户输入回车后,缓冲区中会保存一个换行符。因此,输入换行符可以清空缓冲区。 -
输入文件结束符(EOF):在Windows系统中,我们可以通过键盘输入Ctrl + Z组合键,输入文件结束符。在Linux系统中,我们可以通过键盘输入Ctrl + D组合键,输入文件结束符。输入文件结束符也会清空缓冲区。
-
程序关闭标准输入流:当程序关闭标准输入流(如使用fclose(stdin))时,缓冲区也会被清空。
在标准输出流中,常见的清空缓冲区的情况是:
-
输出了换行符(
\n
):当我们使用printf、puts等函数输出时,当输出字符串中包含换行符,或者我们使用了printf(“\n”)这样的语句时,输出缓冲区中的内容会被立即输出,即清空缓冲区。 -
缓冲区已满:当我们使用printf、puts等函数输出时,如果输出缓冲区已经满了,缓冲区中的内容会被立即输出,即清空缓冲区。
3、缓冲区装满
#include <stdio.h>int main() {char c;printf("请输入多个字符:");while ((c = getchar()) != '\n') {printf("%c", c);}printf("\n");return 0;
}
在上面的程序中,程序通过getchar()
函数不断从输入缓冲区中读取字符,直到读取到回车符\n
为止。如果用户输入的字符超过了输入缓冲区的大小,缓冲区会被装满,后续输入的字符会挤掉之前的字符。
在 C 语言中,可以使用标准库函数 setvbuf() 和 BUFSIZ 常量来获取缓冲区的大小。
下面是一个简单的示例程序,演示如何获取标准输入流 stdin 的缓冲区大小:
#include <stdio.h>int main() {// 将 stdin 的缓冲区设置为不带缓冲setvbuf(stdin, NULL, _IONBF, 0);// 获取缓冲区大小int bufsize = BUFSIZ;printf("stdin buffer size: %d\n", bufsize);// 读取输入char buffer[bufsize];fgets(buffer, bufsize, stdin);// 输出输入内容printf("Input: %s", buffer);return 0;
}
在这个程序中,我们使用 setvbuf() 函数将 stdin 的缓冲区设置为不带缓冲。然后使用 BUFSIZ 常量获取缓冲区大小,并使用 fgets() 函数从标准输入流中读取输入内容。
值得注意的是,在某些系统中,BUFSIZ 的大小可能会很小(例如 512 字节),因此如果需要更大的缓冲区,可以使用 setbuf() 函数来手动设置缓冲区。