C++Builder的基本功能
我们用高级语言写程序,我们很得意,因为高级语言比较接近人类的语言,使我们用起来得心应手,所以我们当然得意。但我们更得意的一定是让程序代码赶快变成可执行文件。
无论是在写代码的过程,还是最后要编译成可执行文件,都需要有一个工具存在。这一工具一般称为编程集成环境(IDE)。之所以称为集成,是因为从写代码到最后软件的出炉,我们需要它的地方实在太多了,这里列出其中最重要的功能项。
1、代码编辑:方便的代码编辑功能。尽管你可以使用记事本、Word或其它任何文本编辑器来写代码,但除非特殊需要,否则那将是极为低效的方法。相反,现在的编程集成环境,都相当的智能,举例如:代码自动功能,可以在很多情况下自动完成我们所需的代码,既准确还迅速。Borland公司出品的编程集成环境不仅有常见的关键字高亮等功能,还支持代码模板,支持键盘宏,同样支持高级的脚本插件功能。
2、界面设计:可视化的程序界面设计功能。你所要产生的窗口,在设计期间就真实地出现,包括字体、颜色和定位。比如:你不仅可以插入falsh的动画,而且无需运行,就直接可以在你的界面上看到该动画的演播,这是别的编程环境不能做到的。
3、程序编译:这是编程工具的主要功能。我们写的代码在成为机器能懂的可执行程序时,必须通过编译。
4、程序调试:如何尽量减少你程序的BUG呢?没有编程集成环境提供的强大调试功能,我们做的程序将毫无质量保证。
5、代码优化:Borland 提供的编译器,不仅在编译速度上一直在美国屡获大奖,而且其代码自动优化功能一直领先对手几近一个时代。使用编程集成环境,我们可以轻松获得更快更优的最终可执行程序文件。
6、辅助程序安装:程序的安装已属于另外一种工具的范畴,但我们仍可以通过编程集成环境来决定最终生成单一可执行文件,还是带有其它动态库。如果是后者,我们还可以通过集成环境来检查程序运行时调用了哪些动态库文件。
C++Builder 提供的功能远不止我上面所说的,并不是因为我嘴笨,而是我认为对一个工具,你只有动手使用,才会真正了解它。
C++是一门语言,而Borland C++Builder 则是语言实现工具。作为一个编程工具,CB提供以上功能正是份内之事。在这个意义上,你可以认为CB是Word2000,而C++则是英语或汉语。正如我们用英语或汉语在Word2000上写出优美文章,编程可说为:我们用C++语言在CB上编写出优美的程序。
VCL vs. MFC
在作为一种编程工具的意义上,我们认为C++Builder和你也许常听的VC(Visual C++)没有什么本质的区别。就像Word2000和WPS2000在本质都是字处理软件。但现在我们要从另外一个角度讨论C++ Builde这个编程工具。这个角度就是“封装”——面向对象编程思想中的最重要也是最基础的概念。
一个要学习编程的人,可能从C开始学起。学C时,我们没有接触那些挺玄的概念,到了C++,一切就来了,什么面向对象,什么封装、继承、多态……于是我们兴奋起来,努力去理解、掌握,运用这些概念所代表的技术,在掌握这些别人暂时未能理解的概念之后而颇有成就感……。
现在我要问的是,为什么要有这些概念?这些技术?正确回答这个问题,不仅有助于我们今后对编程语言各种概念的学习,而且它能让我们避免成为新技术的奴隶,这一切也许听起来有些形而上,不过我想通过以下讨论,至少可以回答一个很现实的问题:为什么要选C++Builder?而不是我们更常听的VC?这是我碰到的编程初学者较疑惑的问题之一。
如果人类长有翅膀,那么飞机大抵永远不会被发明。飞机的发明,是为了弥补人类自已不能飞翔的缺陷。不能说所有的技术都是这样,但C++对于C的发展,完全是为弥补程序员脑力的不足。一个在校生在学会C后,往往并没有机会用C去实践一个大中型的项目,体会不到在一个庞大软件工程中,非面向对象语言的短处,所以在之后学习C++的过程中,也就很难真正体会到面向对象语言的长处。简短一点说:不知道C的短处,就不懂C++的长处。相反,倒是很快就发现C++的缺点:它的代码效率多数情况下都要比C低不少。
前面我们说过低级语言与高级语言的对比,C++语言也正是从语法结构和语言功能上来限定或实现一门编程语言更加接近人在现实生活中的思维习惯,从而达到减轻人的记忆和判断上的负担。这其中最佳的方法之一就是所谓的“封装”。
VC的封装类库称为MFC,它是一种很低阶的封装,它并没有按照人类的思维习惯来重新组织和解释Windows对象(指Windows编程中所需的数据,处理,机制,接口), 而纯粹是API一对一的翻版。这样的封装工作带来代码封装所固有的代码效率降低的副作用,却没有给使用者带来任何方便。如果你是编程初学者,而你身边又有VC高手,那么你一定要多多向他学习请教,因为一个真正的VC编程高手,其同时一定也是一个深刻理解Windows内核机制(消息循环,内存管理,多任务实现,资源使用等),熟悉Windows各种常用API函数等等的高手。
C++Builder对封装库称为VCL(带VC字样,可别以为它是Visual C++,其实它是:Visual Component Library,即:可视控件库)。
VC的MFC和CB的VCL都是基于(但不限于)对Windows API(应用程序接口函数)的封装,为什么要对API进行封装?这就是回到了我们前面说过的,为什么有了C又会有C++的问题。因为操作系统是用C和汇编写成的,它获得到操作系统必须的代码效率,但对应用程序开发者而言,它失去了易用性。所以微软和Borland都使用高级语言对之进行封装工作。二者谁进行得更好呢?
要想成为Windows编程高手,最终一定要绕过各种封装,理解Windows对象。但作为一个初学者,我们必须挑选一个好的封装。下面我们举字体(Font)作为例子,将三者:没有封装过的Windows 字体API、封装过的MFC字体对象和封装过的VCL字体对象做一个对比。为了保证不会有偏倚和差错,有关前二者的代码,都是笔者从MSDN(微软提供的帮助文档)中直接拷贝出来。
Window API | Windows API创建指定样式字体: HFONT CreateFont( int nHeight, // height of font int nWidth, // average character width int nEscapement, // angle of escapement int nOrientation, // base-line orientation angle int fnWeight, // font weight DWORD fdwItalic, // italic attribute option DWORD fdwUnderline, // underline attribute option DWORD fdwStrikeOut, // strikeout attribute option DWORD fdwCharSet, // character set identifier DWORD fdwOutputPrecision, // output precision DWORD fdwClipPrecision, // clipping precision DWORD fdwQuality, // output quality DWORD fdwPitchAndFamily, // pitch and family LPCTSTR lpszFace // typeface name ); |
MFC (Visual C++) | 将HFONT封装为CFont BOOL CFont::CreateFont ( int nHeight, int nWidth, int nEscapement, int nOrientation, int nWeight, BYTE bItalic, BYTE bUnderline, BYTE cStrikeOut, BYTE nCharSet, BYTE nOutPrecision, BYTE nClipPrecision, BYTE nQuality, BYTE nPitchAndFamily, LPCTSTR lpszFacename ); |
VCL(C++Builder) | 将HFONT封装为TFont1 要设置字体名,高度,尺寸等使用以下代码: Font->Name = “宋体”; //设置为宋体 Font->Size = 24; //设置尺寸为24号2 将字体的粗,斜,下划线,删除线再封装为TFontStyle属性: Font->Style = Font->Style << fsBold << fsUnderlien; //字体增加粗体和下划线属性。 对于字体不常用的旋转等属性,不进行封装,你可以直接调用API函数来设置TFont的 Handle属性。 |
比较表中第一行和第二行:前者是原始的API,后者是VC精心的封装成果,可惜二者几近雷同;既然你要封装,你就是要让它变得面向对象,易记易用;一模一样的照抄一遍,然后改改参数的名字,意义何在?如你是想维持代码的效率,那么在繁杂度一样的情况下,为什么我们不直接使用效率更高的API函数呢?
倘若说,MFC的“封装”纯粹是一种多余,那或许也还可以接受。然而MFC偏偏还要在这种冗余的封装上建立自已的应用程序架构,和前面的“封装”一样,MFC建立应用架构的出发点也是良好的,为了方便Windows程序员编程的难度,而结果却更糟糕。有问题的架构犯了类库或接口提供者的大忌:“有协议编程”。
什么叫“有协议编程”?我们先来讲“无协议编程”。所谓“无协议编程”是指接口的提供者在提供接口时,同时也提供接口的使用约定,这一套约定应该在接口所要提供的功能上广泛适用,而无须再有种种特殊的例外,这样的接口显然非常适于使用。学习编程,如果挑错了我们每天都要面对的封装类库,就将永远都在努力处理这些无任何意义的问题。
MFC的CWnd 提供了对Windows最基本的窗口元素的封装,其中对创建窗口的函数的封装为:
未封装的API:
HWND CreateWindow(
LPCTSTR lpClassName, // registered class name
LPCTSTR lpWindowName, // window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle to parent or owner window
HMENU hMenu, // menu handle or child identifier
HINSTANCE hInstance, // handle to application instance
LPVOID lpParam // window-creation data
);
使用这个API函数,我们可以创建各种窗口。
CWnd封装的函数:
virtual BOOL CWnd::Create (
LPCTSTR lpszClassName,
LPCTSTR lpszWindowName,
DWORD dwStyle,
const RECT& rect,
CWnd* pParentWnd,
UINT nID,
CCreateContext* pContext = NULL
);
不用我说,你也能看出这仍然是个改改参数的蹩脚的封装。我们不去管它,现在我们关心的是:CWnd::Create 对CreateWindow 进行了封装,可是这一封装的结果是:原来CreateWindow能实现的一些事情,在 CWnd::Create里突然成了例外。是的,为了适应CWnd在MFC架构中所处的角色,程序员在涉及CWnd时必须记忆这样一条例外:
“CWnd的Create用于创建窗口的实际元素,但其中参数dwStyle不能包含有窗口风格中WS_POPUP(弹出式窗口),如果你要建立一个带有该风格的窗口,请使用CreateEx……”
我仍然要说VC也是一个很优秀的编程工具,但对于不想浪费无谓精力的编程初学者,我个人建议使用Borland C++Builder,因为它实现真正的对象封装,从而使你可以节省不低于80%的时间来学习编程本质的知识——就是我们常说的数据结构与算法,这些东西最终决定你的编程能力。
下面是C++Builder提供我们的最重要的东西:
一、VCL类库:一个好的底层类库,让我们从学习编程最初时刻就自然而然地学会使用面向对象的方法来写程序。它大大降低了我们入门门槛的高度,却又让我们一开始就站在比别人高的位置看待与程序有关的问题;
二、组件技术:组件技术代表了当今编程技术的主要方向,其设计思想与MS力推的Active 控件如出一辙:拥有相同的先进性。只有借助组件技术,我们才有可能从一个初学者,迅速成为可实际工作的编程工作者;另一方面,如果作为组件的提供者,我们可以编写组件的过程中迅速提高自已的编程能力。
C++Builder还提供了许多其它先进技术,如事件委拖等等,归根到底都是通过封装让Windows编程原本需要长期积累才有可能掌握的知识变得直观易懂。如果你刚刚开始学习编程,或者学习较长时间仍没有重大突破,或许使用C++Builder结合本课程系列,是个不错的选择。
附言:微软最近推出的C#相信会对上述MFC的不足做一个收拾,它对C++的扩展与约束与Borland C++Builder对C++的扩展与约束惊人的相似。如果你乐意,我也真的很建议你在学完C++Builde后,继续学习C#。
熟悉Borland C++Builder
一、界面:
启动Borland C++Builder后,你会发现它的窗口和我们常见的一体化窗口的应用程序有所不同:CB的各个子窗口并没有集成在一个主应用窗口中,而是分散为独立的主窗口。
(笔者的桌面分辨率为1024*728,但为了不让图的尺寸太大,我特意将各窗口拉得很小)
如果你打开CB后出现在界面和上图有所不同,可以通过File菜单,再选New Application,建一新的工程,则上图标注的2到4的窗口应该出现。如果仍然有缺,请用鼠标点上图的标注为“1”所示窗口的标题栏(以确保为活动状态),然后多按几次F11或F12,可以在以上窗口来回切换(两者功能有不同,可以自己试一下)。
下面我们来了解这四个窗口:
标注为1的窗口:这是我们比较熟悉的应用程序主窗口,虽然它看上去是一个长条,不过其上有主菜单(MainMenu),工具栏(Toolbar),和别的软件一样。不一样的是这一“长条”的右下部分的多页式工具条。事实上它并不是寻常意义上的工具条,因为其上的每一工具按钮并不提供执行某一命令的功能。我们称它为控件栏。控件是CB提供的先进的编程思想的体现之一,本章后面我们会初步学习如何使用控件。
标注为2的窗口:这是一个文件编辑的窗口,我们之所以称之为代码编辑窗口,或简称为代码窗口,由名及义,这是我们写程序代码的地方。
标注为3的窗口:称它为C++Builder软件的窗口并不妥当,它是我们自已要写软件的窗口。Windows操作系统的应用软件,譬如Word2000,譬如WPS Office,或者简单如画笔、记事本,总是会有一或多个窗口。这是Windows应用软件的典型特征(Windows操作系统也因此称为Windows)。
尽管也可以写没有窗口的应用程序,但大多数情况下我们的程序至少需要一个窗口,所以CB在创建新工程时,总是默认为我们生成一个主窗口,这就是标注为3的窗口—在程序运行前,我们称它为“设计表单”(Form);在程序运行之后,它就是我们程序中的窗口。我们的程序需要有几个不同的窗口,就可在设计期间生成同样多个类似标注为3的设计表单。
我对“表单”这个词总是无法产生具体的概念,可是不仅C++Builder还是Delphi或者Visual Basic(微软的得意之作),包括.net计划中的C#等快速应用程序设计系统,都使用Form这个词来称呼设计期间的窗口。所以我还是统一口径叫表单。但不管怎样,如果你在我的课程中偶尔发现设计窗口这样的说法,不用猜测,你尽管认定我就是在说表单。一个原则:只在设计期间,我们才有可能称它为表单,当窗口运行了,那就是窗口了,我们从不叫一个运行着的窗口为表单。
标注为4的窗口:标题写着“Object Inspector”直译为:对象检视器。事实上CB在调试程序时还有一个“Debug Inspector”,我倒觉得让后者叫前者的名字更合适。因为这里的Object也就对象,可不是我们以前说过面向对象的对象,它其实是用来查看,设置当前放在设计表单上的某个或某些控件的属性值和事件值的工具,后面我们会用到它。控件:也称为“组件”,有些CB的书区分这两者,认为前者是后者的某一特定的子集,就是窗口1右下的控件栏上的控件,至于控件的属性,稍后我们使用时,你就会了解;为了直观,我决定称4号窗口为控件属性检视器,或者属性检视器,或者属性窗口,总之离不了属性二字。
属性检视器有两页:Properties(属性)和Events(事件)。
参照下一页的图,在这里统一一下口径,如图中Properties和Events,我们称它为多页(Properties为第一页,Events为第二页)。
关于分散窗口(苹果机早期的应用程序风格)的得失,我们不想在此讨论。需要说明的是,CB的的各常用子窗口都提供Dockable,因此如果你喜欢集成式的窗口,大可通过鼠标拖拖放放来定制自已的集成窗口。
Dockable是指:拖动窗口A,当经由窗口B的某一边缘地带时,窗口A可以成为窗口B上的子窗口,而停靠在窗口B的某一角落。在CB里,不仅角落可以停靠,当位置为窗口B的中心时,窗口B还能以多页的方式加入窗口B。
大多数软件或许会在退出时保存住最后的窗口位置大小等设置,CB则提供你随时保存,调用各套桌面设置,比如编写代码时的桌面,调试时的桌面等。以下就是笔者常用的,用于编写代码时CB桌面设置之一,它被我存盘为“PowerEditDsk”。
这套桌面集成了类专家(ClassExplorer),工程管理(Project Manager)及消息窗口,任务列表(To-Do List)等窗口于代码编辑窗口内。
鉴于如果各位的桌面设置不统一会造成课程讲解上的一些困难,另外还有一个不是理由的理由:CB有关桌面的设置有烦人的BUG,所以我们的课程使用CB默认的桌面设置,也是文前标有1、2、3、4的那张图中所示的窗口位置。
编写自已的第一个程序,并且用字幕打出“世界你好!”—这是“很早很早”以前一本C程序教材的作者的发明,后来据说成了经典……不过很多人说这太过时了。不管怎样,我认为这作为我们初次使用CB的教学例子仍然很合适。
很多人可能感到有点突然,当CB慢吞吞启动后,一堆界面元素无论如何都让人感到这不是一个轻量级的人物;我们什么编程基础都没有,能驾驭好这个巨人吗?其实这就是CB的强大所在:具有高度的弹性,你可以用它编写很小的程序,也可以集合数十个程序员,用它编写大型软件。
二、牛刀小试:
1、创建空白工程:
这是Windows的天下,尽管在《白话C++》的学习中,Windows编程并非重点,但我们还是选择了Windows作为我们认识CB的起点。
从主菜单中选择 File | New Application;
(约定:在谈菜单命令时,我们约定用这种格式:File | New Application表示如上图的实际操作。)
执行New Application之后,CB为我们新建一个空白工程。
空白工程是指:绝大多数Windows程序所共同拥有的,必须的一个框架。再说白点,就是你每写一个新程序,都需要的一堆完全一样的代码,也就是说在多数情况,这是一步机械的工作。既然是“机械”的工作,当然由机器来完成最合适。
很多年前,笔者在VC和BC都未流行时,做了一个月“机械工”后,和许多那时的程序员一样,笔者很快尝试自已对这部分的代码进行封装。后来笔者又建议单位购买一套中国人写的窗口对象类库;再后来,笔者开始用BC、VC编程至今天。对于大家能有机会直接使用堪称最好封装的VCL学习编程,笔者能说的一句话就是:珍惜你的机会,珍惜你的20元钱。
空白工程带有一个名为“Form1”的表单(上一节图中标有“3”的窗口),这就是程序运行时的主窗口,验证一下你就能明白。
选菜单:Run | Run或者按 F9 键。空白工程是一个完整的工程,在被编译成程序后运行,出现一个标题为“Form1”的窗口。怎么和设计时的那个表单一模一样呢?当然,所见即所得嘛。不一样的地方是设计表单上有一些用于定位的小点,而运行后的窗口没有这些。
上面左图为表单,右图为窗口;表单(Form)是指设计时的窗口;窗口(Window)指运行时的表单。
2、世界你好:
从C++Builder的主窗口(上一节中标有“1”的窗口)右下部的控件栏中找到如下图所示的Label控件,同时记住Label控件在Standard页中。
上图中画有字母“A”的图标按钮即为Label,这个控件用来显示一些简单的文字内容。在鼠标按下该按钮后(按下后按钮的形状如上图),将鼠标挪到表单Form1上随便位置点左键一个Labe1控件将被放在表单上。
用鼠标再点一下Label1,确保它为如上图中的被选中状态(带有八个黑点块)。我们要通过修改属性,让它的内容显示为“Hello,World”。
在主菜单上依次点击View | Object Inspector(或者按F11键),将出现Object Inspector窗口(也就是上一节中标有“4”的窗口),我们称为控件属性检视器;通过“控件属性检视器”,我们可检查并设置当前选中的控件的属性(包括事件)。由于刚才选中了Label(如果不是,请重复用鼠标再点一下表单上的Label1)。操作后Object Inspector窗口的上部应显示如下图:
上面的 Label:TLabel表示当前属性窗口显示的是 Label1的属性。同时我们注意到Caption属性为Label1(注意:Name的属性也为Label1,千万别混了)。Caption意为“标题”,它决定一个Label显示的内容。你应该很明白怎么做了,在图中所示的Caption属性右边的编辑框内,将Label1改为 Hello,World。眼尖的学员一定发现,在改的同时,表单上的Labe1l如我们所愿,显示为“Hello,World”了。笔者我眼力不钝,但还是没有看到结果,原因是有一个窗口挡住了Form1;此时只要按一下F12键,Form1将跳到前面。
完成后按下F9键,只见屏幕一闪,第一个我们参与设计的程序闪亮登场了!
很好,我们已经向这个世界打了招呼,工作似乎有点成果。我们插播一段关于如何存盘的说明:在CB中保存程序,总体上和在字处理里保存一个文档是一样的操作,比如你一般都要给新文档命名,并且找到一个合适的位置保存。
这里讲一下特别之处:
其一、建议大家为本课程中所讲的例子程序准备一个统一的文件夹,然后以这个文件夹为父文件夹,再为每一个例子各建一个子文件夹(除非是教程中特别指出需要将两个例子工程放于一处)。
其二、在字处理中(如MS Word),一个文档就是一个文档,而在CB的程序工程中,一个工程除了工程自身的文件以外,还包含其它配套文件。这些文件中,有些可以在保存其它文件时自动保存,现在需要我们手工保存的是工程文件和代码文件。
保存文件是相当频繁的事,我们几乎不用菜单,而总是使用快捷工具按钮(Toolbar)或者键盘,尤其是后者。这里我只讲一个按钮,这个有一叠软盘的图标表示Save All(保存全部),现在请点它(或按Ctrl + Shift +S)。将弹出两次的存盘对话框,第一次要求存盘的是代码文件,CB默认的文件名为Unit1;我就采用了这个默认名,必竟这只是一个小小的工程。随后是工程文件要求存盘,我一样采用了默认的Project2.bpr的名字。(因为我们经常图省事,直接使用默认名,所以如果不将不同的工程存在各自的文件夹内,重名的文件名就将令事情很糟糕。)
最后是一些提醒:
提醒一:CB默认的存盘位置是其安装目录下的Projects文件夹。建议不要采用它,而是自已在外面新创建一个文件夹。
提醒二:当程序复杂时,程序运行甚至编译时就有可能造成死机。所以在写程序时常常存盘是一种非常良好且必要的习惯。
除了这些,存盘是件再熟悉不过的事了,在File菜单下还有不少和存盘有关的命令。有空大家一定看看。
三、DOS 版 Hello,World:
在本部教程内,我们要集中全部精力,扎扎实实地打下C++ 语言的基础。这是以后我们深入学习任何编程的要点,无论是Windows编程还是你想学Linux编程,也无论我们今后在实际工作中侧重于通讯或数据库或工控;甚至无论我们以后是否用C++这门语言编程;只要有C++根基,你就会比别人更容易掌握各种编程新知。
传言也并非都是假的,关于C++晦涩难懂之类的传言便很现实。另一方面,Windows编程的重点几乎全部都在于如何和Windows操作系统打交道,它要求你有很好的C语言基础。出于这样的考虑及在实际运行中遇到的困难,笔者放弃曾有的,“以语言为中心,同时学会Windows编程基础”的理想。写DOS风格的程序想起来是令人沮丧的;没有华丽的窗口界面,也不再使用那些功能强大的控件,但正因为DOS程序的朴素,它可以使我们更好地将重心放在C++语言本身;等到学习第二部教程时,我们才有足够的底气对Windows世界说一声:我来了!
1、空白控制台工程:
如果你的CB停留在前面Windows版Hello World工程中,请确保退出Hello World的窗口。然后选主菜单:File | Close All,关闭Windows版Hello World工程。
依次点击菜单:File | New,出现一个 New Items对话框,等待我们选择要新建什么。
默认的选中项是上页图中New页内的第一个:Application。如果选它,确认后的工程就是上一节我们创建的空的工程。但现在我们要建立一个DOS风格的程序。
选上图中加有红圈的 Console Wizard (控制台程序向导)。确认后出现向导的第一步:
请您确认将对话框中的相关选项设置成和上图一致,然后点OK按钮。没有表单,只有代码编辑框窗口(第一节的“2”号窗口)内中代码为:
//-------------------------------------------------------------
#pragma hdrstop
//-------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
return 0;
}
//-------------------------------------------------------------
这是C语言的主函数,或者称为入口函数。程序从这个地方作为起点开始执行,以后会有更多讲解。同样地,这也是一个程序框架,一个空白的DOS程序框架。让我们选菜单中Run | Run或直接按F9键,看看结果和前面的Windows空白程序会有什么不同?
眼前出现一个黑色的窗口,然后就一晃而过,回到了C++Builder的界面。 黑色的窗口,在不同版本的Windows中有不同的叫法和不同的实现机制,但都是Windows操作系统兼容DOS的方式。这种DOS方式在术语上称为“控制台/ console”。如果你不明白什么是DOS,请通过其它途径另外学习。
2、用控制台输出“Hello world”:
我们需要加入三行代码才能实现DOS版的Hello World;以下黑体部分为你需要在代码中加入的内容:
(约定:在代码中使用黑体,用来表示您应该输入的部分)
//-------------------------------------------------------------
#include <stdio.h>
#pragma hdrstop
//-------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
printf("Hello,world!");
getchar();
return 0;
}
//-------------------------------------------------------------
这是一个很短的程序,我们稍微做点解说:
如果作一个大的程序,代码一多自已都会看晕,所以就需要在代码中加一些注释,用以解说某行或某段代码的用途,或者用以让代码显得清晰。C++中,常用双斜杠 “//”开始,一直到该行结束的内容来代表注释。上面的代码中,有三行:
//-------------------------------------------------------------
这正是用来从视觉上分隔代码的注释。注释对程序的运行没有任何影响,其作用只用来给人看的。当编译器编译进,它会过滤掉所的有注释内容。
#pragma hdrstop和#pragma argsused两行为编译预处理命令,当编译器在编译本段代码,预处理命令会影响编译的某些行为。
#include <stdio.h>是我们新加的一行,stdio.h是一个文件的名字,扩展名是.h。h是head的意思,所以被称为头文件。这个文件在哪里呢?在你的机器里,在C++Builder安装后的文件夹内的某个子文件夹内;在这个文件夹内,C++Builder为我们提供了上千个头文件。
头文件起什么作用呢?
C++Builder为我们提供了函数库(RTL /runtime library)和类库(VCL);而Windows操作系统(Windows用C写成)则向我们提供了API函数库。“库”是“储存”大量实用的解决各种问题,实现各种功能的“工具”的地方,它们都通过头文件列出清单。
当我们写上#include <stdio.h>这一行时,我们是在向编译器说明:本文件中的程序需要用到stdio.h头文件中所列的某些函数;“include”正是包含之意。
printf("Hello,world!");
getchar();
printft和getchar正是两个在stdio.h中列出来的函数。如果没有#include <stdio.h>这一行,编译器便会报错说不认识这两个符号。换句话说,include语句让编译器知道我们要用的库函数在哪一个库里。我们会在下一节“简单程序调试”中做相应试验。
Print是打印之意,而f则为format,二者结合意为“有格式地打印”。我们的Hello World不需要任何格式。至于getchar从字面上理解是“得到字符”,事实上就是程序会在此处停下来,等待用户输入字符,直到输入一个回车符,程序继续执行。在我们的这个程序里,继续执行的结果是碰上这一行:
return 0;
return的意思是返回,当主函数main返回时,整个程序就结束了。所以,使用这个getchar()的目的很明显:让程序在结束之前可以等我们一下(因为我们需要看一眼输出的“Hello World”)。
Printf的作用是输出(output)内容,而getchar()则是等待你输入,从这一点看,我们也可以理解前面include为什么是 stdio.h了。std是英文标准的前三个字符,而I和O分别是input和output的首字母。
说了很长,但程序运行结果却很简单,这是按F9后的运行结果:
按回车结束程序。结束后,别忘了保存我们的第二个程序。
四、简单程序调试:
1、编译期错误与运行期错误:
如果我在上面的程序输出时,打出的字幕不是“Hello,world”,而是“Hello,word”。你一定会说“哎,哥们,程序错了!”。于是,我就要去代码中查找有关输出的那一句,一看,哟,真少写了一个‘l’。将这一错吴改正,重新运行,检查结果。
这就是一个调试过程,发现错误;查找出错原因;改正;再运行。当然,由于很多时候我们也不是非要等到发现错误了才去检查,有时我们会主动一步步去事先主动检查是否有错。必竟BUG总是常见的。程序错误(BUG)表现上各式各样,但若是从其“发作”的时间上分,则可分为“编译期错误”和“运行期错误”。如果上面说的错误属于后者,因为它对编译过程并不产生什么阻碍,编译可以通过,错误将在运行中表现出来(按时间分还有一种称为“链接期错误”,这里不说)。
在上一节的代码中,我们来故意制造一个编译期的BUG,我们故意将最后一句代码:return 0; 行末的分号“;”删除,代码如下:
int main(int argc, char* argv[])
{
printf("Hello,world!");
getchar();
return 0 //根据约定,我们用粗斜体表示修改过的代码
}
写好代码后按F9,程序并没有跑起来。但在代码编器窗口下面,出现一消息框(如果你没有发现,请在代码窗口中点鼠标右键,将出现右键菜单,选:Message View项)。
编译过程是一个相当复杂的过程,在编译之前,代码会被做很多格式上的转换,人写代码出错的原因也五花八门,所以想让编译器判断到底你犯了什么错吴事实上是很难的,但CB的编译出错消息仍竭尽全力让我们找到出错原因:
[C++ Error]:错误类型;这是在告诉我们,这是一个C++语言语法方面的错误。编译器说对了,因为我们少写一个分号,确实是犯了C++语法错误。
Unit.cpp(13):错误位置;说的是在Unit.cpp这个文件内的第13行,出错的文件显然是说对了,但行数却稍有偏差,少了分号的那行其实是第12行,从状态栏可以看到当前光标位置的行列数。
E2378:错误编号,CB对各错误消息进行了编号。
Return statement missing;错误消息;在这里这个消息完全正确,“return”语句丢了分号。
在使用中,并不是每回都能幸运得到这样准确的错误报告和消息。比如,同样是去掉分号,但这回是把return 0一句的分号补上,转而去掉printf(“Hello world!”)这一行的分号,编译出错消息是这样:
错误消息说我们调用了一个“非函数”,为什么呢?答案是因为没有了分号,编译将两句连成一句:printf(“Helo,world!”)getchar();这样,它就把printf(“Helo,world!”)getchar整个儿认为是一个函数名,于是就不认识它了。
下面我们做一个实验,结果报错也同样为“Call of nonfunction”,但原因不是我们写错了函数名,而是我们使用了库函数,只是没有通过include语句来告诉编译器:这些函数在哪个库里。
//-------------------------------------------------------------
//#include <stdio.h>
#pragma hdrstop
//-------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
printf("Hello,world!");
getchar();
return 0;
}
//------------------------------------------------------------
代码中,我们故意用“//”将#include <stdio.h>从一行代码摇身一变成为一行注释,前面说过注释是仅给人看的,编译看不到这一行,相当于这一行被删除了;现在编译这一行,错误提示如下:
两行同样类型的错误消息,编译器一下子不认识printf和getchar两个函数了。
五、学会使用帮助文档:
经过上一节,同学可能会问三个问题:
第一:怎么知道有printf和getchar这个两个函数呢?
回答:有两个途径,常用的函数会在课程里讲到,其它函数则根据需要在帮助文档中查找。
第二:怎么会知道printf是用来输出,而getchar则可以用于等待输入?
回答:和上面一致,从课程和帮助文档中获得。
第三:就算知道了printf这个函数,又怎么才能知道它在stdio这个头文件对应的库里?
回答还是一样。
学会使用帮助文档很重要,文档有多种,比如有专门的函数大全之类的书,不过现在的文档是帮助文档。初学者在调试程序时,很多人由于英文不佳,于是根本不去动帮助文档。其实当你带些具体的目的去看帮助文档,你会发现就算英文不好,也可以从中获得帮助,解决大部分程序问题。
我们用一节的那个BUG来说明问题。假如你用了printf和getchar这两个函数,但你不知它们包含在哪个库里(也就是说你不知道在#include 之后应该写“stdio.h”),可以这样作:
依次点击菜单:Help | C++Builder Help。
在出现的Windows标准的帮助索引窗口中的索引行内键入:printf回车。
找到的有关printf 的帮助如下一页的图示:
提高英语水平对学习编程的帮助是很大的。
数据类型的基本概念
一、数据类型:
在计算机编程世界中,有哪些基本的数据类型?在人类世界里,数据类型很多,把人类的对万物划分类型的方法照搬入计算机世界,显然不可能,怎么办呢?方法就是:抽象。计算机先哲们为我们做了这一切;其中最重要的类型也称为“C和C++语言的基本数据类型”,只有两个:“数值”和“字符”。
现实生活中,数字被广泛地应用在两种不同的范畴;其一:是那些典型的,需要进行计算的场合;其二,则是那些只用来表示符号的范畴。比如电话号码和车牌号,把两个电话号码进行相加或相减的操作是没有意义的。基于数字的两种完全不一样的使用范畴在被抽象到计算机程序语言时,数字被分到“数值”和“字符”两种类型中;但字符类型并不只有阿拉伯数字,键盘上的字母符号,都属于字符类型。
在C和C++中,“字符类型”其实也可归入数值类型。在某些情况下,它仍然会被用来参与计算。比如在计算机中,字符‘A’加1后,会得到下一个字符‘B’,这种操作符合我们的习惯。
二、整型和实型:
数值类型又被分为“整型”和“实型”。整型就是不带小数位的数,而实型则是指带小数位的数,也称为“浮点数”。我们在生活中一般并不做如此区分。譬如说鸡蛋的价格罢,今儿便宜了,一斤2块整,但这不防碍明儿价涨了,我们说成:涨了,2.30元一斤。在编程时可不能这样,你必须事先考虑到鸡蛋的价格是必须带小数的,就算现在是2块整,也得将价格定为实型,否则,涨成2块3时,计算机会把那0.3元给丢了,还是变成2元整。
你也许会问,为什么不直接就定一个实型就好呢?实型不就包含了整型的值吗(如2.0等于2元)?回答很简单:因为在计算机里,对整数的运算要远远快于对浮点数的运算,所以,在人类的思维习惯与计算机特点之间,必须做一个折衷。折衷的结果就是虽然都是数值类型,但程序员在写程序时,必须事先想好,哪些数据是必须带小数计算的,那些数据定为实型较好,比如工资,没有哪个员工会认为老板可以将自已工资中的小数位忽略不计;而那些可以不用,或者可以不计较小数的数值,则被建议设计成整型,比如人的年纪,虽然可以,但我们很少需要有类似1.6岁这种说法。想想,假如一位女士说自已28岁,而你却立刻纠正她:“不,准确地说,您已经28.7岁了”……结局会怎样呢?为了那0.7岁,不仅你的程序会跑得慢,而且会倍受女人的白眼杀伤。
想想其实也很自然:工资必须设计成实型,而年龄虽然不是必须,但建议设计成整型。
三、数值的范围:
顾及计算机的运行速度,人(程序员)做出了妥协,必须对“整型”或“实型”做出考虑。另一方面,和速度同样重要的是计算机的空间的考虑,它让程序员必须再做一步妥协。
有时候,在计算机中你设置了一个数,当再住这个数加1时,真不幸,计算机会告诉你说:加1是加1了,可是结果变成0,甚至是负数。这就是计算机的空间问题:任何一个量,都有一个大的上限,和小的下限,出了这个范围(比上限还大,比下限还小),就会称为溢出。这是一种物理的现实,也是一种人为的规定。为什么要这样规定?原因是计算机的存储器,特别是其中很重要的内存,其可存储的数据多少总是有限度的(而且,同样大小数,2进制的表达形式比10进制长得多),如果允许编程像生活中一样任何一个数都可以很大很大,也就是这个量表达起来将很长很长,那么存储器的空间就会很快用完!(无穷大就不用说了,因为不可能有无穷大的存储器)。
就是这样,数值类型在被划分为整型和实型后,将根据所占用的空间而被继续划分为几种不同的类型。而我们在考虑工资必须设计成带小数的实型后,必须再面对一个设计上的考虑,工资这个量在程序里要占多大空间?依据其实很简单,就看你的单位最高月薪是多少,凭此找到一个合适的类型。比如月薪不超过1万元,那我们选择一个刚刚可以包含10000的数据类型。
两种基础类型:数值类型和字符类型,前者说了很多,现在我们也来对字符类型做一些附加说明。字符类型要比数值类型简单得多,它不能带小数,所以没有整型实型之说;它占用1个字节,已经是计算机能单独处理的最小空间单位,所以也不存在继续分为不同空间长度的问题,所以我们将以它为例,详细说明有关数据类型的一些基本而重要的概念。
第一:由于计算机和编程都是老外的发明,而老外生活中常用的字符并不多(主要是阿拉伯数字、英文字母、标点符号等),所以字符的宽度被定为1个字节。
1字节 = 8位,所以它能表示的最大数当然是8位都是1(既然2进制的数只能是0或1,如果是我们常见的10进制,那就8位都为9,这样说你该懂了?)。
1字节的二进制数中的最大数是11111111。
无论是什么进制,都是左边是高位,右边是低位。第一章中我们说过,10进制数的最低位(个)的权值是100,第二位是101,第三位是102……,用小学课本上的说法就是:个位上的数表示几个1,十位上的数表示几个10,百位上的数表示几个100……
二进制数则是:第1位数表示几个1 (20),第2位数表示几个2(21),第3位数表示几个4(22),第4位数表示向个8(23)……
在C和C++中,很多计数的习惯都是从0开始,所以在你看明白上面那些内容后,让我们立刻改口换成下面的说法,以后我们只用这种说法:
二进制数:第0位数表示几个1 (20),第1位数表示几个2(21),第3位数表示几个4(22),第4位数表示向个8(23)……按照这种说法,我们可以发现,从右向左数,第n位数的权值 = 2的n次方。
二进制各位权值的计算方法: 第n位权值 = 2n。
下表详细地表示 2进制数11111111 是如何逐位计算,累加得到10进制的值:
第几位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 合计 |
权值 | 27=128 | 26=64 | 25=32 | 24=16 | 23=8 | 22=4 | 21=2 | 20=1 | |
2进制 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | |
10进制 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | 255 |
上表表示了这么一个计算过程(*表示乘号):1 * 27 + 1 * 26 + 1 * 25 + 1 * 24 + 1 * 23 + 1 * 22 + 1 * 21 + 1* 20 = 255
计算结果是:11111111(b) = 255 (d)
(为了不互相混淆,我们在书中常用(b)来表示前面的数是2进制的,而(d)则表示该数是10进制数。同样地,另有8进制数用(o)表示,16进制用(h)表示。不过记住了,这只是在书中使用,在程序中,另有一套表示方法。)
以前我们知道1个字节有8位,现在通过计算,我们又得知:1个字节可以表达的最大的数是255,也就是说表示0~255这256个数。那么两个字节(双字节数)呢?双字节共16位1111111111111111,这个数并不大,但长得有点眼晕。
从现在起,我们要学会这样来表达二制数:
1111 1111 1111 1111,即每4位隔一空格。
双字节数最大值为:
1 * 215 + 1 *214 + 1* 213 + 1 * 212 + 1 * 211 + 1 * 210 + …… + 1 * 22 + 1 * 21 + 1* 20 = 65535
很自然的,我们可以想到,一种数据类型允许的最大值,和它的位数有关。n位二进制数的最大值:
1 * 2(n-1) + 1 * 2(n-2) + ... + 1 * 20
任何一种基本数据类型,都有其范围。比如字符类型,它的最大值是255,那么当一个数在其类型的范围已经是最大值时,如果再往上加1,就会造成“溢出”。
其实,有限定范围的数量并不只在计算机中出现,钟表就是一个例子;10点再加1点是11点,再加1点是12点,可是再加1点,就又回到1点。再如汽车的行程表,假设最多只能显示99999公里,当达到最高值后继续行驶,行程表就会显示为00000公里。
四、有符号数和无符号数:
回头看上一节,我们所讲的数都是正数。同样是年纪和工资,前者不需要有负值,但后者可能需要。
那么,负数在计算机中如何表示呢?关于这一点,你可能听过两种不同的回答。
一种是教科书,它会告诉你:计算机用“补码”表示负数。可是有关“补码”的概念一说就得一节课,这一些我们需要在第6章中用一章的篇幅讲2进制的一切;再者,用“补码”表示负数,其实只是说出一种公式,公式的作用在于告诉你,想得到问题的答案,应该如何计算。却并没有告诉你为什么用这个公式就可以得到答案?
另一种是一些程序员告诉你的:用二进制数的最高位表示符号,最高位是0,表示正数,最高位是1,表示负数。这种说法本身没错,可是如果没有下文,那么它就是错的。至少它不能解释出,为什么字符类型的-1用二进制表示是“1111 1111”(16进制为FF);而不是我们更能理解的“1000 0001”(为什么说后者更好理解呢?因为既然说最高位是1时表示负数,那1000 0001不是正好是-1吗?)。关于这一点,让我们从头说起。
1、你自已决定是否需要有正负。
就像我们必须决定某个量使用整数还是实数,使用多大的范围的数一样,我们必须自已决定某个量是否需要正负。如果这个量不会有负值,那么我们可以定它为带正负的类型。
在计算机中,可以区分正负的类型,称为有符类型,无正负的类型(只有正值):称为无符类型。
数值类型分为整型或实型,其中整型又分为无符类型或有符类型,而实型则只有符类型。
字符类型也分为有符和无符类型。比如有两个量,年龄和库存,我们可以定前者为无符的字符类型,后者定为有符的整数类型。
2、使用二制数中的最高位表示正负。
首先得知道最高位是哪一位?1个字节的类型,如字符类型,最高位是第7位;2个字节的数,最高位是第15位;4个字节的数,最高位是第31位。不同长度的数值类型,其最高位也就不同,但总是最左边的那位(如下示意中的粗体字为最高位)。字符类型固定是1个字节,所以最高位总是第7位。
单字节数:1111 1111
双字节数:1111 1111 1111 1111
四字节数:1111 1111 1111 1111 1111 1111 1111 1111
当我们指定一个数量是无符号类型时,其最高位的1或0和其它位一样,用来表示该数的大小。
当我们指定一个数量是无符号类型时,此时,最高数称为“符号位”。为1时,表示该数为负值,为0时表示为正值。
3、无符号数和有符号数的范围区别。
无符号数中,所有的位都用于直接表示该值的大小,有符号数中最高位用于表示正负,所以,当为正值时,该数的最大值就会变小。我们举一个字节的数值对比:
无符号数:1111 1111值:255 1* 27 + 1* 26 + 1* 25 + 1* 24 + 1* 23 + 1* 22 + 1* 21 + 1* 20
有符号数:0111 1111值:127 1* 26 + 1* 25 + 1* 24 + 1* 23 + 1* 22 + 1* 21 + 1* 20
同样是一个字节,无符号数的最大值是255,而有符号数的最大值是127。原因是有符号数中的最高位被挪去表示符号了。并且,我们知道,最高位的权值也是最高的(对于1字节数来说是2的7次方=128),所以仅仅少于一位,最大值一下子减半。
有符号数的长处是可以表示负数。因此,虽然它的在最大值缩水了,却在负值的方向出现了伸展。我们仍以一个字节的数值对比:
无符号数: 0 ----------------- 255
有符号数: -128 --------- 0 ---------- 127
同样是一个字节,无符号的最小值是 0 ,而有符号数的最小值是-128。所以二者能表达的不同的数值的个数都一样是256个。只不过前者表达的是0到255这256个数,后者表达的是-128到+127这256个数。
一个有符号的数据类型的最小值是如何计算出来的呢?有符号的数据类型的最大值的计算方法完全和无符号一样,只不过它少了一个最高位(见第3点);但在负值范围内,数值的计算方法不能直接使用1* 26 + 1* 25 的公式进行转换。在计算机中,负数除为最高位为1以外,还采用补码形式进行表达。所以在计算其值前,需要对补码进行还原。这些内容我们将在第六章中的二进制知识中统一学习。
这里,先直观地看一眼补码的形式:
以我们原有的数学经验,在10进制中,1表示正1;而加上负号,-1则表示和1相对的负值。
那么,我们会很容易认为在2进制中(1个字节): 0000 0001表示正1,则高位为1后:1000 0001应该表示-1。
然而,事实上计算机中的规定有些相反,请看下表:
二进制值(1字节) | 十进制值 |
1000 0000 | -128 |
1000 0001 | -127 |
1000 0010 | -126 |
1000 0011 | -125 |
... | ... |
1111 1110 | -2 |
1111 1111 | -1 |
首先我们看到,从-1到-128,其二进制的最高位都是1(表中标为红色),正如我们前面的学。
然后我们有些奇怪地发现,1000 0000 并没有拿来表示 -0;而1000 0001也不是拿来直观地表示-1。事实上,在计算机中-1用1111 1111来表示。
怎么理解这个问题呢?先得问一句是-1大还是-128大?
当然是-1大,-1是最大的负整数。以此对应,计算机中无论是字符类型还是整数类型,也无论这个整数是几个字节。它都用全1来表示 -1。比如一个字节的数值中:1111 1111表示-1,那么,1111 1111 - 1 是什么呢?和现实中的计算结果完全一致。1111 1111 - 1 = 1111 1110,而1111 1110就是-2。这样一直减下去,当减到只剩最高位用于表示符号的1以外,其它低位全为0时,就是最小的负值了,在一字节中,最小的负值是1000 0000即-128。
我们以-1为例,来看看不同字节数的整数中,如何表达-1这个数:
字节数 | 二进制值 | 十进制值 |
单字节数 | 1111 1111 | -1 |
双字节数 | 1111 1111 1111 1111 | -1 |
四字节数 | 1111 1111 1111 1111 1111 1111 1111 1111 | -1 |
可能有同学这时会混了,为什么1111 1111有时表示255,有时又表示-1?所以我再强调一下本节前面所说的第2点:你自已决定一个数是有符号还是无符号的。写程序时,指定一个量是有符号的,那么当这个量的二进制各位上都是1时,它表示的数就是-1;相反,如果事选声明这个量是无符号的,此时它表示的就是该量允许的最大值,对于一个字节的数来说,最大值就是255。
这一节课,看似罗嗦,但我希望每位没有编程基础,或者以前对进制,对负值、补码和反码等概念,对数据类型理解不透彻的学员,都能多花点时间反复阅读,直到看懂文中的每一张图表的意思为止。
五、字符集和保留字:
字符集和保留并不专属于“数据类型”的基础知识,它是一门语言最基础的东西,就像字母A-Z对于英语的作用一样,我把它放到这里,更多的是因为这是我们第一次要碰到它,在下一节中马上就要用上了。感觉上它的难度和学会26个字母差不了多少。
字符集:
每种语言都使用一组字符来构造有意义的语句,组成C++程序,最终是以下这些字符(空格这个字符不好表示,就直接写上“空格”两字了,以后同):
26个字母的大小写形式:ABCDEFGHIJKLMNOPQRSTUVWXYZ,abcdefghijklmnopqrst;
10个阿拉伯数字:
0123456789;
其它符号:
+ - * / = , . _ : ; ? \ " ' ~ | ! # % & ( ) [ ] { } ^ < >(空格)
其它符号(包括汉字),可以作为程序中字符串的内容,用于显示等。
最后要说明的是,C和C++语言是区分大小写的语言,也就是说ABC和abc并不相同。
保留字:
保留字也称关键字,它是预先定义好的标识符,这些标识符必须保留给C++语言自身专用,因为它们用来在编译过程中表示特殊的含义;比如,我们想定义一个量为整数类型,那么C++就必须有一个词来表示什么是整数类型,这个词就是一个关键字。
C和C++主要的关键字,我们在章末附表列出。下面先列出本章要用的关键字:
char :字符类型
int : 整型
float :单精度实型(浮点型)
double : 双精度实型
unsigned : 无符号类型
signed : 有符号类型
bool : 布尔类型
true : 布尔类型的真值
false : 布尔类型的假值
void : 无类型
sizeof : 取得指定类型的所占用的范围
typedef : 为某种类型取一别名
六、基本数据类型:
前面讲了一些有关数据类型的各种基本概念,下面是数据类型这一课真正开始的时候,如果在其中你有什么概念不能理解,最好的方法就是首先回头看本章前面的内容。
1、字符型和各种整型、实型:
类型标识符 | 类型说明 | 长度 | 范围 | 备注 |
char | 字符型 | 1 | -128 ~ 127 | -27 ~ (27 -1) |
unsigned char | 无符字符型 | 1 | 0 ~ 255 | 0 ~ (28 -1) |
short int | 短整型 | 2 | -32768 ~ 32767 | 2-15 ~ (215 - 1) |
unsigned short int | 无符短整型 | 2 | 0 ~ 65535 | 0 ~ (216 - 1) |
int | 整型 | 4 | -2147483648 ~ 2147483647 | -231 ~ (231 - 1) |
unsigned int | 无符整型 | 4 | 0 ~ 4294967295 | 0 ~ (232-1) |
float | 实型(单精度) | 4 | 1.18*10-38 ~ 3.40*1038 | 7位有效位 |
double | 实型(双精度) | 8 | 2.23*10-308 ~ 1.79*10308 | 15位有效位 |
long double | 实型(长双精度) | 10 | 3.37*10-4932 ~ 1.18*104932 | 19位有效位 |
unsigned用于修饰int和char类型,它使int或char类型成为无符号类型。
signed是unsigned的反义词,如signed int表示有符号类型,不过signed可以省略,所以上面列出char,short int,int都是有符号类型。
有short int(短整型) ,所以也就有对应long int(长整型)。long int 用于表示4个字节(32位)的整数。但是在我们现在普通使用的32位计算机中,int默认就是4个字节,所以long也是可以省略的。
(较早几年,也就是Windows 3.1/DOS 流行的时候,那时的机器及操作系统都是16位的,这种情况下,int默认是16位的,此时,如果想实现32位整数,就必须定义为这样的类型:long int)。
在浮点数方面,我们最常用的将是double,它的精度适合于我们日常中的各种运算。当然,float的精度也在很多情况下也是符合要求的。
2、布尔型(bool)和无类型(void):
除字符型,整型,实型以外,布尔型和无类型也是较常用的两种数据类型。
布尔型(bool):
布尔类型是C++的内容,C语言没有这一类型;布尔类型的数据只有两种值:true(真) 或 false(假)。
什么时候使用布尔型呢?
履历表中一般有“婚否”这一项,婚否这种数据就适于用真或假来表示。性别“男”或“女”,有时也会用布尔值表示,(一般程序都不约而同地把男性设置“真”,女性设置为“假”。)
无类型(void):
这个类型比较怪,“无”类型,没有类型的类型。或者我们这样认为比较好接受:在不需要明确指定类型的时候,我们可以使用void来表示。
七、为数据类型起别名:typedef
用法:typedef 原类型名 类型的别名;
为什么要给现成的数据类型起别名?
当然这也是为了迁就我们为人类,就像我们给人家起绰号一样,形象好记,不易混淆。
比如,我们在编程中需要使用一些年龄数据,应该使用整型(int)类型。另我们还使用到身高的数据,由于单位采用“厘米”,所以也可能使用int类型。两种数据属于同一数据类型,但所代表的内容却不容混淆。我们可以使用typedef来临时为int取两个别名:
typedef int AGE;
typedef int STATURE;
通过以上两行话(行末都需要以分号结束),我们获得了两种新的数据类型名称。它们的一切属性都和其原名int的数据类型完全一致,只是名字变得有意义了一点而已。
这里,为了说明问题举一个简单的例子,(事实上例子的情况并不值得使用typedef来取别名)来说明typedef常用来为一些复杂的类型取一简单且意义明了的别名;比如定义函数的指针,在C++ Builder的控件编写中,更是有非常频繁地用typedef来为那些定义复杂的事件函数(类成员函数的指针)取别名。
sizeof的使用:
用法: sizeof(数据类型)
sizeof(变量)
C和C++提供了关键字sizeof,用于在程序中测试某一数据类型的占用多少字节。sizeof有两种用法,效果一样。由于我们还没有学变量,所以我们先讲第一种。
sizeof是一种计算,计算的对象是指定的一种数据类型,计算的结果是该数据类型占用的字节数目。如:
sizeof(char) = 1;也就是说char类型占用1个字节。
sizeof(int) = 4;也就是说int类型占用4个字节。
使用sizeof的例子:
下面我们来完成一个实例,这个例子使用sizeof将我们已学的所有数据类型的名称所占用的字节数列出。
首先,新建一个控制台工程,其余操作步骤如下:
1、选择主菜单File | New;
2、New Items 对话框,选 New 页中的 Console Wizard,然后点OK;
3、Console Wizard对话框,Source Type中选中C++,右边的分组中只选中“Console Application”后点击OK按钮。
完成以上步骤后,代码编辑窗口自动新建并打开Unit1.cpp,其代码如下:
//-------------------------------------------------------------
#pragma hdrstop
//-------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
return 0;
}
//-------------------------------------------------------------
保存工程文件为SizeOf.bpr,保存CPP文件为Unit1.cpp。按F9编译并运行这个空白工程,你会发现一个DOS窗口一闪而过,这我们在第二章就知道了。
添加以下代码中的粗体部分:
//-------------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//-------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
getchar();
return 0;
}
//-------------------------------------------------------------
在第二章中我们加过类似的代码,目的都是:让程序执行以后不是自动结束,而是在我们敲一个回车键才结束,因为我们需要查看程序的执行结果。以后我们所有使用控制台形式的程序,都将需要这两行代码,不过以后不会特别指出了,如果你对这两话的作用还有些模糊,最好的办法是现在再按一次F9键,运行这个程序。
下面开始我们的sizeof用法初试。
我们这次的目标是输出所有我们已学的数据类型:整数(含unsigned,short的修饰等);实型(含各精度);字符型;布尔型等数据类型占用的字节数,但我们事实上还没有学过C和C++的任何语法,所以我想我们还是先来一个简单的,确保成功以后,再完成所有的语句。
添加以下代码中的粗体部分:
(为了节省版面,我只抄出需要变化的main那一部分,之前的代码由于不再需要变化就不抄到这里了,省略号只用来示意前面还有代码,你别把……也输入代码中啊!)
……
int main(int argc, char* argv[])
{
cout << "Size of char is : " << sizeof(char) << endl;
getchar();
return 0;
}
//-------------------------------------------------------------
这次输入的是这一行:
cout << "Size of char is : " << sizeof(char) << endl;
这是一行标准的C++ 语句,cout在 C++里表示“标准输出设置”,这曾经是每个学习C++的人必学的内容。在Windows的编程里,它已是昨日黄花,我想诸位就不用再花什么时间去学习它了,只要简单地弄明白它的基本用法就是。
cout和<<合用,后者是两个小于号,你可别老眼昏眼看作是中文的书名号《;<<在C++里属于操作符重载,我们以后在学习时,还会在不同的场合里遇见它的另一位老兄:>>。理解并且迅速记下<<的作用是看它的箭头方向:
<<指向左边,它的意思是把它右边的东西赋予或接到右边。现在,它的左边是cout;cout这里就是屏幕,给屏幕东西,专业的说法就是:输出到屏幕。
图示为:将 "Size of char is : " 输出到屏幕,当然一对双引号是不会被输出的,因为它们用来表示这是一句话。
cout不仅可以输出一句话(字符串),而且可以用来输出各种C和C++基本类型的数据,并且通过<<可以一个接一个地输出所需内容。在上面的那行代码中,它首先输出:"Size of char is : ";然后是输出 sizeof(char)的计算结果;最后输出endl;endl表示换一新行(回车换行)。
理解了cout部分,那一行代码中我们惟一不明白的就是行末的那个分号了。是啊你看到了没有,所有C和C++语句行都在语句后有一个';',和其它C++语句的主要组成部分一样,它必须是英文符号,如果你打入一个中文的符号现在改还来得及。并且从此刻起就记住了,所有C++语句中用到标点符号,除非是指定就要用中文作为输出等参数,否则全都必须是英文的。现在,你可以放心地把那句话原样照敲入你的代码中了。聪明的学员可能已经完成“Ctrl + C"/"Ctrl + V"大法,直接粘贴了,不过如果你真的以前还没写过C和C++代码,还是动手输入。
按F9编译,运行结果如下图:
"Size of char is : 1 "中,最后的那个1就是sizeof(char)的计算结果。至于那个endl呢?你看,光标不是在下一行的行首闪烁吗?如果你把代码改成这样:
cout << "Size of char is : " << sizeof(char);
那么,输出结果几乎一模一样,除了光标改成在行末。 我们之所以需要换行,是因为后面还要输出。
你可能喜欢用中文来输出?好吧。
cout << "char(字符)占用的字节数: " << sizeof(char) << endl;
结果是:
我默认不采用中文的原因只是因为学生容易在切换中英输入法的关节出错,结果在不该使用中文的时候输入了一些中文字符,造成无谓的错误,白白浪费学习时间。
再接再厉,我们输出所有语句。包含我们用typedef自定义了数据类型的别名:
……
int main(int argc, char* argv[])
{
typedef int AGE; //为int 取一个别名
//cout << "char(字符)占用的字节数: " << sizeof(char) << endl;
cout << "Size of char is : " << sizeof(char) << endl;
cout << "Size of unsigned char is : " << sizeof(unsigned char) << endl;
cout << "Size of short int is : " << sizeof(short int) << endl;
cout << "Size of unsigned short int is : " << sizeof(unsigned short int) << endl;
cout << "Size of int is : " << sizeof(int) << endl;
cout << "Size of unsigned int is : " << sizeof(unsigned int) << endl;
cout << "Size of long int is : " << sizeof(long int) << endl;
cout << "Size of unsigned long int is : " << sizeof(unsigned long int) << endl;
cout << "Size of AGE is : " << sizeof(AGE) << endl;
cout << "Size of float is : " << sizeof(float) << endl;
cout << "Size of double is : " << sizeof(double) << endl;
cout << "Size of long double is : " << sizeof(long double) << endl;
cout << "Size of bool is : " << sizeof(bool) << endl;
getchar();
return 0;
}
我很乐意承认,这回我也当了一次聪明人,在完成上面的代码的过程中,一直在用“Ctrl + C”和“Ctrl + V”。关键是复制完之后要及时改变需要变的地方。另外,检查是否改错的一个方法就是利用CBC的代码窗口对关键字变色的特征。如sizeof本身和sizeof()括号中的数据类型关键字都会变色。如果你发现有一处不是这样,那你一定是输入错了。比如你把sizeof(char)写成 sizeof(cahr),那么cahr不会变色。
我们漏了一个类型:void;void是无类型。sizeof无法对无类型取其字节数,这是void的特殊性,当作为“无类型”意义时,它字大小是0字节,如函数的返回值;当作为“未确定”的意义时,它的大小也是未确定的。当然这也是sizeof的一个知识点:它就是计算对像必须确定的类型。
确保上面的代码输入无误后按F9,结果如下:
变量和常量
一、从类型到变量:
我们在上一章的学的数据类型是:char,int,bool等等。
假设我们需要使用一个有关年龄的变量,在C或C++中是这样定义的:int age;。
现在让我们来事先建立一个空的控制台程序,随着本章课程的进展,我们需要不断地在工程中加入零星的代码。
代码文件Unit1.cpp中,手工加入以下的黑体部分:
//------------------------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//------------------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
getchar();
return 0;
}
//------------------------------------------------------------------------
1、定义变量:
语法:数据类型 变量名;
“张三”既可以指张三这个人,也可以是张三的名字。同样,上面的“变量名”,其实也就是变量本身。
举上一节的例子:int age;
其中int是数据类型(整型),而age是变量名,更多的时候,我们就说是变量age,最后是一人分号,它表示定义一个变量在C或C++里一句完整的语句,因为C++的语言总是以分号结束。
声明一个字符类型变量:char letter;
声明一个bool类型的变量:bool do_u_love_me;
其它类型,除了void不能直接定义一个变量以外,格式都是一样的。
void avoid; //错!void类型无法直接定义一个变量。
有时同一数据类型需要多个变量,可以分别定义,也可以一起定义,如:
int a;
int b;
int c;
下面采用一起定义,会更省事:
int a,b,c;
一起定义多个同类型变量的方法是:在不同变量之间以逗号“,”分隔,最后仍以分号“;”结束。
让我们先来试试变量定义,另外趁此机会,看看一个变量在仅仅进行定义之后,它的值会是什么。
继续上一小节的代码,仍然是加入的黑体部分,当然//及其后面的内容是注释,你可以不输入。
......
int main(int argc, char* argv[])]
{
定义变量///
//以下定义三个变量:a,b,c
int a;
double b,c;
//a,b,c仅仅被定义,它的值会是什么?我们用 cout 输出三个变量:
cout << "a = " << a << " b = " << b << " c = " << c << endl;
getchar();
return 0;
}
保存好代码文件和工程文件,然后按F9运行。以下是笔者机器得到结果。
a是1,b和c都像天文数字?嗯,从这里我们学到一个C和C++编程极其重要知识:未初始化的变量,它的值将是未确定的。所谓“未初始化”,就是指这个变量只有定义代码,而没有赋值。
(立即重复执行这段代码,得到结果可能都一样,但这并不说明这些值就是可以确定不变的)。
2、如何为变量命名:
①C和C++的变量的名字中只能有以下字符:大小写字母、阿拉伯数字(但不能做为开头)、下划线 _。
②汉字不能成为变量名。
③不能取名为C和C++的保留字,如:int char;这是不被允许的,char是一个保留字,我们不能再拿来做变量。
④不能超过250个字符,这在BCB里有规定,是一个可以调整的值。
⑤不能以数字开头,如:
int 100; //不行
int 100ren; //不行
⑥不能夹有空格,如:
bool do you love me; //不行
你可以用下划线代替空格,如:
bool do_you_love_me; //OK
⑦不能在同一作用范围内有同名变量(仅C++),如:
int abc;
int abcd;
int abc;//不行
在C里,上面重复定义变量abc是被允许的,关于作用范围,我们以后会讲到。
⑧不要和C或C++中已有的全局变量、函数或类型名取相同的名字,如:
double sin;
这不合适,因为C库为我们提供了计算正弦值的函数,名字就叫 sin;。
⑨不要太长:是的,250个字符其实太长了。如果有个变量名长达30个字母,我看不仅你自已写得累,别人看着也会觉得是在读外国小说,主人公的名字太长,烦人。
⑩不要太短:这个我们放到后面说。
以上几点中,凡标为“不能”的意味如果你违反了,你的程序便会在编译时出错,而“不要”则仅属建议内容,真要这么做,程序也没错;另外,我们还建议要为每个变量取一个有意义的名字。比如 NumberOfStudents,这个变量一看就明白是为“学生的人数”定义的。而如果定义为 aaa,cc,之类,就容易混淆。当然,有一些约定成俗的用法,如字母i,j,等常用来作循环流程中的计数变量。再者,有意义的名字并不是指一定要把变量所要代表的意思用英文句子写出,一般可以用简写,如 NumOfStudent,它同样意义明了,但更简短。而且,如同使用英文一样,有时也可以使用拼音。这里就不举例了,因为笔者连拼音都很次。
前面说到取名不要太短,说的就是避免像aaa,cc之类的图方便但毫无意义,难以阅读的变量命名习惯。
(你很快就会在教程中发现,笔者似乎自已违反了这个规定,用一些a,b,c作为变量名,这不能说是笔者的错,因为会有些时候变量的意义并不重要)。
最后C和C++是区要大小写的语言,所以变量Num和变量num完全是不同的两个变量,大家在定义,使用变量要注意到这一点。
关于变量的命名,我们不做实践,下面附加说说编程中命名的一些风格。
附:关于命名变量的几种风格。
较早以前,程序员喜欢用全部小写的单词来命名变量,如果两个单词,比如my car,常用的命名方法有两种:my_car或myCar。my_car自然看起来清楚,后一种方法被称为“驼峰表示法”因为其中的大写字母看起来相凸起的驼峰。
之所以不使用MyCar的原因是C和C++允许程序自定义数据类型,所以有必要从一个名字上很快地区分它是变量或是数据类型,方法是让自定义的数据类型都用大写开头。
在C++ Builder里,并没有限制大家如何为变量取名,所以为了仍可以很明显的做到上述的区分,CB的方法是对用户自定义的数据类型在前头加一个字母T(Type的首字母)。仍如Horse,则改名为:THorse。前面我们写Windows版的hello world时,使用了一个Label控件,其实,检查代码你会发现,它的类名就叫:TLabel。
最后还有一种方法是匈牙利标记法(Hungarian notation),该法要求在每人变量的前面加上若干个用于表示该变量的数据类型的小写字母,如iMyCar表示这个变量是整型(i表示int);而倘若是cMyCar,则表示这个变量是char类型。该法经过一段时间的训练熟悉以后,会带来一些好处,但问题是如果对自定义的数据类型也按这种方法进行,就不是经过训练就能熟悉了。比如hoWhite,这个名字中的ho表示“马”,真有点强人所难,举个实际存在的例子,在Windows API中,如果你看到:
LPCITEMIDLIST pidlRoot;
想要一眼看明白pidRoot必须要求是你很明白ITEMIDLIST是什么玩意儿了。是的,Windows的API使用的是最后一种方法,在大多数情况下,它的变量的名字都看上去怪怪的。
在本部教程中,我们在正式程序中,最常使用的方法是简单的“驼峰”法。
3、如何初始化变量:
什么时候需要给变量初始化?
int a;
声明了一个整型变量a,但这变量的值是多少?a等于0吗?还是等于100?我们都不知道。“不知道”的意思是:a 有值,但这个值的大小是随机的,所以我们无法确定。无法确定一个变量值是常有的事,比如我们要求用户输入他的年龄。我们需要声明一个整型变量来存储用户的年龄,但在用户输入之前,我们是无法确认它的值的。但有些时候,我们可以事先给变量一个初始时的值。
同样是年龄的问题,虽然我们不知道用户到底几岁,但我们知道他不可能是0岁,所以我们把年龄一开始设为0。为什么要这样?用户有时不小心忘了输入年龄(就像我们在网上填表一样),我们就可以检查年龄是否为0来发现了;另外一种相反的用法是,我们发现大多数用户是8岁(比如一个小学生入学登记表),这时我们初始化年龄变量为8,目的是为了方便用户。
如何为一个变量赋值呢?
答案就像我们初中的代数:设 x = 10;y = 100,用等号。请记住:在现实生活中,等号“=”有两个意义,但在C和C++里=只用来给一个变量赋值。
4、初始化变量的两个时机:
①在定义时初始化变量:
int a = 0;
通过一个等号,我们让a的值等于0;同时定义多个变量时也一样:
int a = 0,b= 1;
当然也可以根据需要,只给部分变量初始化。
int a = 0, b;
或:
int a,b = 1;
在定义以后赋值
int a;
a = 100;
②通过计算得到初始值:
给变量赋值,除了给一个直接的值以外,还可以通过计算获得。如:
int a = -3 + 200 - 5;
或者:
int a = 9;
int b = 3;
int c = a / b;//“/”表示除号。
现在来试试给变量赋值的几种方法。
......
int main(int argc, char* argv[])]
{
/定义变量
//以下定义三个变量:a,b,c
int a;
double b,c;
//用 cout 输出三个变量:
cout << "a = " << a << " b = " << b << " c = " << c << endl;
/初始化变量///
int d = 0;
float e = 1.234, f;
f = 567.8 + 0.2;
cout << "d = " << d << " e = " << e << " f = " << f << endl;
getchar();
return 0;
}
5、变量的取值范围:
①变量允许取值范围 = 变量数据类型的范围:
关于赋值的最后一个需要注意的是:变量的取值范围;变量是有类型的,变量的允许的取值范围等同于其数据类型的范围;比如一个整型数a,它的取值范围就是:-2147483648~2147483647,而如果是一个字符类型,它的取值就不能是-129。
以下是示例:
int a = 3000000000;//错误!
char b = -129;//错误!
我们来写代码实践一下,由于char类型计算机在输出将直接输出字符,我们不好观察结果,所以我们选择了int类型。
......
int main(int argc, char* argv[])]
{
///定义变量//
//以下定义三个变量:a,b,c
int a;
double b,c;
//用 cout 输出三个变量:
cout << "a = " << a << " b = " << b << " c = " << c << endl;
///初始化变量/
int d = 0;
float e = 1.234, f;
f = 567.8 + 0.2;
cout << "d = " << d << " e = " << e << " f = " << f << endl;
//变量值越界/
int g = 3000000000; //给g赋值超过int允许的范围,所以g的值不可能如愿等于3000000000
cout << "g = " << g <<endl;
getchar();
return 0;
}
g 的值已经溢出,它的值究竟是多少?自已按F9运行,看结果吧。
② bool类型的特殊:
至于bool类型,事实上它的内存空间范围和char一样是一字节的(因为计算机能直接处理的最小内存单位是字节),所以按说它也能存256个不同的值,但作为C++的一个规定,强制给它两个值: false,true。那么false是什么呢?其实它是0。而true则是非0的数。也就是说,值0对应为false,而所有非0的数对应成true。
所以,你这样给bool类型的变量赋值自然没错:
bool do_u_love_me = false; //噢,你不爱我。
或:
bool do_u_love_me = true;
但你也可以这样给来:
bool do_u_love_me = 0;
或:
bool do_u_love_me = 1;
当然,我们建议使用第一种方法,那是“正规”的方法。
在C里,并没有bool这个数据类型,所以C程序员采用的方法是自定义:
typedef char BOOL;
如果你在某些书上碰到BOOL,你就可以明白它就是我们学的 bool。
false和true到底是什么?“假”或“真”,很简单啊。噢,我不是问二者的意思,C++之所以要加入bool类型,目的也是为了让程序员在处理那些只用于表示“真或假”、“是或否”等数据时,可以用直观的false和true来表示。而在计算机内部,二者仍然是数。让我们写几行代码,揭开bool类型的“假面”。
......
int main(int argc, char* argv[])]
{
///定义变量//
//以下定义三个变量:a,b,c
int a;
double b,c;
//用 cout 输出三个变量:
cout << "a = " << a << " b = " << b << " c = " << c << endl;
/初始化变量
int d = 0;
float e = 1.234, f;
f = 567.8 + 0.2;
cout << "d = " << d << " e = " << e << " f = " << f << endl;
变量值越界
int g = 3000000000; //给g赋值超过int允许的范围,所以g的值不可能如愿等于3000000000
cout << "g = " << g <<endl;
///bool类型的"真面目"///
bool h = false ,i = true;
cout << "h = " << h << " i = " << i << endl;
getchar();
return 0;
}
运行后……原来,false是0,true是1。
③char类型的特殊:
char的范围是 -128~127
unsigned char的范围是0~255
那么按照前面的说法,我们可以这样为一个字符类型的变量赋值:
char c = 120;
unsigned char uc = 250;
这样看来,所谓的“字符”类型,似乎除了取值范围小一点以外,和整型也没有什么区别,这句话的确没错。对于C和C++来说,字符类型完全可以当成一个整数来对待。事实上,所有信息在计算机里,都是使用数字来表达。英文字母“A”在计算机里表示为65;字母 “B”表示为66,所有你在键盘可以看到的字符,如大小写英文字母、阿拉伯数字符号和标点符号等,都可以有一个相应的数值表示。但要让我们记住65就是“A”,而33就是“!”等255个对应关系,显然很折磨人,所以,计算机高级语言允许我们直接为字符类型变量这样赋值:
char c = 'A';
char d = '!'; //英文感叹号
char e = '.'; //英文句号
char f = ' '; //空格
即:将所要得到的字符用单引号括住(引号''是英文状态下的,千万不要使用中文符号)。
另外,对于一个数值类型,如果它等于120,那么输出时显示120,如果是一个字符类型,输出的是120对应的字符。也就是说:
int k = 120;
char j = 120;
二者虽然值都为120,但输出j时,计算机并不显示120这个值,而是120对应的字符。试试看!
为了不让教程中的代码重复占用版面,省略号省略掉的代码要多点了……
......
/char类型/
int k = 120;
char j = 120;
cout << "k(int) = " << k << " j(char) = " << j << endl;
getchar();
......
输出结果:k是120;但j原来120对应的字母是“x”。写的是120,输出的却是x,很不直观对不?所以,除非我们故意要和自已或者其他看代码的人玩“密码”,否则,还是直接想要什么字符,就写哪个字符吧。
/char类型//
int k = 120;
char j = 120;
cout << "k(int) = " << k << " j(char) = " << j << endl;
char l = 'A';
char m = l + 1;
cout << "l = " << l << " m = " << m << endl;
getchar();
......
输出结果:l为 “A”,而m为“B”,想一想为什么?学完后面内容就有答案。
单引号本身也是一个字符,如何表示单引号呢?是否用'''来表示?在看明白下面的常用字符ASCII码表以后再说。
(ASCII是指:American Standard Code for Information Interchange,美国信息交换标准码。)
值 | 符号 | 值 | 符号 | 值 | 符号 |
0 | 空字符 | 44 | , | 91 | [ |
32 | 空格 | 45 | - | 92 | \ |
33 | ! | 46 | . | 93 | ] |
34 | " | 47 | / | 94 | ^ |
35 | # | 48 ~ 57 | 0 ~ 9 | 95 | - |
36 | $ | 58 | : | 96 | ` |
37 | % | 59 | ; | 97 ~ 122 | a ~ z |
38 | & | 60 | < | 123 | { |
39 | ' | 61 | = | 124 | | |
40 | ( | 62 | > | 125 | } |
41 | ) | 63 | ? | 126 | ~ |
42 | * | 64 | @ | 127 | DEL (Delete键) |
43 | + | 65 - 90 | A ~ Z |
|
|
(其中,0~31都是一些不可见的字符,所以这里只列出值为0的字符,值为0的字符称为空字符,输出该字符时,计算机不会有任何反应。我们以后会学习0字符的特殊作用。)
④转义符的使用:
根据前面的说法,单引号似乎应该表达为:
char c = ''';
但这是错误的,C和C++不认识 ''',因为它容易引起歧义。
另外,有一些字符属于不可见的字符,无法直接写出,比如键盘上大大的回车键,在输入文档时,用它可以输入一个回车换行,显然我们不能这样在C和C++里表示一个回车换行:
char c = '
'
在第一个'和第二个'之间夹一个换行,这样的表示方法不仅不方便,而且C和C++也不认。类似这样的问题还有制表符(键盘上的Tab键)等等。解决的方法是使用转义符,C和C++使用反斜杠“\”作为转义符。如:
'\'' : 表示单引号;
'\"' : 表示双引号;
'\n' : 表示换行(n : line);
下面列出常用的C和C++特殊字符:
字符 | 数值 | 意义 |
'\a' | 7 | 响铃(输出该字符时,屏幕无显示,但喇叭发音) |
'\n' | 10 | 换行(n: line) |
'\t' | 9 | 制表符(横向跳格) |
'\r' | 13 | 回车(return) |
'\\' | 92 | 输出转义符 '/' 本身 |
'\"' | 34 | 双引号 |
'\'' | 39 | 单引号 |
这里顺便解释一下“回车换行”是什么,尽管我们对这个词耳熟得很。“回车换行”是“回车”加“换行”。换行好理解,但什么叫“回车”呢?它和“换行”又有什么关系?
原来,“回车换行”的概念源于早先的打字机,其中有类似于现在打印机中使用的打印头的装置,这个打印头平常停在打印机内的某一端,在打印一行时,则需要向外移动,打印一行结束后,打印头需要回到原来的位置。由于打印头在英文中用“车”来表示,所以这个动作就称为“回车”,用金山词霸的中的解释就是:“将打印或显示位置移到同行起始位置的运动”。对于打印机,假设有两行字,两行之间若光有“回车”,那么这两行字将重叠在一起(对于控制台程序的屏幕,则将回到行首)。如果光有“换行”,则第二行将不从起始位置打起,样子如下:
第一行。
第二行。
只有既输出“回车”又输出“换行”才是我们需要的结果。当然,对于当今的大都软件,往往都把单独的回车或换行直接解释为二者的结合。
转义符的另外一种用法是直接接数值,但必须采用8进制或16进制。这里暂不讲解。
如果需要使用数值表示,最直接的还是使用类似:c = 120;的方法,比如要让变量c的值为单引号,我们至少可以有以下2种方法:
char c = '\''; //使用转义符
char c = 39; //直接赋给字符的ASCII的值
转义符的内容,看上去怪怪的?不过,多用几次我们就会明白。
/char 类型
int k = 120;
char j = 120;
cout << "k(int) = " << k << " j(char) = " << j << endl;
char l = 'A';
char m = l + 1;
cout << "l = " << l << " m = " << m << endl;
///转义符/
cout << "TAB:" << '\t' << "AA" << endl;
cout << "换行:" << '\n' << "AA" << endl;
cout << "回车:" << '\r' << "AA" << endl;
cout << "听到BEEP声了吗?" << '\a' << endl;
cout << '\'' << endl;
cout << '\"' << endl;
cout << '\\' << endl;
getchar();
......
在执行之前,有必要稍作解释。
首先是“AA”是做什么用的。因为制表符、回车和换行等特殊字符,其输出效果是改变光标位置,所以我们需要一些上下文来显示出光标位置的改变效果,这里就随便写个“AA”了事。然后是在cout语句中,平常我们说是使用双引号输出一行话,但如果当前要输出的只是一个字符,我们也可以使用单引号。
至于所谓“BEEP”声,你可别抱太多期望,它只是计算机内置的小喇叭短促一个声音,听起来并不美妙。
现在来看结果(请只关心转义符部分):
关于输出结果的几点说明:
1、需要注意的是 '\t' 在控制台窗口的输出效果,如果前面的字符长度已超过一个制表位,那么后面的第一个'\t'将是没有效用的(要理解这一点,你可以将代码中“TAB”加长一点,如"TABTAB")。
六、变量与内存地址:
此处内容暂缺。
既然变量只不过是内存地址的名称,所以:
4、对变量的操作,等同于对变量所在地址的内存操作。
第五点是反过来说:
5、对指定内存地址的内存操作,等同于对相应变量的操作。
尽管这简直就是在重复,但这一条却是我们今后理解C、C++语言相对于其它很多高级语言的,最灵活也最难学的“指针”概念的基石。
七、常量:
说完变量,我们来学常量。
看一段代码片段,省略号表示可能会有的其它操作。
int a = 3;
....
a = 100;
代码中a是变量,一开始我们初始化为3,后来出于什么需要,我们又将它赋值为100,a的值也就从3变成了100。
代码中3和100就是一种常量,像这种直接在代码写出大小的量,称为立即数,也称为常数,而常数是常量的一种。
常量的定义:常数,或代表固定不变值的名字。
1、几种数据类型常数的表达方法:
①整型常数的表达:
用10进制表示,当然是最常用也是最直观的了,如:7,356,-90,等等。 C和C++语言还允许我们使用8进制或16进制表示,这里且不讲。至于2进制形式,虽然它是计算机中最终的表示方法,但它太长,而且完全可以使用16进制或8进制方便地表达,所以计算机语言不提供用2进制表达常数的方法。
有时,你也会看到一些老的代码中,在一些整型常量后面加一个大写或小写的L字母,如:989L这是什么意思呢?原来,一个常数如果其范围允许,那么计算机默认将其认为是int类型的,那么要让计算机把某个数认为是long int类型,就可以在其后面加L或l。不过,这在以前的16位机器中才有意义。现在,我们的机器都是32位,long int和int完全一样,都是占用4个字节,所以,我们没有必要这样用了。
②实型常数的表达:
实型常数一般只用10进制表示,比如 123.45,或 .123。后者是0.123的简写。不过我个人认为,少写一个0的代价是很容易看错。
实型数还可以使用科学计数法,或曰指数形式,如:123e4或123E4都表示123 * 104,即1230000。
我们学过的实数数据类型有:float、double和long double。在C++中,默认的常数类型是double。比如你写:
1.234;
那么,C++按double类型为这个数分配内存,也就是说为它分配8个字节。如果要改变这一点,可以通过加后缀字母来实现。
加f或F,指定为float类型。
加l或L, 指定为double类型。
以下示例:
12.3f //float类型
12.3 //默认类型(double)
12.3L //long double类型
12.3e400 //long double类型,因为值已不在double类型的取值范围内
③字符常量的表达:
前面讲过,字符常量是用单引号括起来的一个字符,如:'a','b','c',' ','A'等。
可以用转义符来表示一些无法打出的符号,如 '\n','\r','\t'。
这里补充一点:值为0的字符,称为空字符或称为零字符,它用 '\0' 表示。注意,它和阿拉伯数字字符 '0'完全是两个数(请大家查一前面常用字符ASCII值表中,后者的值是多少)。
④字符串常量:
字符串由字符组成,在C和C++语言中,字符串是由一对双引号括起来的字符序列,如:
"Hello, world!"
"How do you do?"
"Hello"
上面3行都是字符串常量。注意,双引号是英文字符。
字符串是由一个个字符排列而成,所以在C和C++中,字符串在内存中的存储方式,我想你可以想象得出,每个字符占用一个字节,一个接一个排列。但是,双引号本身并不存储,因为双引号是为了表达方便,并不是实际内容。下面是示意图:
(为了简单,我们选了例子中最短的字符串)
不过,字符串的存储工作还有一点点事情需要做。举一个直观的例子:如果在上图中的Hello后,内存地址为120006正好还存放一个字符:‘o’。那么,程序就会把Hello 和o连起来认作是一个字符串“Helloo”,为什么呢?
前面我们讲字符类型和整型等变量或常量时,根据数据类型的不同,它们都有已知的和固定的大小。字符串是由字符组成的,虽然每个字符的大小固定1个字节,但字符串的大小却是可变的,所以必须有一个方法来表示一个字符串在哪里结束,而空字符(值为0字符)担起了这个责任。因为空字符没有任何输出效果,所以正常的字符串中是不会出现空字符的。因此,用它来表示一个字符串的结束,再合适不过了。
有两个字符串:
"Hello"
"What"
假设二者在内存中存储的位置正好是连续的,那么内存示意就为:
(为了结束版本,我这里横向表示,并且不再写出仅用于示意的内存地址)
H | e | l | l | o | \0 | W | h | a | t | \0 |
从表中,我们可以看出空字符 '\0'是如何起到标志一个字符结束的作用。
以下是带有字符串的真正的内存存储示意图。
记住,空字符用 '\0'表示。
2、用宏表示常数:
假如我们要写一个有关圆的种种计算的程序,那么∏(3.14159)值会被濒繁用到。我们显然没有理由去改∏的值,所以应该将它当成一个常量对待,那么,我们是否就不得不一遍一遍地写3.14159这一长串的数呢?必须有个偷懒的方法,并且要提倡这个偷懒,因为多次写3.14159,难免哪次就写错了。这就用到了宏,宏不仅可以用来代替常数值,还可以用来代替表达式,甚至是代码段(宏的功能很强大,但也容易出错,所以其利弊大小颇有争议)。今天我们只谈其中代替常数值的功能。
宏的语法为:
#define 宏名称 宏值
比如要代替前面说到的∏值,应为:
#define PAI 3.14159
注意,宏定义不是C或C++严格意义上的语句,所以其行末不用加分号结束。
宏名称的取名规则和变量名一样,所以我们这里用PAI来表示∏,因为C和C++不能直接使用∏字符。有了上面的语句,我们在程序中凡是要用到3.14159的地方都可以使用PAI这个宏来取代。
作为一种建议和一种广大程序员共同的习惯,宏名称经常使用全部大写的字母。假设原来有一段代码:
double zc = 2 * 3.14159 * R; //求圆周长,其中R是代表半径的变量
double mj = 3.14159 * R * R; //求圆面积
在定义了宏PAI以后,我们就可以这样使用:
#define PAI 3.14159
double = 2 * PAI * R; //求圆周长,其中R是代表半径的变量
double = PAI * R * R; //求圆面积
用宏来取代常数,好处是:
1)让代码更简洁明了:
当然,这有赖于你为宏取一个适当的名字。一般来说,宏的名字更要注重有明确直观的意义,有时宁可让它长点。
2)方便代码维护:
就如前面说的3.14159,哪天你发现这个∏值精度不够,想改为3.1415926,那么你只修改一处宏,而不是修改代码中的所有宏。
原来的宏:
#define PAI 3.14159
修改后的宏:
#define PAI 3.1415926
对宏的处理,在编译过程中称为“预处理”。也就是说在正式编译前,编译器必须先将代码中出现的宏,用其相应的宏值替换,这个过程有点你我在文字处理软件中的查找替换。完成预处理后,所有原来的“PAI”都成了立即数3.1415926,所以在代码中使用宏表达常数,归根结底还是使用了立即数,并没有明确指定这个量的类型。这容易带来一些问题,所以C++使用另一更稳妥的方法来代替宏的这一功能。
3、常量定义:
常量定义的格式为:
const 数据类型 常量名 = 常量值;
相比变量定义的格式,常量定义必须以 const 开始,另外,常量必须在定义的同时,完成赋值。
const float PAI = 3.1415926;
const的作用就是指明这个量(PAI)是常量,而非变量。常量必须一开始就指定一个值,然后在以后的代码中,我们不允许改变PAI的值,比如:
const float PAI = 3.14159;
double zc = 2 * PAI * R;
PAI = 3.1415926; //错误!,PAI不能再修改。
double mj = PAI * R * R;
如果一个常量是整型,可以省略指出数据类型,如:
const k = 100;
相当于
const int k = 100;
反过来说,如果不指定数据类型,则编译器将它当成整型,如:
const k = 1.234;
虽然你想让k等于一个实型数,然而,最终k的值其实是1,为编译器把它当成整型常量。
我们建议在定义变量时,明确指出类型,不管它是整型或其它类型。
const int i = 100;
const double di = 100.0;
4、枚举常量:
①为什么需要枚举类型:
生活中有很多信息,在计算机中都适于用数值来表示,比如,从星期一到星期天,我们可以用数字来表示。在西方,洋人认为星期天是一周的开始,按照这种说法,我们定星期天为0,而星期一到六分别用1到6表示。
现在,有一行代码,它表达今天是周3:
int today = 3;
很多时候,我们可以认为这已经是比较直观的代码了,不过可能在6个月以后,我们初看到这行代码,会在心里想:是说今天是周3呢,还是说今天是3号?其实我们可以做到更直观,并且方法很多。
第一种是使用宏定义:
#define SUNDAY 0
#define MONDAY 1
#define TUESDAY 2
#define WEDNESDAY 3
#define THURSDAY 4
#define FRIDAY 5
#define SATURDAY 6
int today = WEDNESDAY;
第二种是使用常量定义:
const int SUNDAY = 0;
const int MONDAY = 1;
const int TUESDAY = 2;
const int WEDNESDAY = 3;
const int THURSDAY = 4;
const int FRIDAY = 5;
const int SATURDAY = 6;
int today = WEDNESDAY;
第三种方法就是使用枚举:
枚举也是我们将学习的第二种用户自定义数据类型的方法,上回我们学过typedef,typedef通过为原有的数据类型取一别名来获得一新的数据类型。枚举类型的原有数据只能是int或char类型(有些编译器则只支持int,如VC)。 枚举类型是在整数的取值范围中,列出需要的某个值作为新数据类型的取值范围。这就像bool类型,其实它是char类型,但它只需要0或1来代表false或true。
这里的例子中,我们用整型来表示周一到周日,整型的取值范围是多少来的?反正很大,可我们只需0到6,并且我们希望这7个值可以有另外一种叫法,以直观地表示星期几。
②定义枚举类型的基本语法:
enum 枚举类型名 {枚举值1,枚举值2,…… };
enum : 是定义枚举类型的关键字。
枚举类型名:我们要自定义的新的数据类型的名字。
枚举值 :可能的个值。比如:
enum Week {SUNDAY,MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY};
这就定义了一个新的数据类型:Week。Week数据类型来源于int类型(默认)。Week类型的数据只能有7种取值,它们是:SUNDAY,MONDAY,TUESDAY……SATURDAY。其中SUNDAY = 0,MONDAY = 1……SATURDAY = 6也就是说,第1个枚举值代表0,第2个枚举值代表1,这样依次递增1。
不过,也可以在定义时,直接指定某个或某些枚举值的数值。比如,对于中国人,可能对于用0表示星期天不是很好接受,不如用7来表示星期天,这样我们需要的个值就是 1,2,3,4,5,6,7可以这样定义:
enum Week {MONDAY = 1,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY};
我们希望星期一仍然从1开始,枚举类型默认枚举值从0开始,所以我们直接指定MONDAY等于1,这样,TUESDAY就将等于2,直接到SUNDAY等于7。
枚举值,我们称为格举常量,因为它一经定义以后就不可再改变,以下用法是错误的:
TUESDAY = 10; //错误!我们不能改变一个枚举值的数值。
用枚举常量来完成表达今天是星期三:
Week today = TUESDAY;
再举一例:在计算机中,颜色值也是使用整数来表示的,比如红色用255表示,绿色用65280表示,而黄色则用65535表示(在自然界中,红+绿=黄,在计算机中也一样,你注意到了吗?)。
假设我们在交警队工作,需要写一个有关红绿灯的程序,我们可以这样定义一个颜色的枚举类型:
enum TLightColor { Red = 255, Green = 65280, Yellow = Red + Green };
③关于枚举常量的输出效果:
bool类型,其实就是个地道的枚举类型。我们已经在前面程序中试过它的输出效果为false输出0,true输出1。假设我们有一个TLightColor类型的变量:
TLightColor redLamp = Red;
然后我们输出 redLamp:
cout << redLamp << endl;
很多学员可能会猜出,输出结果是“255”显然他们比笔者聪明。当时我学习枚举类型,就天真地认为屏幕会打出“Red”(呵呵,别嘲笑我,其实在有时候,比起聪明来,天真更难得……)。
我不在课程里给出如何定义、如何使用和如何输出枚举常量的例程。我想,学到今天,你应该有兴趣,也有能力自已新建一个控制台工程,然后自已写代码实现这一切了。
二进制、八进制和十六进制(选修)
这是一节“前不着村后不着店”的课,不同进制之间的转换纯粹是数学上的计算。不过,你不必担心会有么复杂的,无非是乘或除的计算。生活中其实很多地方的计数方法都多少有点不同进制的影子。比如:我们最常用的10进制,其实起源于人有10个指头,如果我们的祖先始终没有摆脱手脚不分的境况,我想我们现在一定是在使用20进制。
至于二进制……没有袜子称为0只袜子,有一只袜子称为1只袜子,但若有两只袜子,则我们常说的是:1双袜子。
生活中还有:七进制,比如星期。十六进制,比如小时或“一打”,六十进制,比如分钟或角度……
一、为什么需要八进制和十六进制?
编程中我们常用的还是10进制……,必竟C和C++是高级语言。比如:
int a = 100,b = 99;
不过,由于数据在计算机中的表示,最终以二进制的形式存在,所以有时候使用二进制,可以更直观地解决问题。但,二进制数太长了,比如int 类型占用4个字节;32位,比如100,用int类型的二进制数表达将是:0000 0000 0000 0000 0110 0100。
面对这么长的数进行思考或操作,没有人会喜欢。因此C和C++ 没有提供在代码中直接写二进制数的方法。
用16进制或8进制可以解决这个问题。因为,进制越大,数的表达长度也就越短。不过,为什么偏偏是16或8进制,而不其它的诸如9或20进制呢?
2、8、16,分别是2的1次方,3次方,4次方。这一点使得三种进制之间可以非常直接地互相转换。8进制或16进制缩短了二进制数,但保持了二进制数的表达特点。在下面的关于进制转换的课程中,你可以发现这一点。
二、二进制、八进制和十六进制数转换到十进制数:
1、二进制数转换为十进制数:
二进制数第0位的权值是2的0次方,第1位的权值是2的1次方……,所以,设有一个二进制数:0110 0100,转换为10进制为(下面是竖式):
0110 0100换算成十进制:
第0位 0 * 20 = 0
第1位 0 * 21 = 0
第2位 1 * 22 = 4
第3位 0 * 23 = 0
第4位 0 * 24 = 0
第5位 1 * 25 = 32
第6位 1 * 26 = 64
第7位 0 * 27 = 0 +
---------------------------
100
用横式计算为:
0 * 20 + 0 * 21 + 1 * 22 + 1 * 23 + 0 * 24 + 1 * 25 + 1 * 26 + 0 * 27 = 100
0乘以多少都是0,所以我们也可以直接跳过值为0的位:
1 * 22 + 1 * 23 + 1 * 25 + 1 * 26 = 100
2、八进制数转换为十进制数:
八进制就是逢8进1。
八进制数采用 0~7这八个数来表达一个数。
八进制数第0位的权值为8的0次方,第1位权值为8的1次方,第2位权值为8的2次方……
所以,设有一个八进制数1507,转换为十进制为:
用竖式表示:
1507换算成十进制
第0位 7 * 80 = 7
第1位 0 * 81 = 0
第2位 5 * 82 = 320
第3位 1 * 83 = 512 +
--------------------------
839
同样,我们也可以用横式直接计算:
7 * 80 + 0 * 81 + 5 * 82 + 1 * 83 = 839
结果是,八进制数1507转换成十进制数为839。
3、八进制数的表达方法:
C和C++语言中,如何表达一个八进制数呢?如果这个数是876我们可以断定它不是八进制数,因为八进制数中不可能出7以上的阿拉伯数字。但如果这个数是123或567或12345670,那么它是八进制数还是10进制数,都有可能。
所以C和C++规定,一个数如果要指明它采用八进制,必须在它前面加上一个0,如:123是十进制,但0123则表示采用八进制。这就是八进制数在C和C++中的表达方法。由于C和C++都没有提供二进制数的表达方法,所以,这里所学的八进制是我们学习的C和C++语言的数值表达的第二种进制法。
现在,对于同样一个数比如100,我们在代码中可以用平常的10进制表达,例如在变量初始化时:
int a = 100;
我们也可以这样写:
int a = 0144; //0144是八进制的100
一个10进制数如何转成8进制,我们后面会学到。千万记住,用八进制表达时,你不能少了最前的那个0,否则计算机会通通当成10进制。不过,有一个地方使用八进制数时,却不能加0,那就是我们前面学的用于表达字符的“转义符”表达法。
4、八进制数在转义符中的使用:
我们学过用一个转义符'\'加上一个特殊字母来表示某个字符的方法,如:'\n'表示换行(line),而'\t'表示Tab字符,'\''则表示单引号。今天我们又学习了一种使用转义符的方法:转义符'\'后面接一个八进制数,用于表示ASCII码等于该值的字符,比如,查一下第5章中的ASCII码表,我们找到问号字符(?)的ASCII值是63,那么我们可以把它转换为八进值77,然后用 '\77'来表示'?'。由于是八进制,所以本应写成 '\077',但因为C和C++规定不允许使用斜杠加10进制数来表示字符,所以这里的0可以不写。
事实上我们很少在实际编程中非要用转义符加八进制数来表示一个字符,所以,sh上面的内容,大家仅仅了解就行。
5、十六进制数转换成十进制数:
2进制,用两个阿拉伯数字:0、1;
8进制,用八个阿拉伯数字:0、1、2、3、4、5、6、7;
10进制,用十个阿拉伯数字:0到9;
16进制,用十六个阿拉伯数字……等等,阿拉伯人或说是印度人,只发明了10个数字啊?16进制就是逢16进1,但我们只有0~9这十个数字,所以我们用A,B,C,D,E,F这五个字母来分别表示10,11,12,13,14,15,字母不区分大小写。十六进制数的第0位的权值为16的0次方,第1位的权值为16的1次方,第2位的权值为16的2次方……所以,在第N(N从0开始)位上,如果是数X(X 大于等于0,并且X小于等于 15,即:F)表示的大小为 X * 16的N次方。
假设有一个十六进数 2AF5, 那么如何换算成10进制呢?用竖式计算:
2AF5换算成10进制:
第0位: 5 * 160 = 5
第1位: F * 161 = 240
第2位: A * 162 = 2560
第3位: 2 * 163 = 8192 +
-------------------------------------
10997
直接计算就是:
5 * 160 + F * 161 + A * 162 + 2 * 163 = 10997
(别忘了,在上面的计算中,A表示10,而F表示15)
现在可以看出,所有进制换算成10进制,关键在于各自的权值不同。
假设有人问你,十进数1234为什么是一千二百三十四?你尽可以给他这么一个算式:
1234 = 1 * 103 + 2 * 102 + 3 * 101 + 4 * 100
6、十六进制数的表达方法:
如果不使用特殊的书写形式,16进制数也会和10进制相混,随便一个数:9876就看不出它是16进制或10进制。
C和C++规定,16进制数必须以0x开头,比如0x1表示一个16进制数。而1则表示一个十进制,另外如:0xff,0xFF,0X102A等等。其中的x也不区分大小写(注意:0x中的0是数字0,而不是字母O)。
以下是一些用法示例:
int a = 0x100F;
int b = 0x70 + a;
至此,我们学完了所有进制:10进制、8进制和16进制数的表达方式。
最后一点很重要,在C和C++中,10进制数有正负之分,比如12表示正12,而-12表示负12,但8进制和16进制只能表达无符号的正整数,如果你在代码中有-078,或者写-0xF2,C和C++并不把它当成一个负数。
7、十六进制数在转义符中的使用:
转义符也可以接一个16进制数来表示一个字符,如在'?'字符,可以有以下表达方式:
'?' //直接输入字符
'\77' //用八进制,此时可以省略开头的0
'\0x3F' //用十六进制
同样,这一小节只用于了解,除了空字符用八进制数'\0'表示以外,我们很少用后两种方法表示一个字符。
三、十进制数转换到二进制、八进制和十六进制数:
1、10进制数转换为2进制数:
给你一个十进制比如6,如何将它转换成二进制数呢?
10进制数转换成二进制数,这是一个连续除2的过程:
把要转换的数除以2得到商和余数,将商继续除以2直到商为0;最后将所有余数倒序排列,得到的数就是转换结果。
听起来有些糊涂?我们结合例子来说明,比如要转换6为二进制数,那么:
要转换的数是6,6÷2得到商是3,余数是0(不要告诉我你不会计算6÷3);现在商是3,还不是0,所以继续除以2,那就3÷2得到的商是1,余数是1;现在商是1还不是0,所以继续除以2,那就1÷2,得到的商是0,余数是1(拿笔纸算一下1÷2是不是商0余1),好极!现在商已经是0。
我们三次计算依次得到余数分别是:0、1、1,将所有余数倒序排列,那就是:110了!6转换成二进制,结果是110。
把上面的一段改成用表格来表示,则为:
被除数 | 计算过程 | 商 | 余数 |
6 | 6/2 | 3 | 0 |
3 | 3/2 | 1 | 1 |
1 | 1/2 | 0 | 1 |
(在计算机中,÷用 / 来表示)
如果是在考试时,我们要画这样的表还是有点费时间,所更常见的换算过程是使用下图(图1)的连除:请大家对照图、表及文字说明,并且自已拿笔计算一遍如何将6转换为二进制数。
(图一)
说了半天,我们的转换结果对吗?二进制数110是6吗?你已经学会如何将二进制数转换成10进制数了,所以请现在就计算一下110换成10进制是否就是6。
2、10进制数转换为8和16进制数:
非常开心,10进制数转换成8进制的方法,和转换为2进制的方法类似,惟一变化是:除数由2变成8。
来看一个例子,如何将十进制数120转换成八进制数。
用表格表示:
被除数 | 计算过程 | 商 | 余数 |
120 | 120/8 | 15 | 0 |
15 | 15/8 | 1 | 7 |
1 | 1/8 | 0 | 1 |
120转换为8进制,结果为170。
非常非常开心,10进制数转换成16进制的方法,和转换为2进制的方法类似,惟一变化:除数由2变成16。
同样是120,转换成16进制则为:
被除数 | 计算过程 | 商 | 余数 |
120 | 120/16 | 7 | 8 |
7 | 7/16 | 0 | 7 |
120转换为16进制,结果为78。
请拿笔纸,采用(图1)的形式,演算上面两个表的过程。
3、二、十六进制数互相转换:
二进制和十六进制的互相转换比较重要,不过这二者的转换却不用计算,每个C和C++程序员都能做到看见二进制数,直接就能转换为十六进制数,反之亦然。
我们也一样,只要学完这一小节,就能做到。
首先我们来看一个二进制数1111,它是多少呢?你可能还要这样计算:1 * 20 + 1 * 21 + 1 * 22 + 1 * 23 = 1 * 1 + 1 * 2 + 1 * 4 + 1 * 8 = 15。
然而,由于1111才4位,所以我们必须直接记住它每一位的权值,并且是从高位往低位记8、4、2、1,即最高位的权值为23=8,然后依次是22=4,21=2,20=1。记住8421,对于任意一个4位的二进制数,我们都可以很快算出它对应的10进制值。
下面列出四位二进制数xxxx所有可能的值(中间略过部分):
仅4位的2进制数 快速计算方法 十进制值 十六进值
1111 = 8 + 4 + 2 + 1 = 15 F
1110 = 8 + 4 + 2 + 0 = 14 E
1101 = 8 + 4 + 0 + 1 = 13 D
1100 = 8 + 4 + 0 + 0 = 12 C
1011 = 8 + 4 + 0 + 1 = 11 B
1010 = 8 + 0 + 2 + 0 = 10 A
1001 = 8 + 0 + 0 + 1 = 10 9
....
0001 = 0 + 0 + 0 + 1 = 1 1
0000 = 0 + 0 + 0 + 0 = 0 0
二进制数要转换为十六进制,就是以4位一段,分别转换为十六进制。
如(上行为二制数,下面为对应的十六进制):
1111 1101 , 1010 0101 , 1001 1011
F D , A 5 , 9 B
反过来,当我们看到FD时,如何迅速将它转换为二进制数呢?
先转换F:
看到F,我们需知道它是15(可能你还不熟悉A~F这五个数),然后15如何用8421凑呢?应该是8 + 4 + 2 + 1,所以四位全为1 :1111。
接着转换 D:
看到D,知道它是13,13如何用8421凑呢?应该是:8 + 2 + 1,即:1011。所以FD转换为二进制数为:1111 1011。
由于十六进制转换成二进制相当直接,所以当我们需要将一个十进制数转换成2进制数时,也可以先转换成16进制,然后再转换成2进制。
比如,十进制数1234转换成二制数,如果要一直除以2,直接得到2进制数,需要计算较多次数。所以我们可以先除以16,得到16进制数:
被除数 | 计算过程 | 商 | 余数 |
1234 | 1234/16 | 77 | 2 |
77 | 77/16 | 4 | 13 (D) |
4 | 4/16 | 0 | 4 |
结果是16进制为0x4D2,然后我们可直接写出0x4D2的二进制形式:0100 1011 0010,其中对映关系为:
0100 -- 4
1011 -- D
0010 -- 2
同样,如果一个二进制数很长,我们需要将它转换成10进制数时,除了前面学过的方法外,我们还可以先将这个二进制转换成16进制,然后再转换为10进制。
下面举例一个int类型的二进制数:
01101101 11100101 10101111 00011011
我们按四位一组转换为16进制:6D E5 AF 1B
4、原码、反码和补码:
结束了各种进制的转换,我们来谈谈另一个话题:原码、反码和补码。
我们已经知道在计算机中,所有数据最终都是使用二进制数表达;我们也已经学会如何将一个10进制数转换为二进制数;不过,我们仍然没有学习一个负数如何用二进制表达。比如:
假设有一int类型的数,值为5,那么我们知道它在计算机中表示为:
00000000 00000000 00000000 00000101
5转换成二制是101,不过int类型的数占用4字节(32位),所以前面填了一堆0。现在想知道,-5在计算机中如何表示?
在计算机中,负数以其正值的补码形式表达。什么叫补码呢?这得从原码和反码说起。
原码:一个整数,按照绝对值大小转换成的二进制数,称为原码。比如:
00000000 00000000 00000000 00000101 是5的原码。
反码:将二进制数按位取反,所得的新二进制数称为原二进制数的反码。
取反操作指:原为1,得0;原为0,得1。(1变0; 0变1)。比如:
将00000000 00000000 00000000 00000101每一位取反,得11111111 11111111 11111111 11111010。
称:11111111 11111111 11111111 11111010是00000000 00000000 00000000 00000101 的反码。
反码是相互的,所以也可称:11111111 11111111 11111111 11111010 和 00000000 00000000 00000000 00000101 互为反码。
补码:反码加1称为补码。
也就是说,要得到一个数的补码,先得得到反码,然后将反码加上1,所得数称为补码。
比如:00000000 00000000 00000000 00000101 的反码是:11111111 11111111 11111111 11111010。
那么补码为:11111111 11111111 11111111 11111010 + 1 = 11111111 11111111 11111111 11111011
所以,-5在计算机中表达为:11111111 11111111 11111111 11111011。转换为十六进制:0xFFFFFFFB。
再举一例,我们来看整数-1在计算机中如何表示,假设这也是一个int类型,那么:
1、先取1的原码:00000000 00000000 00000000 00000001
2、得反码 :11111111 11111111 11111111 11111110
3、得补码 :11111111 11111111 11111111 11111111
可见,-1在计算机里用二进制表达就是全1,16进制为:0xFFFFFF。
一切都是纸上说的……,说-1在计算机里表达为0xFFFFFF,我能不能亲眼看一看呢?当然可以,利用C++ Builder的调试功能,我们可以看到每个变量的16进制值。
5、通过调试查看变量的值:
下面我们来动手完成一个小小的实验,通过调试,观察变量的值。
我们在代码中声明两个int变量,并分别初始化为5和-5。然后通过CB提供的调试手段,来查看在程序运行时,这两个变量的十进制值和十六进制值。
首先新建一个控制台工程,加入以下黑体部分(就一行):
//-----------------------------------------------------------------------
#pragma hdrstop
//-----------------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
int aaaa = 5, bbbbb = -5;
return 0;
}
//------------------------------------------------------------------------
没有我们熟悉的那一行:
getchar();
所以,如果全速运行这个程序,将只是DOS窗口一闪而过,不过今天我们将通过设置断点,来使程序在我们需要的地儿停下来。
设置断点:是最常用的调试方法之一,使程序在运行时,暂停在某一代码位置。
在CB里,设置断点的方法是在某一行代码上按F5或在行首栏内单击鼠标,如下图:
在上图中,我们在return 0;这一行上设置断点,断点所在行将被CB以红色显示。接着,运行程序(按F9),程序将在断点处停下来。
(请注意两张图的不同,前面的图是运行之前,后面这张是运行中,左边的箭头表示运行运行到哪一行。)
当程序停在断点处时,我们可以观察在当前的代码片段内可见的变量。观察变量的方法很多,这里我们学习使用Debug Inspector (调试期检视)来全面观察一个变量。
以下是调出观察某一变量的Debug Inspector窗口的方法:
①先确保代码窗口是活动窗口(用鼠标点一下代码窗口)。
②按下Ctrl键,然后将鼠标挪到变量aaaa上面,你会发现代码中的aaaa变蓝,并且出现下划线,效果如网页中的超链接,而鼠标也变成了小手状:
③点击鼠标,将出现变量aaaa的检视窗口:
(笔者使用的操作系统为WindowsXP,窗口的外观与Win9X有所不同)
从该窗口,我可以看到:
aaaa :变量名
int :变量的数据类型
0012FF88 :变量的内存地址,地址总是使用十六进制表达
5 :这是变量的值,即aaaa = 5;
0x00000005:同样是变量的值,但采用16进制表示。因为是int类型,所以占用4字节
④关闭前面的用于观察变量aaaa的Debug Inspector窗口。现在,我们用同样的方法来观察变量bbbb,它的值为-5,负数在计算机中使用补码表示。
正如我们所想,-5的补码为:0xFFFFFFFB。
⑤再按一次F9,程序将从断点继续运行,然后结束。
6、本章小结:
很难学的一章?来看看我们主要学了什么:
①我们学会了如何将二、八和十六进制数转换为十进制数(三种转换方法是一样的,都是使用乘法)。
②我们学会了如何将十进制数转换为二、八和十六进制数(方法也都一样,采用除法)。
③我们还学会了如何快速地互换二进制数和十六进制数,要诀就在于对二进制数按四位一组地转换成十六进制数。
在学习十六进制数后,我们会在很多地方采用十六进制数来替代二进制数。
④我们学习了原码、反码和补码,把原码的0变1,1变0就得到反码。要得到补码,则先得到反码,然后加1。
以前我们只知道正整数在计算机里是如何表达,现在我们还知道负数在计算机里使用其绝对值的补码表达,比如:-5在计算机中如何表达?回答是:5的补码。
⑤最后我们在上机实验中,学会了如何设置断点,如何调出Debug Inspector窗口观察变量。
以后我们会学到更多的调试方法。
运算符、表达式和语句
我们已经学会如何用变量来表达数据,比如我们现在要写一个《工资管理系统》……我看到很多学员本来昏昏欲睡的眼睛刷地放出了光芒:“老师,我们现在就能写《工资管理系统》系统了吗?”
回答:不能,我们刚刚学了点基础而已。不过,至少我们可以猜想,要写一个工资管理系统,总得懂得如何表达“工资”这个信息吧?还有像职工的年纪,职工人数等等,都得用C和C++语言来表达,这些我们现在都会啊!
double gongZi; //工资
int nianLin; //年龄
int zhiGongRenShu; //职工人数
看看,上面那些定义变量的代码,你都看得懂,写得出的吧?我们还是颇有成就感的。
本章,我们将学习如何对数据进行运算。
一、算术运算符:
1、加减乘除:
先来学习最基本也最常用的加减乘除等运算。在C++中,加减乘除分别使用字符 '+'、'-'、'*'、'/'作为运算符号。加、减、乘的操作没有什么需要特别说明之处,和生活中的相关运算完全一样,如:
int a = 1 + 2 - 3 * 4;
得到的结果:a等于-9。当然,乘号使用*表示,这你得记住。
除运算除了使用反余杠'/'表示以外,很重要的一点是别忘了,对于整数类型,或字符类型的数据进行除运算时,小数部分将被截掉,因为整型类型的数据不能保存小数部分,如:
int a = 5 / 2;
得到结果:a 等于 2,而不是2.5。
注意:可能大家会以为,之所以 5 /2 结果是2,是因为我们让一个整型变量a等于它,其实原因并不是因为a是int类型,请看:
float a = 5 / 2;
虽然a现在被声明为实型,但执行这句程序,a的值仍然是2。事实上,精度丢失是在计算机计算5/2时就发生了。所以,准确的写法是:
float a = 5.0 / 2;
或者:
float a = 5 / 2.0;
或者:
float a = 5.0 / 2.0;
也就是说,只有除数或被除数至少需要有一个是明确指定为实型,除运算才能得到小数部分。这里我们也更明确类似于5和5.0在计算机中的区别:虽然数值大小一样,但5.0被当成实型数对待,而5则被当成整型数。
2、求模运算:
除了+ - * / 以外, %操作也是C++常用的操作符,%并不是进行“百分比”的运算。在C和C++里,%进行求余数运算,求余数也称“求模”,以下是求余操作的例子:
int a = 5 % 2;
结果是,a等于1,即:5除以2,余数为1。
3、赋值运算:
差点忘了,我们已经很熟悉的等号:=,C和C++里称为赋值操作。看看例子,是不是很熟悉:
int a = 10;
再如:
int b;
b = a;
或:
int c = 12 * 2;
在C++中,可以使用连等操作:
int a ,b;
a = b = 100;
结果是,a和B都为100。
4、自运算:
先来看一个计算机编程中常有的语句例子:
int a = 10;
a = a + 1;
上面的代码执行后,结果a的值是11,可能你不是很理解a = a + 1;这种运算,首先可能会认为,a和a + 1怎么会相等呢?这可是个严重错误,要知道,在C和C++里,'='就是表示赋值操作,至于表示左右两值“相等” 的另有符号。因此a = a + 1,所完成的工作就是:先计算出a + 1的值,然后将该值赋给a。
假设我们的存款原为a,现在存入1元,那么新的存款就等于旧存款加上1元钱,用编程语言表达就是a = a +1;。
在C和C++中,这样的自加操作可以有另一种表达,并且用这一种表达计算机的运算速度比较快;a = a +1;的另一种运算速度较快的表达方法:
a += 1;
+=被定义为一种新的操作符(因此+和=要连着,中间不能有空格);它实现的操作就是将其左边的量在自身的基础上加上右边表达式的值。比如:假设a原来的值为10,那么:
a += 2;
执行这一句后,a的值为12,即 a = 10 + 2;
同样的,减、乘、除和求余都有这种操作符分别为:-= 、 *= 、/= 、%=等,我们以后学习到的另外一些运算符,也有同样的这种对应运算,举一些例子:
假设在分别进行以下操作之前,a原值都为10。
a -= 2;
执行后,a值为8;(a = 10 - 2)
a *= 2;
执行后,a值为20;(a = 10 * 2)
a /= 2;
执行后,a值为5;(a = 10 / 2)
a %= 2;
执行后,a值为0; (a = 10 % 2)
C和C++之所以提供这些操作符,其目的仅仅是为了提高相应操作的运算速度,为什么a += 2;会比a = a + 2;运算得快呢?从编译的角度上看,是因为前者可以生成更短小的汇编代码。
C和C++提供这些别的语言没有的操作符,可以供我们写出优化的代码。
在某些特殊情况下,优化还可以继续,请看下一小节。
5、++和--运算:
当运算是自加或自减1的时候,C和C++提供了更为优化的运算操作符:++,--。
设整型变量a,原值为10,我们已经知道,要实现对其加1,可以有以下两种写法:
方法1: a = a +1;
方法2: a += 1;
我们还知道方法2比方法1好,现在还有方法3,并且是最好的方法,就是用:
++a,或者:a++;
也就是说,在只自加1的情况下,代码 a++或++a可以生成最优化的汇编代码;同样,自减1操作也有对应的操作符:--a或a--;。
设a原值为10,则执行--a或者a--后,a的值都为9。
现在来谈谈++a和a++有什么区别:
在C和C++语言里,++a和--a称为前置运算(prefix),而a++和a--称为后置运算(postfix)。
如果仅仅是进行前置或后置运算,那么结果是相同的,这我们已经在前面讲过,我们以++为例:
设a为10,则无论是++a或a++,执行结果都是让a递增1,成为11。
但在有其它运算的复杂表达式中,前置++运算过程是:先加1,然后将已加1的变量参与其它运算;后置++的运算过程是:先用未加1的变量参与其它运算,然后再将该变量加1。
听起来有些绕,我们举些例子:还是变量a,设原值为10。
例子1:
int b = ++a;//前置++
运算结果:a的值为11,B的值也为11。
计算过程解析:
先计算 ++a,结果a值为11。
然后再计算b = a;结果B值也为11。
例子2:
int b = a++;//后置++
运算结果:a的值为11,但B的值为10。
计算过程解析:
先计算b = a;,因此,b的值是未加1之前的a,所以为值10,然后再计算 a++,a的值为11。
再举一复杂点的表达式:
int a = 10;
int c = 5;
int b = a++ + c;
执行这些代码,b值成为15,倘若换成:
int a = 10;
int c = 5;
int b = ++a + c;
执行这些代码,b值成为16,想一想,为什么?
上面举的是++的例子,对于--操作,其计算顺序的规定一样。
++和--的确能加快运算速度,但它们在前置和后置运算上的微小区别却很空易让你的代码变得不清晰;更为不好的是,不同的编译器可能会对此有不同的解释,比如VC和BC/CB对同一代码会有不同的编译结果,造成代码的运行结果也不一样,这是我们应该尽量避免的。所以我们建议在代码尽量不要依赖于前置和后置运算的区别(尽管它会让你的代码看上去很象“高手”所写)。
二、算术类型转换:
1、隐式类型转换:
类型转换在C和C++中也属于一种运算,前面我们举过一个例子:
float a = 5 / 2 ;
还记得a的计算结果吗?上式中a将得到的值是2,因为在除式5/2中,5和2都是整数,所以计算机按整数类型进行了除运算,结果所有的小数位都被丢失了。
我们列出了三种可以解决小数位丢失的方法:
方法1: float a = 5.0 / 2;
方法2: float a = 5 / 2.0;
方法3: float a = 5.0 / 2.0;
最后一种方法好理解,5.0和2.0都明确指定为实型(double),所以计算结果可以保存小数位。而像第一种:被除数5.0被指定的为实型,但除数2仍然为整型,这两种数据类型的精度不一样,这时,计算机将按哪个类型作为标准呢?
当C++遇到两种不同数据类型的数值进行运算时,会将某个数做适当的类型转换,然后再进行运算,转换总是朝表达能力强的方向进行,并且转换总是逐个运算符的进行。
以下是转换的两条方向线:
char/unsigned char --> short/unsigned short --> int/unsigned int --> double --> long double
float --> double --> long double
像上面的a = 5 / 2,计算机先计算5/2,由于5和2一样是整型,所以计算机不作转换,算出其结果为2,然后赋值给a,因此就算a是float类型,但仍然只能得到2的结果,而不是2.5。
对于a = 5.0 / 2,计算机在计算5.0 / 2时,发现5.0是实型(带小数点),而2是整型,二者不一样,所以将2先自动转换成double数,然后和5.0进行除运算。
这个转换过程,在程序运行时自动进行,称为隐式转换。隐式(自动)转换尽量使我们的程序更加合理,但有时它并不能完全符合我们的要求,比如:
int b = 5, c = 2;
float a = b / c;
由于除号两边的操作数b和c都是有明确类型的变量,所以,这时既不会有隐式转换进行,我们也不能通过加 '.0'来改变其中某个数的数据类型:
float a = b.0 / c.0; //这种写法是错误的,不可能实现。
这种情况下,我们需要显式(强制)类型转换。
2、显式类型转换:
显式类型转换也称为强制类型转换,它的语法形式有两种:
形式1 : (类型名) 变量或数值
形式2 : 类型名(变量或数值)
实际例子如下:
int b = 5, c = 2;
float a = (float)b / c;
或者:
float a = float(b) / c;
两种写法都可以将变量b强制转换为float类型。不过,在要转换的数据类型带有修饰符时,则必须使用第一种型式。比如:(unsigned int) a;其实,两边都加上括号有时更清晰:(unsigned int) (a);
3、关系运算:
“关系”运算?听上去很费解,举个例子说一下:
计算机系一师哥师妹正在处朋友,某晚两人在校园林荫处正在谈情说爱,突然冒出一校监:“说!你俩什么关系?”,果然不愧为计算机系的一对小情侣,以下是他们的回答:
男:“我比她高!”
女:“我比他瘦。”
男:“我比她壮!”
女:“我比他美。”
校监:“我倒!”
所谓的关系运算,在C和C++语言里就是比较运算。
算术运算所得的结果是数值,而关系运算所得的结果为逻辑值,也称布尔值,即我们以前所学的bool类型允许的值:真或假;真用true表示,假用false表示。
关系操作符有:
== (比较左右值是否相等)
> (比较左值是否大于右值)
>= (比较左值是否大于或等于右值,也称为不小于)
< (比较左值是否小于右值)
<=(比较左值是否小于或等于右值,也称为不大于)
!= (比较左右值是否不相等)
比较是否相等,使用两个连写的等号表示,因此 == 和 = 是两个不同的概念,后者是指赋值运算。
C和C++的不等于号用 != 表示,而不是我们更常见的 <>,请大家注意。
下面举一些例子:
int a = 10;
int b = 9;
则:
a == b+1 运算结果: true;
a == b 运算结果: false;
a > b 运算结果: true
a >= b 运算结果: true;
b > a 运算结果: false;
a >= b+1 运算结果: true;
a <= b+1 运算结果: true;
a != b; 运算结果:true;
4、逻辑运算:
逻辑运算有三个操作符:
! (非,取逻辑反,NOT)
&& (逻辑与,并且,AND)
|| (逻辑或,或者,OR)
尽管课程列出了所有可能性,尽管看上去也就这几话,但大家一定要从骨子里头理解这些,不能靠死记硬背来解决问题。
当我们写程序时,就会发现天天在和一堆的真假判断打交道,如果这些最基本的逻辑操作你不理解,那么就会给解决复杂的逻辑问题带来麻烦。
5、表达式:
变量、常量及各种运算符等组成表达式,用于表达一个计算过程。比如写一个计算圆面积,我们可以如下表达:
area = PAI * R * R;
其中,R是某一定义的变量,表示半径;PAI是我们事先定义的一个值为3.14的宏;
PAI * R * R;是一个表达式;area = PAI * R * R;也是一个表达式。表达式组成了C和C++语句,而语句组成C和C++的程序,最简单的如3 + 2 也是一个表达式。
表达式是操作符、操作数和标点符号组成的序列,用于表达一个计算过程。
对表达式的计算,需要考虑其各计算部分的运算优先级,其中最熟悉的莫过于我们小学就学过的“括号优先,先乘除后加减”了,下面的表格列出我们已学过的运算符的优先级。
按优先级高低排列的运算符:
级别 | 运算符 | 说明 |
1 | ( ) | 括号 |
2 | ! +(正号) -(负号) ++ -- sizeof | +,-在这里不是加减,而是指正负号 |
3 | * / % | 乘,除,求模(取余) |
4 | + - | 加减 |
5 | == != | 等于 不等于判断 |
6 | && | 逻辑与 |
7 | || | 逻辑或 |
8 | = += *= /= %= | 赋值,自运算操作符 |
6、语句:
此处内容暂缺。
正是一行行语句组成C和C++程序,结束本章时,我们可以自豪地宣布:我们已经一脚迈入了C++大门的门槛!
C++的顺序流程
你喝一杯水,一般是这样:
1:往杯里倒满开水;
2:等开水冷却;
3:往嘴里倒。
从这个生活的例子中你可以想到,完成事情总是要有顺序的,并且执行顺序往往还很需讲究。譬如喝水的例子,如果你把第2步和第3步调序,结局可能会很难受;而如果你想把第1步放到最后去执行,大概你将永远也喝不了水。程序是用来解决现实生活的问题的,所以流程在程序中同样重要。我们已经学习过语句,当我们写下一行行代码时,这些代码必须按照一定次序被执行,这就是程序的流程。
我们先熟悉最简单的流程:顺序流程。
一、顺序流程:
笔直的长安街,东西走向,长达40公里。顺序流程就像一条笔直的,没有分叉的路,程序执行完第一行,然后第二行、第三行……
我们这一节课用两个例子来熟悉什么叫顺序流程。
电脑,原称计算机(computer),你的电脑是奔III、奔IV或其它什么,据说它每秒钟能计算几百几千万次云云,反正是很厉害的。现在,既然我们学编程了,是该到我们亲手出道题让计算机为我们算算的时候了。一道很简单的题:加法,我们准备做两个版本。先来DOS版本:
1、加法计算器(DOS版):
生成一个空白的控制台工程,黑体是你要加入的。
//----------------------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//----------------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
getchar();
return 0;
}
//----------------------------------------------------------------------
现在,我们来加入实现加法计算的代码:
int main(int argc, char* argv[])
{
int js1,js2; //加数1,加数2
int he; //和
cout << "请输入第1个加数:" << endl;
cin >> js1;
cout << "请输入第2个加数:" << endl;
cin >> js2;
he = js1 + js2; // 和 = 加数1 + 加数2
cout << js1 << " + " << js2 << " = " << he << endl;
getchar();
return 0;
}
//----------------------------------------------------------------------
上面的代码,你应该都能看懂,不过我还是解释一下(你不妨在输入后按一下F9,看一下运行结果,可以更有助于理解)。
这段代码从功能上分,可以分为三个部分:
int js1,js2; //加数1,加数2
int he; //和
这是第一部分,两行代码定义了三个变量:加数1、加数2及和。至于 “//加数1,加数2 ”双斜杠到行末的代码,那是注释。也就是写给程序员自已看的“程序说明”。你可以不理它。
第二部分为输入部分,用来输入:
cout << "请输入第1个加数:" << endl;
cin >> js1;
cout << "请输入第2个加数:" << endl;
cin >> js2;
cout输出一行提示,告诉用户(现在就是我们自已)做什么,而cin则将用户的输入存到变量中,如:
cin > js1;
这一行执行时,会等待用户输入一个数,直到用户回车(别忘了,回车结束输入),用户输入的数值会被自动赋值给变量js1;js1是“加数1”的拼音首字母。
最后一部分实现计算和输出:
he = js1 + js2; // 和 = 加数1 + 加数2
cout << js1 << " + " << js2 << " = " << he << endl;
这段程序的核心代码就是:
he = js1 + js2; // 和 = 加数1 + 加数2
它实现将js1和js2相加,并附值给he。
最后一句 cout 将结果输出,你可以只写成这样:
cout << he;
这样写也把计算结果输出了,但可能会被人说成“用户界面不友好”。噢,来看看我们程序运行时的某种结果,我决定让它计算:2002 + 1973。你要让它算什么?你自已试吧。
虽然是个很不起眼的小程序,虽然只是一道小学低年级的算术题,可是必竟我们亲手证明了我们的爱机具有计算能力,得意5秒钟,我们来继续我们的课程。我将通过单步运行来亲眼看一看程序是如何一步一步地按顺序运行的。如果你在还在运行着程序,回车键关掉黑色运行窗口,切换回C++Builder的代码窗口。
单步运行是一种最必要的调试方法(其它众多调试方法几乎都基于该方法),它可以让程序按代码行一步步运行。
在CB中通过按F7或F8键,可以实现单步运行一个完整的代码行(不一定是物理上的一行代码),F7和F8的功能区别现暂不必理。本章中,我们使用F8,对应的功能菜单为:
从菜单中看到F8对应的功能名称为:Step Over(单步越过)。按下F8后,程序开始运行,但并没有直接出现结果窗口相反,代码窗口出现了变化:
左边栏上的 表示该行是一可以单步中断的代码行,而则指示了当前正要运行的代码行。注意,是正要运行,而不是正在运行。现在让我们再按一次F8,在代码窗口里可以看到程序往下走到第二个可中断行:
再按一次F8程序又往下走了一行(为了方便观察,我将输出窗口拉近,并抓了图)。
输出窗口(DOS窗口)有输出,并且有光标闪烁,但你可以试着在输出窗口里敲敲键盘,会发现你并不能在这一步输入加数1,因为此时程序运行到cin >> js1,但并未执行这一行,只有我们再按一下F8(记得在CB的代码窗口里,而不是在DOS窗口里),程序将要求并允许我们输入加数1。如果你还没有再按一次F8,就现在按一下,结果如下:
我们发现,代码窗口里暂时没有了,因为控制权已暂时移交到我们的程序,它现在可以输入了。我们输入2002,然后回车,很快又出现在代码窗口里,并且,已经越过cin >> js1这一行。
接下来是提示输入第二个加数,大同小异,只要你记得当控制权转到程序时,你需要切换到输出窗口输入第二个数。注意:最后当程序运行:getchar(),需要我们输入一个回车时,控制权也会移到程序。由于是在单步运行,所以当我们在输出窗口最后敲一个回车时,程序同样不会直接运行到结束,所以你仍得到CB的代码窗口里按F8。
getchar()以后,程序仍将在return 0;和最后的一个‘}'上作单步运行两次才退出。如果你懒得这样一步步运行到结束,想结束单步运行,可以直接按F9,程序将恢复全速运行,直接运行到结束。
就这样,我们一步一步地运行完这个程序,在此我们学会了如何通过F8来单步运行程序,我们也理解了什么叫顺序流程。这个程序中每一个可中断点(事实上也是可执行点),在程序运行时,被依照其先后次序,一一执行。
如果你还是不理解什么叫“顺序”,那就从椅子上站起来,然后双眼直直地瞄住一面墙(挑结实一点的),然后直直地走过去,听到“砰”的一声后,你一定会开窍。
(一般不传之秘笈:当程序员觉得被眼前的代码弄得头晕脑胀的时候,大多数人都会起来走走,下楼抽颗烟,或者只是坐坐,吹吹风……最佳方法是挪到不远处某漂亮的女测试员的桌边胡说八道几句,再挪回电脑前,往往发现问题的答案很简单!)
2、加法计算器(WIN版):
我们一直在写DOS下的程序(控制台程序)。趁今天的课程知识点不多,我们来写一个正宗的Windows应用程序,它是实现一个加法计算器。
不要做得太顺手了!听我说,如果刚才那个DOS版你还没保存,请先Shift + Ctrl + S保存全部,接下来,最好执行一下这个菜单命令:File | Close All,如图:
然后,我们重复一下如何建立一个空白的Windows应用程序工程,我们在以前的课程中曾经做过一次,在作业里也考过一次。
其实很简单,菜单:File | New Applicaton (如果是CB6可能略有不同),也可以按工具栏上的这个图标:
然后选择:Application。
新的空白工程建成以后,我们需要来设计程序界面;毕竟,这是一个有图形用户界面(GUI)的Windows程序。
其实很简单,我们需要两个编辑框,让用户输入两个加数;再来一个编辑框用于显示结果;还要一个按钮,当用户按下时,加法运算才开始;当然还需要一些标签用于显示提示文字。
下面先把要用到三种控件在控件栏位置说一下:
这三个控件都在控件栏的Standard页上,如上图:
①标签控件(TLabel);②编辑框控件(TEdit);③按钮控件(TButton)。
先在表单上放三个标签,三个编辑框(在控件栏上按下控件图标,然后在表单上单击):
然后,用鼠标单击表单上的Label1,如果你看不到下面这个窗口(属性检视器),请按F11。
将其Caption(标题)属性由原来默认的Label1,改为“请输入第一个加数:”如下图所示:
同样的方法,分别修改Label2,Label3的标题为:“请输入第二个加数”和“和”。三个编辑框中默认的文本“Edit1”、“Edit2”、“Edit3”我们都不要,编辑框中的文字属于TEdit的Text属性,让我们一起将它们清除:
首先拉动鼠标,一起选中三个编辑框:
将上图中Text属性的值“Edit1”清除。现在,设计结果如图:
前两个编辑框我们准备让用户输入加数,最后一个编辑框要用来显示相加的结果(和),所以不能让用户修改其内容。编辑框(TEdit)有一个属性名为ReadOnly(只读),当它被设置为真(true)时,编辑框的内容不能手工修改。
如上图,将Edit3的ReadOnly属性改为true(原来默认为false)。修改ReadOnly的结果并不能直接在设计期间看出来。最后,让我们再加上一个按钮(TButton),并改变其Caption属性为:“加(&A)”。
界面设计完成,下面开始代码设计。
此处内容暂缺。
以下是运行时的一个界面:
你还可以试着在第三个编辑框里敲敲键盘,会发现的确无法改变其内容。
很开心的一章:我们终于能做些有点意义的小程序了,从身边找一个会电脑,但没有学习编程的家伙,我们大可用这个程序小小的炫耀一番。不过,现在你也应该能理解到,为什么我们在学习C和C++语言时,大多会采用DOS下的程序来作为例子,因为若使用Windows程序,它的界面设计很容易让我们分心。并且,我们还必须面对如Edit2->Text.ToIntDef(0)这些后面才学到的内容。
C++的条件分支语句
一、if...else 语句:
if (条件) //如果
{
分支一 //条件成立时执行
}
else //否则
{
分支二 //条件不成立时执行
}
例一、来一段真实的程序:
假设a,b,c为已定义的三个int变量,有以下代码片段:
if (a > b)
{
c = 1;
}
else
{
c = 0;
}
若a的值为2,b的值为1,那么执行上面代码,c的值将为1;相反,如果a值为1,b值为1,由于条件:a > b不成立,所以程序将执行else后面一对{}中的代码,结果将是c值为0。
用流程图可以直观地在表达程序可能的执行方向;我们来看一看下一页if...else...的流程图:
在流程图中,箭头表示了程序可能的走向,当遇到执行条件(菱形)时,根据条件成立的真假,程序将作出选择,是走分支一还是分支二。但无论经过哪个分支,最后都将同样继续后面的代码。
题一:用户输入一个整数,请写一程序,通过if...else...判断该数是偶数或奇数。
分析:被2整除的整数叫偶数。% 操作符用来求两数相除的余数,比如 5 % 2 得到余数1。那么一个数,比如 a,是否偶数,条件就是 (a % 2) == 0。
打CB,然后新建一个空白控制台工程。在代码中加下以下黑体部分:
//------------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
int num;
cout << "请输入一个整数:";
cin >> num;
if((num % 2) == 0)
cout << num << "是一个偶数。" << endl;
else
cout << num << "是一个奇数。" << endl;
getchar();
return 0;
}
//-------------------------------------------------------------
按F9编译并运行程序后,笔者输入123,以下是屏幕显示结果:
二、if... 语句:
if (条件)
{
分支一
}
if...else...中的 else(否则)并不是必须的,因为有时候,当指定条件成立时,我们执行某些动作,否则,我们不执行那些动作。用流程图表示如下一页所示。
对比if...else...的流程图我们发现if...语句中,当条件不成立时,将绕过分支一的语句,直接执行后面的代码。
题二:用户输入一个字符,用程序判断是否为小写字母,如果是就输出“您输入的字符是小写字母”。
分析:如何判断一个字符是小写字母?让我们查一下ASCII码表。在表里头,小写字母( a ~ z)对应的ASCII值为:
97 ~ 122 | a ~ z |
可见,所有小写字母的值是连续的。那么,判断一个字符是否为小写字母,就看它的值是否大于等于97,并且小于等122。
假设一个字符变量a,要判断它是否>=97,并且<=122,不能这样写:
if ( 97 <= a <= 122) //错误!
而应该使用 && 来表达"并且":
if (a >= 97 && a <= 122)
当然,更直观,更不易出错的写法,是直接使用字符,而不是使用字符的ASCII值:
if (a >= 'a' && a <= 'z')
在BC中新建一个空白控制台工程,在代码中输入以下黑体部分:
//-------------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//-------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
char a;
cout << "请输入一个字符:" ;
cin >> a;
if(a >= 'a' && a <= 'z')
cout << a << "是一个小写字母。" << endl;
getchar();
return 0;
}
//-------------------------------------------------------------
三、? : 表达式:
“? :”表达式我们称为“问号冒号表达式”。
用if...else...和if...语句,已经可以很好地实现所有条件分支的代码。不过C是一门追求简捷高效的语言,它提供的“? :”表达式在某种情况下代替if...else...起到让代码更简捷的作用。
看一看if...else...语句在什么情况下可以简化代码。
首先来看原型:
if (条件)
{
分支一
}
else
{
分支二
}
我们知道,分支一或分支二一般都是一组(多行)语句,用来分别实现条件是否成立时的动作。由于是一组(多行)语句,所以我们有一对{}括在外面,用于形成复合语句。不过,有时候,分支中的语句比较简单,用一句话就可以实现。比如我们所举的例子:
if (a > b)
{
c = 1;
}
else
{
c = 0;
}
在这个例子中,分支一、二都分别只有一条语句。对于只有一条语句的情况,我们可以省略{}(除了在特殊的上下文中外,其它情况下都可以省略。以后我们学习的其它流程控制也一样),如:
if (a > b)
c = 1;
else
c = 0;
看,没有花括号的代码,感觉多紧凑。不过,对于上面的代码,我们还可以继续简化,那就是使用“? :”语句。
c = (a > b)? 1 : 0;
就一行话,多简捷!语句中的问号问的是什么?问的是 a 是否大于b。如果是,则得到值1,否则,得到值0。
? : 表达式格式语法:
(条件) ? 值1 : 值2
例二:设有int变量 a,b,二者均已初始化,要求得到二者中的较大值。
int c = (a > b) ? a : b;
是的,就这么简单,执行上面代码,c 将得到a和b中的较大值。
我们称(a > b) ? a : b为一个表达式,而不称它为完整的一个语句(尽管它的确也可以作一个单独的语句存在)。一般地“? :”表达式拿来作为等号的右值,用于通过条件判断确定一个值后,该值被赋予等号的左值。因此,并不是所有的if..else...语句都可以用“: ?”来代替,只有那些两个分支都只是用来计算一个同一类型的值,然后赋予同一变量的条件分支语句,才适合。
题三:两数取大。
新建一个控制台空白工程。然后在代码中输入以下黑体部分:
//-------------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//-------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
int a,b,c;
cout << "请输入两个整数(用空格分开):" ;
cin >> a >> b;
c = (a > b)? a : b;
cout << c << "大" << endl;
getchar();
return 0;
}
//-------------------------------------------------------------
屏幕输出结果如上图所示,输入的两个数是102和134。
四、多级if...else...语句:
不要害怕为什么一个条件分支就有这么多种语句。多级if..else..语句只是我这么叫它,其实它完全是由if..else..语句组成,就好像楼梯,一个阶的楼梯是楼梯,100个阶的楼梯也是楼梯。
多级 if..else...语法格式:
if(条件1)
{
分支一
}
else if(条件2)
{
分支二
}
else if(条件3)
{
分支三
}
else
{
分支四
}
格式中举出四个分支,实际可以只有三个、两个或者更多个。
多级if...else...语句的流程图:
题四:让我们想像这么一幕情景剧——
时间:XXXX年2月14日;地点:某校园内小公园;
人物:女生一,男生一。
(男生送给女生一束环瑰)
女生(高兴地):多美啊——多少钱买的!
男生:猜。
女生(心里想:如果多于100元我就亲他一口):超过100元吗?
男生:NO。
女生:(心里想:如果多于50元我就许他亲我一口):那,不低于50元吧?
男生:NO。
女生:(心里想:如果多于10元就跟他说声谢谢吧):那是不低于10元了?
男生:NO。
女生:(不再有任何想法,一把丢掉鲜花):呸!
要求根据上面的情景剧,写一程序,用户输入花价后,程序根据不同的花价输出相应的待遇。
新建一个控制台空白工程。然后在代码中输入以下黑体部分:
//-------------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//-------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
int flowerPrice;
cout << "公元2002年2月14日,一小男生向一小女生赠送一束玫瑰……" << endl;
cout << "女生:请输入这束花的价钱。" << endl;
cout << "男生:";
cin >> flowerPrice;
if(flowerPrice > 100)
cout << "女生:哇!我太喜欢这花了,让我亲你一口以示谢意!" << endl;
else if (flowerPrice > 50)
cout << "女生:哈!花美人更美,你不想亲我一口吗?" << endl;
else if (flowerPrice > 10)
cout << "女生:谢谢!" << endl;
else
cout << "女生:什么破花,大头鬼才会喜欢。" << endl;
getchar();
return 0;
}
//-------------------------------------------------------------
以下是运行结果的一种,我实在买不起100元以上的花:
五、switch语句:
多级if...else...显然是为了那些可能需要进行多级判断才能做出选择的情况。如前面的花价。如果正好是大于100元,那么只需判断一次,女生就可以做出决定,但如果是7元钱,那就必须经过“是否大于100?是否大于50?是否大于10?”三次判断。
C为了简化这种多级判断,又提供了switch语句。
switch 语句的格式:
switch ( 整型或字符型变量 )
{
case 变量可能值1 :
分支一;
break;
case 变量可能值2 :
分支二;
break;
case 变量可能值3 :
分支三;
break;
...
default :
最后分支;
}
在 switch 的语法里,我们要学到4个关键字:switch、case 、break、default。
在switch ( 变量 )这一行里,变量只能是整型或字符型。程序先读出这个变量的值,然后在各个"case"里查找哪个值和这个变量相等,如果相等,就算条件成立,程序执行相应的分支,直到碰上break或者switch语句结束。
说起来有点抽象。我们结合一个实例看看。
题五:模拟网络调查,要求输出不同的选项,供网友选择(通过简单地输入数字),程序根据网友的选择,输出相应不同的结果。
新建一控制台空白工程,然后在代码里输入以下黑体部分。
//-------------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//-------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
int fromWay;
cout << "请通过输入序号,选择您如何来到本网站。" << endl;
cout << "1) ---- 通过搜索引擎" << endl;
cout << "2) ---- 通过朋友介绍" << endl;
cout << "3) ---- 通过报刊杂志" << endl;
cout << "4) ---- 通过其它方法" << endl;
cin >> fromWay;
switch (fromWay)
{
case 1 :
cout << "您是通过搜索引擎来到本网站的。" << endl;
break;
case 2 :
cout << "您是通过朋友介绍来到本网站的。" << endl;
break;
case 3 :
cout << "您是通过报刊杂志来到本网站的。" << endl;
break;
case 4 :
cout << "您是通过其它方法来到本网站的。" << endl;
break;
default :
cout << "错误的选择!请输入1~4的数字做出选择。" << endl;
}
getchar();
return 0;
}
以下是输入2时的运行结果。
对照输入结果,我们讲一讲这段代码,其中主要目的是要弄明白switch语句的用法。
首先,int fromWay定义了一个整型变量,准备用来存储用户输入的选择。
cout << "请通过输入序号,选择您如何来到本网站。" << endl;
cout << "1) ---- 通过搜索引擎" << endl;
cout << "2) ---- 通过朋友介绍" << endl;
cout << "3) ---- 通过报刊杂志" << endl;
cout << "4) ---- 通过其它方法" << endl;
这些语句输出提示和选择项,结果如上图。
cin >> fromWay;
这一句则负责等待用户输入,并且将用户的输入存储到 fromWay。
接下来程序遇上了switch语句:
switch (fromWay)
程序将根据fromWay值,在下面的各个case中找到匹配的值。本例中switch带有四个case,值分别是1、2、3、4。在结果图中,由于我输入的是2,所以程序进入下面这个case:
case 2 :
cout << "您是通过朋友介绍来到本网站的。" << endl;
break;
程序打出"您是通过朋友介绍来到本网站的。" 这行内容。最后遇上 break;于是跳出整个switch语句。
同样的道理,如果用户输入的是其它的数,如1或3,则会进入相应的case 1 或 case 3分支。
但如果用户输入的数在所有的case 里都找不到时,会怎么样?比如,用户输入5。这种情况下,default分支将起作用,所有case里的值都不匹配时,switch进入default分支。如果连default也没有,那么程序在switch语句里什么也不做,直接完成switch语句。
我们来看一下如果用户不按所列的情况输入,而造成的结果:
了解一下switch,case,break,default的意思,对理解前面的一切也会有帮助,它们分别是:开关,情况,中断,默认(值)。那么用一句话套起来的说法就是:根据开关值的不同,执行不同的情况,直到遇上中断;如果所有的情况都不符合开关值,那么就执行默认的分支。
关于switch中非常重要的几个注意点如下:
第一、switch( 整型或字符型变量 )中,变量的类型如文中所标,只能是整型和字符类型。它们包含 int,char。当然无符类型或不同的长度整型(unsigned int,short,unsigned char)等都可以。另外,枚举类型(enum)内部也是由整型或字符类型实现。所以也可以。实型(浮点型)数就不行,例如:
float a = 0.123;
switch(a) //错误!a不是整型或字符类型变量。
{
....
}
第二、case之后可以是直接的常量数值,如例中的1、2、3、4,也可以是一个使用常量计算式,如2+2等,但不能是变量或带有变量的表达式,如 a * 2等。当然也不能是实型数,如4.1,或2.0 / 2 等。
switch(formWay)
{
case 2-1 : //正确
...
case a-2 : //错误
...
case 2.0 : //错误
...
}
另外,在case与常量值之后,需要一个冒号,注意不要疏忽。
第三、break 的作用:
break使得程序在执行完选中的分支后,可以跳出整个switch语句(跳到switch接的一对{}之后),完成switch。如果没有这个break,程序将在继续前进到下一分支,直到遇到后面的break或者switch完成。
比如,假设现在程序进入case 1:中的分支,但case 1的分支这回没有加break:
case 1 :
cout << "您是通过搜索引擎来到本网站的。" << endl;
case 2 :
cout << "您是通过朋友介绍来到本网站的。" << endl;
那么,程序在输出 "您是通过搜索引擎来到本网站的。" 之后,会继续输出case 2中的"您是通过朋友介绍来到本网站的。" 。
请大家将前面实例中的代码片段改为如下( 粗体,即将所有的break都通过加//使之无效。):
...
case 1 :
cout << "您是通过搜索引擎来到本网站的。" << endl;
//break;
case 2 :
cout << "您是通过朋友介绍来到本网站的。" << endl;
//break;
case 3 :
cout << "您是通过报刊杂志来到本网站的。" << endl;
//break;
case 4 :
cout << "您是通过其它方法来到本网站的。" << endl;
//break;
default :
cout << "错误的选择!请输入1~4的数字做出选择。" << endl;
...
运行后,结果会是如何?请大家动手试试。
第四、没有default,程序在找不到匹配的case分支后,将在switch语句范围内不做什么事,直接完成switch。大家也可以在实例中将default的代码注释掉,然后试运行,并且在选择时输入5。
...
//default :
//cout << "错误的选择!请输入1~4的数字做出选择。" << endl;
...
第五、必要时,可在各个case中使用{}来明确产生独立的复合语句。
前面我们在讲if...语句和其它流程控制语句时,都使用{}来产生复合语句:
if (条件)
{
分支一;
}
除非在分支中的语句正好只有一句,这里可以不需要花括号{}。但在switch的各个case语句里,我们在语法格式上就没有标出要使用{},请看:
switch ( 整型或字符型变量 )
{
case 变量可能值1 :
分支一;
break;
case 变量可能值2 :
....
}
一般教科书上只是说 case 分支可以不使用{},但这里我想提醒大家,并不是任何情况下case分支都可以不加{},比如你想在某个case里定义一个变量:
switch (formWay)
{
case 1 :
int a=2; //错误。由于case不明确的范围,编译器无法在此处定义一个变量。
...
case 2 :
...
}
在这种情况下,加上{}可以解决问题。
switch (formWay)
{
case 1 :
{
int a=2; //正确,变量a被明确限定在当前{}范围内。
...
}
case 2 :
...
}
由于本注意点涉及到变量的作用范围,所以你如果看得不是很明白,可以暂时放过。
第六、switch并不能代替所有的if..else...语句。这一点你应该知道了,因为前面已说过,它在对变量做判断时,只能对整型或字符型的变量做判断。另外,switch也只能做“值是否相等”的判断。你不能在case里写条件:
switch (i)
{
case (i >= 32 && i<=48) //错误!case里只能写变量的可能值,不能写条件。
...
}
在这种情况下,你只能通过 if...else来实现。
C++的循环语句
一、while循环:
while(条件)
{
条件成立时循环执行的语句;
}
例一:用 while 语句实现求从1到100的累加和:
打开CB;新建一空白的控制台程序;在main()函数体加入下面黑体部分代码;按F9运行;查看运行结果。
//---------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//---------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
int sum = 0; //变量sum将用于存储累加和,将它初始化为0,这很重要。
int i = 1; //i是每次要加的数,它从1开始。
while ( i<= 100)
{
sum += i;
i++;
}
//输出累加结果:
cout << "1到100的累加和为:" << sum << endl;
getchar();
}
sum初始为0,每循环一次,都加上i;i在每次被加后增加1。最终i递增到101,超过了100,这个循环也就完成了任务。
运行程序,输出结果为:
1到100累加和为:5050
例二:用while循环实现简单的统计功能:
下面实现一个学生成绩的统计。由于成绩中包含有80.5这样的需要小数的部分,所以我们使用实数类型。
关闭上面的工程,新建一个控制台工程。在主函数main内加入以下黑体部分的代码:
//--------------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//--------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
float sum,score;
int num;//num用于存储有几个成绩需要统计
int i;//i用于计数
//初始化:
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;
getchar();
}
//--------------------------------------------------------------
输入4个成绩参加统计后运行结果如下:
回车结束上面的程序。
为了更直观地了解循环流程,现在我们来跟踪这段程序中的while循环:
1、首先在循环开始处设置断点(F5 功能):
2、按F9运行程序,在DOS窗口提示“请输入待统计的成绩个数:”时输入4并回车。
3、程序将在一瞬间运行到第一步设置的断点所在行。即while(...)这一行。
此时将鼠标挪到i上,稍等片刻,出现提示“i=1”,同样的方法可以观察num的值。
可见第一遍循环时,i = 1,num = 4,条件:i <= num 显然成立,循环得以继续。
4、按F8键程序往下运行一行,接着再按F8,程序要求输入一个成绩,切换到DOS窗口,随便输入一个数。
回车后,程序运行到下图中蓝底的一行:
5、连续按F8,你将发现程序“回头”运行到 while(...)这一行。此时,i=2;i <= num条件仍然成立。如果想再跟踪一遍循环,可继续按F8;如果想结束跟踪可在断点行上再按一次F5以取消断点;按F9,程序恢复全速运行。
while循环的流程如下图所示:
程序从“前面的语句”开始执行,然后进行条件判断,条件成立则执行一次循环体,然后再重新进行条件判断。这样周而复始,直到某一次判断时条件不成立了为止。
二、do ... while 循环:
do
{
需要循环执行的语句;
}
while(条件);
和while循环最明显的区别在于do...while循环中,判断是否继续循环的条件,放在最后。也就是说,就算是条件一开始就不成立,循环也要被执行一次。
比较下面两段代码,前者使用while循环,后者使用do...while循环语句。
代码段一:
int a = 0;
while( a > 0 )
{
a--;
}
变量a初始值为0,条件 a > 0显然不成立,所以循环体内的a--;语句未被执行。这段代码执行后,变量a值仍为0。
代码段二:
int a = 0;
do
{
a--;
}
while( a > 0 );
在循环执行前,条件 a > 0也不成立,但由于程序在运行到 do...时不先判断条件,而是先运行一遍循环体内的语句a--,使a的值成为-1,然后,程序才判断a > 0,发现条件不成立时结束循环。
do...while中的条件和while循环中的条件一样是:“允许继续循环的条件”,而不是“结束循环的条件”,这和Pascal语言中的do...until正好相反,学习过Pascal(Delphi)的学员得注意。
以笔者的经验,do...while循环用得不多,大多数的循环用while来实现会更直观。下面将1到100的连加程序转换为用do...while来实现:
{
int sum =0;
int i=1;
do
{
sum += i;
i++;
}
while(i<=100);
}
例三:用 do...while实现可以多次统计的程序。
当一个学生,有三门成绩要统计总分,例二中的程序可以很方便地实现;但如果要连续统计一个班级每个学生的这三门成绩,我们就得不断地运行例二的成绩,这显然不方便。
一个同学的三门成绩需要一层循环,要不断地统计多个同学各自的成绩,需要再套上一层循环,即在原来的while循环中加一层do...while循环。
程序的设计思路如下:
程序统计完一遍成绩,就问一句是否要继续统计其他同学的成绩,如果用户输入字母Y或y,表示需要统计一下位,否则程序结束循环。
这个程序是在例二的基础上进行功能改进,粗体部分为新加的代码。
//--------------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//--------------------------------------------------------------
#pragma argsused
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');
}
//--------------------------------------------------------------
上例中,当程序完成一次统计以后,会提问“是否开始新的统计”,用户输入一个字母,存到变量 c,然后程序在do...while的条件里检查C是否等于‘Y’或‘Y’。如果不等于,就结束循环。
由于程序在统计之后有一个提问的时间,所以原来的getchar()就不再需要了。
在这个例子,外层循环使用do...while是最好的选择,因为用户运行这个程序,在多数情况下至少需要统计一次。
最后我们来看do...while循环的流程图,和while的流程图对比一下。
三、for 循环:
for循环是在C和C++里用得最多,也是最灵活的循环语句。要学好它,需要从已经学过的while循环的身上,“挖掘”出有关循环流程的要素,这些要素隐藏在while或do...while的背后,但它将直接体现在for循环的结构上。
循环条件三要素:
第一、条件一般需要进行一定的初始化操作。
例四:用while循环实现1到100累加的代码:
int sum = 0;//变量sum将用于存储累加和,将它初始化为0,这很重要。
int i = 1;//i是每次要加的数,它从1开始。
while ( i<= 100)
{
sum += i;
i++;
}
这段代码中,循环的条件是 i <= 100;因此一开始i肯定需要一个确定的值。前面的int i = 0;这一行代码,在声明变量i的同时,也为i赋了初始值1。这样条件i <= 100得以成立(因为i为1,所以 i <= 100 当然成立)。
第二、循环需要有结束的机会。
程序中最忌“死循环”。所谓“死循环”是指该循环条件永远为真,并且,没有另外的跳出循环的机会(后面将学到)。比如:
while ( 2 > 1 )
{
cout << "死循环" <<endl;
}
执行这段代码,会发现程序停不下来了,因为它的循环条2 > 1永远为true。所以,一个最后可以变成不成立条件在大多数情况下是必需的。
第三、在循环中改变循环条件的成立因素:
这一条和第二条互相配套。
下面的代码如果单独成为一个程序,将同样是一个“死循环”程序:
int i=1;
while ( i<= 100)
{
sum += i;
}
因为i没有被改变的机会,其值永远为1,从而循环条件 i<=100也就永远为真。所以在循环中最后一句(下面加粗部分),不可遗忘。
while ( i<= 100)
{
sum += i;
i++;
}
三要素在for循环结构上体现:
for循环的语法如下:
for(条件初始化;条件;条件改变)
{
需要循环执行的语句;
}
在for的结构中,不仅提供了“条件”的位置,同时也提供了条件初始化,和条件改变的位置。这三者虽然在同一行上,但并不是依次连续地执行。
在for循环中:
1、条件初始化的表达式首先被执行且只被执行一次;
2、程序检查条件是否成立,如果成立就执行循环体中的语句,否则直接结束循环。
3、执行完一遍循环以后,程序执行“条件改变”语句。
1到100整数累加的程序,改为for循环写,是最合适的了,例如:
int sum = 0;
int i;
for( i=1; i <= 100;i++)
{
sum += i;
}
1、程序先执行条件初始化语句i=1;;
2、然后立即判断条件i是否 <= 100,显然,此时该条件成立;
3、于是程序执行循环体内的语句sum += i;;
4、然后,执行改变条件因子的语句:i++;;
5、此时i值变为 2; 程序再次判断条件 i <= 100 ?,依然成立,于是开始第二遍循环……
6、直到变量i可以初始化时才临时声明:for(i = 1;i <= 100;i++)。
for 语句的复合结构,使得程序变得简捷。比如上面的例子中,原来 while或者do...while结构中,循环体内必须用两句语句,现在只需一句(即:i++这一句被移到for的特定位置上),这样我们可以去除花括号。
当然,如果在其它情况下,for的循环体内仍需有多行语句时,{}仍是不可避免的。事实上,就算现在这种情况,我也建议大家使用花括号。这样可以让程序的结构看上去更清晰。
在本例中,如果非要讲究简捷,我们还可以将循环体内的那唯一的一行移到“条件改变”的位置:
for(int i=1; i<=100;sum += i,i++);
sum += i和i++之间用逗号分开。而在for后面的()行末,则直接跟上分号,表示for不必再执行其它的语句。
考虑到后置++的特性(在完成表达式的求值后,才进行加1操作),也可以将sum += i和i++合为一句:
for(int i=1;i<=100;sum += i++);
以上讲了for语句极尽合并之技巧,以求一个简捷。反过来,for语句也可以向 while或do...while语句一样拆开写:
int i = 1;
for(; i <= 100;)
{
sum += i;
i++;
}
看,条件初始化语句被移出for的结构,而条件改变语句则被当成一行普通语句,直接加入到循环体内。而在相应的位置上,只留下分号,用于表示空语句(请注意这一行中有2个分号,分别在 i<=100前后):
for (; i <= 100;)
如上行所示,for循环结构中的“条件初始”和“条件的改变”表达式都被省略,在这种情况下for和while或do...while循环完全一样。比如求1~100累加和:
int i=1,sum=0; for(;i<=100;) { sum += i; i++; } | int i=1,sum=0; while(i<=100) { sum += i; i++; } |
下面分析几个实例:(用于分析的实例不提供上机的完整代码,请同学们自行创建空白工程,然后加入需要代码,确保每个实例都可运行,这是初学者逐步熟练的必经之路……信不信由你。打开CB吧)。
题一:用for循环在屏幕上逐行输出数字:1~200。
分析:这需要一个变量,其值从1变到200,并且每变一次新值,就用 cout语句在屏幕上输出其值。
答案:
for(int i=1;i<=200;i++)
cout << i << endl;
由于循环中执行的动作只有一句,所以我们省略了{}。
题二:6能被1、2、3、6整除,这些数称为6的因子,请循环列出36的所有因子。
分析:求36的因子,就是求1~36中哪些整数可以整除36。我们学过%操作符,它用于求两数相除的余数。所以整除不整除,只要看余数是否为0即可。
答案:
for(int i=1;i<=36;i++)
{
if(36 % i == 0)//余数为0,说明整除
cout << i << " "; //多输出一个空格,用于做两数之间的间隔
}
如果运行程序,得到结果应是:
1 2 3 4 6 9 12 18 36
在这道题中,我们也看到了两种流程的结合:for循环流程和if条件分支流程。复杂问题的解决,往往就是条件流程和循环流程的种种组合,下面要讲的多层循环也是这些组合中一种。
四、多层循环:
有些问题需要多层循环嵌套才能解决。前面可以多次统计的程序,就使用了两层循环。外层的do...while实现重复统计,内层的while循环实现一次统计。
题三:输出以下内容,要求使用两种方法,第一种方法采用单层循环,第二种方法采用双层循环。
123
456
789
方法一:
分析:单层循环的思路是:从1输出到9,并且每输出三个数字,就多输出一个换行符。
答案:
for(int i=1;i<=9;i++)
{
cout << i;
if( i % 3 == 0) //又一次用到“求余”操作。
cout << endl;
}
方法二:
分析:双层循环的思路是:输出三行,每行输出三个数字。
答案:
for(int i=1;i<=3;i++)
{
for(int j=i;j<=i+3;j++)
{
cout << j;
}
cout << endl;
}
代码中,内层的for用于输出每一行的数字,而外层的for则在每一行输出完成后,输出一个换行符,用于换行。需要另加注意的是,内层循环的条件初始化,和外层循环有关,即这一句:int j=i;。正是每次开始内层循环时,j的值都将从当前i的值开始。
这道题似乎让人留恋于用单层循环解决一切,因为看上去用双层循环并不是很直观?
题四:输出以下内容:
1
12
123
1234
12345
123456
1234567
12345678
123456789
本题使用双层循环来实现实为最佳方法。
分析:外层循环用于控制输出9行;内层循环用于输出每行的数字。每一行都是从1开始,但第一行输出1个数字,第二行输出2个,第三行输出3个……
答案:
for(int i=1; i<=9; i++)
{
for(int j=1; j<=i; j++)
{
cout << j;
}
cout << endl;
}
在这道中,内层循环的条件初始化和外层循环无关,但循环条件判断却和外层的i有关了(j<=i)。当然,这并不是必要条件,但内层循环的条件初始化或条件判断,和外层循环的某些因素有关,这是很多多层循环的解决问题的关键!继续一个经典的题目。
题五:输出以下九九口诀表:
1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25
1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=24 7*8=56 8*8=64
1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=36 7*9=63 8*9=72 9*9=81
能独立完成以上所有题目,即表示学完本章。
五、三种循环语句的特点:
while在每一遍循环开始时检查条件是否成立,如果一开始条件就不成立,则循环中的语句将一次也没有执行。
do...while的特殊之处在于,第一遍循环前,并不检查条件,所以,就算条件根本就不成立,循环也将执行一次。
for的特殊之处在于,它除了条件判断以外,还明确地留出了条件初始化,条件变化的位置。所以,有关计数的循环最适于用for来实现。
流程控制拾遗与混合训练
说“拾遗”可能你会以为本章的内容不是重点?那可不是,流程控制的内容并不多,却支撑着所有程序的框架!所有有关流程的内容都是基础加重点,只是本章中继续讲到一些关键字可以改变流程,但并不独自构成完整流程结构。另外,作为流程控制内容的结束章节,我们于最后安排了一些各流程混合使用的训练。
一、break:
1、break的作用与用法:
循环就象绕圈子,比如,体育课,跑1200米,跑道一圈400米,所以我们要做的事就是一边跑一边在心里计数(当然要记数,否则老师万一少计一圈,我们可就玩完了),当计数到3圈时,“循环”结束。如果,我在跑步时不幸由于体力不支而晕倒……怎么办?有两种办法:
①一种是在判断是否继续循环的条件中加入新增条件的判断:
假设原来的循环表达为:
while(已跑完的圈数 < 3)
{
跑一圈……;
}
那么,加上附加条件后,循环表达为:
while(已跑完的圈数 <3 && 我还跑得好好的) //&& 就是"并且",没忘吧?
{
跑一圈……
}
②第二种方法是在循环中使用条件分支,在指定的条件成立时,中途跳出循环,用于实现跳出的关键字为:break。
while(已跑的圈数 < 3 )
{
跑一圈……;
if(我身体感觉不妙)
break;
}
在循环中,每跑完一圈,都检查一下自已是否感觉不妙,如果是则程序执行break,直接跳出while,而不管此时圈数是否到达3圈。
还记得“小女孩买裙子”的故事吗?那时候,我们将“父母不给买小红裙 && 我还没有哭累”作为循环继续的条件,如果使用break,则可以写成这样:
while(父母不给买小红裙)
{
我哭;
if(我哭累了)
break;
}
在循环中,“我”每哭一次,都想想是否累了,如果是,则程序执行break,直接跳出while,而不管此时爸妈是否已经买了我的裙子。通过这两个例子,你应该注意到了,如果要用break,则if的条件(也就是要执行break分支的条件),正好是把原来放在循环判断中的条件反正过来,比如,原来是判断“我还跑得好好的”,现在则是判断“我身体感觉不妙”;原来是判断“我还没有哭累”,现在是判断“我哭累了”。一句话,原来是判断“是否继续循环”,现在是判断“是否跳出循环”……
再来看那个“可以多次统计”的统计程序,看看是否也能把它改成使用break来结束循环。为了节省篇幅同时也是为了突出重点,我们将其中用于实现一次统计的代码,用一句伪代码来实现。(什么叫伪代码?我们用得很经常啊,就是那些用自然语言写的“代码”,这些代码当然无法在计算机上运行,它们只是要方便地表达实际代码要实现的功能)。
int main(int argc, char* argv[])
{
实现统计一个学员的成绩; //伪代码,详细代码请见上章相关部分
do
{
//提问是否继续统计:
cout <<"是否开始新的统计?(Y/N)?";
cin >> c;
}
while( c == 'y' || c == 'Y');
}
改成用 break;
int main(int argc, char* argv[])
{
实现统计一个学员的成绩; //伪代码,详细代码请见上章相关部分
do
{
//提问是否继续统计:
cout <<"是否开始新的统计?(Y/N)?";
cin >> c;
//如果用户输出的不是字母Y,说明他不想继续统计了,我们需要中断循环。
if( c != 'y' && c != 'Y')
break;
}
while (true);
}
请首先看while(true)部分,其条件直接写上真(true)表明这是一个无条件的循环(即,循环将无条件地一直持续下去),这岂不犯了程序界的武林大岂:成了一个“死循环”?其实,相信你已明白,在循环体内,有一个break的分支在呢,当判断用户输入的字母既不是小写的y,也不是大写的Y,break就起它能起的作用了。
三个例子,都是从循环判断的条件摘出一部分或全部(最后一个例子),然后循环体中,采用一个if判断,结束break来跳出循环。可能你会问:为什么要break呢?直接用原来的方法,在while处判断条件不是很好吗?break的长处在于,它可以在循环体内的任意位置进行判断。
继续上一例,假设我们出于慎重,想在用户按入N时,再问他一句是否真的退出统计,则此时显示出了break的方便:
int main(int argc, char* argv[])
{
实现统计一个学员的成绩; //伪代码,详细代码请见上章相关部分
do
{
//提问是否继续统计:
cout <<"是否开始新的统计?(Y/N)?";
cin >> c;
//如果用户输出的不是字母Y,说明他不想继续统计了,我们需要中断循环。
if( c != 'y' && c != 'Y')
{
//出于慎重起见,我们要再问一句用户是否真的不统计了?
cout << "您真的不想继续计算了?(Y:真的结束 / N:继续统计)";
cin >> c;
//这回,如果用户输入Y,表明他真的不统计了:
if( c == 'Y' || c == 'y')
break;
}
}
while (true);
}
在上面例子中,用户的两次输入我们都采用变量c(char类型)接收,但如果第一次输入字母‘Y’时,循环需继续,但如果用户是在第二次输入‘Y',则表示是真的不统计了,循环却必须结束;所以,此时while无法仅凭c的值来做出正确判断,但采用break,正如上面的代码,我们在合适的位置安排一个break,从而直观地实现了。
当然,这里仅为了讲学方便而举此例,如果你真的在程序中为了一个“是否继续统计”而问了用户两遍,可能会被用户骂做“神经质”。不过,如果是删除某些重要数据(直接删除,不可恢复的情况),多问一次就显得很重要了(比如句神英语删除用户操作就会在最后多问一句“真的要说再见吗?我们会想你的……”)。
再举一例,看我们前面关于跑步的例子:
while(已跑的圈数 < 3 )
{
跑一圈……;
if(我身体感觉不妙)
break;
}
这段代码有点问题,因为判断“我身体感觉不妙”是在跑完一圈之后……很可能我在某一圈刚开始跑时就觉得肚子剧痛,极可能是得阑尾炎啊!按照这段程序,我只有坚持跑完一圈后,才能break了……,要完美解决这个问题,我们将在本章再后讲到,现在先采用一个“通融”的办法,我们允许你每跑100米就检查一次吧:
while(已跑完图数 < 3)
{
跑第1个100米;
if(我身体感觉不妙)
break;
跑第2个100米;
if(我身体感觉不妙)
break;
跑第3个100米;
if(我身体感觉不妙)
break;
跑第4个100米;
if(我身体感觉不妙)
break;
}
代码中,我们将1圈拆为4个100米,每跑完1/4我们就检查一次是否身体不对。看明白这个例子,我想你对break的用途和用法,可以算是理解了。
2、break的一个“高级用法”:
本小节不是很适于没有多少实际编程经历的初学者,所以初学者可以跳过,以后再回头阅读,当然,所谓的“高级用法”的确是应该加对引号的,所谈的内容只是一个高手们常用的小小技巧:使用do...break...while简化多级条件判断的结构。
如果你写过不少代码,那么一定会不时遇到类似以下的情况:
假设要找到文件A,复制该文件为B;然后打开B文件,往B文件内写入一些内容;最后再写入成功后,我们需要再进行一些相关操作。
在此过程,遇到以下情况时将放弃后续的操作,认为是操作失败:
①如果A文件不存在;
②如果B文件已经存在,并且询问用户是否覆盖时,用户回答“不”;
③无法复制出B文件;
④无法打开B文件;
⑤无法写入B文件;
⑥无法正常关闭B文件。
用伪代码写该段程序为:
if( A文件存在 )
{
执行A文件的相关操作;
if( B文件不存在 || 用户允许覆盖原有B文件)
{
复制A文件为B文件;
if(复制文件成功)
{
打开B文件;
if(打开文件成功)
{
写入文件;
if(写入成功)
{
关闭B文件;
if(关闭成成功)
{
执行其它必须在一切成功后进行的操作。
……
}
}
}
}
}
}
可能有些操作和判断可以同时处理,但这个程序的繁琐仍然不可避免,而现实中程序的复杂性往往要远过于此例。从语法上看,这个例子没有任何错误,但它的一层套一层的条件判断却让人难以书写、阅读和调试,在复杂的情况就容易造成人为的错误(比如最马虎的,花括号匹配不对等……)。
同样一段代码“程序老鸟”是这样写的:
do
{
if(A文件不存在)
break;
执行A文件的相关操作;
if(B文件存在 && 用户不允许覆盖)
break;
复制A文件为B文件;
if(复制不成功)
break;
打开B文件;
if(打开B文件不成功)
break;
写入文件;
if(写入文件不成功)
break;
关闭B文件;
if(关闭不成功)
break;
执行其它必须在一切成功后进行的操作。
……
}
while(false);
看,代码是不是“直”了很多?这里用了do..while,可是根本不是为了循环,而是为了使用它的break功能。每当有操作不成功时,就直接用break跳出循环,所以循环条件总是一个“永假”false。
在一个程序中,这种结构相当的多,为了更加一步淡化while的原来的循环用途,我们非常值得在代码加入两个共用的宏:
#define BEG_DOWHILE do {
#define END_DOWHILE } while(false);
这里举的是do...while结构,在某些情况下,可以使用while...来实现类似功能。
3、break在for循环中的一点注意:
前面举的例子都是do...while或while,break在for循环也一个样。请看下面例题:
例一:从一开始累加,每次递增1,请问累加到哪个数,累加和超过2000?请输出该数,及当时的累加和。
分析:和求1~100的累加和类似,只是在发现累加和已经超过2000时,就输出当前累加的数,然后结束循环。
for(int i=1,sum=0;;i++)
{
sum += i;
if(sum > 2000)
{
cout << i << "," << sum << endl;
break;
}
}
输出结果为:
63,2016
关于这段例子需要注意三点:
①在循环条件初始的位置,我们同时声明两个变量;
②没有循环条件。为了解这两点注意,请看下面放大图:
(图3)
最后一点需注意的是关于break和“条件因子变化”。我们知道,for每执行一遍循环体后,都将执行一次“条件因子变化”语句(见上图3)。现在需要注意的是:在for循环中,执行break后“条件因子变化”语句同样被跳过,没有执行循环就被中断。
至此,break在while,do...while,for中的用法我们都已见过,不过,你还记得吗,我们最早学到break是在哪里?在讲条件分支语句中的switch里,如果你有点忘了那里的break是起什么作用的,现在就去看看吧。
4、多层循环中的break:
break只能跳出当前层的循环,所以,如果有多层循环,则在内层的break跳出循环后,外层的循环还将继续。
前面说跑步的例子,一圈400米,我们每跑100检查一下是否有肚子疼什么的,如果疼得利害就break后不跑了。这和现实不符,我们应该每跑一步就检查一次是否肚子疼最合理。
一圈得分成几步呢?显然不能再像上面分成四次检查那样写代码了,我们加一层循环,也就是把跑一圈的工作用一个循环来实现:
while(一圈未结束)
{
跑一步;
}
然后,我们在每跑完一步时加入一个判断:
while(一圈未完)
{
跑一步;
if(我身体感觉不妙)
break;
}
把这跑一圈的代码加入外层循环:
while(已跑完图数 < 3)
{
while(一圈未完)
{
跑一步;
if(我身体感觉不妙)
break;
}
}
外层的while用于负责一圈一圈循环跑完三圈,内层的while用于负责一步一步地循环跑完一圈,同时负责每跑一步就检查是否身体不妙,若身体不舒服,就跳出循环不跑了。看起来代码很完美,其实BUG已经产生:问题就在那个break,当“我身体感觉不妙”后,程序遇上break后跳出内层while,落入外层的while;外层的循环可没有被break,所以程序将继续外层的循环。假如你跑第一圈跑了一半时肚子疼,按照这段程序逻辑,那好这第一圈剩下的一半你可以不用跑了,但后面的两圈你还得继续。
解决的第一种方法是:
while(已跑完图数 < 3)
{
while(一圈未完)
{
跑一步;
if(我身体感觉不妙)
break;
}
if(我身体感觉不妙)
break;
}
我们在外层也进行了一次判断,这样当然就可保证从内层跳出来后,外层的循环也被跳出,但在内层已经做过一次“感觉”的情况下,外层还要重新“感觉”一次,这种代码让人不爽,所以我们可以加一个变量,用于记住现在的身体状态:
bool needBreak = false; //是否需要跳出循环
while(已跑完图数 < 3)
{
while(一圈未完)
{
跑一步;
if(我身体感觉不妙)
{
needBreak = true; //做一标志,需要break;
break;
}
if(needBreak)
break;
}
虽然本人的课程并不是绕什么圈子,可是在这有关break的长篇累牍的文字中,想必各位现在头脑里只有一个词,只有一个念头break。想break?后面的课程就不要啦?不可能,我们还是continue吧。
二、continue:
continue汉意为继续,它的作用及用法和break类似,重要区别在于,当前循环遇到break是直接结束循环,而若遇上continue,则是停步当前这一遍循环,然后直接尝试下一遍循环。我把“尝试”加粗以引起注意,为什么要注意原因后面再说,请先看下面关于break和continue的对比:
continue并不结束整个循环,而仅仅是中断的这一遍循环,然后跳到循环条件处,继续下一遍的循环,当然,如果跳到循环条件处,发现条件已不成立,那么循环也将结束,所以我们称为:尝试下一遍循环。
例二:求整数1~100的累加值,但要求跳过所有个位为3的数。
分析:在循环中加一个判断,如果是该数个位是3,就跳过该数不加。
如何判断一个1到100中,哪些整数的个位是3呢?还是 % ,将一个2位以内的正整数除以10以后,余数是3就说明这个数的个位为3。
比如:23除以10,商2余数3,这里我们不需要商,所以用求余(也称为求模)运算:23 % 10 = 3。
int sum = 0;
for(int i = 1; i<=100;i++)
{
if( i % 10 == 3)
continue;
sum += i;
}
cout << sum << endl;
和break正相反:
在for循环中,执行continue后,“条件因子变化”语句没有被跳过,将被执行一次,然后再尝试循环的下一遍。在上例中,当条件i %10 ==3成立时continue被执行,于是,程序首先执行i++;然后执行i <= 100的判断。如果将该段程序改成while,正确答案为:
int sum = 0;
int i = 1;
while(i <= 100)
{
if( i % 10 == 3)
{
i++;
continue;
}
sum += i;
i++;
}
cout << sum << endl;
请注意程序中的两句"i++;",缺一不可。
三、goto:
臭名昭著的goto出场了。
goto的汉义为“转到”,在计算机语言里,它的完整名称为:“无条件跳转语句”,几乎所有高级语言都会劝你尽量不要使用goto,因为它会破坏程序的模块性,严重降低一段程序的可读性。若是老外写的书,则比喻使用大量goto的代码“像意大利面条”,嗯,其实北京的杂酱面也很缠绕……可惜没有走向世界。
goto的用法是,首先要在代码中某处加上一个位标(也称标号),然后在代码中的需要处加上goto,并写出要跳转到的位标。比如,你在第三行代码处加一个位标A : ,然后可以在第10行写上一个goto A,程序执行到该行时,就将跳到第三行。
加位标的方法是在一空行加上位标的名称,命名规则和变量一样,但最后要加上一冒号“:”。
例如:
int i = 1;
A :
cout << i << endl;
i++;
if(i <= 10)
goto A;
... ...
goto虽然号称“无条件跳转”,但事实上是有条件限制的,主要是三条:
①goto只能在当前的同一程序段内跳转;
②goto可以从循环内跳转到循环外的代码,但不能从循环外的代码跳到循环内;
③在有goto的跳转范围内,不能再使用C++允许的临时变量声明。
好了,其实笔者写程序近10年,惟一用到goto的地方就是:将一段简单的程序故意用goto写得面目全非,以期能让破解程序的人因为眼晕而放弃功击……一句老话:如果没有什么特殊理由,不要在程序里使用goto。
四、流程控制强化训练:
这一节将提供一系列的有关流程控制的实例,程序中知识点全部不超过你学到本章时的水平,有些程序需要一些数学上的小小技巧。
所有实例是从建立一个新的控制台工程开始,所以,一些由CB自动生成的代码我将不写出,你应该知道如何将它们写成一个完整的控制台程序。
最后,在例子中会出现不少常见的编程技巧,可能在前面的课程中没有直接讲到,我会对这些技巧进行解说。
1、求绝对值:
例三:用户输入一个整数,请用程序输出绝对值。
分析:
①本例演示了一个最简单的流程控制:if...
②同时你可以学到如何求一个数的绝对值,很简单;另外,看一个数是否为负数,就是看它是否小于0,这也很简单。
③另外,本例使用一个while(true)来无限循环,你可以不断地输入,如果要中止程序,请按Ctrl+C,这是由操作系统提供的DOS窗口下中止程序的热键(因此,本例也无须在最后加getchar();这行代码)。
答案:
#include <iostream.h>
int main(int argc, char* argv[])
{
int num;
while(true)
{
cout << "求绝对值的程序" << endl;
cout << "要中止运行请按 Ctrl + C " << endl;
cout << "==============" << endl;
cout << "请输入一个整数:";
cin >> num;
//正数和0的绝对值是本身,负数的绝对值为其相反数
if(num < 0)
num = -num;
cout << "绝对值为:" << num << endl << endl; //输出两个换行,仅是为了美观
}
}
2、判断用户输入字符的类型:
例四:用户输入一字符,请判断该字符是:大写字母,小写字母,数字字符,其它字符。
分析:
①本题主要演示多级 if..else...
②在ASCII表中,题中所提的前3类字符,其ASCII值都各自连续(换句话说就是:所有的大写字母都是连续的,所有的小写字母也是连续的……)基于这一点,你容易看明白代码中为判断字符类型的方法。
③本解答也采用了循环,所以也不用加getchar()这行代码,关于循环中条件判断方法比较特殊,请见代码后的说明。
#include <iostream.h>
#include <conio.h>
int main(int argc, char* argv[])
{
char ch;
cout << "请输入一个字符:" << endl;
while( (ch = getche()) != '\r' )
{
cout << endl; //加一个换行,仅为了输出美观
if( ch >= 'A' && ch <= 'Z')
cout << ch << "是一个大写字母。" << endl;
else if ( ch >= 'a' && ch <= 'z')
cout << ch << "是一个小写字母。" << endl;
else if( ch >= '0' && ch <= '9')
cout << ch << "是一个数字字符。" << endl;
else
cout << ch << "是一个其它的字符。" << endl;
}
}
在这段代码中,我们用到了getche()库函数,它的声明包含在conio.h文件中,所以本例程除了"#include <iostream.h>"以外,还另需"#include <conio.h>"。
getche()和我们常用的getchar()同样是接收用户从键盘输入的一个字符,但getchar()在用户输入字符后,用户还需要敲一下回车键才能完成输入;而getche()则在用户敲入一个字符后,立即完成。本例中,我们希望如果用户敲一个回车键,则程序自动结束(见下面解析),所以我们采用getche()函数。
现在来看while的循环条件:
while( (ch = getche()) != '\r' )
这行代码依次完成下面两件事:
首先是:ch = getche(),它等待用户敲入一字符,然后将该字符存储在ch变量中,然后是判断条件:(……) != '\r'。程序判断ch是否不等于'\r', '\r'即回车(return)字符。也就是看用户输入的字符是否为回车键,如果不是,则循环继续,如果是,则循环结束。
记住,在C和C++里,一个赋值表达式:A = B,本身也有值,值就是完成赋值后的A。在上例中,A是ch,B是getche()。在C和C++里,几乎所有表达式本身都有值,比如:1+2的值是3;而表达式a = 3的值为3。
理解这段代码,最好的方式就是在CB中运行它,至于我们所要练习的多级if...else在例子中的表现,我不再多说,你需要自已看懂它。
最后解释一下conio.h,其中con即我们总说的控制台,io则和iostream中的io一样,指:input/output。
3、等腰三角形图形的输出:
例五:请输出以下图形:
*
***
*****
*******
*********
分析:
新手刚看这道题,可能觉得无从下手,其实,如果把图形改成一个矩形:
*********
*********
*********
*********
*********
那么就很好解决了:输出5行,其中每行都输出9个*。
for(int i=0;i<5;i++)
{
for(int j=0;j<9;j++)
{
cout << '*';
}
}
对于三角形,程序仍然是这个结构:需要两层循环。同样是要输出5行,所以外层循环不变;不同的地方在于每一行输出的内容。其实三角形同样是输出一个矩形,只不过有些地方要打空格,有些地方要打*,以下我们用“-”表示空格,则三角形实为:
----*----
---***---
--*****--
-*******-
*********
所以,问题的重点在于:在每一行中,哪些地方要输出空格,哪些地方要输出星号?如果我们行和列都从1开始编号,如图:
仔细观察后我们发现,哪一列要打星,哪一列要打空格,主要和该列与第5列(红线所在列)的距离有关:
第1行:只有第5列本身打星,第5列和第5列(自身)的距离是0
第2行:除了第5列以外,增加第4、6列,4和6与5的距离都为1
第3行:增加了3、7两列要打星,3,7两列和5的距离都为2
……
末了,规律就是:在第n行内,凡是和第5列的距离小于n的列,都要打星,其余的列打空格。
下面代码的中,row表示当前行,col表示当前列。
答案:
此处内容暂缺。
以下是输出结果:
在本例中,为了保持大家日常生活的习惯,我对行和列的编号均从1开始,其实,C和C++程序员更习惯于编程从0开始,即原来的第1行现在称为第0行,第1列称为第0列,则相关代码如下(黑体部分为改动处):
for(int row=0;row<5;row++)
{
for(int col=0;col<9;col++)
{
if( col-4 >= -row && col-4 <= row)
cout << "*";
else
cout << " ";
}
cout << endl;
}
学会从0开始索引的思想方法,这也是大家所要注意的,否则在阅读别人代码时会比较困难。
4、输出正弦曲线图:
例六:请在DOS窗口输出正弦曲线图
分析:
①还记得初中代数学的正弦函数吧?
y = sin (x);
当x从0到2π变化时,y的值在-1和+1之间变化。
我们现在的任务就是随着x(位置)的变化,在y的位置上打一个点即可。
②C为我们提供了sin的库函数,只要我们给它x的值它就能计算出相应的y的值,sin(x)函数包含在头文件math.h里。
“math”的意思是:“数学”。
③为了方便,我们将“竖”着输出曲线,即x的值由上而下增长,而y值则在左右“摇摆”。并且,如果y值为负数的话,那么将输出到屏幕的最左外面,所以我们将y值统一加上一值,用于向右偏到合适的位置。至于要加多大的值和第4点有关。
④和前面输出“等腰三角形”类似,假如我们需要在屏幕的某一行最右边(行末)打出一个点,我们的方法是在前边连续地打满空格。正弦值在-1到1之间,我们不可能打零点几个空格,所以,需要正弦值放大一定的倍数。答案:
int main(int argc, char* argv[])
{
#define PI 3.14159
int scale = 30; //放大倍数
double X,Y;
for(float X = 0.0; X <= 2 * PI; X += 0.1)
{
// 乘上scale 是为了放大Y值,而加上scale则是为了向右边偏移
// 以保证所有的点都不会跑出屏幕左边
Y = sin(X) * scale + scale;
//前面打空格
for(int dx = 0;dx<Y;dx++)
cout << ' ';
cout << '.' << endl;
}
getchar();
return 0;
}
完整的代码请查代码文件,由于输出画面太长,所以这里不显示结果图。
5、标准体重计算程序:
尽管类似输出“九九口诀表”、“等腰三角形”和“正弦曲线”这些题目对锻练大家的编程思维颇为有益,但可能很多人都不会喜欢这种题。嗯,这很多人当中,就有我自已一个,所以,我们来点有趣的题目吧。
街上有一种电子称,你站上去一量身高体重,它就会告诉你你的身材是否为标准体重。可是有一天我兴冲冲地往上一站,那机器竟怪里怪气地说:“本仪器不适于非洲儿童……”。害得我当众狼狈而逃!回去后,我痛下血本,每天大鱼大肉,如此月余,自认为横了一点,很想再测测这回该是哪一洲的儿童。然而由于上次的经历已经对我造成了极大的心灵伤害,以致于我上街看见那种电子称就腿软,只好自购小站秤一台,米尺一条,不过,如何计算是否标准体重呢?嗯,就是这节课的“标准体重计算程序”了,计算标准(理想)体重的方法是从网上搜到的。
最近军事科学院还推出一种计算中国人理想体重的方法:
北方人理想体重=〔身高cm—150〕x0.6十50(kg)
南方人理想体重=〔身高cm—150〕x0.6十48(kg) //(原文请见:三九健康网)
考虑到女性一般要比男性轻,所以如果是女性,我们还需要将标准体重减去2公斤。可见,要计算一个人的标准体重,必须知道是男人女人,是北方人还是南方人,及他的身高。用户还必须输入他的现实体重,这样在程序计算出标准体重之后,我们计算实际体重在标准体重百分之几的范围之内,作出不同判断。
代码请见例程文件,加有详细的注解,本题事实上没有什么复杂算法,所以比前面的题都要简单(尽管代码看上去长多了)。在试用时请注意: 程序需要输入数字时,如果不小心输入字母并回车,将引起死循环,这是cin的问题所致,请按Ctrl + C强行退出即可。
在课程的最后,测一下自已的体重与“理想体重”的差距,是个不错的选择……,测好了?能告诉我程序对你的身材所作的评价吗?
函数(一)
函数是C语言的一个重点和难点,我们此次将连续两章进行讲解。本章重点在于彻底理解函数的作用,学会调用函数,学会自已编写函数。我们学习函数第一件事就是问话:干嘛让我学习函数?反过来说就是:函数能为一个程序员做些什么?
一、什么是函数:
我们学了“数据类型、常量、变量”,所以我们有了表达问题中各种数据的能力;我们还学了“流程控制”,所以我们还会针对各种问题,用正确的流程组合解决问题的步骤,从而形成解决问题的方法。
看起来我们已经拥有了从根本上解决任何问题的能力。但家里电视坏了怎么办?呃?这个,我不是学电器专业的。我只会看电视,我不会修理电视。这时候我们的办法是:打一个电话请专业的修理师上门修理。
还有很多问题的解决办法都是和修电视类似,即:我们自已没有这个能力,但我们可以调用一个具备这一能力的人来进行。
函数在程序中就相当于:具备某些功能的一段相对独立的,可以被调用的代码。是的,函数也就是一段代码,代码也就是我们前面的学的“变量,常量,流程控制”等写成的一行行语句。这些语句以一种约定形式存在着,等待我们去调用它。
其实我们已经用过函数了,给你一个数:2.678,能帮我们求出它的正弦值吗?想起来了吗?我们在上一章中学过sin()函数。一段用以被调用的代码,这是函数的本质,当然,使用函数在程序中还有许多其它的作用,但我们将从这个最关键的地方讲起:怎样调用一个函数?
二、如何调用函数:
这一节的任务是通过学会如何调用一个函数,从使用者的角度来了解函数各个重要知识点,从而,也为下一节学习如何写一个函数打下基础。我们相信这样的安排是科学的,因为在生活中,我们也往往先是一个“使用者”,然后才是一个“创造者”或“提供者”。
有哪些函数可调用?
在学会如何调用函数之前,不妨先看看有哪些现成的函数可以调用。
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操作系统,就彻头彻尾是使用动态库;再如一整套Office还有WPS这些都是。一般地说(不绝对),那些提供了安装程序的软件,都是使用动态库的。总之,使用动态库是专业程序的做法。
不管使用动态或静态的库,写程序时都是一样的。只有在最后要链接程序时,我们通过CB设置不同的选项即可。嗯?我说到了“链接”(link)这个词?对了,它就是我们一直加引号的“合并”一词的专业说法。你可以把前面课程上所有的“合并”一词替换为链接,并且不用加引号了。
现在我们来看看CB主要提供哪些类别的库函数(以下内容仅供了解):
①分类判断函数:
这类函数主要用于判断一个字符是什么类型的。就像我们上一章做的“判断用户输入字符的类型”的例子。不使用函数,我们可以以这样的条件判断一个字符是否为小写字母:
if ( ch >= 'a' && ch <= 'z' )
cout << ch << "是一个小写字母。" << endl;
我们也可以直接使用相关的库函数islower:
if ( islower(ch) )
cout << ch << "是一个小写字母。" << endl;
②控制台输入输出函数:
像我们总是使用的getchar()及getche(),这两个函数用来接受用户在控制台程序中的按键输入,另外还有不和输入输出函数。当然,在输出方面,我们几乎都采用cout 来往屏幕输出内容;cin,cout这是C++的方法,如果写C程序(不是C++),则输出更常用的是printf();比如:printf("Hello world!");,这行代码在屏幕上打出一行:“Hello world!”。
除了教学上,或其它一些特殊要求,我们几乎不写控制台式的程序了,我们最终目标是写Windows下的GUI(图形用户界面)程序,而这些控制台输入输出函数,都不能用在GUI程序中,所以,当课例中,用到的某个控制台库函数,我会临时解释一下,其它的,大家就不必花时间了。
③转换函数:
这类函数完成各种数据类型之间的转换,比如把字符串“123”转换数字123,或把小写字母转换为大写字母等等。
④目录管理函数:
目录就是我们现在常说的“文件夹”啦。这些函数可以建立,删除,切换文件夹。一般地,我们已经不再使用,转而使用Windows提供的相关函数。请参看下面的Windows API函数说明。
⑤数学函数:
例如:我们前面说的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;
}
⑥字符串函数:
我们在学习字符串时将用到。
⑦内存管理函数:
我们在学习内存管理时将用到。
⑧杂七杂八的其它函数:这个且不说。
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++当然最合算!
3、VCL库函数:
VCL意为:可视化控件库(Visual Component Library)。事事都直接和Windows的API打交道,编程效率将非常的低。主要表现在两个方面,第一:由于使用API编程是非可视化的,我们将不得不花费非常冗长的时间在处理界面显示的事务上,而界面显示其实不是我们程序的主要逻辑。第二:有关显示等工作的大量代码事实上有很大的相似性,大量重复。我们要么仍受每写一个程序就重复写一堆千篇一律的代码,要么像早期的Windows程序员一样自已动手写一套的类库用来“包装”这段代码,以求每次可以重复利用,但这是件庞大而灵活的工作,显然我们不值得这样做,事实上也不具备这样的能力。笔者在Windows3.1下写程序时,曾经购买过国人高手写的一套这种类库,事实上钱花得不值。很快笔者转向了当时Borland提供的类库:OWL和微软的MFC。VCL提供的也主要是类库,我们暂未学到“类”的概念,所以这时且不详谈。
调用函数前提之一:调用者必须能看到被调用者。
一个“者”字,可能让你以为这里说的是人,其实不是。这里说的调用者,指的是当前程序,而被调用者当然是“将被调用”的函数。本小节的重点是,程序如何才能找到要调用的函数呢?有三种方法:
第一种:将被调用的函数写在当前代码前面。
是啊,我有个朋友是修电视的,有一天他来我家串门,而我家电视正好坏了。
下面我先写一个函数,这个函数的大部分代码我没有写出来,我只是用它表示一个叫“修理电视”的功能。
//本函数实现“修理电视”
void XiuliDianshi()
{
......
}
尽管我们稍后才能学如何自已写函数,但你现在要记住了,上面那几行代码就是一个函数,它的函数名为XiaoliDianshi,意为“修理电视”。
好!有了“修理电视”的函数了,如何调用它呢?下图表示的是正确的情况:
当我们写程序要调用一个函数,而这个函数位于我们现在写的代码前面时,我们就可以直接调用它,这就像修理工就在我们家里一样。注意这里的前面并非仅仅限于“跟前”,如果你的代码有很多行,这个函数在“很前面”,也不妨碍我们调用它。
要注意的是另一面是,当函数在我们的代码后面时,代码就“看”不见这个函数了,下面即为这种错误情况:
第二种:将被调用的函数声明写在当前代码前面。
修理工不在我家,不过,他曾经留给我一张名片,名片上写着:“张三,电视修理工,Tel:1234567,住址:……”。所以我们也能知道他会修理电视,并且知道他的电话和住址,这样就不愁找不到他了,对不?函数也可以有名片,在程序中我们称为函数的“声明”。
下面的代码演示了什么叫函数的“声明”,及它所起的作用:
第三种:使用头文件。
当我们手里有了电视修理工的名片,有了冰箱修理工的名片,有了电脑修理工的名片,名片多了,我们可以将名片整理到一个名片夹。这样做至少有两个好处,其一:便于管理。家里任何电器坏了,只需找“家用电器修理工名片”的名片夹即可。其二:便于多人共用,比如隔壁家想找一个电视修理工,只需上你家借名片夹即可。
在C和C++中,类似“名片夹”功能的文件,称为“头文件”。头文件的扩展名为.h(head)。头文件是放置函数声明的好地方。如何写函数声明下面再说,现在要明白“函数声明”就是给编译器看的函数说明,或曰函数的“自我介绍”。至于为什么叫“头”文件呢?是因为它总是要放在程序代码文件的开头。就相我们在交谈时,开头总是作一番介绍一样(该说法未经证明,仅供参考)。
说千道万,不如先简单地看一眼真实的头文件吧。启动C++Builder并新建一个控制台应用工程。在CB6里,新建控制台工程在File | New | Others 里找,别忘了。
建好后,在代码窗口里加上一行:
#include <stdlib.h>
并且用鼠标在这一行点一下,现在代码窗口里的内容看起来如下:
确保输入光标在单词“stdlib.h”上面闪烁!现在按“Ctrl+回车”,CB将打开光标所在处的文件。
如果你出现的是一个文件打开对话框,那有两点可能,其一是你没有把光标移到指定的单词上,另一可能是你安装CB时没有选择“Full”模式的安装,造成CB没有安装源文件。
以下是打开的 stdlib.h 头文件:
(选择ReadOnly可以把当前文件设置为只读)
打开的文件是C++Builder工程师为我们所写的头文件,请注意千万不要有意无意地改动它!为了保险起见,通过右键菜单选择Read Only,将当前文件设置为只读(如上图)。请大家将这当作一条准则来执行:不管出于什么原因打开CB提供的源文件,立即将其设置为只读。
好了,我们说过“只看一眼”的。关于头文件,在讲完函数以后,还会专门讲到头文件在工程中的应用。现在重复头文件的作用:
函数可以统一在一个头文件中声明,如果在代码中需要使用这些函数,只需要通过“include”语句包含这个头文件,就可以让编译器找到函数。用一句大白话讲就是:要想用函数?请包含它所在的名片夹(头文件)。
函数的“声明”有时被称为函数的“原型”,比如在讲到编译过程时。当我们阅读其它文章时,如果看到“函数原型”一说,希望大家也能明白。
三、调用者必须传递给被调用者正确的参数:
函数的作用是实现某个特定的功能,当我们调用它时,一般需要给它一些数据,这些数据可能是让它直接处理,也可能是辅助它实现具体的功能。
当然有些函数不需要任何外部数据,它就能完成任务。这也很好理解,修理工修理电视是得有台电视,但叫一位歌手到家里随便哼几句歌,你就不用给他什么。关键一句话:函数要不要外部传给它数据,要什么类型的数据,要多少数据,由函数本身决定,而非调用者决定。本例中,电视修理工需要一台电视,这是他的工作性质决定的,不能由请他的人决定。传给函数的数据,我们称为“参数”,英文称为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()函数。这个函数也没有参数,因为我们调它的目的,无非是:喂,告诉你,我一会儿可能要用到随机数,你做好准备吧。
四、如何得到函数的运行结果:
函数总是要实现一定的功能,所以我们也可以认为函数执行起来就像是在做一件事。做一件事一般会有个结果,当然,只是“一般会有”。有些事情真的会有结果吗?嗯?看来,这句话勾起某些同学一些旧事,他们陷入了深深的,似乎很痛苦的回忆之中……对此,为师我表示最大的理解,并有一言相送:“并非是一件事情不会有结果,只是,有时候,我们并不需要结果……”。
此处内容暂缺。
最后一点针对学过PHP,JavaScript,Perl等脚本语言的学员:在C和C++里,一个函数返回值的类型,必须是确定的。不像脚本语言中的函数,可以返回不定类型的结果。
五、调用库函数的实例:
实例一:使用库函数创建或删除文件夹(本例子中删除的文件夹将无法恢复!请大家操作时小心)。
在本实例里,我们将在C盘根目录下创建指定的目录(文件夹),然后再把它删除。需要用到两个函数:
1、mkdir("文件夹名称")
参数是一个字符串,即指定的文件夹名称。返回值比较特殊:整数0表地成功,-1表示失败。比如那个文件夹已经存在,或者你想让它一次创建多级目录,如:C:\ab\12而C:\ab并不存在时。
2、rmdir("文件夹名称")
参数是一个字符串,即指定的文件夹名称。返回同样是0或-1。删除一个文件夹比较容易失败:比如文件夹内还有文件或其它的子文件夹,比如该文件夹正好是当前文件夹,另外,你也不能删除一个根目录,比如你想rmdir("c:")(想删除整个C盘?)。两个函数都在“dir.h”文件里声明,所以我们需要include它。
下面是完整的代码:
此处内容暂缺。
代码里头有一个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对新建文件夹的名称有一些特殊的要求,所以,如果文件夹名称含了一些非法字符,操作将失败。以下是我运行的一个结果:
六、自定义函数:
学会如何调用别人的函数,现在我们来学习如何自已写一个函数。首先看看函数的格式:
1、函数的格式:
定义一个函数的语法是:返回类型 函数名(函数参数定义)
{
函数体
return 结果;
}
其中:
1、返回类型: 指数据类型,如:int ,float,double, bool char ,void 等等。表示所返回结果的类型。如果是void则表示该函数没有结果返回。
2、函数名:命名规则和变量命名一样。注意要能够表达出正确的意义。如果说一个变量命名重在说明它“是什么”的话,则一个函数重在说明它要“做什么”。比如一个函数要实现两数相加,则可以命名为:AddTwoNum,这样有助于阅读。
3、函数参数定义:关于参数的作用,我们前面已说,现在看它的格式:
int AddTwoNum(int a,int b);
函数参数的定义有点类似定义变量,先写参数的数据类型,上例中是int,然后再写参数名。下面是不同之处:
①多个参数之间用逗号隔开,而不是分号。最后一个变量之后则不需要符号。 请对比:
普通变量定义:
int a; //<--以分号结束
int b;
函数中参数定义:
(int a, int b ) //以逗号分隔,最后不必以分号结束
②两个或多个参数类型相同时,并不能同时声明,请对比:
普通变量定义:
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则允许这样定义函数。
2、自定义函数实例:
下面,我们将动手写几个函数,并实现对这些函数的调用。从中我们也将进一步理解函数的作用。
①小写字母转换为大写字母的函数:
实例二:自定义小写字母到大写字母的转换函数。
尽管这个功能很可能已经有某个库函数实现了,但像这种小事,我们不妨自已动手。之所以需要这个函数,缘于最近我们写程序时,经常用到循环,而循环是否结束,则有赖我们向用户提一个问题,然后判断用户的输入;如果用户输入字母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');//现在,只需判断是否为‘Y'了。
return 0;
}
//---------------------------------------------------------------------
完整的代码见相应例子文件。例子只是为了演示如何自已定义函数,并调用,运行时它问一句“继续吗?”你若输入大写或小写的‘y'字母,就继续问,否则就结束循环。函数的返回值也可以直接拿来使用,上面代码中的do...while循环也可以改写成这样:
do
{
cout << "继续吗?(Y/N)";
cin >> ch;
}
while(LowerToUpper(ch) == 'Y');
功能完全一样,但看上去更简洁。请大家进行对比,并理解后面的写法。
本例中的“小写转换大写”的函数,虽然我们已经成功实现,但我们并没有将它的声明放到某个头文件,所以,如果在别的代码文件中,想使用这个函数,还是不方便。确实,我们很有必要为这个函数写一个头文件,在讲完函数后,我们将去做这件事。实例二代表了一种函数的使用需求:我们将一些很多代码都要使用的某个功能,用一个函数实现。这样,每次需要该功能时,我们只需调用函数即可。这是函数的一个非常重要的功能:代码重用。通过函数,不仅仅是让你少敲了很多代码,而且它让整个程序易于维护:如果你发现某个功能实现有误,需要改正或改进,我们现在只需修改实现该功能的函数。如果没有函数?那将是不可想像的。
但是,只有那些一直要使用到的代码,才有必要写成函数吗?并不是这样,有些代码就算我们可能只用一次,但也很有必要写成函数。请看下例:
②使用函数改写“统计程序”:
实例三:使用函数改写第十章“可连续使用的统计程序”。
我们先把第十章的例子拷过来(只拷其中的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()函数也加入使用。
③求多种平面形状的面积
实例四:写一程序,实现求长方形,三角形,圆形,梯形的面积,要求各种形状分别用一个函数处理。
程序大致的流程是:
首先提问用户要求什么形状态的面积?然后根据用户的输入,使用一个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;
}
④主函数:
C和C++被称为“函数式”的编程语言。意指用这门语言写成的程序,几乎都由函数组成,程序运行时,不是在这个函数执行,就是在那个函数内执行。整个程序的结构看上去类似:A函数调用B函数,B函数又调用C函数,而C函数则可能调用了D函数后又继续调用E函数……甚至一个函数还可以调用自身,比如A函数调用A函数,或A调用B,而B又反过来调用A等等……
问题是最开始运行的,是哪个函数?
最开始运行的那个函数,称为主函数。主函数在控制台或DOS应用程序中。为main()函数。在标准的Windows应用程序中,则为名为WinMain()。
1、DOS程序的主函数:
控制台应用程序的主函数:main()我们已经很“熟悉”了,每回写程序都要用到它,只是我们没有专门讲到它。现在回头看看:
int main(int argc, char* argv[])
{
……
return 0;
}
main函数的返回值数据类型为int,参数定义:int argc,char* argv[]的具体含义我们暂不用关心,只需知道,DOS程序或控制台程序中,程序运行时的入口处就是main()函数。
2、Windows程序的主函数:
我们先来创建一个Windows应用程序。注意看课程,不要轻车熟路地生成一个“控制台”工程。
打开CB,选择主菜单File | New | Application,如果是CB,选择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(不用存盘)。
⑤小结:
函数的声明起什么作用?
函数的参数起什么作用?
return语句起什么作用?
大致说说动态库与静态库各自的优缺点?
函数带来的哪两个主要用处?
不看课程,你能自已写出小写字母转换大写的函数吗?
什么叫“由上而下”的编程方法,什么叫“由下而上”的编程方法?
什么叫主函数函数?DOS程序和Windows的主函数一样吗?
第十三章 函数(二)
上一章我们讲了函数最基本的知识,即如何调用一个函数以及如何写一个函数。这一章我们的任务是:重点加深学习函数的返回值和函数的参数;另外我们还将选修函数的递归调用。通过对这两个知识点的深化学习,我们对函数的理解会更深。
一、函数的返回值:
有关函数的返回值,将涉及到函数的这些知识点:函数的类型,return,以及如何得到函数的返回类型。
1、函数的返回类型:
函数的类型,其实是函数返回值的类型。请看例子:
//实现两个整数相加的函数:
int AddTwoNum(int a,int b)
{
return a + b;
}
上面标为红色的int即为函数AddTwoNum的类型,普通的说法是“函数AddTwoNum的返回类型是整型”。也就是说函数AddTwoNum只能返回整型的值,我们看代码:
return a + b;
返回了a + b,其中a和b都是整型,二者相加也是整型。所以这个函数的返回类型正确,下面看一个错误的实例:
int AddTwoNum(float a,float b)
{
return a + b;
}
尽管从逻辑上看,这段代码也没有错误,同样可以实现两个数相加,但我们认为它是有错的代码。因为函数AddTwoNum()的类型仍然规定为int类型,但函数体中的代码,试图返回的却是float类型。为什么说返回的是float类型呢?因为请注意:现在a,b都是float类型了。
不仅这段代码有错,下面的代码也同样错误:
int AddTwoNum(int a, int b)
{
float c = a + b;
return c;
}
注意:写类似上面的代码,编译器会放行,并不出错。因为编译器将一个float类型强制转换为int类型,这就会造成精度丢失。比如调用:AddTwoNum(1.2, 2.4),得到结果为3,而不3.6。
2、return语句:
return语句只在函数内使用。它起到让函数停止运行,然后返回一个值的作用。我们通过一个特殊的对比,可以看到return的第一个作用:让函数停止运行。
| 代码一 | 代码二 |
| void OutputSomething() { cout << "第1行" << endl; cout << "第2行" << endl; cout << "第3行" << endl; }
OutputSomething(); | void OutputSomething() { cout << "第1行" << endl; return; cout << "第2行" << endl; cout << "第3行" << endl; }
OutputSomething(); |
输出结果: | 第1行 第2行 第3行 | 第1行 |
为什么代码二只输出了一行?原因正是因为代码中标成红色的return;当函数执行到return;后就结束了。后面的代码等于白写。这里只是为了突出return的作用才故意这样写。
一个函数没有return;语句也可以自然地结束,比如上面的代码一,当在屏幕上打印完第三行后,函数体内的代码也没了,所以函数自然就结束了,为什么还要return语句呢?
结合流程控制语句和return语句,我们可以控制一个函数在合适的位置返回,并可返回合适的值。
下面的函数实现返回二数中的较大者:
int max(int a, int b)
{
if(a > b)
return a;
return b;
}
这个函数有两个return;但并不是说它会返回两次。而是根据条件来执行不同的返回。执行以下代码来调用上面的函数:
int c = max(10,7);
得到的结果将是C等于10。
这个例子也演示了return 后面可以接一个表达式,然后将该表达式的值返回。请大家想一想调用max(10,7)时,max函数中哪一行的return语句起作用了?想不出来也没关系,我们下一节将通过调试,看最终走的是哪一行。
关于return的最后几句话是:
1、有些函数确实可以不需要return,自然结束即可,如上面的OutputSomething();
2、有些人习惯为return的返回值加一对(),如:return (a); 这样写和 return a;完全一样。当然,在某些特殊的情况下,一对()是必要的。
3、一个函数是void类型时,return不能接返回,这时return仅起结束函数的作用。
4、记得return接的是一个表达式,可以是一个立即数,一个变量,一个计算式,前面我们就看到return a+b;的例子。return甚至也可以接一个函数。
3、跟踪函数:
结合本小节的最后一个例子,我们来学习如何跟踪一个函数,同时我们也将直观地看到return的作用。
我们一直说F7和F8都是单步运行,下面的例子,二者的区别就表现出来了。F7和F8都是单步跟踪,让代码一行行运行,但如果代码调用一个函数,那么,按F8将使调试器直接完成该函数的调用,而按下F7,调试器将进入该函数的内部代码。
例一:调试函数
新建一个控制台的工程,然后加入以下黑体部分的代码。
//-----------------------------------------------------------------------
#include <iostream.h>
#pragma hdrstop
//-----------------------------------------------------------------------
int max(int a, int b)
{
if(a > b)
return a;
return b;
}
//-----------------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
int c = max(10,7);
cout << c << endl;
getchar();
return 0;
}
//-----------------------------------------------------------------------
并在图中所示行加上断点:
现在按F9运行,程序在断点处停下,然后请看准了F7键按下,现在我们跟踪了max()函数:
现在继续按F7或F8键,程序走到:if(a > b) 这一行,然后请将鼠标分别移到该行的a和b上,稍停片刻出现的浮动提示将显示a或b的当前值,相信你会明白很多,至少,你能知道程序下一步何去何从。看一看你想对了没有,再按一次F8或F7:
现在程序即将运行return语句!再按一次F7或F8,再想一想程序将何去何从?
现在,你若是再说不明白return在函数内的作用,就有点过份了吧?
下面大家照我说的继续做两次实验:
第一:重做这个实验,但在第一次需要按F7处,改为按F8,看看事情起了什么变化?
第二:将代码中的int c = max(10,7);改为int c = max(7,10);然后重做这个实验,看看这回走的是哪一行的return语句?
现在我们明白,在跟踪的过程,如果当前代码调用了一个函数,而你想进入这个函数跟踪,最直接的方法就是在调用函数的代码处按F7。
而关于F8与F7键所起的作用,名称为:F7单步进入();F8单步越过。它们分别对应于主菜单上 Run 下的 Trace Into 和 Step Over。如果当前行代码并没有调用函数,则二者的功能完全一样。以后我们讲到无区分的单步跟踪时,将只说按F8。
如果你需要频繁地跟踪某一函数,这种方法并不方便,你可以在设置断点,直接设置在那个函数的定义代码上。然后按F9,程序即可直接停在断点。比如,你可以将断点设置在这一行:int max(int a, int b)。
二、函数的参数:
讲完函数的类型及返回值,我们来看函数另一个重点及难点:参数。
不知上一章关于“修理工需要一台电视”的比喻,有没有在一定程度上帮助大家理解,参数对于一个函数的作用。那时我们只是要大家从概念上明白:参数是调用者给出去的,被调用进来接过使用的数据,就像我们给修理工一台电视,供他修理一样。你只有明白了这点,才可以在下面继续学习参数的具体知识点;否则你应该复习上一章。
1、形参和实参:
隔壁小李开了家服装店,在往服装上贴价钱标签时,小李突发奇想:他决定直接往衣服上贴上和价钱相应的钞票!比如这条裤子值100元,小李就往上贴一张百元大钞,尽管我们无法否认小李是一个非常具有创意的人,但听说最近他一直在招保安,才10来平方的小店里,挤满了一堆保安,不干别的事,就盯着那些贴在衣服上的钱。这当然只是一个笑话……
上一章我们说过,一个函数它需要什么类型的参数,需要多少参数,这都由函数本身决定。这就像一件商品需要多少钱,是由商人来决定,商人认为某件衣服需要100元,他就往上面贴个写着“¥100”价钱的标签,而不是一张真正百元钞票。函数也一样,它需要什么样的参数?这需要在声明和定义时写好。
所谓的形参就是:函数在声明或定义时,所写出的参数定义。
比如有这么一段代码:
由于形参只是形式上的参数,所以在声明一个函数时,你甚至可以不写形参的变量名。如声明某个函数:
int max(int a, int b);
你完全可以这么写:
int max(int ,int );
实参:实际调用时,传给函数的实际参数,称为实参。
请参看上图有关“调用”函数时的参数。
2、参数的传递方式:
参数是调用函数的代码时,传给函数的数据。在C和C++中,参数有两种传递方式,传值方式和传址方式。这两个名词分别指:传递“参数的值”和传递“参数的地址”。
①传值方式:
如果你是第一次学习程序语言,下面代码的执行结果可能让你出乎意料。
例二:传值方式演示
//定义函数F:
void F(int a)
{
a = 10; //函数F只做一件事:让参数a等于10
}
int main(int argc, char* argv[])
{
//初始化a值为0:
int a = 0;
//调用函数F:
F(a);
//输出a的结果:
cout << a << endl;
}
想一想,屏幕上打出的a的值是什么?如果你猜“10”,呵呵,恭喜你猜错了。猜错了是好事,它可以加深你对本节内容的记忆。
如果你不服,立即打开CB将上面代码付诸实际,然后查看结果,那就更是好事!正确答案是,a打出来将是“0”。
代码不是明明将参数a传给函数F了吗?而函数F不是明明让传来的a等于10了吗?为什么打出的a却还是原来的值0呢?
这就是C和C++传递参数的方法之一:值传递。它是程序中最常见的传递参数的方法。
①传值方式:向函数传递参数时,先复制一份参数,然后才将复制品传给参数。这样,函数中所有对参数的操作,就只是在使用复制品。不会对改变传递前的参数本身。
我曾经说过(不是在课程说的),你要学习编程,可以没有任何编程基础,但你至少会要对普通的电脑的操作很熟练,是该考一考你的电脑操作水平了。以下是某用户在电脑上的操作过程,请仔细阅读,然后回答问题。
操作一:用户在C盘上找一个文本文件;
操作二:用户使用鼠标右键拖动该文件到D盘,松开后,出现右键菜单,用户选择“复制到当前位置”,如图:
操作三:用户双击打开复制到D盘的该文件,进行编辑,最后存盘。
请问:C盘上的文件内容是否在该过程受到修改?
答案是不会,因为D盘文件仅是C盘文件的复制品,修改一个复制文件不会造成原文件也受到改动。
前面关于a值为什么不会变,道理和此相同,a被复制了一份。然后才传递给函数F();。请看参数传值方式的图解:
2、传址方式:即传递参数地址。
“地址”?是啊,我们第三次说到它。请大家先复习一下以前的内容:
第三章:3.4.1 内存地址
第五章:5.2 变量与内存地址
地址传递和值传递正好相反,这种方式可以将参数本身传给函数。从而,函数对参数的操作,将直接改变实参的值。
那么,如何才能指定让某个函数的某个参数采用“地址传送”的方式呢?方法很简单,只要在定义函数时,在需要使用地址传送的参数名之前,加上符号:&。如:
void F(int &a) //在形参a之前加一个 &
{
a = 10;
}
笔者我更习惯于把&贴在参数的类型后面:
void F(int& a) //把&贴在类型之后,也可以
{
a = 10;
}
两种书写格式在作用上没有区别,现在让我们用一模一样的代码调用函数F:
例三:地址传递演示:
int main(int argc, char* argv[])
{
//初始化a值为0:
int a = 0;
//调用函数F:
F(a);
//输出a的结果:
cout << a << endl;
}
输出结果,a的值真的被函数F()改为10了。
通过这个例子我们发现,C++中,函数的参数采用“值”或“地址”传递,区别仅仅在于函数的定义中,参数是否加了&符号。在调用函数时,代码没有任何区别。如此产生一个不便之处:我们仅仅通过看调用处的代码,将不好确定某一函数中有哪些参数使用地址传递?我们不得不回头找这个函数的定义或声明。C++的很多地方被反对它的人指责,这是其一。C#语言改进了这一点,要求在调用时也明确指明调用方式。比如,假设有一函数:
int foo(int a ,int &b, int c);
……
在代码某处调用该函数:
int i,j,k;
int r = foo(i,j,k);
如果你看不到前面函数的声明,那么你在读后面的代码时,可能比较难以想起其中的j是采用传址方式。
当然,我们没有必要因此就放弃C++这门强大的语言。如果的确需要让阅读代码的人知道某个地方采用了地址传送,可以加上注释,也可以使用我们以后将学的指针作为参数来解决。
关于地址传送方式的作用及如何实现地址传送,我们已明白。剩下来需要弄明白的是,“地址传送”是如何实现的?
首先,为什么叫“地址”传送?如果你完成了前面指出的复习任务。那么你应该明白了变量与地址的关系,这里我从根本上重述一次:
程序中,数据存放在内存里;内存按照一定规则被编号,这些号就称为内存地址,简称地址;内存地址很长,所以高级语言实现了用变量代表内存地址。所以,一个变量就是一个内存地址。
因此,这里“地址传送”中的“地址”,指的就是变量的地址。那么参数(实参)是变量吗?参数可以是变量,也可以不是变量。我们先来说是的情况。比如前面的例子:
……
int a = 0;
F(a); //正确地调用函数F:参数a是一个变量
……
如果面上面例子中,我们直接传给F函数0,可以吗?
……
F(0); //错误地调用函数F:0是一个常数,不是变量。
……
错误原因是:因为函数F()的参数采用“地址传递”方式,所以它需要得到参数的地址,而0是一个常数,无法得到它的地址。
得出第一个结论:在调用函数时,凡是采用“传址方式”的参数,不能将常数作为该参数。
如果你在程序中违返了这一规定,不要紧,编译器不会放过这一错误。下面让我们来理解为什么传递变量地址可以起到让函数修改参数。这也好有一比,我们再来考一次“电脑操作知识”。以下是某用户在电脑上的操作过程,请仔细阅读,然后回答问题:
操作一:用户在C盘上找一个文本文件;
操作二:用户使用鼠标右键拖动该文件到D盘,松开后,出现右键菜单,用户选择“在当前位置创建方式”,如图:
操作三:用户双击打开在D盘创建的该快捷方式,然后进行编辑,最后存盘。请问:C盘上的文件内容是否在该过程受到修改?
答案:C盘的文件并改变了,因为D盘上的快捷方式,正是C盘上文件的一个“引用”,双击该快捷方式,正是打开了C盘的文件。“地址传递”类似于此,将地址传送给函数,函数对该地址的内容操作,相当于对实参本身的操作。
3、参数的传递过程(选修):
刚讲完“参数的传递方式”,又讲“参数的传递过程”,不禁让人有点发懵:方式和过程有何区别?中学时我对前桌的女生“有意思”,想给人家传递点信息,是往她家打个电话呢?还是来个“小纸条”?这就是“传递方式”的不同。我选择了后者。至于传递过程:刚开始时我把纸条裹在她的头发里,下课时假装关心地“喂,你的头发里掉了张纸……”;后来大家熟了,上课时我轻轻动一下她的后背,她就会不自在,然后在一个合适时机,自动把手别过来取走桌沿的纸条……这就是传递过程的不同吧?(以上故事纯属虚构)
程序是在内存里运行的。所以无论参数以哪一种方式传递,都是在内存中“传来传去”。在一个程序运行时,程序会专门为参数开辟一个内存空间,称为“栈”。栈所在内存空间位于整个程序所占内存的顶部(为了直观,我们将地址较小的内存画在示意图顶部,如果依照内存地址由下而上递增规则,则栈区应该在底部),如图:
当程序需要传递参数时,将一个个参数“压入”栈区内存的底部,然后,函数再从栈区一个个读出参数。
如果一个函数需要返回值,那么调用者首先需要在栈区留出一个大小正好可以存储返回值的内存空间,然后再执行参数的入栈操作。
假设有一函数:
int AddTwoNum(int n1, int n2);
然后在代码某处调用:
....
int a = 1;
int b = 2;
int c = AddTwoNum(a,b);
当执行上面黑体部分,即调用函数的动作发生时,栈区出现下面的操作:
图中标明为返回值预留的空间大小是4个字节,当然不是每个函数都这个大小。它由函数返回值的数据类型决定,本函数AddTwoNum返回值是int类型,所以为4个字节。其它的a,b参数也是int类型,所以同样各占4字节大小的内存空间。
至于参数是a还是b先入栈,这依编译器而定,大多数编译器采用“从右到左的次序”将参数一个个压入。所以本示意图,参数b被先“压”入在底部,然后才是a。这样就完成了参数的入栈过程。根据前面讲的不同“传递方式”,被实际压入栈的数据也就不同。
一、如果是“传值”,则栈中的a,b就是“复制品”,对二者的操作,仅仅是改变此处栈区的内存,和调用处的实参:a,b毫不关联:
二、而在“传址”方式时,编译器会将调用处的a,b的内存地址写入栈区,并且将函数中所有对该栈区内存的操作,都转向调用处a,b的内存地址。请看:
看起来二的图比一要复杂得多。其实实质的区别并不多。你看:
实参a, 值为1, 内存地址为:00129980
实参b, 值为2, 内存地址为:00129984
在一图中,传给函数的是a,b的值,即1,2;
在二图中,传给函数的是a,b的地址,即:00129980,00129984。
这就是二者的本质区别。
“参数的传递过程”说到最后,还是和“参数的传递方式”纠缠在一起。我个人认为,在刚开始学习C++时。并不需要或者甚至就是最好不要去太纠缠语言内部实现的机制,而重在于运用。下面我们就来举一个使用“传址”方式的例子。
题目是:写一个函数,实现将两个整型变量的值互换。
比如有变量:int a = 1, b =2;我们要求将它作为所写函数的参数,执行函数后,a,b值互换。即a为2,b为1。
交换两个变量的值,这也是一个经典题目。并且在实际运用中,使用得非常广泛。事实上很多算法都需要用到它,幸好实现它也非常的简单和直观。典型的方法就是使用“第三者”。你可能感到不解:交换两个变量的值,就让这两个变量各自互换就得了,比如小明有个苹果,小光有个梨子,两人你给我我给你就好了啊,要小兵来做什么?呵,你看吧:
int a = 1, b = 2;
//不要“第三者”的交换(失败)
a = b;
b = a;
好好看看,好好想想吧。当执行交换的第一句:a = b;时,看上去工作得不错,a的值确实由1变成了2。然后再下去呢?等轮到b想要得到a的值时,a现在和b其实相等,都是2,结果b=a;后,b的值还是2.没变。只好让“第三者”插足了……反正程序没有婚姻法。
int a = 1, b = 2;
int c ; //“第三者”
//交换开始:
c = a;
a = b;
b = c;
好了,代码你自已琢磨吧。下面把这些代码写入函数,我命名为Swap;
例四:两数交换。
void Swap(int& a, int& b)
{
int c = a;
a = b;
b = c;
}
int main(int argc, char* argv[])
{
int a,b;
cout << "请输入整数a:" ;
cin >> a;
cout << "请输入整数b":";
cint >> b;
cout << "交换之前,a = " << a << " b = " << b << endl;
Swap(a,b);
cout << "交换之后,a = " << a << " b = " << b << endl;
getch(); //getchar会自动“吃”到我们输入b以后的回车符,所以改为getch(),记得前面有#include <conio.h>
return 0;
}
完整程序请见下载的课程实例。
4、参数默认值:
在手头的某本C++教材里,有关本节内容的第一句话是:“参数默认值也称默认参数值”。对着这话我愣了半天才算明白,所以在后面课程里,有些地方我说“参数默认值”有些地方我又会胡里胡涂说成“默认参数值”。你可别像我一样去“研究”二者的区别呵。个人认为,从词法角度上看,“参数默认值”更准确些。C++支持在定义或声明函数时,设置某些参数的默认值,这一点C不允许。
比如,我们为卖萝卜的大娘写一个计价函数。这个函数需要三个参数:顾客交多少钱?买多少斤萝卜?及萝卜的单价,返回值则是大娘应该找多少钱。例如,顾客交了100元,他买5斤萝卜,单价是1.00元/斤。那么函数就会计算并返回95,表示应该找给顾客95元钱。
//函数定义如下:
float GiveChange(float money, float count, float price)
{
return money - count * price; //找钱 = 已付款 - 数量 * 单价
}
当我们在程序中需要使用该函数时,我们大致是这样调用:
float change = GiveChange(100,5,1);
看上去一切很完美,不过C++允许我们锦上添花。并且不是一朵只为了好看的“花”。现实情况是,萝卜的价钱是一个比较稳定的数,当然并不是不会变,在出现亚洲经济风暴时,萝卜价还是会变,总之是会变,但很少变。
碰上这种情况,我们每回在调用函数时都写上最后一个参数,就有些亏了,这时,我们可以使用“参数的默认值”。
//首先,函数的定义做一点改动:
float GiveChage(float money, float count, float price = 1.0)
{
.....
}
看到变化了吗?并不是指函数体内我打了省略号,而是在函数参数列表中,最后一个参数的定义变为:float price = 1.0。这就是默认参数值,我们指定价格默认为1元。
然后如何使用呢?以后在代码中,当需要计算找钱时,如果价钱没有变,我们就可以这样调用:
float change = GiveChange(100,5); //没有传递最后一个参数。
是的,我没有写最后一个参数:价钱是多少?但编译器发现这一点时,会自动为我填上默认的1.0。如果在代码的个别地方,大娘想改一改价钱,比如某天笔者成了顾客,大娘决定按1斤2毛钱把萝卜卖给我。
我给大娘5毛钱,买2斤:
float changeForBCBSchool = GiveChange(0.5, 2 ,0.2); //你一样可以继续带参数
我想,这个实例很直观,但必须承认这个例子并没有体现出参数默认值的种种优点。不过不管如何,你现在应该对参数的默认值有感性认识。下面学习有关参数默认值的具体规定。
1、必须从最右边开始,然后连续地设置默认值。
如何理解这句话?
首先,我们看关键词“最右边”。也就是说假如一个函数有多个参数,那么你必须从最后一个参数开始设置默认值。如:
void foo(int a, int b, bool c);
那么,下面的设置是正确的:
void foo(int a, int b, bool c = false); //ok,c是最后一个参数
//而,下面是错误的:
void foo(int a, int b = 0, bool c); //fail,b不是最后一参数
然后我们看“连续”,也就是说,从最右边开始,你可以连续地向左设置多个参数的默认值,而不能跳过其中几个。如:
下面的设置是正确的:
void foo(int a, int b=0, bool c = false); //ok ,连续地设置C,b的默认值
同样,这也是正确的:
void foo(int a=100, int b=0, bool c = false); //ok ,连续地设置a,C,b的默认值
//而这样设置是错误的:
void foo(int a=100, int b, bool c = false); //fale,不行,你不能跳过中间的b
2、如果在函数的声明里设置了参数默认值,那么,就不用在函数的定义中再次设置默认值了。
函数的“声明”和“定义”你可能又有些胡涂了,好,就趁此再复习一次。所谓的“定义”,也称为“实现”,它是函数完整的代码,如:
//函数定义如下(函数定义也称函数的实现):
float GiveChange(float money, float count, float price)
{
return money - count * price; //找钱 = 已付款 - 数量 * 单价
}
而函数的“声明”,则是我们上一章不断在说的函数的“名片”,它用于列出函数的格式,函数的声明包含函数的“返回类型,函数名,参数列表”,惟一和函数定义不一样的,就是它没有实现部分,而是直接以一分号结束,如:
//声明一个函数:
float GiveChange(float money, float count, float price); //<---注意,直接以分号结束。
现在和参数默认值有关的是,如果你在函数声明里设置了默认值,那就不用,也不能在函数定义处再设置一次。如,下面代码是正确的:
//----------------------------------
//定义:
float GiveChange(float money, float count, float price)
{
return money - count * price; //找钱 = 已付款 - 数量 * 单价
}
//声明:
float GiveChange(float money, float count, float price = 1.0);
//----------------------------------
而下面的代码有误:
//定义:
float GiveChange(float money, float count, float price = 1.0)
{
return money - count * price; //找钱 = 已付款 - 数量 * 单价
}
//声明:
float GiveChange(float money, float count, float price = 1.0);
//---------------------------------
3、默认值可以最常见的常数,或全局变量,全局常量,甚至可以是一个函数的调用。关于题中的“全局”,我们还没有学习,这时理解就是在程序运行区别稳定存在的变量或常量。下面举一个让我们比较狐疑的,使用函数来作默认参数的例子:
//某个返回double的函数:
double func1();
double func2(double a, double b = func1()); //func1()的执行结果将被用做b的默认值。
三、函数重载:
重,重复也。载者,承载也。“重复”一词不用解释,“承载”不妨说白一点,认为就是“承负”。函数的“重载”,意为可以对多个功能类似的函数使用相同的函数名。
1、重载的目的:
有这个需要吗?不同的函数取相同的名字?这不会造成混乱?在现实生活中,我们可一点也不喜欢身边有哪两个人同名。当然有这个必要,“函数名重载”是C++对C的一种改进(因为C也不支持重载)。想一想那个求“二数较大者”的max函数吧。如果不支持函数名重载,那么就会有以下不便:
int max(int a, int b);
这是前面我们写的,用以实现两数中较大者的函数。比如你传给它20,21,那么,它将很好地工作,返回21。现在,我们想求20.5和21.7两个实数中较大者?对不起,max函数要求参数必须为int类型,所以传给它20.5,21.7。
float larger = max(20.5,21.7);
编译器不会让这行代码通过。它会报错说“参数不匹配”。好吧,我们只好为实数类型的比较也写一个参数,但C语言不允许函数重名,所以我们只好另起一个名字:
float maxf(float a, float b);
你可能会说,那就不要int版的max,只要这个float版本的:
float max(float a, float b);
因为,实数版本的完全可以处理整数,说得没错,但这不是一个好办法。其一,我们已知道实数和整数相比,它有计算慢,占用空间大的毛病;其二,float版本的max函数,其返回值必然也是float类型,如果你用它来比较两个整数:
int larger = max(1, 2);
编译器将不断警告你,“你把一个float类型的值赋值给一个int类型的变量”。编译器这是好心,它担心你丢失精度,但这会让我们很烦,我们不得不用强制类型转换来屏蔽这条警告消息:
int larger = (int) max(1,2);
这样的代码的确不是好代码。好吧,就算你能容忍这一切,下一问题是,我写了一个求3个整数谁最大的函数。这回你没有理由因为要写三个参数的版本,就把两个参数的版本扔了。最好还是换名:
int max_3(int a, int b, int c);
看着max_3这个函数名字,我不禁想起前几天在yahoo申请免费电子信箱,我想叫 nanyu@yahoo.com.cn ,它却坚持建议我改为:nanyu1794@yahoo.com.cn(1794?一去就死?),折腾了我两个半小时,我才找到一个可以不带一串数字,又让我能接受点的呢称。
结论是:不允许重名的世界真的有些烦。C++看到了这一点,所以,它允许函数在某些条件的限制下重名,这就是函数重载。
前面有关max()的问题,现在可以这样解决:
//整数版的max()
int max(int a, int b);
//单精度实数版的max()
float max(float a, float b);
//双精度实数版的max();
double max(double a, double b);
//甚至,如果你真的有这个需要,你还可以来一个这种版本的max();
double max(int a, double b);
//接下来是三个参数的版本:
int max(int a, int b, int c);
double max(double a, double b, double c);
上面林林总总的求最大值函数,名字都叫max();好处显而易见:对于实现同一类功能的函数,只记一个名字,总比要记一堆名字要来得舒服。
2、函数重载的规则:
有一个问题,那么多max函数,当我们要调用其中某一个时,编译器能知道我们到底在调用哪一个吗?如何让编译器区分出我们代码中所调用的函数是哪一个max,这需要有两个规则。
实现函数重载的规则一:同名函数的参数必须不同,不同之处可以是参数的类型或参数的个数。
如果你想写两个同名函数,错误一:
int max(int a, int b);
int max(int c, int d);
看上去这两个函数有些不同,但别忘了,形参只是形式,事实上两个声明都可以写成:
void max(int, int);
所以请记住,仅仅参数名不一样,不能重载函数。
错误二:
float max(int a, int b);
int max(int a, int b);
两个函数的不同之处在于返回类型,对不起,C++没有实现通过返回值类型的不同而区分同名函数的功能。
所以记住:仅仅返回值不一样,不能重载函数。
正因为函数的重载机制和函数的参数息息相关,所以我们才把它紧放在“函数参数”后面。但函数重载并不能因此就归属于“参数”的变化之一,以后我们会学习不依赖于参数的重载机制。
实现函数重载的规则二:参数类型的匹配程度,决定使用哪一个同名函数的次序。
若有这三个重载函数:
1)int max(int a, int b);
2)float max(float a, int b);
3)double max(double a, double b);
现在我这样调用:
int larger = max(1, 2);
被调用的将是第1)个函数。因为参数1,2是int类型。而:
double larger = max(1.0, 2);
被调用的将是第……,注意了!是第3)个函数。为什么?首先它不能是第1)个,因为虽然参数2是int类型,但1.0却不是int类型,如果匹配第1)函数,编译器认为会有丢失精度之危险。
然后,你可能忘了,一个带小数的常数,例如1.0在编译器里,默认为比较保险的double类型(编译器总是害怕丢失精度)。
最后,关于这两个规则,都是在同名的函数参数个数也相同的情况下需要考虑,如果参数个数不一样:
int max(int a, int b);
int max(int a, int b ,int c);
当然就没有什么好限制了,编译器不会傻到连两个和三个都区分不出,除非……,实现函数重载的附加规则:有时候你必须附加考虑参数的默认值对函数重载的影响。
比如:
int max(int a, int b);
int max(int a, int b ,int c = 0);
此例中,函数重载将失败,因为你在第二个max函数中设置了一个有默认值的参数,这将造成编译器对下面的代码到底调用了哪一个max感到迷惑。不要骂编译器笨,你自已说吧,该调用哪个?
int c = max(1, 2);
没法断定,所以你应该理解、接受、牢记这条附加规则。事实上影响函数重载的还有其它规则,但我们学习这些就够了。
3、参数默认值与函数重载的实例:
例五:参数默认值、函数重载的实例
有关默认值和函数重载的例子,前面都已讲得很多。这里的实例仅为了方便大家学习。请用CB打开下载的配套例子工程。所用的就是上面提到的例子,希望大家自已动手分别写一个默认值和重载的例子。
4、inline 函数:
从某种角度上讲,inline对程序影响几乎可以当成是一种编译选项(事实上它也可以由编译选项实现)。
①什么叫inline函数?
inline(小心,不是online),翻译成“内联”或“内嵌”。意指:当编译器发现某段代码在调用一个内联函数时,它不是去调用该函数,而是将该函数的代码,整段插入到当前位置。
这样做的好处是省去了调用的过程,加快程序运行速度,(函数的调用过程,由于有前面所说的参数入栈等操作,所以总要多占用一些时间)。这样做的不好处:由于每当代码调用到内联函数,就需要在调用处直接插入一段该函数的代码,所以程序的体积将增大。拿生活现象比喻,就像电视坏了,通过电话找修理工来,你会嫌慢,于是干脆在家里养了一个修理工。这样当然是快了,不过,修理工住在你家可就要占地儿了。(某勤奋好学之大款看到这段教程,沉思片刻,转头对床上的“二奶”说:
“终于明白你和街上‘鸡’的区别了”。
“什么区别?”
“你是内联型。”)
内联函数并不是必须的,它只是为了提高速度而进行的一种修饰。要修饰一个函数为内联型,使用如下格式:
inline 函数的声明或定义
简单一句话,在函数声明或定义前加一个 inline 修饰符。
inline int max(int a, int b)
{
return (a>b)? a : b;
}
②inline函数的规则:
规则一:一个函数可以自已调用自已,称为递归调用(后面讲到),含有递归调用的函数不能设置为inline;
规则二、使用了复杂流程控制语句:循环语句和switch语句,无法设置为inline;
规则三、由于inline增加体积的特性,所以建议inline函数内的代码应很短小,最好不超过5行。
规则四、inline仅做为一种“请求”,特定的情况下,编译器将不理会inline关键字,而强制让函数成为普通函数。出现这种情况,编译器会给出警告消息。
规则五、在你调用一个内联函数之前,这个函数一定要在之前有声明或已定义为inline,如果在前面声明为普通函数,而在调用代码后面才定义为一个inline函数,程序可以通过编译,但该函数没有实现inline。比如下面代码片段:
//函数一开始没有被声明为inline:
void foo();
//然后就有代码调用它
foo();
//在调用后才有定义函数为inline
inline void foo()
{
......
}
代码foo()函数最终没有实现inline;
规则六、为了调试方便,在程序处于调试阶段时,所有内联函数都不被实现。
最后是笔者的一点“建议”:如果你真的发觉你的程序跑得很慢,99.9%的原因在于你不合理甚至是错误的设计,而和你用不用inline无关。所以, inline根本不是本章的重点。有关inline 还会带来的一些其它困扰,我决定先不说了。
5、函数的递归调用(选修):
第4次从洗手间里走出来,在一周前拟写有关函数的章节时,我就将递归调用的内容放到了最后。函数递归调用很重要,但它确实不适于初学者在刚刚接触函数的时候学习。
①递归和递归的危险:
递归调用是解决某类特殊问题的好方法。但在现实生活中很难找到类似的比照。有一个广为流传的故事,倒是可以看出点“递归”的样子。“从前有座山,山里有座庙,庙里有个老和尚,老和尚对小和尚说故事:从前有座山……”在讲述故事的过程中,又嵌套讲述了故事本身。这是上面那个故事的好玩之处。
一个函数可以直接或间接地调用自已,这就叫做“递归调用”。C和C++语言不允许在函数的内部定义一个子函数,即它无法从函数的结构上实现嵌套,而递归调用的实际上是一种嵌套调用的过程,所以C和C++并不是实现递归调用的最好语言。但只要我们合理运用,C和C++还是很容易实现递归调用这一语言特性。先看一个最直接的递归调用:
有一函数F();
void F()
{
F();
}
这个函数和“老和尚讲故事”是否很象?在函数F()内,又调用了函数F()。这样会造成什么结果?当然也和那个故事一样,没完没了。所以上面的代码是一段“必死”的程序。不信你把电脑上该存盘的存盘了,然后建个控制台工程,填入那段代码,在主函数main()里调用F()。看看结果会怎样?WinNT,2k,XP可能好点,98,ME就不好说了……反正我不负责。出于“燃烧自己,照亮别人”的理念,我在自已的XP+CB6上试了一把,下面是先后出现的两个报错框:
这是CB6的调试器“侦察”到有重大错误将要发生,提前出来的一个警告。我点OK,然后无厌无悔地再按下一次F9,程序出现真正的报错框:
这是程序抛出的一个异常,EStackOverflow这么看:E字母表示这是一个错误(Error),Stack正是我们前面讲函数调用过程的“栈”,Overflow意为“溢出”。整个 StasckOverflow 意思就:栈溢出啦!
“栈溢出”是什么意思你不懂?拿个杯子往里倒水,一直倒,直到杯子满了还倒,水就会从杯子里溢出了。栈是用来往里“压入”函数的参数或返回值的,当你无限次地,一层嵌套一层地调用函数时,栈内存空间就会不够用,于是发生“栈溢出”。
(必须解释一下,在本例中,void F()函数既没有返回值也没有参数,为什么还会发生栈溢出?事实上,调用函数时,需要压入栈中的,不仅仅是二者,还有某些寄存器的值,在术语称为“现场保护”。正因为C和C++使用了在调用时将一些关键数值“压入”栈,以后再“弹出”栈来实现函数调用,所以C和C++语言能够实现递归。)
这就是我们学习递归函数时,第一个要学会的知识:
逻辑上无法自动停止的递归调用,将引起程序死循环,并且,很快造成栈溢出。怎样才能让程序在逻辑上实现递归的自动停止呢?这除了要使用到我们前面辛辛苦苦学习的流程控制语句以后,还要掌握递归调用所引起的流程变化。
②递归调用背后隐藏的循环流程:
递归会引起什么流程变化?前面的黑体字已经给出了答案:“循环”。自已调用自已,当然就是一个循环,并且如果不辅于我们前面所学的if...语句来控制什么时候可以继续调用自身,什么时候必须结束,那么这个循环就一定是一个死循环。如图:
递归调用还可间接形成:比如 A() 调用 B(); B() 又调用 A(); 虽然复杂点,但实质上仍是一个循环流程:
在这个循环之里,函数之间的调用都是系统实现,因此要想“打断”这个循环,我们只有一处“要害”可以下手:在调用会引起递归的函数之前,做一个条件分支判断,如果条件不成立,则不调用该函数。图中以红点表示。
现在你明白了吗?一个合理的递归函数,一定是一个逻辑上类似于这样的函数定义:
void F()
{
……
if(……) //先判断某个条件是否成立
{
F(); //然后才调用自身
}
……
}
在武侠小说里,知道了敌人的“要害”,就几乎掌握了必胜的机会;然而,“递归调用”并不是我们的敌人。我们不是要“除掉”它,相反我们利用它。所以尽管我们知道了它的要害,事情还要解决。更重要的是要知道:什么时候该打断它的循环?什么时候让它继续循环?这当然和具体要解决问题有关。所以这一项能力有赖于大家以后自已在解决问题不断成长。就像我们前面的讲的流程控制,就那么几章,但大家今后却要拿它们在程序里解决无数的问题。
(有些同学开始合上课本准备下课)程序的各种流程最终目的是要合适地处理数据,而中间数据的变化又将影响流程的走向。在函数的递归调用过程中,最最重要的数据变化,就是参数。因此,大多数递归函数,最终依靠参数的变化来决定是否继续。(另外一个依靠是改变函数外的变量)。所以我们必须要彻底明了参数在递归调用的过程中如何变化。
③参数在递归调用过程中的变化:
我们将通过一个模拟过程来观察参数的变化。这里是一个递归函数:
void F(int a)
{
F(a+1);
}
和前面例子有些重要区别,函数F()带了一个参数,并且,在函数体内调用自身时,我们传给它当前参数加1的值,作为新的参数。红色部分的话你不能简单看过,要看懂。现在,假设我们在代码中以1为初始参数,第一次调用F():
F(1);
现在,参数是1,依照我们前面“参数传递过程”的知识,我们知道1被“压入”栈,如图:
F()被第1次调用后,马上它就调用了自身,但这时的参数是 a+1,a就是原参数值,为1,所以新参数值应为2。随着F函数的第二次调用,新参数值也被入栈:
再往下模拟过程一致。第三次调用F()时,参数变成3,依然被压入栈,然后是第四次……递归背后的循环在一次次地继续,而参数a则在一遍遍的循环中不断变化。由于本函数仍然没有做结束递归调用的判断,所以最后的最后:栈溢出。要对这个函数加入结束递归调用的逻辑判断是非常容易的。假设我们要求参数变到10(不含10)时,就结束,那么代码如:
void F(int a)
{
if( a < 10)
F(a+1);
}
终于有了一个安全的递归调用例子了。不过它似乎什么也没有做,我们加一句输出代码,然后让它做我们有关递归的第一个实例吧。
④一个安全的递归调用函数实例:
例六:用递归实现连续输出整数1到9。
//递归调用的函数:
void F(int a)
{
if( a < 10)
{
cout << a;
F(a+1);
}
}
//然后这样调用:
F(1);
完整的代码请见下载的相应例子。输出将是:
123456789
请大家自行模拟本题函数的调用过程。
⑤递归函数的返回:
这里并不是要讲递归函数的返回值。
天气还不是很冷,你能把身上的衣服脱光一下吗?当初你穿衣服时,一定是先穿上最里层的衣服,然后穿上第二层的,再穿上第三层。现在让你脱衣服,你就得先脱外层,再脱稍里一层,然后才是最内层。
函数的递归调用,和穿衣脱衣类似,不过内外相反而已。开始调用时,它是外层调内层,内层调更内一层。等到最内层由于条件不允许,必须结束了,这下可好,最内层结束了,它就会回到稍外一层,稍外一层再结束时,退到再稍外一层,层层退出,直到最外层结束。
如果用调用折线图来表示前例,则为:
本小节不是讲递归函数的返回值,而是讲递归函数的返回次序。前面听说要脱衣服而跑掉或跑来的同学,可以各回原位了。
做为本小节的一个例子,我只给实例的代码,请大家考虑会是什么输出结果。考虑并不单指托着腮做思考状(你以为你是大卫?)。另外,我相信有很多同学有小聪明,他们凭感觉就可以猜出结果。聪明很好,但千万别因为聪明而在不知不觉中失去动手调试程序的动力,代码其实只是在上例中再加上一行。
例七:递归函数的返回次序:
//递归调用的函数:
void F(int a)
{
if( a < 10)
{
cout << a;
F(a+1);
cout << a;
}
}
//然后这样调用:
F(1);
完整代码见下载的例子。
⑥小结:
我们讲了函数是如何通过return 返回一个值,这个的值的类型就是函数类型。讲完return之后,我们还“深入函数”内部进行跟踪,其实就是一个按F7还是F8的问题。
关于参数,我们讲了什么叫形参,函数在声明或定义处的参数,称为形参,它实际上位于栈内的某个内存地址。至于实参,就是调用时函数时所用的参数,它不在栈内存区里。一句话:一个参数在未被“压入”栈内,就称为实参,并“压入”栈内了,就成了形参。呵,如果你刚才没有选修“参数的传递过程”这一节,可能现在有些后悔了。
C++支持的设置参数默认值比较好玩,大家在实际编程中可能会常用。你知道“从右到左,连续”这两个词和默认值的关系吧。函数的重载就更有意思了,不过记住了,只对“功能类似”的函数进行重载吧,把一些功能互异的函数全叫成同名,那么效果就适得其反。另外,由于重载规则的限制,有时候与其在为排列同名函数的参数表头痛,不如还是恢复老办法,直接另取个函数名就是。学了一项技术,千万不要有非要用上的想法。是改变名字还是改变参数,怎样方便怎样来。inline 函数?估计很多同学什么也没记住,只记了关于“大款”的讲话,哎,失败的教育。最后是函数的递归调用,没什么好说的。我认为学习编程很重要的一点是学会看别人的代码(当然,那个人应该水平比你高)。大家自已尝试一下写几个安全的递归调用,然后有机会看别人如何用递归解决实际问题。至于本章的课程,希望你常回头看。后面的课程,将不断地用到函数了。大家多动手写一些有关这两章的练习程序。