且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门

更新时间:2022-03-08 01:53:15

点击查看第一章
点击查看第三章

第2章

C语言快速入门

2.1 C语言的语法特点

C语言是一门语法精简的语言,它的关键字仅有32个,C语言以main函数为主函数,程序编译运行之后,执行的就是main函数的内容,因此,纵观C语言的很多程序,就会发现它们形成了一道有趣的风景线:头文件和C代码文件以main函数为中心构造,在main函数中调用这些文件中编写的代码,引用头文件。C语言程序实质上就是在程序中调用 C标准库提供的函数、其他C库提供的函数、操作系统提供的API接口、自己定义的函数,同时应用适当的数据结构和算法来完成工作。C语言主要包含如下关键字。

  • auto:声明自动变量。
  • short:声明短整型变量或函数。
  • int: 声明整型变量或函数。
  • long:声明长整型变量或函数。
  • float:声明浮点型变量或函数。
  • double:声明双精度变量或函数。
  • char:声明字符型变量或函数。
  • struct:声明结构体变量或函数。
  • union:声明共用数据类型。
  • enum:声明枚举类型。
  • typedef:用以为数据类型取别名。
  • const:声明只读变量。
  • unsigned:声明无符号类型变量或函数。
  • signed:声明有符号类型变量或函数。
  • extern:声明变量是在其他文件中声明的。
  • register:声明寄存器变量。
  • static:声明静态变量。
  • volatile:声明变量在程序执行中可被隐含地改变。
  • void:声明函数无返回值或无参数,声明无类型指针。
  • if:条件语句。
  • else:条件语句否定分支(与if连用)。
  • switch:用于开关语句
  • case:开关语句分支。
  • for:一种循环语句。
  • do:循环语句的循环体。
  • while:循环语句的循环条件。
  • goto:无条件跳转语句。
  • continue:结束当前循环,开始下一轮循环。
  • break:跳出当前循环。
  • default:开关语句中的“其他”分支。
  • sizeof:计算数据类型长度。
  • return:子程序返回语句(可以带参数,也可以不带参数)循环条件。

C语言虽然精简,但功能却很强大,其不但能够完成比它更复杂的程序语言所做的事情,而且还能做其他语言不擅长的工作,比如,MySQL(当今世界最流行的开源关系型数据库管理系统)、Nginx(高性能的 HTTP 和 反向代理服务器)、SQLite(嵌入式的轻型数据库)、GNOME桌面(通常运行在Linux/UNIX系统下的桌面系统)、OpenCV(跨平台计算机视觉库)等都是C语言的杰作,尤其是在操作系统内核的设计与研发领域,它的“兄弟”C++也不是对手(目前还没有出现一款流行于世的C++制作的操作系统内核)。

2.2 猜数字游戏

本节将应用C语言制作猜数字游戏,以帮助读者快速复习C语言的基础知识。猜数字游戏的规则具体如下:输入一个1-500以内的正整数,程序根据玩家输入的数字,提示该数字比正确答案大,或者比正确答案小,如果等于正确答案就提示猜中了。比如,要猜的数字是85,玩家第一次输入90,则提示比要猜的数字大,第二次输入80,则提示比要猜的数字小,第三次输入85,则提示猜中了。下面就来分步讲解整个游戏的制作过程。

2.2.1 编写输入数字的C代码

首先,参照第1章介绍的编辑C程序的方法,编写源代码文件2-1.c,代码如程序2-1所示:

带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门

接着,使用PuTTY等SSH客户端登录Ubuntu后,在终端编译2-1.c:

$ gcc 2-1.c -o myguess

最后,运行该程序进行验证,程序接受一个数字输入后,将输入的数字输出到屏幕中,代码如下:

$ ./myguess
你好,请输入一个数字:55
你输入的数字是:55

纵观程序2-1及其执行结果,可以发现,C语言使用“;”作为语句的结尾;可使用printf函数完成屏幕输出,在输出时可使用“n”表示换行符;使用scanf函数从键盘中接受指定格式的数据录入,其中“%d”表示整数格式,scanf的第2个参数是输入的变量的地址(即&mynum,其中“&”是取地址符)。

2.2.2 限制输入数字的范围

