第十二章 函数(一)
12.1 函数的引入
12.2 学会调用函数
?? 12.2.1 哪些函数可调用?
????? 12.2.1.1 库函数
????? 12.2.1.2 操作系统的 API 函数
????? 12.2.1.3 VCL 库函数
?? 12.2.2 调用者必须能“找”得到被调用者
?? 12.2.3 调用者必须传递给被调用者正确的参数
?? 12.2.4 如何得到函数的运行结果
?? 12.2.5 调用库函数的实例
12.3 自定义函数
?? 12.3.1 函数的格式
?? 12.3.2 自定义函数实例
????? 12.3.2.1 小写字母转换为大写字母的函数
????? 12.3.2.2 使用函数改写“统计程序”
????? 12.3.2.3 求多种平面形状的面积
12.4 主函数
?? 12.4.1 DOS程序的主函数
?? 12.4.2 Windows程序的主函数
12.5 小结
函数是C语言的一个重点和难点,我们此次将连续两章进行讲解。本章重点在于彻底理解函数的作用,学会调用函数,学会自已编写函数。
秉承我们“以人为本:)”的学习方法,我们学习函数第一件事就是问话:干嘛让我学习函数?反过来说就是:函数能为一个程序员做些什么?
12.1 函数的引入
家里地板脏了怎么办?
拿起扫帚,自个儿扫呗。当然,在扫之前要对地板上的各种“脏”东西定好数据类型,针对不同的“数据类型”,我们需要进行不同的处理,比如是废纸,则无情地扫到垃圾桶;但若是在地上发现一张百元大钞,则应该脉脉含情地捡起放在胸口:“你让我找得好苦”。
在扫地的过程中,当然也无处不在使用“流程控制”。比如家里有三间房子,则应该是一个循环。而每一间房子的打扫过程也是一个循环过程:从某个角落的地板开始,向另一个角落前进,不断地重复扫把的动作。中间当然还需进行条件判断:比如前面所说的对地面脏物的判断,再如:if (这一小块地面不脏),则 continue 到下一块地面……
我们学了“数据类型、常量、变量”,所以我们有了表达问题中各种数据的能力;
我们还学了“流程控制”,所以我们还会针对各个问题,用正确的流程组合解决问题的步骤,从而形成解决问题的方法。
看起来我们已经拥有了从根本上解决任何问题的能力。但--
家里电视坏了怎么办?
呃?这个,我不是学电器专业的。我只会看电视,我不会修理电视。
这时候我们的办法是:打一个电话请专业的修理师上门修理。
还有很多问题的解决办法都是和修电视类似,即:我们自已没有这个能力,但我们可以调用一个具备这一能力的人来进行。
函数在程序中就相当于:具备某些功能的一段相对独立的,可以被调用的代码。是的,函数也就是一段代码,代码也就是我们前面的学的“变量,常量,流程控制”等写成的一行行语句。这些语句以一种约定形式存在着,等待我们去调用它。
其实我们已经用过函数了:给你一个数:2.678,能帮我们求出它的正弦值吗?想起来了吗?我们在上一章中学过sin()函数。
一段用以被调用的代码,这是函数的本质。当然,使用函数在程序中还有许多其它的作用,但我们将从这个最关键的地方讲起:怎样调用一个函数?
12.2 学会调用函数
?这一节的任务是通过学会如何调用一个函数,从使用者的角度来了解函数各个重要知识点。从而,也为下一节学习如何写一个函数打下基础。我们相信这样的安排是科学的,因为在生活中,我们也往往是先是一个“使用者”,然后才是一个“创造者”或“提供者”。
12.2.1 哪些函数可调用?
在学会如何调用函数之前,不妨先看看有哪些现成的函数可以调用。
12.2.1.1 库函数
C++ Builder 提供了数百个库函数。之所以称为“库”函数,是因为这些函数被集中在一个或几个文件里,这些文件就像存放函数仓库,当我们需要时,程序就可以从“库”中调用。
库文件又分为两种形式:
第一种是把不同的函数分门别类地放在不同的文件里。比如和数学计算有关的,放到一个文件,和I/O操作有关的,放到另一个文件。这样做的结果是:文件很多,但每个文件都比较小。这种库我们称为“静态库”。
使用静态库的好处是:当我们的程序调用到某一库的函数是,C++ Builder 可以将这个库文件直接和我们的程序“合并”到一起。这样,我们提供给用户程序时,只需要提供一个可执行文件(比如叫:A.exe)。用户得到这个程序时,不用安装其它文件,就可以运行了。
使用静态库的坏处是:假如你需要向用户提供两个可执行文件,比如A.exe和B.exe,两个文件可能都用到同一库文件,所 以同一个库函数既被“合并”入A.exe,也被合并入B.exe,造成了事实上的空间浪费。另外,虽然说每人静态库的文件都比较小,但如果一个程序“合并”了不少库文件,那么这个程序的可执行文件体积仍然不可避免地变得比较大。
和静态库相对,另外一种库称为“动态库”。它的做法是:把所有函数不管三七二十一,都放在一个文件里。这样做的结果:库文件只有一个,但体积很大。
使用动态库的坏处是:动态库不允许“合并”到你的程序中--显然也不适于合并,因为动态库太大了。所以若你使用动态库,在发布你的应用程序时,你必须向你的用户提供动态库文件。
使用动态库的好处在于:如果你向用户提供的是一套程序,比如有A.exe,B.exe,C.exe...,那么这些可执行文件都可以使用同一个动态库,所以尽管你需额外提供一个很大的动态库,但你的各个应用程序却都很小。当然,采用动态库发布程序时,一般来说你还需要向用户提供一个安装程序,很多动态库要被安装到Windows目录的system或system32子目录下。
什么时候使用静态库,什么时候使用动态库?当你只是写一个小小应用程序时,显然大多数人喜欢只提供一个单独.exe文件。比如情人节到了,你觉得通过网络向你的girlfriend发一个电子贺卡太俗(前几年还很风雅呵:),同时也不能突显你作为一个程序员的实力--风水轮流转啊,前年搞网络的人还笑话程序员是“传统工业”--所以你决定用C++ Builder写一个电子贺卡,这时你可不能用动态库啊,否则挤爆了女友的信箱,嘿嘿,这个情人节就有你好受的了……
相反,一个稍大点软件系统,你就应该采用动态库。大的如整个Windows操作系统,就彻头彻尾是使用动态库;再如一整套MS Office,还有WPS,这些都是。一般地说(不绝对),那些提供了安装程序的软件,都是使用动态库的。总之,使用动态库是专业程序的做法。
(又有人举手打断我的课程,说我们什么时候才能自已写个电子贺卡?回答是下一部教程《白话Windows编程》,顺便说说,下部教程很贵很贵的--吓你的:)
不管使用动态或静态的库,写程序时都是一样的。只有在最后要链接程序时,我们通过CB设置不同的选项即可。嗯?我说到了“链接”(link)这个词?对了,它就是我们一直加引号的“合并”一词的专业说法。你可以把前面课程上所有的“合并”一词替换为链接,并且不用加引号了。
现在我们来看看CB主要提供哪些类别的库函数(以下内容仅供了解):
1、分类判断函数:
这类函数主要用对判断一个字符是什么类型的。就像我们上一章做的“判断用户输入字符的类型”的例子。不使用函数,我们可以这样的条件判断一个字符是否为小写字母:
if ( ch >= 'a' && ch <= 'z' )
???? cout << ch << "是一个小写字母。" << endl;
我们也可以直接使用相关的库函数 islower:
if ( islower(ch) )
???? cout << ch << "是一个小写字母。" << endl;
2、控制台输入输出函数:
像我们总是使用的getchar(),及getche();这两个函数用来接受用户在控制台程序中的按键输入。另外还有不和输入输出函数。当然,在输出方面,我们几乎都采用 cout 来往屏幕输出内容。cin, cout这是C++的方法,如果写C程序(而不是C++),则输出更常用的是printf();比如:
printf("Hello world!");
这行代码在屏幕上打出一行:"Hello world!"。
除了教学上,或其它一些特殊要求,我们几乎不写控制台式的程序了,我们最终目标是写Windows下的GUI(图形用户界面)程序,而这些控制台输入输出函数,都不能用在GUI程序中。所以,当课程例中用到的某个控制台库函数,我会临时解释一下,其它的,大家就不必花时间了。
3、转换函数:
这类函数完成各种数据类型之间的转换,比如把字符串“123”转换数字123,或把小写字母转换为大写字母等等。
5、目录管理函数:
目录就是我们现在常说的“文件夹”啦。这些函数可以建立,删除,切换文件夹。一般地,我们已经不再使用,转而使用Windows提供的相关函数。请参看下面的Windows API函数说明。
6、数学函数:
例如我们前面说的sin()函数,其它的各种三角函数,还有求整,求绝对值,求随机数,求对数等。
这些函数大都枯燥无味,其中的随机函数倒是有趣点。很多游戏程序都要使用到它。这里粗略讲讲。
什么叫随机?大白话说就是:一件事情的结果有几种相同概率的可能。比如你扔一个硬币到地上,可能是正面,也可能是反面朝上,两种可能的概率都是50%。但如果你要考虑硬币还有“立”着在地上的可能,那么这种可能就不属于随机的范畴了。下面的程序随机生成一个0~99的数,然后要求你输入一个0~99之间的数,如果这你输入的和它生成的数相等(概率为1%),就表示你中奖了。
//虽然属于数学类函数,但随机函数其实放在标准库(stdlib)里:
#include <stdlib.h>
#include <iostream.h>
int main(int argc, char* argv[])
{????? //在第一次调用随机数之前,要调用一次这个函数,
?? //这个函数称为“随机种子函数”
?? randomize();
?
?? //随机函数:random(int n)的用法:
?? //随机返回一个 0~ (n-1) 之间的整数,
?? //如: int x = random(100),则x值将是0到99之间的一个数。
?? int x = random(100);
?? int y;
?? cout << "请输入一个0~99的整数:";
??
?? cin? >> y;
?? if( x == y) //可能性为1%
????? cout << "恭喜!您中奖了!" << endl;
?? else
???? cout << "谢谢使用。" << endl;
}
7、字符串函数:
我们在学习字符串时将用到。
8、内存管理管理函数:
我们在学习内存管理时将用到。
9、杂七杂八的其它函数。
这个且不说。
12.2.1.2 操作系统的 API 函数
大家总该知道什么叫操作系统吧?Windows就是一套操作系统,另外如UNIX,Linux也是,当然我们最常用的是前者。操作系统有两个主要任务:
第一是给普通用户提供一套界面,比如桌面啦,任务条啦,及任务条上的开始按钮,桌面上的图标;还有资源管理器等等。这一些我们都称为“用户界面”。它的作用是让用户“用”这台电脑。因此我们也可以称它为用户与电脑之间的“接口”。
第二就是给我们这些程序员的接口,我们所写的程序是运行在操作系统上,就必须和操作系统有着千丝万缕的关系。比如我们想在屏幕上显示一个窗口,那么我们所做的事是“请求操作系统为我们在屏幕上画一个窗口”,同样在有了窗口后,我们想在窗口上画一条直线,那么也是“请求操作系统在座标(2,1)-(100,200)之间画一条直线”。
那么,这些“请求”是如何实现的呢?其实也是调用函数,调用操作系统为我们准备的各种函数。这些函数同样是放在库文件里,当然,由于这些库文件是操作系统提供的,每一台装有相同操作系统的电脑都有这些库,所以它不用安装,所以它当然采用了动态库的形式。 对于我们正在用的Windows,这些库一般都放在Windows的安装目录:Windows,主要是Windows\System或System32下。那里有一堆的.dll,其中有不少文件就是操作系统的动态库文件。
我们写的程序,一般称为“应用程序”(Application Program),所以Windows为我们提供的库函数也就称为“应用程序接口”(Application Program Interface),缩写即:API。
在本部教程,我们主要学习C++语言本身,只有学好C、C++语言,才有可能学会用C、C++语言来和操作系统打交道。要知道所有在API函数都声明为C语言的形式,这是因为,Windows本身也是主要用C语言写成的。结论是:学习C、C++语言非常重要,并且,如果想在操作系统上写程序,那么学习C、C++当然最合算!
12.2.1.3 VCL 库函数
VCL意为:可视化控件库(Visual Component Library),事事都直接和Windows的API打交道,编程效率将非常的低。主要表现两个方面:第一,由于使用API编程是非可视化的,我们将不得不花费非常冗长的时间在处理界面显示的事务上,而界面显示其实不是我们程序的主要逻辑。第二,有关显示等工作的大量代码事实上有很大的相似性,大量重复。我们要么仍受每写一个程序就重复写一堆千篇一律的代码,要么像早期的Windows程序员一样自已动手写一套的类库用来“包装”这段代码,以求每次可以得重复利用。但这是件庞大而灵活的工作,显然我们不值得这样做,事实上也不具备这样的能力。笔者在Windows3.1下写程序时,曾经购买过国人高手写的一套这种类库,事实上钱花得不值。很快笔者转向了当时Borland提供的类库:OWL和微软的MFC。
VCL提供的也主要是类库,我们暂未学到“类”的概念,所以这时且不详谈。
12.2.2 调用者必须能“找”得到被调用者
调用函数前提之一:调用者必须能看到被调用者。
一个“者”字,可能让你以为这里说的是“人”,其实不是,这里说的调用者指的是当前程序,而被调用者当然是“将被调用”的函数。
不过,确实,这里拿人来比喻是再合适不过了。
就拿前面说的“找电视修理工”的例子来说:
要修电视,显然要能找到电视修理工。这个道理很明显。
所以本小节的重点其实是:程序如何才能找到要调用的函数呢?
有三种方法:
第一种、将被调用的函数写在当前代码前面
修理工正在我家喝茶呢!是啊,我有个朋友是干这活的,有一天他来我家串门,而我家电视正好坏了。
下面我先写一个函数,这个函数的大部分代码我没有写出来--根本写不出来。我只是要用它表示一个叫“修理电视”的功能。
//本函数实现“修理电视”
void XiuliDianshi()
{
?? ......
}
尽管我们稍后才能学如何自已写函数,但你现在要记住了,上面那几行代码就是一个函数,它的函数名为:XiaoliDianshi,意为“修理电视”。
好!有了“修理电视”的函数了,如何调用它呢?下图表示的是正确的情况:
当我们写程序要调用一个函数,而这个函数位于我们现在在写的代码前面时,我们就可以直接调用它,这就像修理工就在我们家里一样。注意这里的前面并非仅限于“跟前”,如果你的代码很多行,这个函数在“很前面”,也不妨碍我们调用它。
要注意的是另一面:当函数在我们的代码后面时,代码就“看”不见这个函数了。下面即为这种错误情况:
第二种、将被调用的函数声明写在当前代码前面
修理工不在我家,不过,他曾经留给我一张名片,名片上写着:“张三,电视修理工,Tel:1234567,住址:……”。所以我们也能知道他会修理电视,并且知道他的电话和住址,这样就不愁找不到他了,对不?
函数也可以有名片,在程序中我们称为函数的“声明”。
下面的代码演示了什么叫函数的“声明”,及它所起的作用:
第三种:使用头文件
当我们手里有了电视修理工的名片,有了冰箱修理工的名片,有了电脑修理工的名片……名片多了,我们可以将名片整理到一个名片夹。这样做至少有两个好处:
其一:便于管理。家里任何电器坏了,只需找“家用电器修理工名片”的名片夹即可。
其二:便于多人共用,比如隔壁家想找一个电视修理工,只需上你家借名片夹即可。
C,C++中,类似“名片夹”功能的文件,称为“头文件”。头文件的扩展名为 .h(head)。头文件是放置函数声明的好地方。如何写函数声明下面再说,现在要明白,“函数声明”就是给编译器看的函数说明,或曰函数的“自我介绍”。至于为什么叫“头”文件呢?是因为它总是要在程序代码文件的开头。就你我们在交谈时,开头总是大家各作一番介绍一样。(该说法未经证明,仅供参考:)
说千道万,不如先简单地看一眼真实的头文件吧。
启动C++ Builder。然后新建一个控制台应用工程。在CB6里,新建控制台工程在File | New | Others 去找,别忘了。
(CB6启动为什么这么慢啊!我且先上趟洗手间)
然后在代码窗口里,加上一行:
#include <stdlib.h>
并且用鼠标在这一行点一下,现在代码窗口里的内空看起来如下:
确保输入光标在单词“stdlib.h” 上面闪烁!现在按 Ctrl + 回车,CB将打开光标所在处的文件。
(如果你出现的是一个文件打开对话框,那有两点可能,其一是你没有把光标移到指定的单词上,另一可能是你安装CB时没有选择“Full”模式的安装,造成CB没有安装源文件。)
以下就是打开的 stdlib.h 头文件:
(选择ReadOnly可以把当前文件设置为只读)
打开的文件是C++ Builder工程师为我们所写的头文件,请注意千万不要有意无意地改动它!为了保险起见,通过右键菜单,选择Read Only将当前文件设置为只读(如上面右图)。请大家将这当作一条准则来执行:不管出于什么原因打开CB提供的源文件,立即将其设置为只读。
好,我们说过“只看一眼”的。关于头文件,在讲完函数以后,还会专门讲到头文件在工程中应用。现在重复头文件的目的:
函数可以统一在一个头文件中声明,代码中需要使用这些函数,只需通过“include”语句包含这个头文件,就可以让编译器找到函数。
用一句大白话讲就是:要想用函数?请包含它所在的名片夹(头文件)。
函数的“声明”有时被称为函数的“原型”,比如在讲到编译过程时。当我们阅读其它文章时,如果看到“函数原型”一说,希望大家也能明白。
12.2.3 调用者必须传递给被调用者正确的参数
现在,我能找到修理工,而且他已经到我家。
“电视呢?”他说。
“就是它”我指着家里的苏泊尔高压锅,“劳驾,把它修修,最近它总漏气。”
“可是,我好象是来修理电视的?”
“知道,现在你先修高压锅。”
“好吧,我试试……先用电笔试试它哪里短路。”
显然我这是在胡搅蛮缠。电视修理工要开始干活,就得给他电视。给他一只高压锅他不能开工。
函数也一样,函数的目的是实现某个特定功能,当我们调用它时,我们一般需要给它一些数据,这些数据可能是让它直接处理,也可能是辅助它实现具体的功能。
当然有些函数不需要任何外部数据,它就能完成任务。这也很好理解,修理工修理电视是得有台电视,但叫一位歌手到家里随便哼几句歌,你就不用给他什么。
关键一句话:函数要不要外部传给它数据,要什么类型的数据,要多少数据,由函数本身决定,而非调用者决定。本例中,电视修理工需要一台电视,这是他决定的,不能由请他的人决定。
传给函数的数据,我们称为“参数”,英文为:parameter。
基于此,我们发现所写的 XiuliDianshi()函数有很大的不足,那就是它没有参数。现在我们假设有一种数据类型为“电视机”,嗯,就假设这种数据类型叫作:TDianshi。
加入参数的XiuliDianShi()函数变为:
XiuliDianshi (TDianshi ds)
{
}
看一个实际的例子。上一章我们曾经学过sin()函数。现在我们来看看sin()函数的声明。看看它声明需要什么参数。
关闭刚才的工程,CB会问你是否存盘,统统不存(如果你要存,就存到别的什么地方去,不要存在CB默认的目录下)。然后重新创建一个空白的控制台工程。在代码窗口里加入以下两行黑体代码:
//---------------------------------------------------------------------------
//包含“数学库函数”的头文件,因为sin()函数的声明在这个头文件里:
#include <math.h>
#pragma hdrstop
//---------------------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
?? double b = sin(3.14159);
?? return 0;
}
//---------------------------------------------------------------------------
并不需要编译及运行这个程序。因为我们只是想找到sin()函数的声明。
本来,我们可以通过老办法来找到sin函数声明。按Ctrl+回车键打开math.h文件,然后通过Ctrl+F打开查找对话框,找到sin函数。不过CB为我们提供了一种更方便的查找函数声明的方法,有点像我们在网页点击链接:
请按住Ctrl键不放,然后将鼠标移到代码中的 sin处,注意要准确在移到sin字母身上,发现什么?呵,sin出现了超链接效果:
点一下,CB将自动打开math.h头文件,并且跳转到sin函数的声明处。
(以上操作的成功依赖于你正确地照我说的,在代码中加入#include<math.h>这一行,当然你在安装CB时也必须选择了安装源代码。最后,成功打动后,记得将math.h文件设置为只读。)
从图中我们看到,sin函数的参数只有一个:__x,类型要求是double(双精度浮点数,如果你忘了,复习第四章)。
所以,当我们调用sin函数来求正弦值时,我们最好应该给它一个double类型的数,如:
double x = 3.1415926 * 2;
double y = sin(x);
当然, 我们传给它一个整数:
double y = sin(0);
或者,传给它一个单精度浮点数:
float x = 3.14;
double y = sin(x);
这些都是可以的。这并不违反“参数类型由函数本身决定,不能由调用者决定”的原则。因为在第七章第二节讲算术类型转换时,我们知道一个整数,单精度浮点数,都可以隐式地转换为双精度浮点数。并且属于安全的类型转换,即转换过程中,数据的精度不会丢失。(反过来。一个double类型转换为int类型,就是不安全的转换。比如3.14159转换为整型,就成了3。)
有些函数并不需要参数,比如,我们用了许多次的控制台函数:getchar();。这个函数要做的事就是:等待用户输入一个字符并回车。前面讲数学函数时,举的随机数例子。要想让程序能够产生真正的随机数,需要让程序事先做一些准备。所以我们调用randmize()函数。这个函数也没有参数。因为我们调它的目的,无非是:喂,告诉你,我一会儿可能要用到随机数,你做好准备吧。
12.2.4 如何得到函数的运行结果
函数总是要实现一定的功能,所以我们也可以认为函数执行起来就像是在做一件事。
做一件事一般会有个结果,当然,只是“一般会有”。有些事情真的会有结果吗?嗯?看来,这句话勾起某些同学一些旧事,他们陷入了深深的,似乎很痛苦的回忆之中……对此,为师我表示最大的理解,并有一言相送:“并非是一件事情不会有结果,只是,有时候,我们并不需要结果……”。
写函数的人就是这样的啊。函数需要什么参数,由写函数的人决定,函数返回什么结果,也由他们决定。如果他们认定这个函数不需要什么结果,那么这个函数就将写成返回void类型。void是“无类型”之意,这就相当于这个函数没有返回结果。
举修理电视的例子来说,我们认为它至少应该返回一个bool值,即真或假。真表示电视修好了,假表示电视修不好。
bool XiuliDianshi(TDianshi ds);
然后,我们如何得知结果呢?
bool jg = XiuliDianshi(ds);
看,我们也声明了一个bool变量,然后让它等于这个函数,这就可以得到函数的返回值。
来看一个实例,仍然是sin函数。
double x = 3.1415926;
double y = sin(x);
y值将是一个非常接近0的值。
getchar();是一个不需要参数的函数,但它有返回值。它返回用户输入的字符(事实上它返回的是整型)。所以我们可以这样用:
char c = getchar();
c将得到用户输入的字符。
而另一个例子: randomize()函数,则赤条条地来,赤条条地走,潇洒得很。根本就不打算返回什么。连到底准备成功了吗?都不返回--因为它认定自已一定会执行成功。
还需说明的是,有时函数是有返回值,但我们并不在意。还是getchar();我们不是一直在使用它来“暂时”停止程序,以期能看到DOS窗口上的输出结果吗?这时,用户输入什么键我们都不在意。所以我们总这么写:
getchar();
就完事,并没让谁去等于谁。
最后一点针对学过PHP,JavaScript,Perl等脚本语言的学员:在C,C++里,一个函数返回值的类型,必须是确定的。不像脚本语言中的函数,可以返回不定类型的结果。
12.2.5 调用库函数的实例
实例一:使用库函数创建或删除文件夹。
(本例子中删除的文件夹将无法恢复!请大家操作时小心。)
在本实例里,我们将“大胆地”在C盘根目录下创建指定的目录(文件夹),然后再把它删除。
使用到两个函数:
1、mkdir("文件夹名称")
参数是一个字符串,即指定的文件夹名称。
返回值比较特殊:整数:0表地成功,-1表示失败:比如那个文件夹已经存在,或者,你想让它一次创建多级目录,如:C:\abc\123,而C:\abc并不存在。
2、_rmdir("文件夹名称")
参数是一个字符串,即指定的文件夹名称。
返回同样是0或-1。删除一个文件夹比较容易失败:比如文件夹内还有文件或其它子文件夹,比如该文件夹正好是当前文件夹,另外你也不能删除一个根目录,比如你想删除:"c:\" !!!(想删除整个C盘?病毒?)
两个函数都在“dir.h”文件里声明,所以我们需要include它。
下面是完整的代码:
//---------------------------------------------------------------------------
#include <dir.h>
#include <iostream.h>
#pragma hdrstop
//---------------------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
?? char path[50];
?? char ch;
???
?? do
?? {
????? //让用户选择操作项:?
????? cout << "0、退出本程序" << endl;
????? cout << "1、创建文件夹" << endl;
????? cout << "2、删除文件夹" << endl;
????? cout << "请选择:";
????? cin >> ch;
??????
????? //如果输入字符'0',则结束循环以退出:
????? //请注意break在这里的用法:
????? if(ch == '0')
????? {
???????? break;
????? }
??????
????? //如果输入的既不是1,也不是2,要求重新输入,
????? //请注意continue在这里的用法:
????? if(ch != '1' && ch != '2')
????? {
????????? cout << "输入有误,请重新选择!" << endl;
????????? continue;
?????? }
?????? //不管是创建还是删除,总得要用户输入文件夹名称:
?????? cout << "请输入文件夹的绝对路径:" ;
?????? cin >> path;
?????? //先定义一个bool变量,用来判断操作是否成功:
?????? bool ok;
?????? //现在需要区分用户想做什么了:
?????? if(ch == '1') //创建文件夹:
?????? {
?????????? ok = (0 == mkdir(path)); //若mkdir返回结果等于0,表示操作成功
?????? }
?????? else //否则就是要删除了!
?????? {
?????????? ok = (0 == _rmdir(path)); //同样,_rmdir也是返回0时表示成功
??????? }
//给出结论:
?????? if(ok)
?????? {
??????????? cout << "恭喜!操作成功。" << endl;
??????? }
?????? else
?????? {
???????????? cout <<"抱歉,操作失败,请检查您的输入。" << endl;
??????? }
?? }
?? while(true);
?? return 0;
}
//---------------------------------------------------------------------------
代码里头有一个do...while循环,一个continue,和break;另有几个if...else,这些相信你可以边运行程序,边看明白其间的逻辑。惟一陌生的是最开头的一句:
char path[50];
这里涉及到了“数组”的知识。针对本例,你可以这样理解:
char ch;?
这一行我们能看懂,定义了一个字符类型的变量,ch。ch变量的空间是1个字节,能存储一个字符,因此你可以用它存储诸如:'A','2','H'等,但现在我们要输入的是:"c:\abcd"这么一句话,所以变量ch无法胜任。C,C++提供了数组,我们可以通过定义数组来存储同一类型的多个数据。如:
char path[50];
本行定义了path这个数组,它可以存储50 个 char类型的数据。
注意,path只能存储最多50个字符。所以在运行本例时,不要输入太长的文件夹名称。
另外,Windows对新建文件夹的名称有一些特殊的要求,所以如果文件夹名称含了一些非法字符,操作将失败。
以下是我运行的一个结果:
12.3 自定义函数
学会如何调用别人的函数,现在我们来学习如何自已写一个函数。首先迅速看看函数的格式:
12.3.1 函数的格式
定义一个函数的语法是:
返回类型? 函数名(函数参数定义)
{
? 函数体
? return 结果;
}
其中:
1、返回类型: 指数据类型,如:int ,float,double, bool char ,void 等等。表示所返回结果的类型。如果是void则表示该函数没有结果返回。
2、函数名:命名规则和变量命名一样。注意要能够表达出正确的意义。如果说一个变量命名重在说明它“是什么”的话,则一个函数重在说明它要“做什么”。比如一个函数要实现两数相加,则可以命名为:AddTwoNum,这样有助于阅读。
3、函数参数定义:关于参数的作用,我们前面已说。现在看它的格式:
int AddTwoNum(int a,int b);
函数参数的定义有点类似定义变量,先写参数的数据类型,上例中是int,然后再写参数名。下面是不同之处:
3.1 多个参数之间用逗号隔开,而不是分号。最后一个变量之后则不需要符号。 请对比:
普通变量定义:
int a;? //<--以分号结束
int b;
函数中参数定义:
(int a, int b ) //以逗号分隔,最后不必以分号结束
3.2 两个或多个参数类型相同时,并不能同时声明,请对比:
普通变量定义:
int a,b; //多个类型相同的变量可以一起定义。
函数中参数定义:
AddTwoNum(int a, b) //这是错误的。
4、函数体:函数体用一对{}包括。里面就是函数用以实现功能的代码。
5、return 结果:return 语句其实属于函数体。由于它的重要性,所以单独列出来讲。“return”即“返回”,用来实现返回一个结果。“结果”是一个表达式。记住:当函数体内的代码执行到return语句时,函数即告结束,如果后面还有代码,则后面的代码不被执行。依靠流程控制,函数体里可以有多个return语句。当然,对于不需要返回结果的函数,可以不写return 语句,或者写不带结果的return语句。这些后面我们都将有例了演解。return 返回的结果,类型必须和前面“返回类型”一致。
一个最简单的例子,也比一堆说明文字来得直观,下面我写一个函数,用于实现两个整数,返回相加的和。这当然是一个愚不可及的函数,两数相加直接用+就得,写什么函数啊?
//愚不可及的函数:实现两数相加
//参数:a:加数1,b:加数2;
//返回:相加的和
int AddTwoNum(int a, int b)
{
?? return a + b;
}
例子中,谁是“返回类型”,谁是“函数名”?谁是“参数定义”?哪些行是“函数体”?这些你都得自已看明白。这里只想指出:这是个极简单的函数,它的函数体内只有一行代码:即return a+b;语句,直接返回了a+b的结果。
最后说明一点:C,C++中,不允许一个函数定义在另一个函数体内。
void A()
{
?? void B()? //错误:函数B定义在函数A体内。
?? {
???? ....
?? }
?? ...
}
如上代码中,函数B“长”在函数A体内,这不允许。不过有些语,如Pascal则允许这样定义函数。
12.3.2 自定义函数实例
下面我们将动手写几个函数,并实现对这些函数的调用。从中我们也将进一步理解函数的作用。
12.3.2.1 小写字母转换为大写字母的函数
实例二:自定义小写字母到大写字母的转换函数。
尽管这个功能很可能已经有某个库函数实现了,但像这种小事,我们不妨自已动手。
之所以需要这个函数,缘于最近我们写程序时,经常用到循环,而循环是否结束,则有赖我们向用户提一个问题,然后判断用户的输入;如果用户输入字母Y或y,则表示继续,否则表示退出。
每次我们都是这样判断的:
if(ch == 'Y' || ch == 'y')
{
??? ...
}
平常我们的键盘一般都是在小写状态,因为用户有可能不小心碰到键盘的“Caps Lock”,造成他所输入的任何字母都是大写的--尽管键盘上有个大小写状态指示灯,但有谁会去那么注意呢?所以如果你的程序仅仅判断用户是否输入‘y'字母,那么这个用户敲了一个‘Y',结果程序却“很意外”的结束了?显然这会让用户很小瞧你:才三行程序就有BUG。
(一般不传之秘笈:用户就像女友一样,需要“哄”:有时你发现软件中存在一项潜在的,系统级的严重BUG,你自已惊出一身冷;但在用户那里,他们却纠缠你立即改进某个界面上的小小细节,否则就要抛弃这个软件--就像你的女友,天天和你吃萝卜秧子没有意见,但情人节那天忘了送花,她就对你失望透了。)
言归正传!现在问题,我讨厌每回写那行条件都既要判断大写又要判断小写。解决方法是,在判断之前,把用户输入的字母统统转换为大写!
下面是将用户输入字符转换为大写的函数。要点是:
1、用户输入的字符不一定是小写字母,说不定已经是大写了,甚至可能根本就不是字母。所以在转换之前需要判断是否为小写字母。
2、小写字母‘a’的ASCII值比大写字母‘A'大32,这可以从第五章的ASCII码表中查到。不过我不喜欢查表,所以最简单的方法就是直接减出二者的差距。所有字母的大小之间的差距都一样。这是我们得以转换大小写字母的前提。
//函数:小写字母转换为大写字母。
//参数:待转换的字母,可以不为小写字母;
//返回:如果是小写字母,返回对应的大写字母,否则原样不动返回。
char LowerToUpper(char ch)
{
?? //判断是否为小写字母:
?? if(ch >= 'a' && ch <= 'z')
?? {
????? ch -= ('a' - 'A'); //相当于 ch -= 32; 或 ch = ch - 32;
?? }
?? //返回:
?? return ch;
}
这个函数也再次提醒我们,在ASCII表里,大写字母的值其实比小写字母小。所以,小写字母转换为大写,用的是“减”。小写字母减去32,就摇身一变成了大写。
现在,有了这个函数,假设我们再遇上要判断用户输入是’y'或‘n’的情况,我们就方便多了。
作为一种经历,我们此次采用将函数放在要调用的代码之前。
//------------------------------------------------------------------------
//函数:小写字母转换为大写字母。
//参数:待转换的字母,可以不为小写字母;
//返回:如果是小写字母,返回对应的大写字母,否则原样不动返回。
char LowerToUpper(char ch)
{
?? //判断是否为小写字母:
?? if(ch >= 'a' && ch <= 'z')
?? {
????? ch -= ('a' - 'A'); //相当于 ch -= 32; 或 ch = ch - 32;
?? }
?? //返回:
?? return ch;
}
//------------------------------------------------------------------------
int main(int argc, char* argv[])
{
?? char ch;
?? do
?? {
????? cout << "继续吗?(Y/N)";
????? cin >> ch;
????? //调用函数,将可能小写字母转换为大写:
????? ch = LowerToUpper(ch);
?? }
?? while(ch == 'Y');
???
?? return 0;??? }
//------------------------------------------------------------------------
完整的代码见相应例子文件。例子只是为了演示如何自已定义函数,并调用。运行时它问一句“继续吗?”你若输入大写或小写的‘y'字母,就继续问,否则结束循环。
函数的返回值也可以直接拿来使用。上面代码中的do...while循环也可以改写的这样:
?? do
?? {
????? cout << "继续吗?(Y/N)";
????? cin >> ch;
?? }
?? while(LowerToUpper(ch) == 'Y');
? 功能完全一样,但看上去更简洁。请大家进行对比,并理解后面的写法。
本例中的“小写转换大写”的函数,虽然我们已经成功实现,但我们并没有将它的声明放到某个头文件,所以,如果在别的代码文件中,想使用这个函数,还是不方便。确实,我们很有必要为这个函数写一个头文件,在讲完函数后,我们将去做这件事。
实例二代表了一种函数的使用需求:我们将一些很多代码都要使用的某个功能,用一个函数实现。这样,每次需要该功能时,我们只需调用函数即可。这是函数的一个非常重要的功能:代码重用。通过函数,不仅仅是让你少敲了很多代码,而且它让整个程序易于维护:如果发现一某个功能实现有误,需要改正或改进,我们现在只需修改实现该功能的函数。如果没有函数?那将是不可想像的。
但是,只有那些一直要使用到的代码,才有必要写成函数吗?并不是这样。有些代码就算我们可能只用一次,但也很有必要写在函数。请看下例。
12.3.2.2 使用函数改写“统计程序”
实例三:使用函数改写第十章“可连续使用的统计程序”。
我们先把第十章的例子拷过来(只拷其中的main()函数部分):
int main(int argc, char* argv[])
{
?? float sum,score;
?? int num; //num 用于存储有几个成绩需要统计。
?? int i;?? //i 用于计数
?? char c; //用来接收用户输入的字母
do
?? {
???? //初始化:
????? sum = 0;?
????? i = 1;
????? cout << "====成绩统计程序====" << endl;
????? //用户需事先输入成绩总数:
????? cout << "请输入待统计的成绩个数:";
????? cin >> num;
????? cout << "总共需要输入"<< num << "个成绩(每个成绩后请加回车键):" << endl;
????? while ( i <= num)
????? {
???????? cout << "请输入第" << i << "个成绩:";
???????? cin >> score;??????????? sum += score;??????????? i++;???????? }???????? //输出统计结果:???????? cout << "参加统计的成绩数目:" << num << endl;?? ????? cout << "总分为:" << sum << endl;
???????
????? //提问是否继续统计:??
????? cout <<"是否开始新的统计?(Y/N)?";
????? cin? >> c;
???? }
???? while( c == 'y' || c == 'Y');
}
//---------------------------------------------------------------------------
我们将要对这段代码所作的改进是:将其中完成一次统计功能的代码,写入到一个单独的函数。
//函数:实现一个学员的成绩统计:
//参数:无
//返回:无
void ScoreTotal()
{
?? float sum,score;
?? int num; //num 用于存储有几个成绩需要统计。
?? int i; //i 用于计数
?? sum = 0;
?? i = 1;
?? cout << "====成绩统计程序(Ver 3.0)====" << endl;
?? //用户需事先输入成绩总数:
?? cout << "请输入待统计的成绩个数:";
?? cin >> num;
?? cout << "总共需要输入"<< num << "个成绩(每个成绩后请加回车键):" << endl;
?? while ( i <= num)
?? {
????? cout << "请输入第" << i << "个成绩:";
????? cin >> score;
????? sum += score;
????? i++;
?? }
?? //输出统计结果:
?? cout << "参加统计的成绩数目:" << num << endl;
?? cout << "总分为:" << sum << endl;
?}
?//---------------------------------------------------------------------------
我只是将一些代码从在原来的位置抽出来,然后放到ScoreTotal()函数体内。接下来,请看原来的main()函数内的代码变成什么:
//---------------------------------------------------------------------------
int main(int argc, char* argv[])
{
?? char c;
???
?? do
?? {
????? //调用函数实现一次统计:
????? ScoreTotal();
???
????? //提问是否继续统计:
????? cout <<"是否开始新的统计?(Y/N)?";
????? cin >> c;????? }
?? while(c == 'Y' || c == 'y');?
?}
//---------------------------------------------------------------------------
看,当实现统计一次的功能的代码交由ScoreTotal()处理之后,这里的代码就清晰多了。
函数的另一重要作用:通过将相对独立的功能代码写成独立的函数,从而使整体程序增加可读性,同样有益于代码维护。这称为“模块化”的编程思想。“模块化”的思想并不与C++后面提倡的“面向对象”的编程思想相抵触。而函数正是C,C++中实现“模块化”的基石。
实例三的演变过程也向我们展示了一种编写程序的风格:当一个函数中的代码看上去很长时,你就应该去检查这段代码,看看中间是否有哪些逻辑是可以独立成另外一个函数?在本例子中,main()函数中套了两层循环,但这两种循环相互间没有多大逻辑上的联系:内层用于实现一次完整的统计功能,外层则只负责是否需要继续下一次的统计。所以,把内层循环实现的功能独立“摘”出去,这是一个非常好的选择。
我们阅读VCL的源代码时(用Pascal实现),发现尽管VCL是一套庞大的类库,但其内部实现仍保持了相当好的简约风格,很少有代码超过200行的函数。这的确可以作为我们今后编写软件的楷模。
本例的完整请见相关例子文件。其中我还把前例的LowerToUpper()函数也加入使用。
12.3.2.3 求多种平面形状的面积
实例四:写一程序,实现求长方形,三角形,圆形,梯形的面积,要求各种形状分别用一个函数处理。
程序大致的流程是:
首先提问用户要求什么形状态的面积?然后根据用户的输入,使用一个switch语句区分处理,分别调用相应的函数。求不同形状态的面积,需要用户输入不同的数据,基于本程序的结构,我们认为将这些操作也封装到各函数比较合适。
先请看main()函数如何写:
int main(int argc, char* argv[])
{
char ch;
do
{
cout << "面积函数" <<endl;
cout << "0、退出 "<< endl //<--没有分号!用一个cout输出多行,只是为了省事
<< "1、长方形" << endl
<< "2、三角形" << endl
<< "3、圆形" << endl
<< "4、梯形" << endl; //<--有分号
cin >> ch;
if(ch == '0')
break;
switch(ch)
{
case '1' : AreaOfRect(); break; //长方形
case '2' : AreaOfTriangle(); break; //三解形
case '3' : AreaOfRound(); break; //圆形
case '4' : AreaOfTrape(); break; //梯形
default :
cout << "输入有误,请在0~4之间选择。" << endl;
}
}
while(true);
}
函数main()的任务很清晰:负责用户可以连续求面积,这通过一个do...while实现,同时负责让用户选择每次要计算面积的形状,这通过一个switch实现。而具体的,每一个平面图形的面积计算,都通过三个自定义的函数来实现。尽管我们还没有真正实现(编写)这三个函数,但这并不影响我们对程序整体架构的考虑。
当我们学会如何编写函数的时候,我们就必须开始有意识地考虑程序架构的问题。如果说变量,表达式等是程序大厦的沙子,水泥;而语句是砖头钢筋的话,那么函数将是墙,栋梁。仅仅学会写函数是不够的,还需要学习如何把一个大的程序分划为不同的功能模块,然后考虑这些模块之间的关系,最终又是如何组合为完整系统。
实例四的目的在于向我们演示:当你写一个程序时,有时候你不必去考虑一些小函数的具体实现,相反,你就当它们已经实现了一样,然后把精力先集中在程序总体架构上。
这种写程序的方法,我们称为“由上而下”型,它有助于我们把握程序主脉,可以及时发现一个程序中潜在的重要问题,从而使我们避免在开发中后期才发现一些致命问题的危险;同时也避免我们过早地在一些程序上的枝节深入,最终却发现这些枝节完全不必要。
不过,当程序很庞大时,想一次性理清整个程序的脉胳是不可能的,很多同样是重要的方向性修改都必须在对具体的事情有了分析后,才能做出准确的调整。另外,采用“由上而下”的开发方法时,有时也会遇上开发到后期,发现某些枝节的难度大大超过来原来的预估,需要占用过多开工期,甚至可能根本无法实现的危险。所以,我们还得介绍反方向的方法“由下而上”法。
采用“由下而上”时,我们会事先将各个需要,或者只是可能需要的细小功能模块实现出来,然后再由这些模块逐步组合成一个完整系统。采用由下而上的方法所写的代码还易于测试,因为这种代码不会过早地与其它代码建立关系,所以可以独立地进行测试,确保无误后,再于此基础上继续伸展。
一个小实例子引出这个大话题,有些远了,只是希望学习我的教程学员,能比其它途径学习编程的人,多那么一点“前瞻”能力。
最后,我给出AreaOfRect()函数的完整代码。另外几个函数,有劳各位自已在实例的源代码添加完整。
void AreaOfRect()
{
? int x,y;
? cout << "请输入长方形的长:";
? cin >> x;
? cout << "请输入长方形的宽:";
? cin >> y;
? cout <<"面积为:" << (x * y) << endl;
}
12.4 主函数
C,C++被称为“函数式”的编程语言。意指用这门语言写成的程序,几乎都由函数组成,程序运行时,不是在这个函数执行,就是在那个函数内执行。整个程序的结构看上去类似:A函数调用B函数,B函数又调用C函数,而C函数则可能调用了D函数后又继续调用E函数……甚至一个函数还可以调用自身,比如A函数调用A函数,或A调用B,而B又反过来调用A等等……
问题是最开始运行的,是哪个函数?
最开始运行的那个函数,称为主函数。主函数在控制台或DOS应用程序中。为main()函数。在标准的Windows应用程序中,则为名为WinMain()。
12.4.1 DOS程序的主函数
控制台应用程序的主函数:main()我们已经很“熟悉”了,每回写程序都要用到它,只是我们没有专门讲到它。现在回头看看:
int main(int argc, char* argv[])
{
……
return 0;
}
main函数的返回值数据类型为int,参数定义:int argc,char* argv[]的具体含义我们暂不用关心,只需知道,DOS程序或控制台程序中,程序运行时的入口处就是main()函数。
12.4.2 Windows程序的主函数
我们先来创建一个Windows应用程序。请注意看课程,不要轻车熟路地生成一个“控制台”工程。
请打开CB,(如果你正打开着CB,请先关闭原来的工程),然后选择主菜单File | New | Application,如果是CB5,选择File | New Application。
下一步请选择主菜单 Project | View Source,该命令将让CB在代码窗口中打开工程源文件,主函数WinMain正是在该文件中。请你在工程源文件(默认文件名:Project1.cpp)中找到WinMain()。
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
这行代码看上去很复杂,但无变不离其宗,你现在尽可以从位置上判断:函数名无疑是WinMain(),而WINAPI估计是“返回类型”,至于"HINSTANCE, HINSTANCE, LPSTR, int"则必是参数定义。尽管还有些细节需要确定,但我们现在能够看懂这些就已经是95%掌握了学习的重点。其它的且先放过。
由于现在我们很少采用Windows程序来做来实例,所以有必要验证一番,WinMain是否真的是Windows应用程序运行时的第一个函数?
还记得F8或F7吗?(有个女生站起,声音响亮:我记得F4!!!没听说要扩编为F8啊?)
在调试程序时,F8或F7键可以让程序单步运行,现在我们就来按一次F8,看看程序“迈”出的第一步是否就是WinMain()函数?请在CB里按F8。
程序先编译一番,然后便如上图直接运行到WinMain()这一行。我们这一章的任务也就完成了。按F9让程序恢复全速运行。然后关闭CB(不用存盘)。
12.5 小结
函数的声明起什么作用?
函数的参数起什么作用?
return 语句起什么作用?
大致说说动态库与静态库各自的优缺点?
函数带来的哪两个主要用处?
不看课程,你能自已写出小写字母转换大写的函数吗?
什么叫“由上而下”的编程方法,什么叫“由下而上”的编程方法?
什么叫主函数函数?DOS程序和Windows的主函数一样吗?