第十二章 函数(一)
函数是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 头文件:
打开的文件是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;
//提问是否继续统计: 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的主函数一样吗? |