游戏要求输入1-500以内的整数,但在运行程序2-1时,输入900、-10等不符合要求的数字,仍然能够通过。比如,在下面所示的运行结果中,900和-10均通过了输入程序的测试:

$ ./myguess
你好,请输入一个数字:900
你输入的数字是:900
你好,请输入一个数字:-10
你输入的数字是:-10

本步骤的目标就是让程序拒绝接受不合法的数字,并提示玩家重新输入,因此,需要修改程序,限制玩家输入数字的范围,修改的代码如程序2-2所示:

带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门

编译并运行程序2-2,从下述运行效果来看,错误的数字并没有被接受,但要想重新输入,必须再运行一次程序:

$ gcc -o myguess 2-2.c
$ ./mynum
你好,请输入一个数字:988
数字仅限于1-500之间,请重新运行本程序!

程序2-2使用了C语言的“if...else...”条件语句,这是很多语言都有的一个机制(包括一些函数语言,比如Haskell的“if...then...else....”),“if…else…”条件语句分为两个部分:第1个部分是if语句段,表示如果if后面所跟的条件满足要求的话,则执行if语句段; 第2个部分是else语句段,表示如果if后的条件不满足要求时执行的语句段。
什么是条件满足与不满足呢?C语言可理解为:如果条件的返回值非0则表示条件满足,如果是0则表示条件不满足。
可以将多个条件组合成一个综合条件作为“if...else...”条件语句的条件,方式是使用“||”(表示或者)或“&&”(表示并且),比如,程序2-2的条件是“mynum>500 ||mynum<1”。
用非0与0来判断条件的真假让C语言的条件语句具备较强的灵活性,但是这会带来一个困扰:在C语言条件语句中,NULL和0的值是一样的,而NULL常用于指针和对象,0常用于int等整型数,这就意味着,如果出现了类似下面的语句块,则是对含有指针变量的条件进行判断。例如,在下面这种形式的代码中,mypoint指向了其他变量的内存地址,如果指针变量mypoint指向的地址为NULL,则表示mypoint指向的地址是无效的,否则是有效的。示例代码如下:
if (mypoint!=NULL){
............//指针指向内容有效时执行的语句块
}
else{
............ .//指针指向内容无效时执行的语句块
}
也可以在条件中使用“!”(表示逻辑非),进一步简化对mypoint是否有效的判断,示例代码如下:
if (!mypoint){
............//指针指向内容有效时执行的语句块
}
else{
............ .//指针指向内容无效时执行的语句块
}

2.2.3 引入循环机制,允许重新输入

循环是计算机科学运算领域的用语,也是一种常见的控制流程,循环是指一段代码在程序中只出现一次,但可能会连续运行多次的语句。通过C语言的循环语句,程序2-3实现了在玩家输入错误数字的情况下,可再次输入,而不是直接退出程序的功能:

带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门

编译并运行程序2-3,当输入的数字不合法时(分别输入了1234、-12进行测试),程序没有接受玩家输入,而是提示重新输入数字,运行结果如下:

$ gcc 2-3.c -o myguess
$ ./myguess
你好,请输入一个数字:1234
数字仅限于1-500之间
你好,请输入一个数字:-12
数字仅限于1-500之间
你好,请输入一个数字:88
你输入的数字是:88

程序2-3中使用了C语言的while循环来实现玩家反复输入数字,直到输入的数字在1-500之内才退出循环的功能。
while循环的语法格式如下:
while(条件){
语句块
}
此外,程序2-3在while的条件“while (!ispass)”中使用了逻辑“!”,简化了程序。
C语言还有另一种循环形式“do...while...”循环,它的语法格式如下(不要忘记在最后一行的条件后加上“;”):
do{
语句块
}while (条件);
用“do…while...”循环对程序2-3进行修改,修改后的代码如程序2-4所示:

带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门

编译并运行程序2-4,下面是运行结果,当输入8889时,程序会提示数字不合法,并让玩家重新输入:

$gcc 2-4.c -o myguess
$ ./myguess
你好,请输入一个数字:8889
数字仅限于1-500之间
你好,请输入一个数字:12
你输入的数字是:12

程序2-3、程序2-4使用了变量ispass作为是否退出循环的依据,如果ispasss不为真,则表示玩家输入的数字不合法,需要继续输入,否则退出循环。
除了使用ispass这类变量控制C语言循环之外,还可以直接通过break语句退出循环(注意,只能退出break语句本身所在的那层循环)。通过break语句的使用,程序2-5完成了与程序2-3、程序2-4同样的功能,程序代码如下:

