编程基础
进制转换
二、八、十六进制转十进制
将八进制数字 423.5176 转换成十进制
423.5176 = 4×8^2 + 2×8^1 + 3×8^0 + 5×8^-1 + 1×8^-2 + 7×8^-3 + 6×8^-4 = 275.65576171875(十进制)
总之任何一种进制转十进制,都可参考二进制的转换。转换的次方:……543210.(-1)(-2)(-3)(-4)(-5)……
十进制转二、八、十六进制
整数部分
也都可参考已熟悉的十进制转二进制(除以进制数,取余,从下往上排)。
小数部分
(!从没了解过!)
小数部分*进制数,不断取整。从上往下排。
二进制与八进制、十六进制的转换(很方便)
二与八
每三位二进制数字转换为一位八进制数字。
二转八:1,110,111,100 → 1674
八转二:2743 → 10,111,100,011
二与十六
每四位二进制数字转换为一位十六进制数字。
二转十六: 10,1101,0101,1100 → 2D5C
十六转二:A5D6 → 1010,0101,1101,0110
内存中的存储
计算机的0和1——电子元器件通电或断电。
将8个元器件看做一个单位。1个元器件称为1比特(Bit)或1位,8个元器件称为1字节(Byte)。
ACSII编码
字符编码:规定了如何将文字的编号存储到计算机中。
字符集:定义了文字和二进制的对应关系,为字符分配了唯一的编号。
可以将字符集理解成一个很大的表格,它列出了所有字符和二进制的对应关系,计算机显示文字或者存储文字,就是一个查表的过程。
ASCII :American Standard Code for Information Interchange,美国信息交换标准代码
标准 ASCII 编码共包含了 128 个字符,用一个字节就足以存储(实际上是用一个字节中较低的 7 位)。
中文编码
GB2312 --> GBK --> GB18030 是中文编码的三套方案,出现的时间从早到晚,收录的字符数目依次增加,并且向下兼容。
GB2312 和 GBK
从整体上讲,GB2312 和 GBK 的编码方式一致,具体为:
对于 ASCII 字符,使用一个字节存储,并且该字节的最高位是 0,这和 ASCII 编码是一致的,所以说 GB2312 完全兼容 ASCII。
对于中国的字符,使用两个字节存储,并且规定每个字节的最高位都是 1。
例如:
对于字母
A
,它在内存中存储为 01000001;对于汉字
中
,它在内存中存储为 11010110 11010000。
GB18030
GB18030 为了容纳更多的字符,并且要区分两个字节和四个字节。
对于 ASCII 字符,一个字节存储,最高位是 0,和 ASCII、GB2312、GBK 编码一致。
常用的中文字符,使用两个字节存储,并且规定第一个字节的最高位是 1,第二个字节的高位最多只能有一个连续的 0(第二个字节的最高位可以是 1 也可以是 0,但是当它是 0 时,次高位就不能是 0 了)。
注意对比 GB2312 和 GBK,它们要求两个字节的最高位为都必须为 1。
对于罕见的字符,使用四个字节存储并且规定第一个和第三个字节的最高位是 1,第二个和第四个字节的高位必须有两个连续的 0。
例如对于字母
A
,它在内存中存储为 01000001;对于汉字
中
,它在内存中存储为 11010110 11010000;对于藏文
གྱུ
,它在内存中的存储为 10000001 00110010 11101111 00110000。
GB18030的判断过程
如果遇到的字节的最高位是 0,那么就会断定该字符只占用了一个字节;
如果遇到的字节的最高位是 1,那么该字符可能占用了两个字节,也可能占用了四个字节,不能妄下断论,所以还要继续往后扫描:
如果第二个字节的高位有两个连续的 0,那么就会断定该字符占用了四个字节;
如果第二个字节的高位没有连续的 0,那么就会断定该字符占用了两个字节。
当字符占用两个或者四个字节时,GB18030 编码要检测两次,处理效率比 GB2312 和 GBK 都低。
GBK 于 1995 年发布,这一年也是互联网爆发的元年,国人使用电脑越来越多,中文版 Windows 下的很多程序默认使用的就是 GBK 编码。
Unicode字符集,将全世界的文字存储到计算机
ASCII、GBK、Shift_Jis、ISO/IEC 8859 等地区编码都是各个国家为了自己的语言文化开发的,不具有通用性,在一种编码下开发的软件或者编写的文档,拿到另一种编码下就会失效,必须提前使用程序转码。
Unicode 也称为统一码、万国码。
Unicode 只管制定字符的编号,字符编码决定怎么存储。
Unicode 可以使用的编码方案有三种,分别是:
UTF-8:一种变长的编码方案,使用 1~6 个字节来存储;
UTF-32:一种固定长度的编码方案,不管字符编号大小,始终使用 4 个字节来存储;
UTF-16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变。
UTF:Unicode Transformation Format ,统一编码转换格式。后面的数字表明至少使用多少个比特位(Bit)来存储字符。
UTF-8
UTF-8 的编码规则很简单:
如果只有一个字节,那么最高的比特位为 0,这样可以兼容 ASCII;
如果有多个字节,那么第一个字节从最高位开始,连续有几个比特位的值为 1,就使用几个字节编码,剩下的字节均以 10 开头。
具体的表现形式:
0xxxxxxx:单字节编码形式,这和 ASCII 编码完全一样,因此 UTF-8 是兼容 ASCII 的;
110xxxxx 10xxxxxx:双字节编码形式(第一个字节有两个连续的 1);
1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式(第一个字节有三个连续的 1);
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式(第一个字节有四个连续的 1)。
xxx 就用来存储 Unicode 中的字符编号。
例子:
字符 | 字母N | 符号æ | 中文⻬ |
---|---|---|---|
Unicode 编号(二进制) | 0,1001110 | 11,100110 | 0010,1110 11,101100 |
Unicode 编号(十六进制) | 4E | E6 | 2E EC |
UTF-8 编码(二进制) | 01001110 | 11000011 10100110 | 11100010 10111011 10101100 |
UTF-8 编码(十六进制) | 4E | C3 A6 | E2 BB AC |
对于常用的字符,它的 Unicode 编号范围是 0 ~ FFFF,用 1~3 个字节足以存储,只有及其罕见,或者只有少数地区使用的字符才需要 4~6个字节存储。
UTF-32
UTF-32 是固定长度的编码,始终占用 4 个字节,足以容纳所有的 Unicode 字符。
所以直接存储 Unicode 编号即可,不需要任何编码转换。浪费了空间,提高了效率。
UTF-16(看不懂)
使用 2 个或者 4 个字节来存储。
对于 Unicode 编号范围在 0 ~ FFFF 之间的字符,使用两个字节存储,直接存储 Unicode 编号,不进行编码转换。跟 UTF-32 非常类似。
对于 Unicode 编号范围在 10000~10FFFF 之间的字符,UTF-16 使用四个字节存储,具体来说就是:将字符编号的所有比特位分成两部分,较高的一些比特位用一个值介于 D800~DBFF 之间的双字节存储,较低的一些比特位(剩下的比特位)用一个值介于 DC00~DFFF 之间的双字节存储。
参考下方表格:
Unicode 编号范围 (十六进制) | 具体的 Unicode 编号 (二进制) | UTF-16 编码 | 编码后的 字节数 |
---|---|---|---|
0000 0000 ~ 0000 FFFF | xxxxxxxx xxxxxxxx | xxxxxxxx xxxxxxxx | 2 |
0001 0000—0010 FFFF | yyyy yyyy yyxx xxxx xxxx | 110110yy yyyyyyyy 110111xx xxxxxxxx | 4 |
位于 D800~0xDFFF 之间的 Unicode 编码是特别为四字节的 UTF-16 编码预留的,所以不应该在这个范围内指定任何字符。如果你真的去查看 Unicode 字符集,会发现这个区间内确实没有收录任何字符。
UTF-16 要求在制定 Unicode 字符集时必须考虑到编码问题,所以真正的 Unicode 字符集也不是随意编排字符的。
三者区别(只记了UTF-8的优势)
只有 UTF-8 兼容 ASCII,UTF-32 和 UTF-16 都不兼容 ASCII,因为它们没有单字节编码。
UNIX 家族的操作系统(Linux、Mac OS、iOS 等)内核都采用 UTF-8 编码。
UTF-8 的缺点是效率低,但随着算法的逐年精进,UTF-8 字符串的定位效率越来越高,往往不再是槽点了。
宽字符和窄字符
多字节字符,或窄字符:
编码方式采用 1~n 个字节存储,是变长的,例如 UTF-8、GB2312、GBK 等。
宽字符:
编码方式是固定长度的,不管字符编号大小,始终采用 n 个字节存储,例如 UTF-32、UTF-16 等。
C语言初探
全角和半角输入法的区别
在计算机屏幕上,一个汉字要占两个英文字符的位置。
人们把一个英文字符所占的位置称为“半角”,相对地把一个汉字所占的位置称为“全角”。
标点符号、英文字母、阿拉伯数字等这些字符,在半角状态它们被作为英文字符处理,而在全角状态作为中文字符处理。
半角输入:
C语言中文网!Hello C,I like!
全角输入:
C语言中文网!Hello C,I like!
另外最重要的一点:相同”字符在全角和半角状态下对应的编码值(例如 Unicode 编码、GBK 编码等)不一样,所以它们是不同的字符。
源文件
就是保存代码的文件。
不同语言有不同的文件后缀:.c
、.cpp
、.java
、.pt
等。方便编译器识别和程序员理解。
源文件其实就是纯文本文件,它的内部并没有特殊格式。
后缀并不会导致该文件的内部格式发生改变。
通过编译器直接编译文本代码
1、用任意文本编辑器写好代码(!!犯了很多极其弱智的错误,忘了 “ 或 ; 等);
2、找到编译C语言的 gcc.exe 文件;
3、添加环境变量;
4、在源代码所在目录打开cmd
4.1、在cmd中输入gcc (源代码文件名) -o (编译结果文件名)
(注意源代码文件得是 .c 后缀,且没有语法错误。
4.2、如果上一步成功,该目录下会多出一个 exe文件。直接在cmd中输入文件名即可执行。(cmd中输入dir
可查看该目录下的所有文件名)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-857AjjHT-1632375572637)(D:\!!C++\学习笔记\C语言部分.assets\image-20201129142005019.png)]
补充:Windows的环境变量
我的电脑——右键->属性——高级->环境变量中的Path项。
向其中添加可执行文件(gcc)的目录后,就能在cmd中使用gcc命令了。
添加前
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kVAFYCjO-1632375572639)(D:\!!C++\学习笔记\C语言部分.assets\image-20201129140107800.png)]
添加
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jv8YK3fV-1632375572641)(D:\!!C++\学习笔记\C语言部分.assets\image-20201129135954946.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bquXDTUM-1632375572642)(D:\!!C++\学习笔记\C语言部分.assets\image-20201129135854113.png)]
添加后
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jx38BMvy-1632375572643)(D:\!!C++\学习笔记\C语言部分.assets\image-20201129140229752.png)]
在指定目录打开cmd
在目录栏直接输入cmd即可。效果和Git的Git Bash Here一样。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zm8F9jYW-1632375572645)(D:\!!C++\学习笔记\C语言部分.assets\image-20201129140603077.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y05JDnHW-1632375572646)(D:\!!C++\学习笔记\C语言部分.assets\image-20201129140614262.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2gYxLLzk-1632375572647)(D:\!!C++\学习笔记\C语言部分.assets\image-20201129140625956.png)]
编译和链接(补课:宏)
可执行文件:.exe
后缀。CPU只能识别二进制代码。
C语言编译过程
1、预处理:处理#部分,不检查语法错误
cmd命令:
gcc -E (源代码文件名.c) -o (生产的文件名.i)
得到的 .i 后缀文件:处理过
#
的代码,如#include
的复制和#define
的替换等。#if abc printf("hello world\n"); #end if
通常情况下,在生成的
.i
文件中,这段代码会不出现(因为#if abc
不成立,相当于#if 0
。对应地#if 1
可看做条件成立)。可以在cmd命令中加入额外代码,使得abc条件成立。
cmd代码:-D(变量名)
gcc -E (源代码文件名.c) -o (生产的文件名.i) -Dabc
2、编译:生成汇编文件,有语法检查。
cmd命令:
gcc -S (源代码文件名.i) -o (生产的文件名.s)
3、汇编:生成二进制文件
cmd命令:gcc -c (源代码文件名.s) -o (生产的文件名.o)
4、链接:链接C语言运行需要的各种库,生产
.exe
文件cmd命令:
gcc (源代码文件名.o) -o (生产的文件名)
编译器(Compiler):一个软件。识别高级语言代码中的词汇、句子以及各种特定的格式,并将他们转换成计算机能够识别的二进制形式。
代码经过编译后,形成目标文件(Object File)。
目标文件经过链接(Link)以后才能变成可执行文件。
编译后的文件还需要和系统组件(比如标准库、动态链接库等)结合起来才能运行,这些组件都是程序运行所必须的。
链接器(Linker):也是一个软件。将所有二进制形式的目标文件和系统组件组合成一个可执行文件。
IDE(集成开发环境)
IDE,Integrated Development Environment。
实际开发中,除了编译器是必须的工具,我们往往还需要很多其他辅助软件,例如:
- 编辑器:用来编写代码,并且给代码着色,以方便阅读;
- 代码提示器:输入部分代码,即可提示全部代码,加速代码的编写过程;
- 调试器:观察程序的每一个运行步骤,发现程序的逻辑错误;
- 项目管理工具:对程序涉及到的所有资源进行管理,包括源文件、图片、视频、第三方库等;
- 漂亮的界面:各种按钮、面板、菜单、窗口等控件整齐排布,操作更方便。
集成开发环境就是一系列开发工具的组合。
工程/项目
一个真正的软件包含很多功能,需要很多的源代码文件,以及图片、视频、音频、控件、库(也可以说框架)等其它资源。
将这些资源统一的文件夹就可视为工程/项目( Project 的不同翻译)。
C89、C99、C11 标准
为了统一C语言版本而制定的C语言标准。
现有的教程(包括书籍、视频、大学课程等)大都是针对 C89 编写的,是C语言的核心。后来的 C99、C11 新增的特性并不多,只是在“打补丁”。
C语言有很多编译器
C语言没有官方机构,也不属于任何公司,只有一个指定标准的委员会。
任何个人、组织都能开发C语言编译器,开发的编译器遵守哪个标准,完全遵守或部分遵守,都没有限制。
所以会出现相同的代码在不同的编译器下会有不同的结果。
安装程序
基本原理是相同的,主要的思想就是将程序的二进制可执行文件拷贝到某个目录,设置一些路径。如果程序运行时需要一些库,将这些库拷贝到系统目录即可。
程序的安装基本上要经过下面四个步骤:
- 将程序的可执行文件从安装包所在的位置,拷贝到要安装的目录。
一般所谓的“绿色软件”到此就安装结束了,可以使用了。
- 如果有必要,可以向系统目录拷贝一些动态链接库(DLL)。(可选操作)
比如大型游戏,可能需要很多动态链接库(DLL)的支持,这时候程序可能会将这些 DLL 拷贝到系统库的默认目录。
有些程序用到的 DLL 文件不是系统必需的,只能由程序自己使用,这样放在系统目录里就不太合适,安装的软件多了,就会造成系统臃肿,所以这些 DLL 会被拷贝到程序的安装目录。
- 向系统注册表中写入相应的设置项,注册程序或者库的安装信息。(可选操作)
安装前,用户可能会对软件做一些设置,安装时,这些设置就会被写入注册表。
另外,当安装程序将 DLL 文件拷贝到系统目录时,一些 DLL 还需要向系统注册,告诉系统我在这里,不然使用的时候可能会找不到。
- 在开始菜单或者桌面上位程序创建快捷方式。(可选操作)
变量和数据类型
变量的存储和数据类型
内存中存取数据要明确三件事情:
- 数据存储在哪里;
- 数据的长度;
- 数据的处理方式。
变量名——存在哪
和日常放东西一样,找一个小箱子来存放物品,一来显得不那么凌乱,二来方便以后找到。
计算机也是这个道理,先在内存中找一块区域,规定用它来存放整数,并起一个好记的名字,方便以后查找。
例如
int a;
的意思:在内存中找一块区域,命名为 a,用它来存放整数。
数据类型——处理方式、长度
问题:数据存在内存中,通过变量名查找到。但如何使用该数据?
答:通过数据类型确定数据的解释方式。
数据长度
即数据占用的字节数。占用越多,存储的数据越多。
确定数据长度也会影响到对数据的正确使用。而这也是通过数据类型来确定的。
强类型语言:在定义变量时必须指明数据类型。
弱类型语言:在定义变量时不必指明数据类型;且变量类型是灵活可变的(先赋给 int
,再赋给 string
)。
C语言屏幕输出数据
puts("C语言中文网");
puts 是 output string 的缩写,只能用来输出字符串,不能输出整数、小数、字符等
printf("C语言中文网");
可以输出整数、小数、单个字符等,并且输出格式也可以自己定义,例如:
- 以十进制、八进制、十六进制形式输出;
- 要求输出的数字占 n 个字符的位置;
- 控制小数的位数。
printf
是 print format 的缩写,意思是“格式化打印”。这里所谓的“打印”就是在屏幕上显示内容
printf 输出数字
int abc=999;
printf("%d", abc);
printf("The value of abc is %d !", abc);
printf("a=%d, b=%d, c=%d", a, b, c);
%d
,d 是 decimal 的缩写,意思是十进制数
%d
与abc
对应,会用abc
的值来替换%d
。按顺序对应,第三行
printf
会输出“a=100, b=200, c=300”
%d
称为格式控制符,它指明了以何种形式输出数据。格式控制符均以%
开头,后跟其他字符。
除了 %d,printf
支持更多的格式控制,例如:
- %c:输出一个字符。c 是 character 的简写。
- %s:输出一个字符串。s 是 string 的简写。
- %f:输出一个小数。f 是 float 的简写。
puts()
输出完成后会自动换行,而 printf ()
不会,要自己添加换行符\n
。
在字符串中书写长文本
1、直接塞进puts()
或printf ()
中。
2、连续调用多个puts()
,塞入拆分后的长文本。
3、在一个puts()
函数中,将一个较长的字符串分割成几个较短的字符串。
puts(
"C语言中文网,一个学习C语言和C++的网站,他们坚持用工匠的精神来打磨每一套教程。"
"坚持做好一件事情,做到极致,让自己感动,让用户心动,这就是足以传世的作品!"
"C语言中文网的网址是:http://c.biancheng.net"
);
这只是形式上的分割,编译器在编译阶段会将它们合并为一个字符串,它们放在一块连续的内存中。
C语言中的整数
C语言通常使用int
来定义整数(int
是 integer 的简写)
一般占用 4 个字节(Byte)的内存,共计 32 位(Bit)
不考虑正负,最大值为 2^(32)-1 = 4,294,967,295 ≈ 43亿。
太大
在实际开发中,很少会用到这么大的数字,往往会空闲出两三个字节,这些字节就白白浪费掉了。
早期使用C语言时,或者在单片机和嵌入式系统中,内存都是非常稀缺的资源,所有的程序都在尽力节省内存。
太小
43 亿虽然已经很大,但要表示全球人口数量还是不够,必须要让整数占用更多的内存,才能表示更大的值。
对4字节整数的改进
让整数占用更少的内存可以在 int
前边加 short
,让整数占用更多的内存可以在 int
前边加 long
,例如:
short int a = 10;
long int n = 562131;
这样 a 只占用 2 个字节的内存,而 n 可能会占用 8 个字节的内存。
int
可以省略,直接使用short
、long
。short a = 10; long n = 562131;
写法更加简洁,实际开发中常用
int
是基本的整数类型,short
和 long
是在 int
的基础上进行的扩展,short
可以节省内存,long
可以容纳更大的值。
short
、int
、long
是C语言中常见的整数类型,其中 int
称为整型,short
称为短整型,long
称为长整型。
整型的长度
C语言并没有严格规定 short、int、long 的长度,只做了宽泛的限制:
short
至少占用 2 个字节。int
建议为一个机器字长。32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节。short
的长度 ≤int
,long
的长度 ≥int
。
2 ≤
short
≤int
≤long
16 位环境
short
——2 Byte ,int
——2 Byte,long
——4 Byte
16 位环境多用于单片机和低级嵌入式系统,在PC和服务器上已经见不到了。
32 位的 Windows、Linux 和 Mac OS
short
——2 Byte ,int
——4 Byte,long
——4 Byte
PC和服务器上的 32 位系统占有率也在慢慢下降,嵌入式系统使用 32 位越来越多。
64 位环境下,不同的操作系统会有不同的结果:
操作系统 | short | int | long |
---|---|---|---|
Win64(64位 Windows) | 2 | 4 | 4 |
类Unix系统(包括 Unix、Linux、Mac OS、BSD、Solaris 等) | 2 | 4 | 8 |
char
长度始终是 1Byte。
需要注意的是,sizeof
是C语言中的操作符,不是函数,所以可以不带( )
。
short a = 10;
int b = 100;
int short_length = sizeof a; //没带()
int int_length = sizeof(b);
不同类型的输出( h、 、l )
使用不同的格式控制符可以输出不同类型的整数,它们分别是:
%hd
用来输出 short int 类型,hd 是 short decimal 的简写;%d
用来输出 int 类型,d 是 decimal 的简写;%ld
用来输出 long int 类型,ld 是 long decimal 的简写。
当控制符与参数类型不匹配时:
如果控制符对应的类型能容纳参数数字,则没问题。
使用
%d
输出 short,或者使用%ld
输出 short、int 时,不管值有多大,都不会发生错误。使用
%hd
输出 int、long,或者使用%d
输出 long 时,如果要输出的值比较小,一般也不会发生错误,如果要输出的值比较大,就很有可能发生错误。
书写二进制、八进制、十六进制
数字默认是十进制的,不需要任何特殊格式。
二进制、八进制或十六进制数字需要特殊格式。
二进制(0B)
由 0 和 1 两个数字组成,使用时必须以0b
或0B
(不区分大小写)开头。
//合法的二进制
int a = 0b101; //换算成十进制为 5
int b = -0b110010; //换算成十进制为 -50
int c = 0B100001; //换算成十进制为 33
//非法的二进制
int m = 101010; //无前缀 0B,相当于十进制
int n = 0B410; //4不是有效的二进制数字
标准的C语言并不支持上面的二进制写法,只是有些编译器自己进行了扩展,才支持二进制数字。
八进制(0)
由 0~7 八个数字组成,使用时必须以0
开头(注意是数字 0,不是字母 o)
//合法的八进制数
int a = 015; //换算成十进制为 13
int b = -0101; //换算成十进制为 -65
int c = 0177777; //换算成十进制为 65535
//非法的八进制
int m = 256; //无前缀 0,相当于十进制
int n = 03A2; //A不是有效的八进制数字
十六进制(0X)
由数字 0~9、字母 A~F 或 a~f(不区分大小写)组成,使用时必须以0x
或0X
(不区分大小写)开头。
//合法的十六进制
int a = 0X2A; //换算成十进制为 42
int b = -0XA0; //换算成十进制为 -160
int c = 0xffff; //换算成十进制为 65535
//非法的十六进制
int m = 5A; //没有前缀 0X,是一个无效数字
int n = 0X3H; //H不是有效的十六进制数字
输出 ( o、d、x )
short | int | long | |
---|---|---|---|
八进制 | %ho | %o | %lo |
十进制 | %hd | %d | %ld |
十六进制 | %hx 或者 %hX | %x 或者 %X | %lx 或者 %lX |
十六进制数字的表示用到了英文字母,有大小写之分,要在格式控制符中体现出来:
- %hx、%x 和 %lx 中的
x
小写,表明以小写字母的形式输出十六进制数;- %hX、%X 和 %lX 中的
X
大写,表明以大写字母的形式输出十六进制数。
八进制数字和十进制数字不区分大小写,所以格式控制符都用小写形式。
大写形式行为未定义,不同编译器不同结果。
一个数字不管以何种进制来表示,都能够以任意进制的形式输出。数字在内存中始终以二进制的形式存储。
在输出中加上前缀,方便区分各个进制的数字
在格式控制符中加上#
即可输出前缀,例如 %#x、%#o、%#lX、%#ho 等。
short a = 0b1010110; //二进制数字
int b = 02713; //八进制数字
long c = 0X1DAB83; //十六进制数字
printf("a=%ho, b=%o, c=%lo\n", a, b, c); //以八进制形似输出
printf("a=%hd, b=%d, c=%ld\n", a, b, c); //以十进制形式输出
printf("a=%hx, b=%x, c=%lx\n", a, b, c); //以十六进制形式输出(字母小写)
printf("a=%hX, b=%X, c=%lX\n", a, b, c); //以十六进制形式输出(字母大写)
运行结果:
a=126, b=2713, c=7325603
a=86, b=1483, c=1944451
a=56, b=5cb, c=1dab83
a=56, b=5CB, c=1DAB83
short a = 0b1010110; //二进制数字
int b = 02713; //八进制数字
long c = 0X1DAB83; //十六进制数字
printf("a=%#ho, b=%#o, c=%#lo\n", a, b, c); //以八进制形似输出
printf("a=%hd, b=%d, c=%ld\n", a, b, c); //以十进制形式输出
printf("a=%#hx, b=%#x, c=%#lx\n", a, b, c); //以十六进制形式输出(字母小写)
printf("a=%#hX, b=%#X, c=%#lX\n", a, b, c); //以十六进制形式输出(字母大写)
运行结果:
a=0126, b=02713, c=07325603
a=86, b=1483, c=1944451
a=0x56, b=0x5cb, c=0x1dab83
a=0X56, b=0X5CB, c=0X1DAB83
正负数
C语言中将最高位作为符号位。以 int 为例,它占用 32 位的内存,0~30 位表示数值,31 位表示正负号。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1U8r93n7-1632375572650)(D:\!!C++\C语言部分.assets\111A25222-0.png)]
short、int 和 long 类型默认都是带符号位的。
如果不希望设置符号位,可以在数据类型前面加上 unsigned 关键字。
unsigned short a = 12;
unsigned int b = 1002;
unsigned long c = 9892320;
如果是unsigned int
类型,那么可以省略 int ,只写 unsigned。
unsigned n = 100;
无符号数的输出( d → u )
八进制、十六进制默认无符号,不支持输出有符号数。
在实际开发中,也基本没有“输出负的八进制数或者十六进制数”这样的需求。
unsigned short | unsigned int | unsigned long | |
---|---|---|---|
十进制 | %hu | %u | %lu |
整数在内存中的存储------
加法和减法是计算机中最基本的运算,计算机时时刻刻都离不开它们,所以它们由硬件直接支持。为了提高加减法的运算效率,硬件电路要设计得尽量简单。
对于有符号数,内存要区分符号位和数值位。
对于计算机来说,就要设计专门的电路,这无疑增加了硬件的复杂性,增加了计算的时间。要是能把符号位和数值位等同起来,让它们一起参与运算,不再加以区分,这样硬件电路就变得简单了。
加法和减法可以合并为加法运算。减去一个数相当于加上这个数的相反数。
符号位和数值位合并;加法减法合并。
只要设计一种简单的、不用区分符号位和数值位的加法电路,就能同时实现加法和减法运算。
简化硬件电路的代价,是有符号数在存储和读取时都要进行转化。
原码、反码、补码
原码:
二进制形式。
反码:
正数 —— 不变。
负数 —— 除符号位以外全部取反。
例如
short a = 6;
,a 的原码和反码都是0000 0000 0000 0110
;更改 a 的值
a = -18;
,此时 a 的反码是1111 1111 1110 1101
。
补码:
正数 —— 不变。
负数 —— 反码 + 1 。
short a = 6;
原码和反码都是
0000 0000 0000 0110
a = -18;
补码是
1111 1111 1110 1110
。
总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7CdLdmJZ-1632375572651)(D:\!!C++\学习笔记\C语言\C语言部分.assets\1121446295-0.jpg)]
黑马视频的误导
“以十进制输入/赋值时,给的是原码;八和十六是补码。打印时同理。”
别管这句话,将输入、输出分开看做2个独立的部分。并记住!:计算机内部是以补码的形式存储数值的(见下一节)。
输入时根据正负数判断:
给的是正数,如
char c = 129;
直接存储(正数的补码就是原码);给的是负数,如
char c = -1;
转换后存储。
输出时根据变量类型判断(不考虑上溢下溢和类型提升):
unsigned类型:直接取出数值输出。
signed类型:转为原码后再输出。
例子:
int main() { // 129(1000 0001)是正数,补码形式仍是1000 0001,直接存储。 char c1 = 129; // 有符号类型输出,将1000 0001“还原”为原码1111 1111,得到-127。 printf("%d\n", c1); // -1是负数,原码1000 0001转为补码1111 1111后存储。 char c2 = -1; // 有符号类型输出,将1111 1111还原为原码1000 0001,得-1。 printf("%d\n", c2); // 正数,存储1000 0001 unsigned char uc1 = 129; // 无符号类型输出,将1000 0001视为原码,输出129。 printf("%d\n", uc1); // 负数,转为补码1111 1111后存储。 unsigned char uc2 = -1; // 无符号类型输出,将1111 1111视为原码,输出255。 printf("%d\n", uc2); }
[root@localhost ~]# ./test -127 -1 129 255
推演出反码、补码的诞生 !
假设 6 和 18 都是 short 类型的,现在我们要计算 6 - 18 的结果,根据运算规则,它等价于 6 + (-18) = -12。
如果采用原码计算,那么运算过程为:
6 - 18 = 6 + (-18)
= [0000 0000 0000 0110]原 + [1000 0000 0001 0010]原
= [1000 0000 0001 1000]原
= -24
直接用原码表示整数,让符号位也参与运算,对于类似上面的减法来说,结果显然是不正确的。
并且会导致0有两种表示方法(1000 0000 和 0000 0000)。
继续探索,不断试错,后来设计出了反码。下面演示了反码运算的过程:
6 - 18 = 6 + (-18)
= [0000 0000 0000 0110]反 + [1111 1111 1110 1101]反
= [1111 1111 1111 0011]反
= [1000 0000 0000 1100]原
= -12
结果正确。
然而,这样还不算万事大吉,将减数和被减数交换一下位置,也就是计算 18 - 6 的结果
18 - 6 = 18 + (-6)
= [0000 0000 0001 0010]反 + [1111 1111 1111 1001]反
= [1 0000 0000 0000 1011]反 //!!!
= [0000 0000 0000 1011]反
= [0000 0000 0000 1011]原
= 11
真实的结果应该是 12 ,少了 1。
//部分最高位的 1 是加法运算过程中的进位,它溢出了,内存容纳不了了,所以直接截掉。
使用反码计算,小数减大数没问题,但大数减小数的结果始终少 1。
相差的 1 要进行纠正,但是又不能影响小数减大数。于是人们又绞尽脑汁设计出了补码:给反码打一个“补丁”。
补码计算的过程:
6 - 18 = 6 + (-18)
= [0000 0000 0000 0110]补 + [1111 1111 1110 1110]补
= [1111 1111 1111 0100]补
= [1111 1111 1111 0011]反
= [1000 0000 0000 1100]原
= -12
18 - 6 = 18 + (-6)
= [0000 0000 0001 0010]补 + [1111 1111 1111 1010]补
= [1 0000 0000 0000 1100]补 //溢出位
= [0000 0000 0000 1100]补
= [0000 0000 0000 1100]反
= [0000 0000 0000 1100]原
= 12
5 - 13 = 5 + (-13)
= [0000 0000 0000 0101]补 + [1111 1111 1111 0011]补
= [1111 1111 1111 1000]补
= [1111 1111 1111 0111]反
= [1000 0000 0000 1000]原
= -8
13 - 5 = 13 + (-5)
= [0000 0000 0000 1101]补 + [1111 1111 1111 1011]补
= [1 0000 0000 0000 1000]补 //溢出位
= [0000 0000 0000 1000]补
= [0000 0000 0000 1000]反
= [0000 0000 0000 1000]原
= 8
分析解释:
设2个整数a、b都 >0,a < b。
小数减大数:
a - b = a + (-b) = c < 0
结果为负数,b转为补码时 +1,c转为原码时 -1。刚好抵消。
大数减小数:
b - a = b + (-a) = e > 0
结果为正数,b转为补码时 +1,e不需要转换。整个式子结果 +1,刚好补上反码相加的空缺。
补码这种天才般的设计,一举达成了开头提到的两个目标(符号位、数值位合并,加减法合并),简化了硬件电路。
[1000 0000 …… 0000 0000]补
是一个特殊的补码,计算机直接规定这个补码对应的值就是 -2^31
整数的取值范围以及数值溢出
无符号数
无符号数(unsigned 类型)的取值范围(或者说最大值和最小值)很容易,将内存中的所有位(Bit)都置为 1 就是最大值,都置为 0 就是最小值。
例:unsigned char :1Byte,2^8 - 1 = 255,取值范围是 0~255。
unsigned char | unsigned short | unsigned int(4字节) | unsigned long(8字节) | |
---|---|---|---|---|
最小值 | 0 | 0 | 0 | 0 |
最大值 | 2^8 - 1 = 255 | 2^16 - 1 = 65,535 ≈ 6.5万 | 2^32 - 1 = 4,294,967,295 ≈ 42亿 | 2^64 - 1 ≈ 1.84×1019 |
有符号数
有符号数以补码的形式存储,计算取值范围也要从补码入手。
补码 | 反码 | 原码 | 值 |
---|---|---|---|
1111 1111 | 1111 1110 | 1000 0001 | -1 |
1111 1110 | 1111 1101 | 1000 0010 | -2 |
…… | …… | …… | …… |
1000 0010 | 1000 0001 | 1111 1110 | -126 |
1000 0001 | 1000 0000 | 1111 1111 | -127 |
1000 0000 | – | – | -128 |
0111 1111 | 0111 1111 | 0111 1111 | 127 |
0111 1110 | 0111 1110 | 0111 1110 | 126 |
…… | …… | …… | …… |
0000 0001 | 0000 0001 | 0000 0001 | 1 |
0000 0000 | 0000 0000 | 0000 0000 | 0 |
加粗一行:为什么是 -128?
0000 0000 和 1000 0000 若使用原码解读,都可解读为 0。浪费了一个状态表示。为了尽量利用资源,扩大存储范围,就催生出了补码(又一原因)。
按之前补码原码的转换规则:1000 000 - 1 = 1111 111(运算时看作7位数计算),再取反得到原码 1000 000 = 128。
所以补码 1000 0000 似乎又可以同时表示-128和128,而为了统一规则:”最高位为1的数字为负数“。所以记为 -128。
数值溢出
无符号数、有符号数负数:
所有位都置 1,再+1使得占用位超出容量,最高位的 1 被截掉,余下的都置 0。
有符号数正数:
除了最高位(符号位)以外都置 1,+1后最高位(符号位)置 1,其余都为 0。
C语言的小数
C语言中小数的指数形式为:
aEn 或 aen
例子:
2.1E5 = 2.1×10^5,其中 2.1 是尾数,5 是指数。
a 为尾数部分,是一个十进制数;n 为指数部分,是一个十进制整数。
float 单精度浮点型,始终占 4 Byte。
double 双精度浮点型,始终占 8 Byte。
小数的输出
加上 l 就是double。( 可以将double理解为“long float” )
- %f 以十进制形式输出 float 类型;
- %lf 以十进制形式输出 double 类型;
- %e 以指数形式输出 float 类型,输出结果中的 e 小写;
- %E 以指数形式输出 float 类型,输出结果中的 E 大写;
- %le 以指数形式输出 double 类型,输出结果中的 e 小写;
- %lE 以指数形式输出 double 类型,输出结果中的 E 大写。
%f 和 %lf 默认保留六位小数。
float b = 128.101;
printf("b=%f, f);
//输出 b=128.100998
这和小数在内存中的存储形式有关,很多简单的小数不能精确存储
智能的输出方式:使用%g
%g 会对比小数的十进制形式和指数形式,以最短的方式来输出小数。
float a = 0.00001;
float b = 30000000;
float c = 12.84;
float d = 1.229338455;
printf("a=%g \nb=%g \nc=%g \nd=%g\n", a, b, c, d);
运行结果:
a=1e-05
b=3e+07
c=12.84
d=1.22934
a 的十进制形式是 0.00001,占用七个字符的位置,a 的指数形式是 1e-05,占用五个字符的位置,指数形式较短,所以以指数的形式输出。
b 的十进制形式是 30000000,占用八个字符的位置,b 的指数形式是 3e+07,占用五个字符的位置,指数形式较短,所以以指数的形式输出。
c 的十进制形式是 12.84,占用五个字符的位置,c 的指数形式是 1.284e+01,占用九个字符的位置,十进制形式较短,所以以十进制的形式输出。
d 的十进制形式是 1.22934,占用七个字符的位置,d 的指数形式是 1.22934e+00,占用十一个字符的位置,十进制形式较短,所以以十进制的形式输出。
%g 默认最多保留六位有效数字,包括整数部分和小数部分。
%g 不会在最后强加 0 来凑够有效数字的位数。
除了 %g,还有 %lg、%G、%lG:
同理,加上 l 输出double,大写的 G 使输出的指数 E 大写。
数字后缀
整数默认类型 int ,小数默认类型 double 。
long a = 100;
int b = 294;
float x = 52.55;
double y = 18.6;
在整数后面紧跟 l 或者 L(不区分大小写)表明该数字是 long 类型
在小数后面紧跟 f 或者 F(不区分大小写)表明该数字是 float 类型
long a = 100l;
int b = 294;
short c = 32L;
float x = 52.55f;
double y = 18.6F;
float z = 0.02;
小数在内存中的存储
浮点数是数字(或者说数值)在内存中的一种存储格式,它和定点数是相对的。
定点数
指小数点的位置是固定的,不会向前或者向后移动。
假设用4个字节(32位)来存储无符号的定点数,并且约定:前16位表示整数部分,后16位表示小数部分,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5dnsUiOM-1632375572653)(D:\!!C++\C语言部分.assets\1134205554-0.png)]
不管什么时候,整数部分始终占用16位(不足16位前置补0),小数部分也始终占用16位(不足16位后置补0)。
例如,在内存中存储了 10101111 00110001 01011100 11000011,则
整数部分是 10101111 00110001,
小数部分是 01011100 11000011 。
精度
小数部分的最后一位可能是精确数字,也可能是近似数字。剩余的31位都是精确数字。所以整体的精度为 31~32 位。
数值范围
所有位(bit)都置为 1,小数的值最大,为 2^16 - 1 = 65 535 。
符号位置 1,其余为 0。小数的值最小,为2^(-16) = -65 536
综述
精度高,但范围小。
例如,电子的质量为:
0.0000000000000000000000000009 克 = 9 × 10^(-28) 克
科学的方案是使用指数形式存储,能节省且更直观。以指数的形式来存储小数的解决方案就叫做浮点数。
浮点数
C语言标准规定,小数在内存中以科学计数法的形式来存储,具体形式为:
小
数
=
(
−
1
)
正
负
号
×
尾
数
×
进
制
数
指
数
小数 = (-1)^{正负号} × 尾数 × 进制数^{指数}
小数=(−1)正负号×尾数×进制数指数
正负号:0或1。
尾数:有效数字。
指数:整数,可正可负。
以 19.625 为例
进制数为10:
19.625 = 1.9625 × 10^1
进制数为2:
19.625 = 10011.101 = 1.0011101×2^4
当进制确定以后,指数实际上就成了小数点的移动位数。移动方向和位数由指数决定:
指数 > 0 —— 小数点向右移动即可还原小数的值。
指数 < 0 —— 小数点向左移动即可还原小数的值。
将这种表示小数的方式称为浮点数。
计算机中的存储以 2 作为基数。这样的话,还要确定正负号、位数、指数 3个不确定元素。
仍以 19.625 为例:
正负号为 0,尾数为 1.0011101,指数为 4。
符号的存储
和整数一样,用一个位表示,0表示正、1表示负。
尾数的存储
采用二进制形式后,尾数部分的取值范围为 1 ≤ 尾数< 2,意味着:尾数的整数部分一定为 1,是一个恒定的值,无需在内存中提现出来,可以直接截掉,只保存小数点后的二进制数字。
对于 1.0011101,就是把 0011101 放入内存。
如果采用其它进制,例如10,那么尾数的整数部分就不是固定的,可能的取值有1~9。这样尾数的整数部分就不能省略了。
指数部分
指数是一个整数,并且有正负之分。
为了提高效率,避免繁琐的转换,指数的存储并没有像整数那样采用补码加符号位的形式。
float 和 double 在内存中的存储方式如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Oweq23H-1632375572654)(D:\!!C++\学习笔记\C语言部分.assets\113420K92-1.png)]
浮点数的内存分为 3 部分,当浮点数的类型确定后,每一部分的位数就固定了。
符号位和尾数部分都能直接存储,指数部分的存储需要一些处理:
以 float 为例,指数部分占 8bit,能表示从0~255的值,取中间值127,存入时加上127,读取时再减去127。这样一来就省去了负数。
所以指数部分范围是-127~128 ?
double 同理,也是存储+中间值,读取-中间值。
中间值的公式是median = 2^(n-1) - 1
,n为指数部分所占位数。
float,中间值为 2^(8-1) - 1 = 127
;对于 double,中间值为 2^(11-1) -1 = 1023
。
综上所述,float 类型的 19.625 在内存中的值为:0 10000011 001 1101 0000 0000 0000 0000。
用代码验证 float 的存储
//浮点数结构体 typedef struct { unsigned int nMant : 23; //尾数部分 unsigned int nExp : 8; //指数部分 unsigned int nSign : 1; //符号位 } FP_SINGLE; int main() { char strBin[33] = { 0 }; float f = 19.625; FP_SINGLE *p = (FP_SINGLE*)&f; itoa(p->nSign, strBin, 2); printf("sign: %s\n", strBin); itoa(p->nExp, strBin, 2); printf("exp: %s\n", strBin); itoa(p->nMant, strBin, 2); printf("mant: %s\n", strBin); return 0; } //itoa:将十进制数转换成二进制的字符串 //char *itoa (int value, char *str, int base ); //int value 被转换的整数,char *string 转换后储存的字符数组,int radix 转换进制数
精度问题
一个有限位数的整数一定能转换成有限位数的二进制。
但一个有限位数的小数并不一定能转换成有限位数的二进制数,只有末位是 5 的小数才有可能转换成有限位数的二进制。
float 和 double 的尾数部分是有限的,所以就有了精度问题。
float 尾数部分 23 位,加上隐含的 1,共 24 位,但只能保证 23 位是准确的。
转换为十进制,整体精度为 7~8 位。
double 同理,二进制形式的精度为 52~53 位,十进制形式的精度为 15~16 位。
IEEE 754 标准
Intel 为了统一浮点数的处理而设计的标准,几乎被所有计算机采用。
-
当指数的所有二进制位既不全为 0 也不全为 1 时,称之为“规格化浮点数”;
-
当指数的所有二进制位都是 0 时,我们将这样的浮点数称为“非规格化浮点数”
-
当指数的所有二进制位都是 1 时,作为特殊值对待。
非规格化浮点数(指数全 0)
尾数隐含的整数部分变成了 0,并且用 1 减去中间值才能还原真实的指数。
float:0.尾数 * 2^(-126)
double:0.尾数 * 2^( -1022)
为什么是用 1 去减中间数?
非规格化浮点数存在的意义:为了存储更小的数。
规格化浮点数绝对值最小值:尾数全为 0,指数仅最低位为 1——1.0*2^(-126)。对于科学计算,还不够小,于是就引入了非规格化浮点数。
非规格化浮点数绝对值最小值:尾数仅最低位为 1——2^(-23) × 2^(-126) = 2^(-149),比规格化浮点数小23个量级。
规格化浮点数能够很平滑地过度到非规格化浮点数,二者之间无论是内存中的存储,还是科学技术法的表示,都不存在缺口。
最大非规格化数:
0 - 00…00 - 11…11——位数全0,尾数全1——1.11…11 × 2^(-127)
最小规格化数:
0 - 00…01 - 00…00——位数仅最低位1,尾数全0——1.0 × 2^(-126)
最大非规格化数 + 1 = 最小规格化数
特殊值(指数全 1)
如果此时尾数的二进制位都为 0,则表示无穷大:
- 如果符号 sign 为 1,则表示负无穷大;
- 如果符号 sign 为 0,则表示正无穷大。
如果此时尾数 mant 的二进制位不全为 0,则表示 NaN(Not a Number),也即这是一个无效的数字,或者该数字未经初始化。
+0 和 -0 的表示
对于非规格化浮点数,当尾数的所有二进制位都为 0 时,整个浮点数的值就为 0:
- 如果符号 sign 为 0,则表示 +0;(浮点数的所有位都为0)
- 如果符号 sign 为 1,则表示 -0。(浮点数仅有符号数为1)
舍入模式
最近的值
浮点数尾数部分优先,通常要舍掉多余的位。
IEEE 754 的舍入模式类似四舍五入,但对 .5 的处理,优先靠近偶数:0.5 取 0、1.5 取 2、2.5 取 2。
+∞方向
1.1取2、-1.1取-1
-∞方向
1.1取1、-1.1取-2
取整
字面意思
C语言中使用英文字符
英文字符直接用 char 类型存储。
char 长度为1Byte,只能容纳ASCII码表中的字符,也就是英文字符。
char a = '1';
输出
方法一、专门的字符输出函数 putchar()
。
方法二、通用格式化输出 printf()
,char 对应的格式控制符是%c
。
putchar(c); putchar('\n');
//使用 printf 输出
printf("%c %c %c\n", a, b, c);
char 实际存储的是字符对应的 ASCII 码,char 类型可以赋值给 int 类型。
引申:
%c
、%d
都可用以输出 int 和 char 类型。
%c
方式输出 ASCII码 对应 字符,%d
方式输出字符本身。char a = 'E'; char b = 70; int c = 71; int d = 'H'; printf("a: %c, %d\n", a, a); printf("b: %c, %d\n", b, b); printf("c: %c, %d\n", c, c); printf("d: %c, %d\n", d, d); 输出结果: a: E, 69 b: F, 70 c: G, 71 d: H, 72
C语言中没有专门用来存储字符串的类型。只能用数组或指针来间接存储字符串。
C语言中使用中文字符
char类型只有1个字节长度,无法存储中文字符。
在C语言中,存储汉字等ASCII编码之外的字符,要用专门的字符类型,即宽字符编码方式。
常用的款字符编码有 UTF-16 和 UTF-32 。
微软编译器采用 UTF-16 编码,使用2个字节存储一个字符,用 unsigned short 类型就可以容纳。
GCC、LLVM/Clang(内嵌于 Xcode 中)采用 UTF-32 编码,使用 4 个字节存储字符,用 unsigned int 类型就可以容纳。
为了解决兼容性问题,C语言推出了一种新的类型,叫做 wchar_t。w 是 wide 的首字母,t 是 type 的首字符,wchar_t 的意思就是宽字符类型。wchar_t 的长度由编译器决定:
- 在微软编译器下,它的长度是 2,等价于 unsigned short;
- 在GCC、LLVM/Clang 下,它的长度是 4,等价于 unsigned int。
wchar_t 其实是用 typedef 关键字定义的一个别名。
如何在代码中使用?
在字符前加上L
前缀。例如L'A'
、L'9'
、L'中'
、L'国'
、L'。'
。
加上
L
前缀后,所有的字符都将成为宽字符,占用 2 个字节或者 4 个字节的内存,包括 ASCII 中的英文字符。
wchar_t a = L'A'; //英文字符(基本拉丁字符)
wchar_t b = L'9'; //英文数字(阿拉伯数字)
wchar_t c = L'中'; //中文汉字
wchar_t d = L'国'; //中文汉字
wchar_t e = L'。'; //中文标点
wchar_t f = L'ヅ'; //日文片假名
wchar_t g = L'♥'; //特殊符号
wchar_t h = L'༄'; //藏文
之后就将不带L
前缀的字符称为窄字符,使用 ASCII 编码;
带L
前缀的字符称为宽字符,使用 UTF-16 或者 UTF-32 编码。
输出
putchar、printf 只能输出不加L
前缀的窄字符。
加了L
前缀的宽字符必须使用 <wchar.h> 头文件中的宽字符输出函数,它们分别是 putwchar 和 wprintf:
putwchar 函数专门用来输出一个宽字符,对应 putchar ;
wprintf 对应printf,除了可以输出单个宽字符,还可以输出宽字符串。宽字符对应的格式控制符为
%lc
。
如果希望设置为中文简体环境,在 Windows 下写作:
setlocale(LC_ALL, "zh-CN");
在 Linux 和 Mac OS 下写作:
setlocale(LC_ALL, "zh_CN");
wchar_t a = L'A'; //英文字符(基本拉丁字符)
wchar_t b = L'9'; //英文数字(阿拉伯数字)
wchar_t c = L'中'; //中文汉字
wchar_t d = L'国'; //中文汉字
wchar_t e = L'。'; //中文标点
wchar_t f = L'ヅ'; //日文片假名
wchar_t g = L'♥'; //特殊符号
wchar_t h = L'༄'; //藏文
//将本地环境设置为简体中文
setlocale(LC_ALL, "zh_CN");
//使用专门的 putwchar 输出宽字符
putwchar(a); putwchar(b); putwchar(c); putwchar(d);
putwchar(e); putwchar(f); putwchar(g); putwchar(h);
putwchar(L'\n'); //只能使用宽字符
//使用通用的 wprintf 输出宽字符
wprintf(
L"Wide chars: %lc %lc %lc %lc %lc %lc %lc %lc\n", //必须使用宽字符串
a, b, c, d, e, f, g, h
);
运行结果:
A9中国。ヅ♥༄
Wide chars: A 9 中 国 。 ヅ ♥ ༄
输出宽字符串
对应的格式控制符是%ls
。
int main(){
wchar_t web_url[] = L"http://c.biancheng.net";
wchar_t *web_name = L"C语言中文网";
//将本地环境设置为简体中文
setlocale(LC_ALL, "zh_CN");
//使用通用的 wprintf 输出宽字符
wprintf(L"web_url: %ls \nweb_name: %ls\n", web_url, web_name);
return 0;
}
C语言的编码讨论
C语言是 70 年代的产物,那个时候只有 ASCII,各个国家的字符编码都还未成熟,所以C语言不可能从底层支持 GB2312、GBK、Big5、Shift-JIS 等国家编码,也不可能支持 Unicode 字符集。
在C语言中,只有 char 类型的窄字符才使用 ASCII 编码,char 类型的窄字符串、wchar_t 类型的宽字符和宽字符串都不使用 ASCII 编码。
对于窄字符串,C语言并没有规定使用哪一种特定的编码,只要选用的编码能够适应当前的环境即可,所以,窄字符串的编码与操作系统和编译器有关。
源文件使用什么编码
源文件保存代码,要尽量压缩文件体积,以节省硬盘空间或者网络流量,而代码中大部分的字符都是 ASCII 编码中的字符,用一个字节足以容纳,所以 UTF-8 编码是一个不错的选择。
UTF-8 兼容 ASCII,代码中的大部分字符可以用一个字节保存;另外 UTF-8 基于 Unicode,支持全世界的字符。
常见的 IDE 或者编辑器,例如 Xcode、Sublime Text、Gedit、Vim 等,在创建源文件时一般默认使用 UTF-8 编码。但 Visual Studio 是个奇葩,它默认使用本地编码来创建源文件。
所谓本地编码,就是像 GBK、Big5、Shift-JIS 等这样的国家编码(地区编码);针对不同国家发行的操作系统,默认的本地编码一般不同。简体中文本的 Windows 默认的本地编码是 GBK。
编译器往往支持多种编码格式的源文件。
#include <stdio.h>
int main()
{
puts("C语言中文网");
printf("http://c.biancheng.net");
return 0;
}
源码包含中文,肯定不能用 ASCII 编码。
微软编译器使用本地编码来保存这些字符。不同地区的 Windows 版本默认的本地编码不一样.
GCC、LLVM/Clang 编译器使用和源文件相同的编码来保存这些字符:
如果源文件使用的是 UTF-8 编码,那么这些字符也使用 UTF-8 编码;
如果源文件使用的是 GBK 编码,那么这些字符也使用 GBK 编码。
C语言转义字符
字符集(Character Set)为每个字符分配了唯一的编号,该编号就是编码值。
在C语言中,一个字符除了可以用它的实体(也就是真正的字符)表示,还可以用编码值表示。使用编码值来间接地表示字符的方式称为转义字符(Escape Character)。
转义字符以
\
或者\x
开头,以\
开头表示后跟八进制形式的编码值;以
\x
开头表示后跟十六进制形式的编码值。对于转义字符来说,只能使用八进制或者十六进制。
char a = '\61'; //字符1
char b = '\141'; //字符a
char c = '\x31'; //字符1
char d = '\x61'; //字符a
char *str1 = "\x31\x32\x33\x61\x62\x63"; //字符串"123abc"
char *str2 = "\61\62\63\141\142\143"; //字符串"123abc"
char *str3 = "The string is: \61\62\63\x61\x62\x63" //混用八进制和十六进制形式
对于 ASCII 编码,0~31(十进制)范围内的字符为控制字符。完整的列表如下:
转义字符 | 意义 | ASCII码值(十进制) |
---|---|---|
\a | 响铃(BEL) | 007 |
\b | 退格(BS) ,将当前位置移到前一列 | 008 |
\f | 换页(FF),将当前位置移到下页开头 | 012 |
\n | 换行(LF) ,将当前位置移到下一行开头 | 010 |
\r | 回车(CR) ,将当前位置移到本行开头 | 013 |
\t | 水平制表(HT) 。 一般相当于四个空格,或者 tab 键的功能。 | 009 |
\v | 垂直制表(VT) | 011 |
\’ | 单引号 | 039 |
\" | 双引号 | 034 |
\\ | 反斜杠 | 092 |
\n
和\t
是最常用的两个转义字符:
标识符、关键字、注释、表达式和语句
标识符:程序员自己起的变量名。字母、数组、下划线。字母或下划线开头。
关键字:由编程语言自己规定的。
注释:// 或 /* */ 。
表达式和语句:概念在C语言中并没有明确的定义。
表达式可以看做一个计算公式:3*4+5
、a=c=d
等。结果必定是一个值,如 printf("hello")
的结果是 5。
语句的范围更加广泛,不一定是计算,不一定有值,可以是某个操作、某个函数、选择结构、循环等。以分号;
结束的往往称为语句,而不是表达式。
的算术运算
除法
若除数、被除数都是整数,结果也是整数。
若有一个是小数,则结果也是小数,double类型小数。
如果除数为0,则在编译阶段无法发现错误,运行时会崩溃。
取余
运算数都必须是整数,出现小数会报错。
A % B = C ,C 的正负由 A 的正负决定。
变量的定义位置以及初始值
为了让编译器方便给变量分配内存,C89 标准规定,所有的局部变量(函数内部的变量)都必须定义在函数的开头位置,在定义完所有变量之前不能有其它的表达式。
这一死板的规定在 C99 标准中取消了。
局部变量不会默认初始化。有的编译器会默认初始化,有的不会。
(其他的都了解了)
运算符的优先级和结合性
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
---|---|---|---|---|---|
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | |
() | 圆括号 | (表达式) 函数名(形参表) | |||
. | 成员选择(对象) | 对象.成员名 | |||
-> | 成员选择(指针) | 对象指针->成员名 | |||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
(类型) | 强制类型转换 | (数据类型)表达式 | |||
++ | 自增运算符 | ++变量名 变量名++ | 单目运算符 | ||
– | 自减运算符 | –变量名 变量名– | 单目运算符 | ||
* | 取值运算符 | *指针变量 | 单目运算符 | ||
& | 取地址运算符 | &变量名 | 单目运算符 | ||
! | 逻辑非运算符 | !表达式 | 单目运算符 | ||
~ | 按位取反运算符 | ~表达式 | 单目运算符 | ||
sizeof | 长度运算符 | sizeof(表达式) | |||
3 | / | 除 | 表达式 / 表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | 双目运算符 | ||
% | 余数(取模) | 整型表达式%整型表达式 | 双目运算符 | ||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | 双目运算符 | ||
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | 双目运算符 | ||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | 双目运算符 | ||
< | 小于 | 表达式<表达式 | 双目运算符 | ||
<= | 小于等于 | 表达式<=表达式 | 双目运算符 | ||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | 双目运算符 | ||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | |
/= | 除后赋值 | 变量/=表达式 | |||
*= | 乘后赋值 | 变量*=表达式 | |||
%= | 取模后赋值 | 变量%=表达式 | |||
+= | 加后赋值 | 变量+=表达式 | |||
-= | 减后赋值 | 变量-=表达式 | |||
<<= | 左移后赋值 | 变量<<=表达式 | |||
>>= | 右移后赋值 | 变量>>=表达式 | |||
&= | 按位与后赋值 | 变量&=表达式 | |||
^= | 按位异或后赋值 | 变量^=表达式 | |||
|= | 按位或后赋值 | 变量|=表达式 | |||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 |
大部分运算符的优先级和数学中是一样的。
数据类型转换
将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换,例如:
float f = 100;
int n = f;
在不同类型的混合运算中,编译器也会自动地转换数据类型,将参与运算的所有数据先转换为同一种类型,然后再进行计算。转换的规则如下:
- 转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。例如,int 和 long 参与运算时,先把 int 类型的数据转成 long 类型后再进行运算。
- 所有的浮点运算都是以双精度进行的,即使运算中只有 float 类型,也要先转换为 double 类型,才能进行运算。
- char 和 short 参与运算时,必须先转换成 int 类型。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wQvwpPm4-1632375572656)(D:\!!C++\学习笔记\C语言部分.assets\134S535R-0.png)]
无论是自动类型转换还是强制类型转换,都只是为了本次运算而进行的临时性转换,转换的结果也会保存到临时的内存空间,不会改变数据本来的类型或者值。
int main(){
double total = 400.8; //总价
int count = 5; //数目
double unit; //单价
int total_int = (int)total;//!!!!
unit = total / count;
printf("total=%lf, total_int=%d, unit=%lf\n", total, total_int, unit);
return 0;
}
//!!!!部分:total 变量被转换成了 int 类型才赋值给 total_int 变量,而这种转换并未影响 total 变量本身的类型和值。
自动类型转换可能会使数值失真,不会对程序带来严重后果。
强制类型转换风险较高。
(除此以外都是基础的,没有什么需要额外注意)
– C语言输入输出 –
printf()
在C语言中,有三个函数可以用来在显示器上输出数据,它们分别是:
- puts():只能输出字符串,并且输出结束后会自动换行。
- putchar():只能输出单个字符。
- printf():可以输出各种类型的数据。
printf() 是最灵活、最复杂、最常用的输出函数,完全可以替代 puts() 和 putchar()。
printf()的格式控制符:
格式控制符 | 说明 |
---|---|
%c | 输出一个单一的字符 |
%hd、%d、%ld | 以十进制、有符号的形式输出 short、int、long 类型的整数 |
%hu、%u、%lu | 以十进制、无符号的形式输出 short、int、long 类型的整数 |
%ho、%o、%lo | 以八进制、不带前缀、无符号的形式输出 short、int、long 类型的整数 |
%#ho、%#o、%#lo | 以八进制、带前缀、无符号的形式输出 short、int、long 类型的整数 |
%hx、%x、%lx %hX、%X、%lX | 以十六进制、不带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字也小写;如果 X 大写,那么输出的十六进制数字也大写。 |
%#hx、%#x、%#lx %#hX、%#X、%#lX | 以十六进制、带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字和前缀都小写;如果 X 大写,那么输出的十六进制数字和前缀都大写。 |
%f、%lf | 以十进制的形式输出 float、double 类型的小数 |
%e、%le %E、%lE | 以指数的形式输出 float、double 类型的小数。如果 e 小写,那么输出结果中的 e 也小写;如果 E 大写,那么输出结果中的 E 也大写。 |
%g、%lg %G、%lG | 以十进制和指数中较短的形式输出 float、double 类型的小数,并且小数部分的最后不会添加多余的 0。如果 g 小写,那么当以指数形式输出时 e 也小写;如果 G 大写,那么当以指数形式输出时 E 也大写。 |
%s | 输出一个字符串 |
printf()的高级用法
#include <stdio.h>
int main()
{
int a1=20, a2=345, a3=700, a4=22;
int b1=56720, b2=9999, b3=20098, b4=2;
int c1=233, c2=205, c3=1, c4=6666;
int d1=34, d2=0, d3=23, d4=23006783;
printf("%-9d %-9d %-9d %-9d\n", a1, a2, a3, a4);
printf("%-9d %-9d %-9d %-9d\n", b1, b2, b3, b4);
printf("%-9d %-9d %-9d %-9d\n", c1, c2, c3, c4);
printf("%-9d %-9d %-9d %-9d\n", d1, d2, d3, d4);
return 0;
}
运行结果:
20 345 700 22
56720 9999 20098 2
233 205 1 6666
34 0 23 23006783
%-9d
中,d
表示以十进制输出,9
表示最少占9个字符的宽度,宽度不足以空格补齐,-
表示左对齐。综合起来,%-9d
表示以十进制输出,左对齐,宽度最小为9个字符。
printf() 格式控制符的完整形式
%[flag][width][.precision]type
1) type 表示输出类型,比如 %d、%f、%c、%lf,type 就分别对应 d、f、c、lf。
type 这一项必须有,输出时必须要知道是什么类型。
2) width 表示最小输出宽度,也就是至少占用几个字符的位置;例如,
%-9d
中 width 对应 9,表示输出结果最少占用 9 个字符的宽度。当输出结果的宽度不足 width 时,以空格补齐(默认在左边补齐空格)。
输出结果的宽度超过 width 时,width 不再起作用,按照数据本身的宽度来输出。
3) .precision 表示输出精度,也就是小数的位数(输出float、double时)
- 当小数部分的位数大于 precision 时,会按照四舍五入的原则丢掉多余的数字;
- 当小数部分的位数小于 precision 时,会在后面补 0。
.precision 也可以用于整数和字符串:
- 用于整数时,.precision 表示最小输出宽度,宽度不足时会在左边补 0。
- 用于字符串时,.precision 表示最大输出宽度,或者说截取字符串。当字符串的长度大于 precision 时,会截掉多余的字符;当字符串的长度小于 precision 时,.precision 就不再起作用。
%m.nd
宽度为m,n位小数。m是非限定的,即实际长度超过了m,就按实际长度显示;
n是限定的,会截掉多的部分四舍五入
例子:
#include <stdio.h> int main(){ int n = 123456; double f = 882.923672; char *str = "abcdefghi"; printf("n: %.9d %.4d\n", n, n); printf("f: %.2lf %.4lf %.10lf\n", f, f, f); printf("str: %.5s %.15s\n", str, str); return 0; }
运行结果:
n: 000123456 123456 f: 882.92 882.9237 882.9236720000 str: abcde abcdefghi
4) flag 是标志字符。例如,
%#x
中 flag 对应 #,%-9d
中 flags 对应-
。下表列出了 printf() 可以用的 flag:
标志字符 含 义 - -
表示左对齐。如果没有,就按照默认的对齐方式,默认一般为右对齐。+ 用于整数或者小数,表示输出符号(正负号)。如果没有,那么只有负数才会输出符号。 空格 用于整数或者小数,输出值为正时冠以空格,为负时冠以负号。 # 对于八进制(%o)和十六进制(%x / %X)整数,# 表示在输出时添加前缀;八进制的前缀是 0,十六进制的前缀是 0x / 0X。对于小数(%f / %e / %g),# 表示强迫输出小数点。如果没有小数部分,默认是不输出小数点的,加上 # 以后,即使没有小数部分也会带上小数点。 例子:
printf("m=%10d, m=%-10d\n", m, m); //演示 - 的用法 printf("m=%+d, n=%+d\n", m, n); //演示 + 的用法 printf("m=% d, n=% d\n", m, n); //演示空格的用法 printf("f=%.0f, f=%#.0f\n", f, f); //演示#的用法
printf()的缓冲区
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("C语言中文网");
sleep(5); //程序暂停5秒钟
printf("http://c.biancheng.net\n");
return 0;
}
//输出结果:程序等待5秒后,2个printf同时输出。
sleep() 是 Linux 和 Mac OS 下特有的函数,不能用于 Windows。Windows 下也有功能相同的暂停函数,叫做 Sleep()。起到同样效果的代码为
Sleep(5000); //程序暂停5秒钟
。
将第一行printf()
修改为
printf("C语言中文网\n");
输出结果:第一行printf()
先输出,等待5秒,第二行printf()
输出。
因为缓冲区的存在导致了代码输出时间差异。
从本质上讲,printf() 执行结束以后数据并没有直接输出到显示器上,而是放入了缓冲区,直到遇见换行符\n
才将缓冲区中的数据输出到显示器上。
将上方两版本代码放入windows中运行,两行printf()
始终差5秒时间输出。
因为 Windows 和 Linux、Mac OS 的缓存机制不同。
输入输出的“命门”就在于缓存。
在屏幕任意位置输出字符
(看个大概就行了)
因为用到了 Windows 操作系统的功能,所以代码不能在 Linux 和 Mac OS 下运行。
第一步就是搞定光标定位问题,C语言本身并不支持该功能,必须使用 Windows 提供的接口,所以要引入 windows.h 头文件。
光标定位需要使用 windows.h 头文件中的SetConsoleCursorPosition
函数,它的使用方式为:
SetConsoleCursorPosition(HANDLE hConsoleOutput, COORD dwCursorPosition);
例如,将光标定位到控制台的第3行第3列:
#include <stdio.h>
#include <windows.h>
int main(){
//定义光标位置
COORD coord;
coord.X = 3; //第3行
coord.Y = 3; //第3列
//获取控制台缓冲区句柄
HANDLE ConsoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
//设置光标位置
SetConsoleCursorPosition(ConsoleHandle, coord);
//输出字符串
printf("http://c.biancheng.net\n");
return 0;
}
这样,程序就可以从第3行第3列开始输出了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OpDCdqFy-1632375572657)(D:\!!C++\学习笔记\C语言\03输入输出.assets\10394944M-0.png)]
任意设置光标位置
SetConsoleCursorPosition()
SetConsoleTextAttribute()
#include <stdio.h>
#include <windows.h>
//设置光标位置
void setCursorPosition(int x, int y);
//设置文字颜色
void setColor(int color);
int main(){
setColor(3);
setCursorPosition(3, 3);
puts("★");
setColor(0XC);
setCursorPosition(1, 1);
puts("◆");
setColor(6);
setCursorPosition(6, 6);
puts("■");
return 0;
}
//自定义的光标定位函数
void setCursorPosition(int x, int y){
COORD coord;
coord.X = x;
coord.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
}
//自定义的文字颜色函数
void setColor(int color){
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color) ;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jtc70biN-1632375572658)(D:\!!C++\学习笔记\C语言\03输入输出.assets\image-20201203124541264.png)]
scanf()
在C语言中,有多个函数可以从键盘获得用户输入:
- scanf():和 printf() 类似,scanf() 可以输入多种类型的数据。
- getchar()、getche()、getch():这三个函数都用于输入单个字符。
- gets():获取一行数据,并作为字符串处理。
scanf 是 scan format 的缩写,意思是格式化扫描,也就是从键盘获得用户输入,和 printf 的功能正好相反。
代码:
#include <stdio.h>
int main()
{
int a = 0, b = 0, c = 0, d = 0;
scanf("%d", &a); //输入整数并赋值给变量a
scanf("%d", &b); //输入整数并赋值给变量b
printf("a+b=%d\n", a+b); //计算a+b的值并输出
scanf("%d %d", &c, &d); //输入两个整数并分别赋值给c、d
printf("c*d=%d\n", c*d); //计算c*d的值并输出
return 0;
}
scanf("%d %d", &c, &d);
"%d %d"之间是有空格的,所以输入数据时也要有空格。对于 scanf(),输入数据的格式要和控制字符串的格式保持一致。
(实际上对空格的数量没有约束)
scanf("%d %d", &a, &b);
scanf("%d %d", &a, &b);
输入数据时,数据间有多少个空格都一样
&为取地址符,数据是以二进制的形式保存在内存中的,字节(Byte)是最小的可操作单位。为了便于管理,我们给每个字节分配了一个编号,可以通过编号操作该字符。
这个编号就是地址。
int a = 'F'; int b = 12; int c = 452; printf("&a=%p, &b=%p, &c=%p\n", &a, &b, &c);
输出结果:
&a=0x18ff48, &b=0x18ff44, &c=0x18ff40
%p
是一个格式控制符,它表示以十六进制的形式(带小写的前缀)输出数据的地址。如果写作%P
,那么十六进制的前缀也将变成大写形式。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rzLPHfsZ-1632375572659)(D:\!!C++\学习笔记\C语言\03输入输出.assets\10452364L-1.png)]
scanf()的缓冲区
从本质上讲,我们从键盘输入的数据并没有直接交给 scanf(),而是放入了缓冲区中,直到我们按下回车键,scanf() 才到缓冲区中读取数据。如果缓冲区中的数据符合 scanf() 的要求,那么就读取结束;如果不符合要求,那么就继续等待用户输入(例如缺少数据),或者干脆读取失败(例如数据类型错误)。
#include <stdio.h>
int main()
{
int a = 1, b = 2, c = 3, d = 4; //修改处:给变量赋予不同的初始值
scanf("%d", &a);
scanf("%d", &b);
printf("a=%d, b=%d\n", a, b);
scanf("%d %d", &c, &d);
printf("c=%d, d=%d\n", c, d);
return 0;
}
运行结果:
12 60 a10↙
a=12, b=60
c=3, d=4
第三个 scanf() 要求输入两个十进制的整数,a10 无论如何也不符合要求,所以只能读取失败。
这说明 scanf() 不会跳过不符合要求的数据,遇到不符合要求的数据会读取失败,而不是再继续等待用户输入。
格式控制符
scanf() 和 printf() 虽然功能相反,但是格式控制符是一样的,单个字符、整数、小数、字符串对应的格式控制符分别是 %c、%d、%f、%s。
格式控制符 | 说明 |
---|---|
%c | 读取一个单一的字符 |
%hd、%d、%ld | 读取一个十进制整数,并分别赋值给 short、int、long 类型 |
%ho、%o、%lo | 读取一个八进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型 |
%hx、%x、%lx | 读取一个十六进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型 |
%hu、%u、%lu | 读取一个无符号整数,并分别赋值给 unsigned short、unsigned int、unsigned long 类型 |
%f、%lf | 读取一个十进制形式的小数,并分别赋值给 float、double 类型 |
%e、%le | 读取一个指数形式的小数,并分别赋值给 float、double 类型 |
%g、%lg | 既可以读取一个十进制形式的小数,也可以读取一个指数形式的小数,并分别赋值给 float、double 类型 |
%s | 读取一个字符串(以空白符为结束) |
对字符串的读取
字符串有2中定义:
char str1[] = "http://c.biancheng.net";
char *str2 = "C语言中文网";
str1形式的字符串所在的内存既有读取权限又有写入权限。
str2形式的字符串所在的内存只有读取权限,没有写入权限。
printf()、puts() 等字符串输出函数只要求字符串有读取权限,
scanf()、gets() 等字符串输入函数要求字符串有写入权限
第二种形式只能用于输出函数。
system
system() 是个库函数
作用:在程序中启动另一个程序;
函数的参数(字符串):要启动的程序的路径
例子:
int main()
{
system("mspaint");//启动自带的画图软件
return 0;
}
//一些程序,系统能根据环境变量找到程序位置,如上所示
路径例子:(windows中的路径要用“\\”或“/”;Linux中用“\”)“D:\\for_c++\\STL_Practices\\Debug\\STL_Practices.exe”
system("pause");//pause程序等待用户输入任意按键
C语言标准输入 scanf()
从键盘读入数据
int num = 0;
scanf("%d", &num);
系统中有一个文件:标准输入(stdin),当敲击键盘时,就会将数据写到该文件中,包括回车(\n)。
scanf("%d", &num);
:从标准输入中读数据,读取0~9("%d"
),遇到“\n”后结束。
如果遇到0~9以外的字符,停止读取。
scanf("%d %d", &num1, &num2);
//读入2个整数,输入时用空格或回车分隔
读入多个字符:
char c = 0;
char num = 0;
scanf_s("%c", &c);
printf("c = %d\n", c);
// scanf_s("%c", &num);
scanf_s("%c", &num);
printf("num = %d\n", num);
输入:a(回车)
输出:97
10
10就是回车'\n'的ascii码,被读入了第二个字符变量
解决方法:用空格分隔,或取消上方的注释,
VS中直接用scanf会报错C4996
c4996
数组
int a[5] = { 1,2,3,4,5 };
auto b = &a + 1;
这里变量b的类型是 int (*b)[5]
。一个指向容量为5的int类型数组
的指针(这也是&a的结果?)。
而 b 所指的位置是&a[5] + 4Byte 的位置,相当于a的end()。也就是数组的后1Byte的后一Byte。
getchar()、getche()、getch()
三个函数都用于输入单个字符。
getchar()
是scanf("%c", c)
的替代品,除了更加简洁,没有其它优势。
#include <stdio.h>
int main()
{
char c;
c = getchar();
printf("c: %c\n", c);
return 0;
}
输入示例:
@↙
c: @
getche()
没有缓冲区,输入一个字符后会立即读取,不用等待用户按下回车键。
#include <stdio.h>
#include <conio.h>
//Windows独有的头文件,所以getche()不是标准函数
int main()
{
char c = getche();
printf("c: %c\n", c);
return 0;
}
输入示例:
@c: @
getch()
也没有缓冲区,输入一个字符后会立即读取。
特别之处是它没有回显,看不到输入的字符。从用户的角度看,就好像根本没有输入一样。
可用于输入密码等情况。
getch() 也位于 conio.h 头文件中,也不是标准函数
#include <stdio.h>
#include <conio.h>
int main()
{
char c = getch();
printf("c: %c\n", c);
return 0;
}
输入@后,getch() 会立即读取完毕,接着继续执行 printf() 将字符输出。但是由于 getch() 没有回显,看不到输入的@字符,所以控制台上最终显示的内容为c: @。
gets()
字符串输入函数
与scanf()的区别:
scanf()遇到空格会结束;gets()会读取空格,遇到回车时才结束。
#include <stdio.h>
int main()
{
char author[30], lang[30], url[30];
gets(author);
printf("author: %s\n", author);
gets(lang);
printf("lang: %s\n", lang);
gets(url);
printf("url: %s\n", url);
return 0;
}
缓存
Buffer或Cache。是内存空间的一部分,用来暂时保存输入或输出的数据。
引入的目的(基本上和OS一样,复习):
1、协调输入输出设备和CPU的运行速度差。
程序先将数据放入缓冲区中,然后继续执行,当缓冲区中的数据满足要求(足够长,或数据输入完成),就将所有数据一次性写入硬盘,输入同理。减少了等待时间。
2、减少硬件设备读写次数。
程序使用系统调用通知OS调用硬件驱动程序。要经过多层转换,减少读写操作,就是减少转换次数,能提高程序性能。
缓冲区的类型
由数据刷新(清空缓冲区)的时机区分。
1、全缓冲
缓冲区被填满后才进行真正的输入输出操作。
典型代表是对硬盘文件的读写。
在实际开发中,将数据写入文件后,打开文件并不能立即看到内容,只有清空缓冲区,或者关闭文件,或者关闭程序后,才能在文件中看到内容。这种现象,就是缓冲区在作怪。
2、行缓冲
在输入或者输出的过程中遇到换行符时,才执行真正的输入输出操作。
行缓冲的典型代表就是标准输入设备(也即键盘)和标准输出设备(也即显示器)。
**对于printf():**只有遇到换行符
\n
后才会将数据输出到显示器上。
**对于scanf():**不管用户输入多少内容,只要不按下回车键,就不进行真正的读取。
也是先存入缓冲区,用户按下回车键,产生换行符
\n
,才会刷新缓冲区,进行真正的读取。
3、无缓冲
立即进行输入输出。
getche()、getch() 就不带缓冲区,输入一个字符后立即就执行了,根本不用按下回车键。
Windows 下的 printf() 也不带缓冲区。
一些输出错误信息的函数也没有缓冲区
C语言标准的规定
C语言标准规定,输入输出缓冲区要具有以下特征:
- 当且仅当输入输出不涉及交互设备(键盘、显示器等)时,它们才可以是全缓冲的。
- 错误显示设备不能带有缓冲区。
现代计算机没有专门的错误现实设备了,所以由专门处理向显示器显示错误信息的函数替代,如
perror()
。
1) 输入设备
scanf()、getchar()、gets() 就是从输入设备(键盘)上读取内容。对于输入设备,没有缓冲区将导致非常奇怪的行为,比如,我们本来想输入一个整数 947,没有缓冲区的话,输入 9 就立即读取了,根本没有机会输入 47,所以,没有输入缓冲区是不能接受的。Windows、Linux、Mac OS 在实现时都给输入设备带上了行缓冲,所以 scanf()、getchar()、gets() 在每个平台下的表现都一致。
但是在某些特殊情况下,我们又希望程序能够立即响应用户按键,例如在游戏中,用户按下方向键人物要立即转向,而且越快越好,这肯定就不能带有缓冲区了。Windows 下特有的 getche() 和 getch() 就是为这种特殊需求而设计的,它们都不带缓冲区。
2) 输出设备
printf()、puts()、putchar() 就是向输出设备(显示器)上显示内容。对于输出设备,有没有缓冲区其实影响没有那么大,顶多是晚一会看到内容,不会有功能性的障碍,所以 Windows 和 Linux、Mac OS 采用了不同的方案:
- Windows 平台下,输出设备是不带缓冲区的;
- Linux 和 Mac OS 平台下,输出设备带有行缓冲区。
缓冲区的刷新(清空)
将缓冲区中的内容送达到目的地。缓冲区的刷新遵循以下的规则:
- 不管是行缓冲还是全缓冲,缓冲区满时会自动刷新;
- 行缓冲遇到换行符
\n
时会刷新; - 关闭文件时会刷新缓冲区;
- 程序关闭时一般也会刷新缓冲区,这个是由标准库来保障的;
- 使用特定的函数也可以手动刷新缓冲区。
scanf()
scanf() 是从标准输入设备(键盘)读取数据,带有行缓冲区的,这让 scanf() 具有了一些独特的“性格”,例如可以连续输入、可以输入多余的数据等。反过来,scanf() 也出现了一些奇怪的行为,例如,有时候两份数据之间有空格会读取失败,而有时候两份数据之间又必须有空格。
scanf() 的这些特性根源就是行缓冲区。
当遇到 scanf() 函数时,程序会先检查输入缓冲区中是否有数据:
- 如果没有,就等待用户输入。用户从键盘输入的每个字符都会暂时保存到缓冲区,直到按下回车键,产生换行符
\n
,输入结束,scanf() 再从缓冲区中读取数据,赋值给变量。 - 如果有数据,那就看是否符合控制字符串的规则:
- 如果能够匹配整个控制字符串,那最好了,直接从缓冲区中读取就可以了,就不用等待用户输入了。
- 如果缓冲区中剩余的所有数据只能匹配前半部分控制字符串,那就等待用户输入剩下的数据。
- 如果不符合,scanf() 还会尝试忽略一些空白符,例如空格、制表符、换行符等:
- 如果这种尝试成功(可以忽略一些空白符),那么再重复以上的匹配过程。
- 如果这种尝试失败(不能忽略空白符),那么只有一种结果,就是读取失败。
所以,scanf() 并不是直接让用户从键盘输入数据,而是先检查缓冲区,处理缓冲区中的数据。
例子
1、连续输入
#include <stdio.h>
int main()
{
int a, b, c;
scanf("%d", &a);
scanf("%d", &b);
scanf("%d", &c);
printf("a=%d, b=%d, c=%d\n", a, b, c);
return 0;
}
运行结果:
100 200 300↙
a=100, b=200, c=300
第一个 scanf() 的控制字符串是
"%d"
,会匹配到第一个整数,也就是 100。将 100 赋值给变量 a,并将内部的位置标记移动到 100 以后,此时缓冲区中剩下
200 300↙
。换行符也是一个字符,也会进入缓冲区。
第二个 scanf() 的控制字符串也是"%d",需要读取一个整数,而此时缓冲区中的内容是
200 300↙
,开头是一个空格,并不是一个有效的数字,不符合控制字符串的规则。空格是一个空白符,此处是可以忽略的,于是 scanf() 忽略空格后再继续匹配,就得到了数字 200,终于匹配成功了。
到了第三个 scanf(),缓冲区中剩下
300↙
,同样会忽略开头的空格,匹配到数字 300。
最终,三个 scanf() 都匹配成功了,缓冲区中只留下了↙
。程序运行结束后,会释放缓冲区内存。
2、读取失败
#include <stdio.h>
int main()
{
int a, b=999;
char str[30];
printf("b=%d\n", b);
scanf("%d", &a);
scanf("%d", &b);
scanf("%s", str);
printf("a=%d, b=%d, str=%s\n", a, b, str);
return 0;
}
运行结果:
b=999
100 http://c.biancheng.net↙
a=100, b=999, str=http://c.biancheng.net
第一个scanf()等待用户输入,匹配到100,赋值给变量a,移动位置指针。
第二个scanf()直接读取缓冲区数据,忽略掉空格后仍不是需要的整数,匹配失败,不给b赋值。
匹配失败意味着不会移动内部的位置指针,此时缓冲区中的内容仍然是
http://c.biancheng.net↙
。正好匹配第三个scanf(),去掉空格后赋值。
3、不能忽略空白符的情形
#include <stdio.h>
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("b=%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
输入示例:
a=99↙
a=99, b=2
输入
a=99
,按下回车键,程序运行结束,只有第一个 scanf() 成功读取了数据,第二个 scanf() 仿佛没有执行一样,根本没有给用户任何机会去输入数据。
第一个 scanf() 执行完后,将 99 赋值给了 a,缓冲区中只剩下一个换行符
\n
;到了第二个 scanf(),发现缓冲区中有内容,但是又不符合控制字符串的格式,于是尝试忽略这个空白符。注意,这个时候的空白符是不能忽略的,所以就没有办法了,只能读取失败了。
实测发现,空白符在大部分情况下都可以忽略,前面的两个例子就是这样。但是当控制字符串不是以格式控制符 %d、%c、%f 等开头时,空白符就不能忽略了,它会参与匹配过程,如果匹配失败,就意味着 scanf() 读取失败了。
本例中,第二个 scanf() 的开头并不是格式控制符,而是写死的b
字符,所以不会忽略换行符,而换行符和b
又不匹配,只能读取失败。
换一种输入方式:
a=99 b=200↙
a=99, b=2
第二个 scanf() 也读取失败了。执行到第二个 scanf() 时,缓冲区中剩下b=200↙
,开头的空格依然不能忽略,仍和控制字符串不匹配,读取失败。
正确的输入方式:不让两份数据之间有空白符。
a=99b=200↙
a=99, b=200
再做一些修改:
将第二个 scanf() 改成下面的样子:
scanf("%d", &b);
运行结果:
a=100↙
200↙
a=100, b=200
第二个 scanf() 的控制字符串以%d
开头,就可以忽略换行符了。忽略换行符以后,缓冲区中就没有内容了,所以会等待用户输入。输入 200 以后,第二个 scanf() 就匹配成功了,将 200 赋值给变量 b。
为什么只有当控制字符串以格式控制符开头时,才会忽略换行符呢?教材暂无定论。
刷新缓冲区
作用:
1、对于输出操作,清空缓冲区会使得缓冲区中的所有数据立即显示到屏幕上;因为没有地方存放了,就只能输出。(把缓冲区的数据“赶到”显示器上)
2、对于输入操作,清空缓冲区就是抛弃缓冲区中的残存字符,让程序直接等待用户输入。避免残存字符引发一些奇怪的行为。
清空输出缓冲区
操作
fflush(stdout);
fflush() 是一个专门用来清空缓冲区的函数,stdout 是 standard output 的缩写,表示标准输出设备,也即显示器。
整个语句的意思是,清空标准输出缓冲区,或者说清空显示器的缓冲区。
代码
Windows 平台下的 printf()、puts()、putchar() 等输出函数都是不带缓冲区的,所以不用清空,下面的代码演示了如何在 Linux 和 Mac OS 平台下清空缓冲区:(行缓冲区)
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("C语言中文网");
fflush(stdout); //本次输出结束后立即清空缓冲区
sleep(5);
printf("http://c.biancheng.net\n");
return 0;
}
之后的补充:fflush(stdin) 这种不标准的写法只适用于一部分编译器,通用性非常差。
清空输入缓冲区
“没有一种既简洁明了又适用于所有平台的清空输入缓冲区的方案。只有一种很蹩脚的方案能适用于所有平台,那就是将输入缓冲区中的数据都读取出来,但是却不使用。”
1、使用 getchar() 清空缓冲区
getchar() 是带有缓冲区的,每次从缓冲区中读取一个字符,包括空格、制表符、换行符等空白符,只要我们让 getchar() 不停地读取,直到读完缓冲区中的所有字符,就能达到清空缓冲区的效果。
int c;
while((c = getchar()) != '\n' && c != EOF);
该代码不停地使用 getchar() 获取缓冲区中的字符,直到遇见换行符
\n
或者到达文件结尾才停止。
例子
#include <stdio.h>
int main()
{
int a = 1, b = 2;
char c;
scanf("a=%d", &a);
while((c = getchar()) != '\n' && c != EOF); //在下次读取前清空缓冲区
scanf("b=%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
输入示例:
a=100↙
b=200↙
a=100, b=200
a=100b=200↙
b=300↙
a=100, b=300
//第一次输入的多余部分“b=200”没有起作用
“这种方案的关键之处在于,getchar() 是带有缓冲区的,并且一切字符通吃,或者说一切字符都会读取,不会忽略。不过这种方案有个缺点,就是要额外定义一个变量 c,对于有强迫症的读者来说可能有点难受。 ”
2、使用 scanf() 清空缓冲区
使用类似于正则表达式的通配符,就可以读取所有的字符,包括空格、换行符、制表符等空白符。
scanf("%*[^\n]"); //清除缓冲区,仅剩下最后一个\n
scanf("%*c"); //清除最后的\n
第一个 scanf() 将逐个读取缓冲区中
\n
之前的其它字符,% 后面的 * 表示将读取的这些字符丢弃,遇到\n
字符时便停止读取。此时,缓冲区中尚有一个
\n
遗留,第二个 scanf() 再将这个\n
读取并丢弃,这里的星号和第一个 scanf() 的星号作用相同。
由于所有从键盘的输入都是以回车结束的,而回车会产生一个
\n
字符,所以将\n
连同它之前的字符全部读取并丢弃之后,也就相当于清除了输入缓冲区。
scanf高级用法
具体应用操作,需要时再看
C语言非阻塞式键盘监听
例子:
#include <stdio.h>
#include <conio.h>
int main(){
char ch;
int i = 0;
//循环监听,直到按Esc键退出
while(ch = getch()){
if(ch == 27){
break;
}else{
printf("Number: %d\n", ++i);
}
}
return 0;
}
这段代码虽然达到了监听键盘的目的,但是每次都必须按下一个键才能执行 getch() 后面的代码,即程序会卡在 while(ch = getch())
位置等待直到键盘有数据输入。
在 Windows 系统中,conio.h
头文件中的kbhit()
函数就可以用来实现非阻塞式键盘监听。
修改后的代码:
#include <stdio.h>
#include <windows.h>
#include <conio.h>
int main(){
char ch;
int i = 0;
//循环监听,直到按Esc键退出
while(1){
if(kbhit()){ //检测缓冲区中是否有数据
ch = getch(); //将缓冲区中的数据以字符的形式读出
if(ch == 27){
break;
}
}
printf("Number: %d\n", ++i);
Sleep(1000); //暂停1秒
}
return 0;
}
kbhit() 函数会检测缓冲区中是否有数据,如果有的话就返回非 0 值,没有的话就返回 0 值。但是 kbhit() 不会读取数据,数据仍然留在缓冲区。