c语言学习之路
懂一门编程语言对运维工作的推进和能力的提升是巨大的,在本文记录本人的学习之旅,跟大家分享一下。
所有的程序都是运行在内存中的,内存的利率的提升是很重要的。
内存《《《《《《 固态硬盘 《《《《《《《《《《《机械硬盘
数据类型
int 整数型 short短整数 long 长整数 double 双精度 float 单精度 char 字符类型 void 无类型
%d 是整数的形式输出。
%c:输出一个字符。c 是 character 的简写。
%s:输出一个字符串。s 是 string 的简写。
%f:输出一个小数。f 是 float 的简写。
例子
- #include <stdio.h>
- int main()
- {
- puts(
- "C语言中文网,一个学习C语言和C++的网站,他们坚持用工匠的精神来打磨每一套教程。"
- "坚持做好一件事情,做到极致,让自己感动,让用户心动,这就是足以传世的作品!"
- "C语言中文网的网址是:http://c.biancheng.net"
- );
- return 0;
- }
多个字符串输出可以用这种格式。
\n
是一个整体,组合在一起表示一个换行字符。换行符是 ASCII 编码中的一个控制字符,无法在键盘上直接输入,只能用这种特殊的方法表示,被称为转义字符,我们将在《C语言转义字符》一节中有具体讲解,请大家暂时先记住\n
的含义。
所谓换行,就是让文本从下一行的开头输出,相当于在编辑 Word 或者 TXT 文档时按下回车键。
puts 输出完成后会自动换行,而 printf 不会,要自己添加换行符,这是 puts 和 printf 在输出字符串时的一个区别。
让整数占用更少的内存可以在 int 前边加 short,让整数占用更多的内存可以在 int 前边加 long,例如:
short int a = 10;
short int b, c = 99;
long int m = 102023;
long int n, p = 562131;
也可以将 int 省略,只写 short 和 long,如下所示:
short a = 10;
short b, c = 99;
long m = 102023;
long n, p = 562131;
这样的写法更加简洁,实际开发中常用。
int 是基本的整数类型,short 和 long 是在 int 的基础上进行的扩展,short 可以节省内存,long 可以容纳更大的值。
short、int、long 是C语言中常见的整数类型,其中 int 称为整型,short 称为短整型,long 称为长整型。
细心的读者可能会发现,上面我们在描述 short、int、long 类型的长度时,只对 short 使用肯定的说法,而对 int、long 使用了“一般”或者“可能”等不确定的说法。这种描述的言外之意是,只有 short 的长度是确定的,是两个字节,而 int 和 long 的长度无法确定,在不同的环境下有不同的表现。
一种数据类型占用的字节数,称为该数据类型的长度。例如,short 占用 2 个字节的内存,那么它的长度就是 2。
实际情况也确实如此,C语言并没有严格规定 short、int、long 的长度,只做了宽泛的限制:
- short 至少占用 2 个字节。
- int 建议为一个机器字长。32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节。
- short 的长度不能大于 int,long 的长度不能小于 int。
总结起来,它们的长度(所占字节数)关系为:
2 ≤ short ≤ int ≤ long
sizeof 操作符
获取某个数据类型的长度可以使用 sizeof 操作符,如下所示:
- #include <stdio.h>
- int main()
- {
- short a = 10;
- int b = 100;
- int short_length = sizeof a;
- int int_length = sizeof(b);
- int long_length = sizeof(long);
- int char_length = sizeof(char);
- printf("short=%d, int=%d, long=%d, char=%d\n", short_length, int_length, long_length, char_length);
- return 0;
- }
在 32 位环境以及 Win64 环境下的运行结果为:
short=2, int=4, long=4, char=1
在 64 位 Linux 和 Mac OS 下的运行结果为:
short=2, int=4, long=8, char=1
sizeof 用来获取某个数据类型或变量所占用的字节数,如果后面跟的是变量名称,那么可以省略( )
,如果跟的是数据类型,就必须带上( )
。
需要注意的是,sizeof 是C语言中的操作符,不是函数,所以可以不带( )
,后面会详细讲解。
不同整型的输出
使用不同的格式控制符可以输出不同类型的整数,它们分别是:
%hd
用来输出 short int 类型,hd 是 short decimal 的简写;%d
用来输出 int 类型,d 是 decimal 的简写;%ld
用来输出 long int 类型,ld 是 long decimal 的简写。
下面的例子演示了不同整型的输出:
- #include <stdio.h>
- int main()
- {
- short a = 10;
- int b = 100;
- long c = 9437;
- printf("a=%hd, b=%d, c=%ld\n", a, b, c);
- return 0;
- }
运行结果:
a=10, b=100, c=9437
在编写代码的过程中,我建议将格式控制符和数据类型严格对应起来,养成良好的编程习惯。当然,如果你不严格对应,一般也不会导致错误,例如,很多初学者都使用%d
输出所有的整数类型,请看下面的例子:
- #include <stdio.h>
- int main()
- {
- short a = 10;
- int b = 100;
- long c = 9437;
- printf("a=%d, b=%d, c=%d\n", a, b, c);
- return 0;
- }
运行结果仍然是:
a=10, b=100, c=9437
当使用%d
输出 short,或者使用%ld
输出 short、int 时,不管值有多大,都不会发生错误,因为格式控制符足够容纳这些值。
当使用%hd
输出 int、long,或者使用%d
输出 long 时,如果要输出的值比较小(就像上面的情况),一般也不会发生错误,如果要输出的值比较大,就很有可能发生错误,例如:
- #include <stdio.h>
- int main()
- {
- int m = 306587;
- long n = 28166459852;
- printf("m=%hd, n=%hd\n", m, n);
- printf("n=%d\n", n);
- return 0;
- }
在 64 位 Linux 和 Mac OS 下(long 的长度为 8)的运行结果为:
m=-21093, n=4556
n=-1898311220
输出结果完全是错误的,这是因为%hd
容纳不下 m 和 n 的值,%d
也容纳不下 n 的值。
读者需要注意,当格式控制符和数据类型不匹配时,编译器会给出警告,提示程序员可能会存在风险。
二进制 八进制 十六进制
二进制 0b 0B 开头
八进制 0开头
十六进制 0x 0X开头
八进制数字和十进制数字不区分大小写,所以格式控制符都用小写形式。如果你比较叛逆,想使用大写形式,那么行为是未定义的,请你慎重:
区分不同进制数字的一个简单办法就是,在输出时带上特定的前缀。在格式控制符中加上#
即可输出前缀,例如 %#x、%#o、%#lX、%#ho 等,请看下面的代码:
- #include <stdio.h>
- int main()
- {
- 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); //以十六进制形式输出(字母大写)
- return 0;
- }
函数
- scanf():和 printf() 类似,scanf() 可以输入多种类型的数据。
- getchar()、getche()、getch():这三个函数都用于输入单个字符。
- gets():获取一行数据,并作为字符串处理。
scanf() 是最灵活、最复杂、最常用的输入函数,但它不能完全取代其他函数,大家都要有所了解。
- #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;
- }
1) getchar()
最容易理解的字符输入函数是 getchar(),它就是scanf("%c", c)
的替代品,除了更加简洁,没有其它优势了;或者说,getchar() 就是 scanf() 的一个简化版本。
下面的代码演示了 getchar() 的用法:
- #include <stdio.h>
- int main()
- {
- char c;
- c = getchar();
- printf("c: %c\n", c);
- return 0;
- }
输入输出的“命门”就在于缓冲区
分配缓冲区就是必不可少的。每次调用读写函数,先将数据放入缓冲区,等数据都准备好了再进行真正的读写操作,这就大大减少了转换的次数。实践证明,合理的缓冲区设置能成倍提高程序性能。缓冲区其实就是一块内存空间,它用在硬件设备和用户程序之间,用来缓存数据,目的是让快速的 CPU 不必等待慢速的输入输出设备,同时减少操作硬件的次数
根据输入输出分为输出缓存和输入缓存
根据内容分为: 全缓存 行缓存 不带缓存。
1) 全缓冲
在这种情况下,当缓冲区被填满以后才进行真正的输入输出操作。缓冲区的大小都有限制的,比如 1KB、4MB 等,数据量达到最大值时就清空缓冲区。
在实际开发中,将数据写入文件后,打开文件并不能立即看到内容,只有清空缓冲区,或者关闭文件,或者关闭程序后,才能在文件中看到内容。这种现象,就是缓冲区在作怪。
2) 行缓冲
在这种情况下,当在输入或者输出的过程中遇到换行符时,才执行真正的输入输出操作。行缓冲的典型代表就是标准输入设备(也即键盘)和标准输出设备(也即显示器)。
C语言标准规定,输入输出缓冲区要具有以下特征:
- 当且仅当输入输出不涉及交互设备时,它们才可以是全缓冲的。
- 错误显示设备不能带有缓冲区。
缓冲区的刷新(清空)
所谓刷新缓冲区,就是将缓冲区中的内容送达到目的地。缓冲区的刷新遵循以下的规则:
- 不管是行缓冲还是全缓冲,缓冲区满时会自动刷新;
- 行缓冲遇到换行符
\n
时会刷新; - 关闭文件时会刷新缓冲区;
- 程序关闭时一般也会刷新缓冲区,这个是由标准库来保障的;
- 使用特定的函数也可以手动刷新缓冲区
缓冲区位于用户程序和硬件设备之间,用来缓存数据,目的是让快速的 CPU 不必等待慢速的输入输出设备,同时减少操作硬件的次数。对于 IO 密集型的网络应用程序,比如网站、数据库、DNS、CDN 等,缓冲区的设计至关重要,它能十倍甚至一百倍得提高程序性能
该如何消除这些负面影响呢?思路其实也很简单,在输入输出之前清空(刷新)缓冲区即可:
- 对于输出操作,清空缓冲区会使得缓冲区中的所有数据立即显示到屏幕上;很明显,这些数据没有地方存放了,只能输出了。
- 对于输入操作,清空缓冲区就是丢弃残留字符,让程序直接等待用户输入,避免引发奇怪的行为
最靠谱、最通用、最有效的清空输入缓冲区的方案就是使用 getchar() 或者 scanf() 将缓冲区中的数据逐个读取出来,其它方案都有或多或少的问题
常用的连字符举例:
[*]%表示读取 abc...xyz 范围内的字符,也即小写字母;
[*]%表示读取 ABC...XYZ 范围内的字符,也即大写字母;
[*]%表示读取 012...789 范围内的字符,也即十进制数字。
你也可以将它们合并起来,例如:
[*]%表示读取大写字母和小写字母,也即所有英文字母;
[*]%表示读取所有的英文字母和十进制数字;
[*]%表示读取十六进制数字。
请看下面的演示:
#include <stdio.h>int main(){ char str[30]; scanf("%", str);//只读取字母 printf("%s\n", str); return 0;[*]}
们完全可以模拟密码输入的效果,请先看下面的代码:
- #include <stdio.h>
- #include <conio.h>
- #include <ctype.h>
- #define PWDLEN 20
- void getpwd(char *pwd, int pwdlen);
- int main(){
- char pwd[PWDLEN+1];
- printf("Input password: ");
- getpwd(pwd, PWDLEN);
- printf("The password is: %s\n", pwd);
- return 0;
- }
- /**
- * 获取用户输入的密码
- * @param pwd char* 保存密码的内存的首地址
- * @param pwdlen int 密码的最大长度
- **/
- void getpwd(char *pwd, int pwdlen){
- char ch = 0;
- int i = 0;
- while(i<pwdlen){
- ch = getch();
- if(ch == '\r'){ //回车结束输入
- printf("\n");
- break;
- }
- if(ch=='\b' && i>0){ //按下删除键
- i--;
- printf("\b \b");
- }else if(isprint(ch)){ //输入可打印字符
- pwd[i] = ch;
- printf("*");
- i++;
- }
- }
- pwd[i] = 0;
- }
运行结果:
Input password: *********
The password is: 123456789
代码中定义了一个函数 getpwd(),它有两个参数:pwd 为保存密码的内存的首地址,pwdlen 为密码的最大长度。
函数通过 while 循环来不断读取用户输入的字符,并逐一对它们进行处理:
- 如果用户按下回车键,表示输入结束了,getch() 将会读取到
\r
; - 如果用户按下删除键,表示删除前面的字符,getch() 将会读取到
\b
; - 如果用户输入可打印字符,那么就读取该字符并回显星号。
while 循环中之所以使用 getch() 来获取字符,是因为该函数既没有回显也没有缓存,可以立即读取到用户输入的字符,并且不会在屏幕上显示出来。
从循环条件 i<pwdlen 可以看出,当用户输入的密码超过最大长度时跳出循环,结束输入。
用户按下回车键时,getchar() 将读取到\n
字符,而 getch() 将读取到\r
字符。也就是说,对于不同的字符输入函数,回车键产生的字符不同,这个细节读者要引起注意。
删除密码的思路
密码保存在字符数组中,当用户按下删除键时,不仅要删除前面的星号,还应该删除字符数组中前面的元素。
需要注意的是,数组中的元素在内存中是连续分布的,无法直接删除。上面代码中,我们并没有对数组元素进行任何操作,而是将变量 i 减 1,跳过要删除的字符。这些本该被删除的字符依然留在数组中,它们会被后续输入的字符覆盖掉。
第29行代码中,我们通过printf("\b \b");
来删除前面的星号。\b
表示退格,也就是光标向后移动一个位置。退格,输出空格,再退格就能删除前面的星号。例如我们输入了密码 123,它在屏幕上显示为:
***_
输出一个退格,光标向前移动一个字符的位置,变为:
***
此时再输出一个空格,星号就会被覆盖掉,变为:
** _
虽然星号被空格替换掉了,但是光标也同时向后移动了一个位置,这样光标和星号之间就有了一个字符的间隔,所以还需要再输出一个退格,消除间隔。再输出一个退格后变为:
**_
函数体最后一行代码也至关重要,它向字符数组中添加了字符串结束符,决定者密码在何处结束
所谓键盘监听,就是用户按下某个键时系统做出相应的处理,本章讲到的输入输出函数也是键盘监听函数的一种,例如 getchar()、getche()、getch() 等。下面的代码演示了 getche() 函数的使用:
- #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;
- }
在 Windows 下的运行结果:
Number: 1 //按下任意键
Number: 2 //按下任意键
Number: 3 //按下任意键
Number: 4 //按下任意键
Number: 5 //按下Esc键退出
这段代码虽然达到了监听键盘的目的,但是每次都必须按下一个键才能执行 getch() 后面的代码,也就是说,getch() 后面的代码被阻塞了。
阻塞式键盘监听用于用户输入时一般没有任何问题,用户输入完数据再执行后面的代码往往也符合逻辑。然而在很多小游戏中,阻塞式键盘监听会带来很大的麻烦,用户要不停按键游戏才能进行,这简直就是灾难,所以在小游戏中一般采用非阻塞式键盘监听:用户输入数据后程序可以捕获,用户不输入数据程序也可以继续执行。
在 Windows 系统中,conio.h
头文件中的kbhit()
函数就可以用来实现非阻塞式键盘监听。
conio.h 是 Windows 下特有的头文件,所以 kbhit() 也只适用于 Windows,不适用于 Linux 和 Mac OS。
用户每按下一个键,都会将对应的字符放到输入缓冲区中,kbhit() 函数会检测缓冲区中是否有数据,如果有的话就返回非 0 值,没有的话就返回 0 值。但是 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;
- }