带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门

编译运行程序2-5,从下面的运行结果可以看出,程序2-5与程序2-3以及程序2-4的功能一样,程序在玩家输入错误的情况下,提示玩家重新输入,具体如下:

$gcc 2-5.c -o myguess
$ ./myguess
你好,请输入一个数字:8788
数字仅限于1-500之间
你好,请输入一个数字:66
你输入的数字是:66

2.2.4 产生1~500以内的随机整数

为增加游戏的趣味性,编写代码产生1~500以内的随机整数,并将这个整数作为被猜数字,这样玩家每次运行游戏,需要猜的都是不同的数字。为保证需要猜的整数在1~500之间,需要按如下方式对随机整数进行加工(“%”为取余操作符):
1~500以内的被猜数字=随机整数%499+1
借助stdlib.h中定义的srand函数来生成公式右边所需要的随机数,该函数需要一个数值作为产生随机数的种子(也就是这个函数的唯一参数),通常使用当前时间值作为参数,当前时间值可以通过time函数(以0作为参数调用,该函数定义于time.h中)生成。程序2-6通过srand函数生成随机数,代码如下:

带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门

编译并运行程序2-6,运行结果表示,程序产生了1~500以内的2个随机整数429与44:

$gcc a.c -o mytest
$./mytest
第一个随机数:429 第二个随机数:44

可将程序2-6中的代码稍做修改,与程序2-5结合,将程序2-6中产生随机数的代码定义为函数getnumber,以供main函数调用,最终代码如程序2-7所示:

带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门

编译并运行程序2-7,观察以下运行结果,玩家猜测数字为55,最后一行输出了被猜的数字为109。

$ gcc guessnum.c -o myguess
$ ./myguess
你好,请输入一个数字:55
你输入的数字是:55
number:109

为了验证随机数效果,程序2-7中最后一个printf语句获取到要猜的随机整数,并输出到屏幕,但游戏中不能把结果告诉玩家,因此,接下来需要继续完善程序2-7,加入更多的功能。

2.2.5 反复接收玩家输入,直到猜中数字为止

C程序通过“if…else if…else…”语句块来实现条件语句的组合,该组合中包含有多个不同的条件,可用于定义满足各个条件时执行的代码块。语句块格式如下:
if (条件1){
.........//条件1满足时执行的代码块
}
else if(条件2){
.......... //条件2满足时执行的代码块
}
..........
else if(条件n){
......... //条件n满足时执行的代码块
}
else{
......... //以上所有条件均不满足时执行的代码块
}
在程序2-7中增加条件语句组合,改进猜数字游戏,实现玩家输入的数字与被猜数字的比较,并根据比较结果为玩家提示数字大了或小了的信息,当玩家输入的数字与被猜数字相同时,提示玩家猜中了,修改后的代码如程序2-8所示:

带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门

编译并运行程序2-8,玩家输入数字之后,程序提示输入的数字与被猜的数字相比是大了还是小了,进行几次尝试之后,玩家成功猜中数字,程序提示“祝贺您,您猜中了!”,运行结果如下所示:

$ gcc guessnum.c -o myguess
$ ./myguess
你好,请输入一个数字:55
你输入的数字是:55
数字小了!
你好,请输入一个数字:280
你输入的数字是:280
数字小了!
你好,请输入一个数字:350
你输入的数字是:350
数字小了!
你好,请输入一个数字:400
你输入的数字是:400
数字小了!
你好,请输入一个数字:488
你输入的数字是:488
数字大了!
你好,请输入一个数字:420
你输入的数字是:420
数字小了!
你好,请输入一个数字:450
你输入的数字是:450
数字大了!
你好,请输入一个数字:440
你输入的数字是:440
数字大了!
你好,请输入一个数字:430
你输入的数字是:430
祝贺您,您猜中了!

2.2.6 自动猜数算法

