c语言学习之路

懂一门编程语言对运维工作的推进和能力的提升是巨大的,在本文记录本人的学习之旅,跟大家分享一下。

所有的程序都是运行在内存中的,内存的利率的提升是很重要的。

内存《《《《《《 固态硬盘 《《《《《《《《《《《机械硬盘

 

数据类型

 

int  整数型    short短整数    long   长整数   double  双精度   float 单精度   char   字符类型       void  无类型

 

%d 是整数的形式输出。
%c:输出一个字符。c 是 character 的简写。
%s:输出一个字符串。s 是 string 的简写。
%f:输出一个小数。f 是 float 的简写。

 

例子

  1. #include <stdio.h>
  2. int main()
  3. {
  4. puts(
  5. "C语言中文网,一个学习C语言和C++的网站,他们坚持用工匠的精神来打磨每一套教程。"
  6. "坚持做好一件事情,做到极致,让自己感动,让用户心动,这就是足以传世的作品!"
  7. "C语言中文网的网址是:http://c.biancheng.net"
  8. );
  9. return 0;
  10. }

多个字符串输出可以用这种格式。

 

\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 操作符,如下所示:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. short a = 10;
  5. int b = 100;
  6. int short_length = sizeof a;
  7. int int_length = sizeof(b);
  8. int long_length = sizeof(long);
  9. int char_length = sizeof(char);
  10. printf("short=%d, int=%d, long=%d, char=%d\n", short_length, int_length, long_length, char_length);
  11. return 0;
  12. }

在 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 的简写。


下面的例子演示了不同整型的输出:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. short a = 10;
  5. int b = 100;
  6. long c = 9437;
  7. printf("a=%hd, b=%d, c=%ld\n", a, b, c);
  8. return 0;
  9. }

运行结果:
a=10, b=100, c=9437

在编写代码的过程中,我建议将格式控制符和数据类型严格对应起来,养成良好的编程习惯。当然,如果你不严格对应,一般也不会导致错误,例如,很多初学者都使用%d输出所有的整数类型,请看下面的例子:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. short a = 10;
  5. int b = 100;
  6. long c = 9437;
  7. printf("a=%d, b=%d, c=%d\n", a, b, c);
  8. return 0;
  9. }

运行结果仍然是:
a=10, b=100, c=9437

当使用%d输出 short,或者使用%ld输出 short、int 时,不管值有多大,都不会发生错误,因为格式控制符足够容纳这些值。

当使用%hd输出 int、long,或者使用%d输出 long 时,如果要输出的值比较小(就像上面的情况),一般也不会发生错误,如果要输出的值比较大,就很有可能发生错误,例如:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int m = 306587;
  5. long n = 28166459852;
  6. printf("m=%hd, n=%hd\n", m, n);
  7. printf("n=%d\n", n);
  8. return 0;
  9. }

在 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 等,请看下面的代码:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. short a = 0b1010110; //二进制数字
  5. int b = 02713; //八进制数字
  6. long c = 0X1DAB83; //十六进制数字
  7. printf("a=%#ho, b=%#o, c=%#lo\n", a, b, c); //以八进制形似输出
  8. printf("a=%hd, b=%d, c=%ld\n", a, b, c); //以十进制形式输出
  9. printf("a=%#hx, b=%#x, c=%#lx\n", a, b, c); //以十六进制形式输出(字母小写)
  10. printf("a=%#hX, b=%#X, c=%#lX\n", a, b, c); //以十六进制形式输出(字母大写)
  11. return 0;
  12. }

 

 

函数

  • scanf():和 printf() 类似,scanf() 可以输入多种类型的数据。
  • getchar()、getche()、getch():这三个函数都用于输入单个字符。
  • gets():获取一行数据,并作为字符串处理。


scanf() 是最灵活、最复杂、最常用的输入函数,但它不能完全取代其他函数,大家都要有所了解。

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int a = 0, b = 0, c = 0, d = 0;
  5. scanf("%d", &a); //输入整数并赋值给变量a
  6. scanf("%d", &b); //输入整数并赋值给变量b
  7. printf("a+b=%d\n", a+b); //计算a+b的值并输出
  8. scanf("%d %d", &c, &d); //输入两个整数并分别赋值给c、d
  9. printf("c*d=%d\n", c*d); //计算c*d的值并输出
  10. return 0;
  11. }

 

1) getchar()

最容易理解的字符输入函数是 getchar(),它就是scanf("%c", c)的替代品,除了更加简洁,没有其它优势了;或者说,getchar() 就是 scanf() 的一个简化版本。

下面的代码演示了 getchar() 的用法:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. char c;
  5. c = getchar();
  6. printf("c: %c\n", c);
  7. return 0;
  8. }

 

输入输出的“命门”就在于缓冲区

分配缓冲区就是必不可少的。每次调用读写函数,先将数据放入缓冲区,等数据都准备好了再进行真正的读写操作,这就大大减少了转换的次数。实践证明,合理的缓冲区设置能成倍提高程序性能。缓冲区其实就是一块内存空间,它用在硬件设备和用户程序之间,用来缓存数据,目的是让快速的 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;[*]}

 

 

们完全可以模拟密码输入的效果,请先看下面的代码:

  1. #include <stdio.h>
  2. #include <conio.h>
  3. #include <ctype.h>
  4. #define PWDLEN 20
  5. void getpwd(char *pwdint pwdlen);
  6. int main(){
  7. char pwd[PWDLEN+1];
  8. printf("Input password: ");
  9. getpwd(pwd, PWDLEN);
  10. printf("The password is: %s\n", pwd);
  11. return 0;
  12. }
  13. /**
  14. * 获取用户输入的密码
  15. @param pwd char* 保存密码的内存的首地址
  16. @param pwdlen int 密码的最大长度
  17. **/
  18. void getpwd(char *pwdint pwdlen){
  19. char ch 0;
  20. int i 0;
  21. while(i<pwdlen){
  22. ch getch();
  23. if(ch == '\r')//回车结束输入
  24. printf("\n");
  25. break;
  26. }
  27. if(ch=='\b&& i>0)//按下删除键
  28. i--;
  29. printf("\b \b");
  30. }else if(isprint(ch))//输入可打印字符
  31. pwd[i= ch;
  32. printf("*");
  33. i++;
  34. }
  35. }
  36. pwd[i0;
  37. }

运行结果:
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() 函数的使用:

 
  1. #include <stdio.h>
  2. #include <conio.h>
  3. int main(){
  4. char ch;
  5. int i = 0;
  6. //循环监听,直到按Esc键退出
  7. while(ch = getch()){
  8. if(ch == 27){
  9. break;
  10. }else{
  11. printf("Number: %d\n", ++i);
  12. }
  13. }
  14. return 0;
  15. }

在 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() 不会读取数据,数据仍然留在缓冲区,所以一般情况下我们还要结合输入函数将缓冲区种的数据读出。请看下面的例子:

 
  1. #include <stdio.h>
  2. #include <windows.h>
  3. #include <conio.h>
  4. int main(){
  5. char ch;
  6. int i = 0;
  7. //循环监听,直到按Esc键退出
  8. while(1){
  9. if(kbhit()){ //检测缓冲区中是否有数据
  10. ch = getch(); //将缓冲区中的数据以字符的形式读出
  11. if(ch == 27){
  12. break;
  13. }
  14. }
  15. printf("Number: %d\n", ++i);
  16. Sleep(1000); //暂停1秒
  17. }
  18. return 0;
  19. }
posted @ 2021-04-06 12:52  woaibaobei  阅读(102)  评论(0编辑  收藏  举报