文章目录
7. 避免重复的好办法——函数
还记得数学中的函数吗?
给定两个实数集 D D D和 M M M,若有对应法则 f f f,使对每一个 x ∈ D x\in D x∈D,都有唯一的 y ∈ M y\in M y∈M与它相对应,则称 f f f是定义在数集 D D D上的函数,记作: f : D → M , f:D\rightarrow M, f:D→M,数集 D D D称为函数 f f f的定义域, x x x所对应的 y y y称为 f f f在点 x x x处的函数值,常记为 f ( x ) f(x) f(x).全体函数值的集合 f ( D ) = { y ∣ y = f ( x ) , x ∈ D } ( ⊂ M ) f(D) = \{y|y=f(x),x \in D\}(\subset M) f(D)={y∣y=f(x),x∈D}(⊂M)称为 f f f的值域.1
函数的定义阐释了这样一个过程:通过对应法则 f f f将 x x x对应到某一个确定的 y y y,在此引用一下高中数学老师的比喻: f f f就像是一个加工机, x x x通过这个加工机之后得到了我们需要的 y y y。这个例子对于解释编程语言中的函数来说是相当好的,我们向函数传入某些参数,然后函数通过内部的一些运算操作最终输出我们需要的内容。
(1).为什么函数可以避免重复?
简要回答就是,函数可以将很多具体的过程抽象成为一个过程,举个例子,我们在之前说到的猜数游戏中有这么一个步骤——将输入的数字与a进行比较,最终输出一个文本,告知玩家猜测的结果如何:
if (input > a) {
printf("%d is bigger than a\n", input);
}
else if (input < a) {
printf("%d is smaller than a\n", input);
}
else {
printf("You're right!\n");
printf("a = %d\n", a);
success = 1;
break; // 跳出循环,之后会说
}
那么假设说,我们把这个猜数的判定抽象为一个函数:check(a, input),然后我们只要向check传入a和input,它就会帮助我们完成这些判断和输出结果的过程,那我们可以把猜数游戏简化为以下的样子 (伪代码):
#include <stdio.h>
int main()
{
int a = 10, input = 0;
do {
scanf("%d", &input); // 每次循环都猜一次数字
} while(check(a, input) 不正确); // 如果check(a, input)不正确就继续循环
return 0;
}
判断猜没猜对这样的步骤看起来并不复杂,只是因为情况不多且整个程序的代码量不大,如果之后你的程序中涉及到多层的判定,可能就需要一层一层嵌套的去写条件语句, 这样看样子很麻烦呢!所以让我们看看下面这个例子。
(2).回文素数
求出 A A A到 B B B之间所有回文素数的个数。
单点时限: 1.0 sec
内存限制: 512 MB
输入格式:
第一行,一个正整数 A A A;
第二行,一个正整数 B B B。
其中 1 ≤ A ≤ B ≤ 1 0 9 1\leq A \leq B \leq 10^9 1≤A≤B≤109。
输出格式
一行,一个正整数,表示 [ A , B ] [A,B] [A,B]范围内的回文素数个数。
回文素数:既是素数又是回文数的数即为回文素数。2
我们不细究这道题究竟应该怎么做,因为单点时限为1秒,而且测试样例中有一个 A = 1 , B = 1 0 9 A = 1,B = 10^9 A=1,B=109的情况,用一般的思路无法解决(聪明的你可能知道,用埃拉托色尼筛法打表是很快的,不过经过我测试, 1 0 9 10^9 109以内的素数通过埃筛大概需要22秒左右,通过其他优化可能可以更快,如果你能想到办法,可以在评论区一起讨论一下哦),我们只研究判断回文素数这个过程。
我们可以把判断回文素数的过程拆解为先判断是否为素数,再判断是否为回文数这样两个过程。
#1.判定一个数是否为素数:
素数是除了自身和1以外没有其他因子的数字,那么很自然我们可以想到一个办法来判断一个数字n是不是素数:首先1肯定是因子,除2以外,如果从 2 → n − 1 2 \to n-1 2→n−1之中存在一个数字 i i i为 n n n的因子,那 n n n就肯定不是素数了,对于是否为因子的这个事情,我们可以利用取余运算符判断: i i i为 n n n的因子 ⇔ \Leftrightarrow ⇔ n n n可以整除 i i i ⇔ \Leftrightarrow ⇔ n n n对 i i i取余等于0,所以判定条件也出现了:n % i == 0,这样就可以利用循环和条件语句进行判断了:
#include <stdio.h>
int main()
{
int a = 0, check = 1;
scanf("%d", &a);
if (a <= 1) {
printf("%d is not a prime number\n", a);
}
else if (a == 2) {
printf("%d is a prime number\n", a);
}
else {
for (int i = 2; i < a-1; i++) {
if (a % i == 0) {
printf("%d is not a prime number\n", a);
check = 0;
break;
}
}
if (check) {
printf("%d is a prime number\n", a);
}
}
return 0;
}
它很好地完成了我们需要的功能,不过有这么一个问题,从2遍历到n-1会不会有点太慢了呢?假设对于一个合数
n
n
n,一定存在一对因子
a
,
b
且
a
≤
b
a, b且a \leq b
a,b且a≤b,有
n
=
a
×
b
n = a\times b
n=a×b,假设我们按照i从2到n-1遍历,那么第一次i遍历到a,可以判断a为n的一个因子,如果继续遍历下去,i会遍历到b。
但是有一个问题,a和b是一对因子,只要找到了a,那么b也是一定能通过
n
÷
a
n \div a
n÷a找到,所以这告诉我们一个很重要的事情:如果一个数字有非1与其本身的因子,那么在某个数字
k
k
k之前一定可以找到这个数字的因子,所以我们只要找到这个数字
k
k
k,然后i从2遍历到它,如果中间不出现因子,那这个数字
n
n
n就一定是素数了。那么问题来了:
k
=
?
\large k = ?
k=?
前面的条件中有
a
,
b
且
a
≤
b
a, b且a \leq b
a,b且a≤b为一对因子,
a
和
b
a和b
a和b的极限条件是
a
=
b
a = b
a=b,所以当
a
=
b
a = b
a=b(如下图)时,
n
=
a
2
⇔
a
=
b
=
n
,
\large n = a^2 \Leftrightarrow a = b = \sqrt{n},
n=a2⇔a=b=n,
而
a
<
b
a < b
a<b的情况下,也一定满足
a
<
n
<
b
a < \sqrt{n} < b
a<n<b成立,因此,我们便找到了:
k
=
[
n
]
+
1
\large k = [\sqrt{n}] + 1
k=[n]+1
因为循环变量是i,而且因子的极限为
n
\sqrt{n}
n,所以我们取证后还要再加一,避免漏掉情况,因此以后,我们只需要让i从2遍历到
[
n
]
+
1
[\sqrt{n}] + 1
[n]+1就可以了!对于素数,相当于少了
n
−
k
n - k
n−k次搜索,效率的提升是相当明显的。
#2.判定一个数是否为回文数:
上面说的有点多哈,不过我相信你能从中学到一些什么的,接下来我们来思考怎么判断回文数。
例如对于这样一个数字:
1122112211
\large 1122112211
1122112211
我们从两头开始看,1和1对应,消掉,变成
12211221
12211221
12211221,再消掉头和尾
221122
221122
221122,一直这么循环消去头和尾,最终就什么都不剩了,那么它就是回文数了,或者对于:
11221912211
\large 11221912211
11221912211
一直完成消去头和尾的过程,最终剩了一个
9
9
9,我们可以判定它也是回文数,所以聪明的你肯定已经想到怎么做了吧:
#include <stdio.h>
#include <math.h> // 要包含另一个头文件math.h
int main()
{
int input = 0;
scanf("%d", &input);
int cnt = 0, temp = input;
while (temp != 0) {
temp /= 10;
cnt++; // 先数数这个数字有多少位
}
temp = input;
int head = 0, tail = 0, check = 1;
while (temp > 9) {
head = temp / (int)pow(10, cnt-1); // 提取输入数字的最高位
// pow(a, b)用于求a的b次方,之后会提到
tail = temp % 10; // 提取输入数字的最低位
if (head != tail) {
check = 0;
break;
}
temp %= (int)pow(10, cnt-1); // 去掉最高位
temp /= 10; // 去掉最低位
cnt -= 2; // 去完之后总位数减2
}
if (check) {
printf("%d is a palindrome number!\n", input);
}
return 0;
}
不过这样的方法是有局限性的,因为int类型的值最大只能到 2 , 147 , 483 , 647 2,147,483,647 2,147,483,647,即便是无符号超长整型long long也只能达到 18 , 446 , 744 , 073 , 709 , 551 , 615 18,446,744,073,709,551,615 18,446,744,073,709,551,615。既然我们只需要两边依次向里夹,对应位置的数字相等即可,那么就可以有一个更加普适的方法:我们可能不需要将它保存为一个数字,而是一个字符串,不过限于当前的知识,我就不提了。(如果你学过python的话,你应该可以想到一个非常巧妙的办法——把字符串翻转一下)
#3.把他们综合起来
等一下,不会你真的想把这两个主体代码都有20多行的判断通过if-else连接起来吧,现在你能理解我之前说的情况不多且整个程序的代码量不大是什么意思了吧,如果有这么两个函数isPrime(int x)和isPalindrome(int x),就可以把代码写成这样了:
if (isPrime(x)) {
if (isPalindrome(x)) {
printf("%d\n", x);
}
}
看起来是不是又清爽又简洁呢?这便是函数可以发挥的作用。那么接下来,就让我们正式介绍一下C语言中的函数吧!
(3).函数的基本结构
在C语言中,一个函数的基本构造如下:
[FTypeName] FunctionName([A1TypeName] Argument1, [A2TypeName] Argument2, ...)
{
// Some Operation steps
return TheResult;
}
-
[FTypeName] 是函数的返回值的类型,可以是我们之前所说的各种数据类型,如果不返回任何内容,也可以使用void作为其类型
-
FunctionName 是函数的名字,函数的命名有以下要求:
(1).只能以字母/下划线开始
(2).不能以数字开始
(3).不允许使用关键字(保留字) -
[A1TypeName] Argument1 构成了函数的一个参数,[A1TypeName]为该参数的类型,Argument1则为它的名字,它是形式上的参数,可以提醒使用函数的我们应该传入什么类型的参数,除此之外,写函数的时候我们不能完全确定使用时传入的具体参数是什么,因此如果要涉及对参数进行操作,就一定要一个形式上的参数Argument用于操作,等到真正调用的时候,传入的参数也会按照对形式参数Argument的操作,对具体参数进行操作。
-
函数名后的圆括号内写的许多形式参数构成了参数表,参数表是任意的,完全由编写它的程序员指定,如果圆括号为空,则代表我们不知道这个函数是否有参数,它可能有,也可能没有,如果确定没有参数时,应该在圆括号内写一个void,表示该函数不需要任何传入参数。
-
return TheResult; 是函数的最后一步,它会返回一个运算的结果,记得要保证返回值的类型与函数的类型一致,否则就会出现编译错误,如果函数类型为void,那么可以不写return语句,也可以写return;。return语句的另一个重点是,一个函数如果执行了return语句,这一次调用函数就会立刻结束,即便是return之后还有语句,也不会再执行了。
以上是函数的基本构造,还有一个要注意的点是,传入参数的过程仅仅传入了参数的值,并没有将真正的参数传入,换而言之,想要通过函数修改一个变量的值,仅仅传入变量作为参数是做不到的,我们可以看看下面这个例子:
#include <stdio.h>
void changeValue(int x)
{
x = 2;
printf("In changeValue function, x = %d\n", x);
}
int main()
{
int x = 3;
changeValue(x);
printf("In main function, x = %d\n", x);
return 0;
}
我们发现,x的值在调用了changeValue函数之后依旧没有发生改变,如果你真的想要修改x的值,就需要通过指针来完成,在C++中,我们还可以通过引用的方式做到,但是现在我们先不提这个事情,我们可以把changeValue这个函数改造成如下的样子,换个方式完成修改x的工作:
#include <stdio.h>
int changeValue(int x)
{
x = 2;
printf("In changeValue function, x = %d\n", x);
return x;
}
int main()
{
int x = 3;
x = changeValue(x);
printf("In main function, x = %d\n", x);
return 0;
}
我们将改变后的x的值返回,再赋值给x,就完成了对x值的修改。
(4).来看看我们的老朋友(main函数)
相信一开始学习C语言的时候,打印一个Hello, world!出来都要先写那么一大堆东西,真的是有点难以理解呢!我们看完了C语言中函数的基本结构之后会发现:
int main()
{
...
return 0;
}
老朋友main也是符合函数的结构的,所以说,我们最开始写的这个main(),也同样是一个函数,main函数是作为C/C++程序的入口出现的,一个需要运行的程序就必须要有main函数,不信的话你可以试试看下面这个代码:
#include <stdio.h>
int x = 0;
这回黑框框都弹不出来了,说undefined reference to ‘WinMain’,也就是说,我们没有main函数是没有办法执行的。
而我们的老朋友也有一些你所不知道的秘密,其实main函数也是可以有参数表的,不像是我们一开始写的一个空参数表,它的真正模样如下:
int main(int argc, char* argv[]);
这两个参数具体的含义可能会有点难以理解,在讲完了字符串、数组等内容后,我会再具体展开,在这之前,大家喜欢怎么书写main只要能够编译通过,都是可以的。
还有一件事:return 0; 有的时候我们写代码到最后可能会发现,会出现这么一段文本“… with return value 0 ”,这个return value 0就是我们在main函数最后写的return 0; 如果main函数正常执行了,就会出现这个值,这说明我们没有出现什么严重的问题,但如果以后的某天你写的时候发现文本变成了“… with return value 一个非零的值”,除非是你自己改过return的值,否则肯定是程序出现了什么问题。
(5).又臭又长的函数定义(定义与声明)
小明是一个刚学习编程的人,他刚刚学习了函数的基本构造,于是写出了一大堆的函数,他的代码如下(具体定义内容就略去了):
#include <stdio.h>
int f1(...) {
...
...
...
...
return ...;
}
int f2(...) {
...
...
...
...
return ...;
}
... (此处略去100个如上函数)
int main()
{
...
return 0;
}
函数的出现的确使得main函数里的代码更加简明了,但是由此产生了一个问题,函数的定义需要定义在main函数之前,否则的话就没有办法在main里面调用了,如果函数很多的话,main就会跑到很下面去,这样有点不太方便了。所以,与变量类似,函数也可以先声明再定义:
#include <stdio.h>
int f1(...);
int f2(...);
...
int main(int argc, char* argv[])
{
...
return 0;
}
int f1(...)
{
...
}
int f2(...)
{
...
}
...
这样一来,main函数就可以保持在很上面了,像是int f1(…);这样只指出了函数的类型,函数名和参数表的行为叫做函数的声明,这使我们可以调用这个函数而不出错,而声明出的内容,我们称为函数原型,函数原型必须与函数定义配套出现。之后,必须要定义这个函数,否则调用的时候还是会出现编译错误,定义的时候就如同之前定义在main函数之前一样,遵循函数的结构,但记住,第一行的函数类型,函数名和参数表需要与声明中的内容完全一致,否则依旧会出现编译错误。
(6).再谈作用域
刚刚举的例子中有一个修改x值的函数changeValue,我们发现传入x之后,即便在函数内修改了x,函数作用域外的x也没有发生变化。我们之前就说过,任何一对花括号都可以产生一个更小的局部作用域,对于函数也是这样,函数的括号内的x也是更小的局部作用域中的局部变量,这个局部变量会随着函数作用域的消失和消亡,所以说,我们在函数内修改的这个x不会影响到更大的作用域中的同名变量的值。
还有一件事,我们定义函数的时候用的变量名也是x,那也就说明,在一个大作用域中的不同小作用域内可以定义具有相同名称的变量,甚至是只有一个大作用域包括一个小作用域:
{
int a = 10;
{
int a = 12;
}
}
这样的情况下,也是可以定义同名的变量的,也就是说,不同的作用域内可以存在相同名称的变量,也可以调用更大作用域的内的变量,如果小作用域中没有重新定义一个新的同名变量,在小作用域中对变量的修改,就是对更大作用域内变量的修改,这段话可能有点难以理解,举个例子:
#include <stdio.h>
int a = 10; // 把a定义在全局作用域内
void changeValue();
int main()
{
printf("Now, a = %d\n", a);
a = 20;
printf("Now, a = %d\n", a);
changeValue();
printf("Now, a = %d\n", a);
return 0;
}
void changeValue()
{
a = 30;
}
那么这个时候a的值就随着我们的想法变化了,所以在函数内修改一个函数外的值的一种方法就是:不向函数传参,使得函数内修改的变量就是全局作用域内的对应变量,从而达到在函数内修改函数外变量的目的。但是一定要注意!这个变量a只能是全局作用域内的变量,否则会因为a没有声明而产生编译错误!
(7).让代码变的更简洁的重中之重——编译预处理
我们都知道,C语言是一门编译型语言,与python之类的解释型语言最大的区别是,运行C语言需要经过编译、链接等过程,而python只需要经过解释器就可以直接运行,所以python有一个交互界面,你向其中输入一行代码,它就能立马运行并且输出结果。
当我们写完代码后,就会点击IDE上的编译运行按钮,这是很方便的,因为一键就可以解决这个其中的问题,但是实际上C的编译运行过程并没有如此简单。一般来说,“编译”分为以下的步骤:
在这个教程中,我们会更关注汇编之前的内容,具体的编译和链接过程等等内容就需要你自己去了解了。本节中提到的编译预处理,就是通过预处理器实现的一系列过程。而预处理器就是以#开头的一系列语句。
#1.头文件与#include指令
记得吗,你迄今写的每一个C语言代码文件的第一行都是:
#include <stdio.h>
这个stdio.h,就是一个头文件(Header File),头文件都以.h作为后缀名,在头文件中,保存着一系列函数的声明等内容。按照我们之前的说法,一个函数如果要正常调用就需要声明和定义,那我们调用的printf(),scanf()这两个函数,声明在哪里?定义在哪里?这很奇怪啊,你我要是函数不写个声明定义,连编译都通不过。
事实上,printf()和scanf()以及一系列标准库中的函数都在头文件中声明。好,现在声明解决了,定义怎么办呢?一般来说,一个具有函数声明的头文件都会有一个与之文件名相同的对应.c文件,例如这里的stdio.h应该有一个对应的stdio.c文件(一般来说是,但是如果你之后去找的话可能会发现自己找不到在这个文件),在这个.c文件中就有头文件中声明的函数对应的定义,这样一来有什么好处呢?
你会发现函数的声明和定义都被保存到其他文件去了,我们的这个主要文件就只需要完成main函数的编写就好了,这对于一个函数定义和声明很多的项目来说是很有利的,每个文件都负责自己的部分,而程序主体对应的函数就完成程序主体的内容就好了,这样的程序设计方式叫做模块化程序设计,即把分属于不同部分的内容独立为不同的文件,在最后通过一些语言特性将他们组合起来的过程。在这里,我们只需要通过#include 的方式将他们包含进来就可以了。
给你布置一个小任务:自行上网搜索你现在所用的IDE如何创建一个项目,然后完成一些函数的声明和定义,放在function.h和function.c中,之后再在main.c中调用,完成一个小项目的编写。
这个过程中你可能会发现,你自己写的function.h好像不能通过
#include <function.h>
包含进入你的程序。事实上,一般的编译器会有默认的include路径,在这个路径下的头文件可以直接使用<xxx.h>直接包含,而其他路径下的就需要通过 "xxx.h"进行包含,如果xxx.h与项目位于同一个文件夹下,则可以写相对路径,否则就需要以绝对路径的方式写下,不然你的编译器怎么知道这个文件在哪里呢?
总结一下,如果是C标准库中的头文件或是你自行修改过了include路径,通过以下方式包含:
#include <xxx.h>
否则,其他情况下都应该通过以下方式包含:
#include "xxx.h" // 绝对路径或相对路径
那#include到底是拿来干啥用的,下面我们做这样一个实验(熟知如何利用命令编译的小伙伴可以一起来试试)(每一段是不同的文件)
// Filename : function.h
int f1(int x);
const double PI = 3.14;
// Filename : function.c
#include "function.h"
int f1(int x)
{
return x+1;
}
// Filename : main.c
#include "function.h"
const int A = 1;
int main()
{
return 0;
}
我在WSL Ubuntu下创建了对应的三个文件:
在Terminal中使用以下命令(在Windows下也可以在cmd或者Powershell使用相同命令且将main.out改为main.exe,然后要记得在对应路径下输入以下指令):
gcc function.c main.c -o main.out --save-temps
然后看看文件夹下面产生了些什么文件:
其中.s是汇编文件, .o是编译的目标文件, .i是经过编译预处理的文件,我们来看看main.i中有些什么内容:
忽略掉那些带#和数字的行,我们发现在function.h中的f1声明,以及PI这个常量的定义都被 “复制粘贴” 到了main.i中,也就是说,通过include命令包含可以将头文件中声明和定义的一些内容直接 “复制粘贴” 到对应的文件下,这样也就不难理解,为什么我们平时用的printf和scanf链声明都看不到了呢!
#2.#define和#undef语句
#define语句是一类相对特殊的语句,我们可以通过#define来定义一个宏(Macro),具体说宏是什么,我也说不清,但是我们先来看个例子:
#include <stdio.h>
#define PI 3.14
int main()
{
printf("%f\n", PI);
return 0;
}
这个PI就被打印出来了,3.14,好神奇啊,我们在“定义”这个PI的时候没有说它的数据类型,但用%f格式化就可以顺利打印出来了。那我们也按照探究include的方法,也使用
gcc main.c -o main.out --save-temp
来自行编译,并查看main.i文件:
我们知道#include 会复制粘贴头文件的内容,这一次编译完了在int main()之前多了好长一大串东西出来,这就是stdio.h这个头文件的具体内容,还没见过吧?不过我们关心的点不在这里,我们本来在stdio.h定义的内容和main函数定义之间还有一条 #define PI 3.14来着,这里就消失了,而且继续往后看,之前写了PI的地方,现在变成了3.14,哇哦,现在你理解#define 做什么工作了吧。
#define后的第一个值是宏的名字,一般采用全大写,第二个值是它对应的值,或者是你想让预处理器帮你复制过去的内容,不填也是可以的,意思就是有一个叫这个名字的宏,但不做其他操作。
什么意思?复制过去的内容?举个例子:
#include <stdio.h>
#define PI 3.14
#define AREA(X) PI * (X) * (X)
int main()
{
double r = 2;
printf("%f\n", AREA(r));
return 0;
}
这可就有意思了,我们用宏定义了一个函数!它可以给我们计算圆的面积,既然我们前面说了是复制,那么你也想得到,编译预处理完了之后这个printf就变成了:
printf("%f\n", 3.14 * (r) * (r));
是不是还挺有意思的,这样以宏定义的函数叫做宏函数,因为没有普通函数调用过程的开销,宏函数的效率是更高的,所以以后有些简单函数你就可以用这样的方式完成了哦,不过一定要记住一点,变量名需要加上括号,因为宏函数是直接“复制粘贴”完成的,有的时候调用可能会引发一些优先级的问题,比如我们可以看到下面这个例子:
#include <stdio.h>
#define PI 3.14
#define AREA(X) PI * X * X
int main()
{
double r = 2;
printf("%f\n", AREA(r + 2));
return 0;
}
那么经过编译预处理后,printf行就会变成:
printf("%f\n", 3.14 * r + 2 * r + 2);
这显然就不对了,你说是吧?
#undef MacroName 则是取消定义一个宏。
#3.#ifndef,#else,#endif语句
这三个语句是宏内使用if-else语句,不过#ifndef只能用来判定后面所跟的宏是否已经定义,例如说:
#ifndef PI
#define PI
#include <math.h>
... // 程序段1
#else
... // 程序段2
#endif // 结束判断要用#endif
他们的搭配可以使程序根据宏的情况来执行相应内容,那有什么用呢?我们用一用之前的function和main,将main.c改写为:
#include "function.h"
#include "function.h"
const int A = 1;
int main()
{
return 0;
}
只是再#include 了一遍function.h,会发生什么呢?
就出现了如上的编译错误,很容易看到,便一起说出现了redefinition——重定义,也就是说在同一个作用域内,同名的变量不能被声明/定义第二次,否则就会报错,因此如果我们写的项目比较复杂,各个文件之中都要相互include,怎么样保证每一个头文件都只被include一次呢?我们可以利用这样一个方法解决:
#ifndef FUNCTION
#define FUNCTION
#include <math.h>
... // 程序段1
#endif // 结束判断要用#endif
在每一个include之前加入一个宏定义,这个宏不需要值,只要存在即可,当别的文件开始include之前他们也要检查是否存在这个宏,如果存在,那就不再进行include操作,如果不存在,则可以正常完成include操作,这样一来就可以很好地避免程序结构复杂所引起的重复包含问题。
(8).要学会用轮子(更多内置函数)
我们在之前的素数判断例子中用到了sqrt(x)这样一个函数,它和printf(),scanf()一样,都是C语言标准库中已经定义好的函数,所以就可以直接拿来用了,有这样一些常用函数和需要引入的头文件,请详细阅读并参考:
请着重了解math.h中的各种函数,以及stdlib.h中的rand()函数,还有更多标准库的内容大家都可以在网上找到。
(9).更加完善的猜数游戏——利用C的内置函数
上一部分提到了stdlib.h中的rand()函数,这个函数可以通过种子产生一个随机整数,我们只要通过rand() % k的方式,即可取得 [ 0 , k − 1 ] [0, k-1] [0,k−1]之间的一个随机数,于是我们将猜数游戏修改如下:
#include <stdio.h>
#include <stdlib.h>
int check(int a, int input);
int main()
{
int a = rand() % 101, input = 0;
do {
scanf("%d", &input); // 每次循环都猜一次数字
} while(!check(a, input));
return 0;
}
int check(int a, int input) // 用于检查猜测的数字是否正确
{
if (input > a) {
printf("%d is bigger than a\n", input);
}
else if (input < a) {
printf("%d is smaller than a\n", input);
}
else {
printf("You're right!\n");
printf("a = %d\n", a);
return 1;
}
return 0;
}
这回的猜数游戏就是真正的猜数游戏了!数字由计算机随机生成,还使用了函数来简化代码,增强了可读性,编程是不是很有意思呢?如果有兴趣的话,你也可以自己编一些这样的简单游戏来练练手。
小结
函数真的是C语言中相当重要的部分,它可以以抽象化的方式简化我们编程的过程。编译预处理也是一个重要的内容,为了之后利用C语言完成更大项目的编写,我们必须了解并熟悉运用它,而最后,我们利用所学只是完成了真正的猜数游戏,如果有兴趣,你也可以做出自己的游戏来,如果可以的话可以分享给大家看看!下一节我们将讲到C语言的灵魂——数组与指针。