能不能让电脑程序拥有智能,让程序来猜数字呢?肯定可以,只需要编写C程序实现某种算法即可。在国内,算法最早出现在《周髀算经》 《九章算术》之中;在国外,古希腊数学家欧几里得(Euclid,约公元前325年—公元前265年)出版了《几何原本》,闻名于世。人类历史上第一次将算法编写为程序的是Ada Byron,其于1842年为巴贝奇分析机编写求解伯努利微分方程的程序,Ada Byronl也因此被大多数人认为是世界上第一位程序员。
算法的核心是创建问题抽象的模型和明确求解目标,之后可以根据具体的问题选择不同的模式和方法完成算法的设计。为了能让程序实现自动猜数,必须假设一个前提:程序不知道要猜的数字,也就是说这个算法中只能与要猜的数字进行比较,而不能直接“知道”要猜的数字值。分析算法目标,可使用类似于折半查找法的算法,折半查找法又称二分查找法,是一种在有序数组中查找某一特定元素的搜索算法。查找过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从新的中间元素开始进行比较。如果在某一步骤中数组为空,则代表找不到。这种搜索算法每进行一次比较都会使搜索范围缩小一半。
例如,在一个升序排列的数字列表2、6、7、34、76、123、234、567、677、986中查找数字123所处的位置,算法过程具体如下:首先,将first(下限)指向最小数字2的位置,last(上限)指向最大的数字986的位置,得到first的值为1,而last的值为10,计算位于它们的mid(中间位置)为6,数字为76;然后将76与123进行比较,发现123比76大,于是将first设为mid之后的位置(即6),last不变,按与上一步同样的方法,计算first与last的mid为8,将last设为中间位置的前一位置;最后,再次计算新的mid,直到mid处的值等于123为止,这样就能成功找到123的位置,处于数字列表的第6个位置。整个过程如图2-1所示。
折半查找算法查找的范围每次缩小一半,因此查找效率较高,我们可以借鉴这个思想,设计自动猜数算法:当输入一个数字时,会得到一个反馈,输入的数字相对被猜数字是大了还是小了,将被猜数字作为查找目标,将1到输入数字的范围作为查找范围,实现自动猜数。如果输入数字大了,就将输入数字作为查找范围的上限,如果输入数字小了,就将输入数字作为查找范围的下限,每输入一次数字,就缩小了查找范围的一半,这样很快就能猜中,赢得游戏的胜利,算法过程具体如下。

带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门

1)设数字范围R为1~500。
2)取范围R以内的中间值A,把A作为程序模仿人类猜测出的数字。
3)将猜测的数字A与被猜的结果B进行比较
a)如果A>B,则将R的上限设为A,回到第2步。
b)如果A<B,则将R的下限设为A,回到第2步。
c)如果A=B,则退出程序,提示猜中数字,进入第4步。
4)在屏幕上输出A和B,并提示猜中数字。

带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门
带你读《C指针原理揭秘:基于底层实现机制》之二:C语言快速入门

编译后程序的运行结果如下,仅仅7次,程序就猜出了数字:

$gcc guessnum.c -o myguess
$ ./myguess
程序猜的数字为250,数字大了!
程序猜的数字为125,数字小了!
程序猜的数字为187,数字大了!
程序猜的数字为156,数字小了!
程序猜的数字为171,数字大了!
程序猜的数字为163,数字小了!
程序猜的数字为167,被猜的数字为167,猜中了!

程序2-9中使用了C语言的数组概念,数组的定义如下所示:

类型 数组名[数组长度]={用逗号分隔的数组初始值}

程序2-9中将myrange变量(表示猜数范围)定义为一个包含2个元素(第1个元素1表示下限,500表示上限)的数组,如下面代码所示:

int myrange[2]={1,500};

数组元素引用的方式是数组名[数组索引],其中数组索引从0开始。例如,程序2-10计算猜数时,通过数组索引形式取得myrange数组的上限与下限后,计算它们之间的平均值取得猜数范围内的中间值,如下面代码所示:

mynum=(myrange[0]+myrange[1])/2;

2.3 小结

C语言是一门语法精简且功能强大的语言,其关键字仅有32个,它以main函数为中心,不但可以完成普通程序语言能做的工作,而且还可以做其他语言不擅长的工作,比如Linux内核、SQLite嵌入式数据库,等等。本章以猜数字游戏制作为例,依次介绍了基本输入输出、条件语句、循环语句、随机数生成等基本语法,最后以二分查找算法为基础,讲解了自动猜数算法,以帮助读者快速学习C语言知识以及算法基础。