第5单元 函数本单元教学目标介绍C++函数的定义、声明和调用方法。
学习要求熟练掌握C++函数的编写和调用方法,以及内联函数、函数重载和递归函数的概念。
授课内容函数是C++程序的构成基础。C++程序都是由一个个函数所组成的,即便是最简单的程序,也得有一个main()函数。因此,一个C++程序无论多么复杂,规模有多么大,程序的设计最终都落实到一个个函数的设计和编写上。
在C++中,函数是构成程序的基本模块,每个函数具有相对独立的功能。C++的函数有三种:主函数(即main()函数)、C++提供的库函数和用户自己定义的函数。
合理地编写用户自定义函数,可以简化程序模块的结构,便于阅读和调试,是结构化程序设计方法的主要内容之一。
5.1定义和调用函数一个函数必须先定义后才能使用。所谓定义函数,就是编写完成函数功能的程序块。定义函数的一般格式为:
<函数值类型声明> <函数名>(<参数说明>)
{
<函数体>
}
其中:
1.函数值类型声明:即调用该函数后所得到的函数值的类型。例如,例1-2中的函数grav()的函数值的类型是double,即双精度浮点类型。函数值是通过函数体内部的return语句提供的,其格式为:
return <表达式>;
功能是将表达式的值作为函数值返回。在编写函数时要注意用return语句提供的函数值的类型应与函数声明中的函数值类型一致,否则可能出现错误。
有些函数可能没有函数值,或者说其函数值对调用者来说是不重要的。这时调用该函数实际上是为了得到运行该函数内部的程序段的其他效果。这一点与数学中的函数概念有所不同,需特别注意。如果要声明一个函数确实没有返回值,可以使用声明符void。例如主函数
void main()
{
… …
}
既没有返回值,也不需要参数。对于一个被声明为void类型的函数,编译程序如果发现在程序中用到了其返回值,或者在该函数中出现了有返回值的return语句,都会报告相应的错误信息,便于检查程序是否有错。
2.参数声明:C++函数的参数声明格式为:
<类型><参数1>,<类型><参数2>,…,<类型><参数n>
例如
int array[],int count
3.函数体:函数体本身是一个分程序,由语句和其他分程序组成。C++语句以分号“;”结束,一行上可以书写多个语句,一个语句也可以分开写在连续的若干行上(但名字、语句标识符等不能跨行书写)。C++的语句可以分为声明语句和执行语句两类,在一个函数体(或分程序中)这两种语句可以交替出现,但对某具体变量来说,应先声明,后使用。
[例5-1] 编写一个求阶乘n!的函数。
算 法:阶乘n!的定义为:
n! = n×(n(1)×(n(2)×...×2×1
且规定0! = 1。
程 序:
// Example 5-1:计算阶乘n!
#include <iostream.h>
// 函数 fac():计算阶乘
long fac(int n)
{
long result = 1L;
if(n<0)
return -1L;
else if(n==0)
return 1L;
while (n>1)
{
result *= n;
n--;
}
return result;
}
分 析:因为即使n的数值并不大(例如n = 10),其阶乘值就可能超出int型数的表示范围。因此我们将fac()函数的函数值类型定为long类型。如果n为负数,则函数fac()返回(1L,负值在正常的阶乘值中是不会出现的,正好用作参数错误的标志。
该函数定义了一个阶乘的算法。该函数一经定义,就可以在程序中多次地使用它。函数的使用是通过函数调用来实现的。
在C++程序中,除了main()函数以外,任何一个函数都不能独立地在程序中存在。任一函数的执行都是通过在main()函数中直接或间接地调用该函数来引发的。调用一个函数就是执行该函数之函数体的过程。
函数调用的一般形式为:
<函数名>(<实参表>)
函数的调用既可以出现在表达式可出现的任何地方,也可以以函数调用语句(后加分号)的形式独立出现。实参表是调用函数时所提供的实在参数值,这些参数值可以是常量、变量或者表达式。调用函数时提供给函数的实参应该与函数的参数表中的参数的个数和类型一一对应。特别应该注意,C++中实参与参数变量之间数据的传递是按照“值传递”的方式进行的,函数的参数实际上是定义于函数中的局部变量,在调用函数时由实参为这些参数变量提供初值。
[例5-2] 阶乘函数的调用。
程 序
// Example 5-2:测试阶乘计算函数的主程序
void main()
{
int n;
cout << "Please input a number n to calculte n!,";
cin >> n;
cout << n << "! = " << fac(n) << endl;
}
输 入:Please input a number n to calculte n!,5
输 出:5! = 120
5.2 函数原型
C++规定,函数和变量一样,在使用之前也应该事先声明。函数的定义可视为对函数的声明。因此,在前面各单元的例子中,函数的定义均放在程序的前部。另外,在C++中还有一种函数的引用性声明,即函数原型(Function Prototype),通常也称其为函数声明。函数原型的一般形式为
<函数返回值的类型声明> <函数名> (<参数表>);
其中各部分的意义与函数定义相同。
函数原型与函数定义的区别在于:函数原型没有函数体部分,且是用分号结束的,就像变量的声明。
有了函数原型,则只要将其放在对函数的调用之前,则即使函数的定义放在其引用之后,也不会引起编译失败。这就为进行结构化、模块化程序设计提供了极大的方便。
[例5-3]使用函数原型。
// Example 5-3:求两数中的大数
#include <iostream.h>
void main()
{
int max(int x,int y);
cout << "Enter two integer:";
int a,b;
cin >> a >> b;
cout << " The maxium number is" << max(a,b) << endl;
}
int max(int x,int y)
{
return x>y?x:y;
}
分 析:尽管函数max()的定义出现在对它的调用之后,然而由于使用了函数原型,程序就能成功地编译通过。函数原型向编译器提供了函数的名字、值的类型和参数的个数及类型等信息。在函数原型中,参数的名字也可以省略,因此,上例中的函数原型也可以改写为
int max(int,int);
5.3函数间的参数传递在C++程序中,利用函数不仅可以改善程序的可读性,而且提高了程序的灵活性。由于函数通常是用于实现一个具体功能的模块,所以它必然要和程序中的其他模块交换信息。实际上,一个函数可以从函数之外获得一些数据,并可向其调用者返回一些数据。这些数据主要是通过函数的参数与函数的返回值来传递的。
函数可以没有参数,也可以有一个或多个参数。在参数表中声明的参数(变量)叫做函数的形式参数,简称形参。在调用函数时,一般须为每一个形参给出其实际数据,即实际参数,简称实参。实参可以是变量、常量、表达式,也可以是一个函数调用,但每个实参的数据类型应该与其所对应的形参的数据类型相匹配。
在调用一个带有参数的函数时,就存在一个实参与形参结合方式的问题。在C++中,实参与形参有3种结合方式:值调用、地址调用和引用调用。本单元中仅使用了值调用方式,其余两种结合方式将在以后介绍。
值调用的特点是调用时实参仅将其值赋给了形参,因此,在函数中对形参值的任何修改都不会影响到实参的值。前面介绍的例子中的函数调用均为值调用。值调用的好处是减少了调用函数与被调用函数之间的数据依赖,增强了函数自身的独立性。
然而,由于被调用函数向调用函数传递的数据仅有一个返回值,有时显得不够用。在这种情况下,可以考虑用实参由函数内向调用函数传送信息,这可以通过用函数来修改实参的值来实现。为此,可使用第6单元中介绍的地址调用和引用调用。
5.4 局部变量和全局变量根据作用域的不同,可以将C++程序中的变量分为局部变量和全局变量。局部变量是在函数或分程序中声明的变量,只能在本函数或分程序的范围内使用。而全局变量声明于所有函数之外,可以为本源程序文件中位于该全局变量声明之后的所有函数共同使用。
全局变量的用途是在各个函数之间建立某种数据传输通道。通常,我们使用返回值和参数表在函数之间传递数据,这样做的好处是数据流向清晰自然,易于控制。但有时会遇到这种情况,某个数据为许多函数所共用,且其流向本身就很清晰,为了简化函数的参数表,可以将其声明为全局变量。例如,在设计图书资料管理系统时,可以将图书卡片数据声明为一个全局数组,由“购买新书”函数模块将新书书卡加入该数组中,“借阅”、“还书”、“查询”和“统计”等函数分别参考该数组的内容管理图书资料的流通和利用。
初看起来全局变量可以为所有的函数所共用,使用灵活方便,因此颇为一些初学者所喜爱,在程序中大量使用。实际上,滥用全局变量会破坏程序的模块化结构,使程序难于理解和调试。因此要尽量少用或不用全局变量。
如果在一段程序中,既有全局变量,也有局部变量,而且全局变量和局部变量的变量名相同,这时会出现什么情况呢? 请看下面的例子。
int x; // 声明全局变量
int func1(int x) // 函数func1()有一个名为x的参数
{
y = x;
...,..
}
int func2(int y)
{
int x; // 函数func2()中声明了一个名为x的局部变量
...,..
}
void main()
{
...,..
x = 0; // 在主函数中为全局变量x赋值
...,..
}
在上面的程序中一共有3个变量x:一个是全局变量,一个是函数func1()的参数,还有一个是函数func2()中的局部变量。 虽然我们说全局变量的作用范围是整个源程序,但就上面这段程序而言,只有在主函数中才能使用全局变量x,而在其他两个函数中的 x均是它们的参数或局部变量。这种现象可以用“地方保护主义”形象地说明。
也可以在函数内部的变量声明语句之前加上外部说明符extern说明该函数中用的就是外部变量,而非一个同名的局部变量。例如
int x = 0; // 全局变量声明
...,..
func()
{
extern int x,y; // 变量x,y都是相应的外部变量
...,..
}
int y = 0;
...,..
与此类似,声明于函数或分程序中的局部变量的作用域是整个函数或分程序,包括其中嵌套的所有其他分程序。但是,在内层分程序中声明的变量与其外层的函数或分程序中声明的变量重名时,仍是按上述原则确定各自的作用域,即内层变量声明优先于外层变量声明。不过,为了使程序清晰易读,最好不要在嵌套的分程序中声明重名的变量。
自学内容
5.5内联函数在调用函数时,系统要做许多工作,主要包括断点现场保护、数据进栈、执行函数体、保存返回值、恢复现场和断点等,开销很大。
有些函数的函数体比较简单(如例5-3中的函数max()),在调用时,执行函数体所消耗的时间与函数调用时的其他时间开销相比就显得微不足道。如果该函数被频繁调用,则附加的时间开销将大得不容忽视。
C++为了解决这一矛盾,提供了一种称做“内联函数”的机制。该机制通过将函数体的代码直接插入到函数调用处来节省调用函数的时间开销,这一过程叫做内联函数的扩展。由于在扩展时对函数的每一次调用均要进行扩展,所以内联函数实际上是一种用空间换时间的方案。
要定义一个内联函数,只需在定义函数时将该函数用关键字inline修饰即可。
[例5-4] 将例5-3中的函数max()改写成内联函数。
程 序:
// Example 5-4:求两数中的大数
#include <iostream.h>
inline int max(int x,int y)
{
return x>y?x:y;
}
void main()
{
cout << "Please enter two integer:";
int a,b;
cin >> a >> b;
cout << "The maximum is" << max(a,b) << endl;
}
分 析:本例与例5-3的功能完全相同,但它们的执行方式却不同。在本例中不再有对max()函数的调用,编译器将把该例中的输出语句处理成如下形式:
cout << "The maximum is" << a> b?a:b << endl;
使用内联函数时应注意:
1.在C++程序中,除了在函数体中含有循环、switch分支和复杂嵌套的if语句的函数外,所有的函数均可以被声明为内联函数。
2.内联函数的定义必须出现在对该函数的调用之前,如例5-4所示。这是因为编译器在对函数调用语句进行代换时,必须事先知道代换该语句的代码是什么。如果像例5-3那样,即使在函数原型和函数定义处均加上关键字inline也不行。
3.由于计算机的资源总是有限的,使用内联函数虽然节省了程序运行的时间开销,但却增大了代码占用内存的空间开销。因此在具体编程时应仔细地权衡时间开销与空间开销之间的矛盾,以确定是否采用内联函数。
5.6带有缺省参数的函数
C++允许在函数声明或函数定义中为参数预赋一个或多个缺省值,这样的函数就叫做带有缺省参数的函数。在调用带有缺省参数的函数时,如果为相应参数指定了参数值,则参数将使用该值;否则参数使用其缺省值。例如,某函数的声明为:
double func(double x,double y,int n = 1000);
则其参数n带有缺省参数值。如果以
a = func(b,c)
的方式调用该函数,则参数n取其缺省值1000,而如果以
a = func(b,c,2000);
的方式调用该函数,则参数n的值为2000。
使用带有缺省参数的函数时应注意:
所有的缺省参数均须放在参数表的最后。如果一个函数有两个以上缺省参数,则在调用时可省略从后向前的连续若干个参数值。例如对于函数
void func(int x,int n1 = 1,int n2 = 2);
若使用func(5,4);的方式调用该函数,则n1的值为4,n2的值为2。
缺省参数的声明必须出现在函数调用之前。这就是说,如果存在函数原型,则参数的缺省值应在函数原型中指定,否则在函数定义中指定。另外,如果函数原型中已给出了参数的缺省值,则在函数定义中不得重复指定,即使所指定的缺省值完全相同也不行。
5.7函数重载在C++的函数库中,有4个功能相似的函数:
int abs(int);
double fabs(double);
1ong labs(1ong);
这些函数的原型均在头文件math.h中声明,其功能依次为求整型量、双精度实型量和长整型量的绝对值。同是求某数的绝对值,要用不同的函数实现,不但增加了程序员的记忆难度,也增加了出错的可能性。能否将求绝对值的方法看成是一个通用的方法,用同一函数形式调用呢?C++中的函数重载可以满足这一要求。
所谓函数重载,即若干参数和返回值不同的函数共用一个函数名。
[例5-5] 重载绝对值函数。
程 序:
// Example 5-5:重载绝对值函数
#include <iostream.h>
int abs(int x)
{
return x>0?x:-x;
}
double abs(double x)
{
return x>0?x:-x;
}
1ong abs(1ong x)
{
return x>0?x:-x;
}
void main()
{
int x1 = 1;
double x2 = 2.5;
1ong x3 = 3L;
cout << "|x1| = " << abs(x1) << endl;
cout << "|x2| = " << abs(x2) << endl;
cout << "|x3| = " << abs(x3) << endl;
}
输 出: |x1| =1
|x2| = 2.5
|x3| = 3
分 析:本例中定义了3个同名的函数abs(),分别为求整型量、实型量和长整型量绝对值的函数。在main()函数中分别调用这3个函数求x1,x2,x3的绝对值。
重载的函数既然函数名相同,那么编译器是根据什么确定一次函数调用是调用的哪一个函数呢?编译器是根据函数参数的类型和个数来确定应该调用哪一个函数的。因此,重载函数之间必须在参数的类型或个数方面有所不同。只有返回值类型不同的几个函数不能重载。
重载函数从一个方面体现了C++对多态性的支持,即实现了面向对象(OOP)技术中所谓“一个名字,多个人口”,或称“同一接口,多种方法”的多态性机制。
5.8 C++的库函数为了方便程序员编程,Visual C++软件包提供了大量已预先编制的函数,即库函数。对于库函数,用户不用定义也不用声明就可直接使用。由于C++软件包将不同功能的库函数的函数原型分别写在不同的头文件中,所以,用户在使用某一库函数前,必须用inc1ude预处理指令给出该函数的原型所在头文件的文件名。例如,欲使用库函数sqrt (),由于该函数的原型在头文件math.h中,所以必须在程序中对该函数调用前写一行:
#include <math.h>
在第3单元已经介绍了字符串处理类库函数。C++的库函数极多,很难一一列举。本教程的附录4:“常用库函数”给出了部分常用库函数的原型及其说明。学习库函数的用法,最好的方法是通过联机帮助查看该函数的说明,如有疑问则再编一小验证程序实际测试其参数和返回值。
5.9 自动变量和静态变量根据变量的生存期,还可以将变量分为自动变量和静态变量。
静态变量的特点是在程序开始运行之前就为其分配了相应的存储空间,在程序的整个运行期间静态变量一直占用着这些存储空间,直到整个程序运行结束。因此静态变量的生存期就是整个程序的运行期。所有的全局变量都是静态变量。另外,在主函数的开始声明的局部变量也具有和整个程序运行期相同的生存期。如果在声明静态变量的同时还说明了初值,则该初值也是在分配存储的同时设置的,以后在程序的运行期间不再重复设置。
自动变量的特点是在程序运行到自动变量的作用域(即声明了自动变量的那个函数或分程序)中时才为自动变量分配相应的存储空间,此后才能向变量中存储数据或读取变量中的数据。一旦退出声明了自动变量的那个函数或分程序之后,程序会立即将自动变量占用的存储空间释放,被释放的空间还可以重新分配给其他函数中声明的自动变量使用。因此自动变量的生存期从程序进入声明了该自动变量的函数或分程序开始,到程序退出该函数或分程序时结束。在此期间之外自动变量是不存在的。自动变量的初值在每次为自动变量分配存储后都要重新设置。
自动变量对存储空间的利用是动态的,通过分配和回收,不同函数中声明的自动变量可以在不同的时间中共享同一块存储空间,从而提高了存储器的利用率。显然,前面介绍的局部变量(也包括函数的参数)都是自动变量。同样显然的是在整个程序运行过程中,一个自动变量可能经历若干个生存期。而在自动变量的各个不同生存期中程序为该变量分配的存储空间的具体地址可能并不相同,因此在编写程序时,不能期望在两次调用同一函数时,其中声明的同一个局部变量的值之间会有什么联系。
然而,有时需要在函数中保留一些变量的值,以便下次进入该函数以后仍然可以继续使用。使用全局变量当然可以达到这一目的,但是使用全局变量会使程序变得难于阅读、难于调试。此时我们可以将该变量声明为静态局部变量。声明格式是在原来的变量声明语句前面加上static构成。例如
int func()
{
static int count = 0;
...,..
return ++count;
}
函数func()中声明了一个静态局部变量count。 静态局部变量同时具有局部变量和自动变量的特点。静态局部变量count只能在其定义域(函数func())中使用,但其生存期却与整个程序的运行期相同。在程序运行之前就为该变量分配了相应的存储空间并赋了初值0,以后每次进入函数func()时可以对其进行操作,但在离开函数func()后count占用的存储空间并不释放,其中的内容也就不会发生变化,直到下一次进入该函数后又可以继续使用该变量了。静态变量的初值是在程序开始运行之前一次设置好的,不象自动变量,每次为其分配存储空间时都要重新设置一次。
函数func()中的局部变量count的作用是统计调用函数func()的次数。
5.10 变量使用小结在本节中将C++中变量的使用方法简要总结如下:
(1) 最常用的变量形式是局部变量。一般的局部变量都是自动变量,其作用域为定义局部变量的函数或分程序,生存期为程序执行到变量定义域中的期间,即每次进入其定义域时才为局部变量分配存储空间,并设置初值(如果需要的话)。
(2) 可以通过在声明语句前面加上保留字“static”将局部变量声明为静态局部变量。静态局部变量的作用域仍为声明局部变量的函数或分程序,但其生存期扩大到整个程序的运行期,在程序运行之前即为变量分配存储并设置初值,该初值在以后每次调用该函数时不在重新设置。静态局部变量的主要用途是保存函数的执行信息。
(3) 声明于所有函数之外的变量称为全局变量,全局变量都是静态的,即具有和程序执行期相同的生存期。一般来说,全局变量的作用域还可以扩充到其他源程序中,只要在相应的源程序文件中加入外部函数声明语句即可。
(4) 上面所说的变量包括数组。但是数组和变量有一点不同,就是不能为局部自动数组设置初值,只有全局数组或静态数组可以设置初值。数组初值的设置方法是将所有元素的初值放在花括号中,并用逗号隔开。例如语句
static int a[]={0,0,0,0,0};
声明了一个有5个整型元素的静态数组,其每个元素的初值均为0。
调试技术
5.11 Developer Studio的跟踪调试功能调试器是Developer Studio中最出色的部件之一,可以帮助你找到在软件开发中可能遇到的几乎每个错误。
Developer Studio中的项目可以产生两种可执行代码,分别称为调试版本和发布版本。调试版本是在开发过程中使用的,用于检测程序中的错误;发布版本是最终结果,是面向用户的。调试版本体积较大,而且通常要比发布版本慢。这是因为调试版本中充满了编译器放在目标文件中的符号信息。记录了程序中函数和变量的名字及其在源代码中的位置。通过这些符号信息,调试器可以将源代码的每一行与可执行代码中相应的指令联系起来。
发布版本只包含由编译器优化的可执行代码,没有符号信息。正因为如此,发布版本不能用调试器进行调试。
Developer Studio默认的配置是调试版本,可通过菜单选项Build\Set Active Configuration…在两种版本之间切换。当前配置显示在Build工具栏中。
调试器的主要调试手段有设置断点、跟踪和观察。所谓断点,即程序中的某处。在调试时使程序执行到断点处停下来,通过观察程序变量、表达式、调试输出信息、内存、寄存器和堆栈的值来了解程序的运行情况,或进一步跟踪程序的运行。在当前编辑位置设置一个断点最直接的方式是使用快捷键F9,或用鼠标点击Build MiniBar工具条上的手形图标。也可以在要设置断点的语句上调出右键快捷菜单,通过选择Insert/Remove Breakpoint选项设置断点。断点用编辑窗口左边框上的大红圆点表示,非常醒目。取消一个断点的方法类似,只要在有断点的语句上重新使用快捷键F9即可取消已设置的断点。有时并不想删除断点,而只是希望暂时禁止断点,以后该断点可能还会用到。这时,可从语句行的右键快捷菜单上选择Disable Breakpoint,禁止当前行的断点。被禁止的断点标志变成空心圆。要恢复被禁止的断点,可从含有被禁止断点的语句行的右键菜单中选择Enable Breakpoint命令。
如果已设置好了断点,则可通过子菜单Build/Start Debug调用调试器。该子菜单有4个选项,分别为:
Go(快捷键为F5):从当前语句开始执行程序,直到遇到一个断点或程序结束。用Go命令启动调试器时,从头开始执行程序。
Step Into(快捷键为F11):单步执行每一程序行,遇到函数时进入函数体内单步执行。
Run To Cursor(快捷键为Ctrl+F10):运行程序至当前编辑位置。
Attach To Process:将调试器与当前运行的其中某个进程联系起来,这样就可以跟踪进入进程内部,就象调试项目工作区中当前打开的应用程序一样调试运行中的进程。
当被调试的程序停在某个断点上时,编辑器左边框上的对应位置会出现一个黄色箭头指示被中断的语句。此时Developer Studio的版面布置会一些发生变化,如菜单栏中以调试(Debug)菜单项代替了建立(Build)菜单项并出现了一Debug工具栏。调试工具栏如图5-4所示。
Debug工具栏可分为4个区,第1,2两个区中是常用的调试命令,包括:
(1)Restart(快捷键:Ctrl+Shift+F5):终止当前的调试过程,重新开始执行程序,停在程序的第1条语句处(类似Step Into命令的结果)。
(2)Stop Debugging(快捷键:Shift+F5):退出调试器,同时结束调试过程和程序运行过程。
(3)Break Execution:终止程序运行,进入调试状态。多用于终止一个进入死循环的程序。
(4)Apply Code Changes(快捷键:Alt+F10):当源程序在调试过程中发生改变,重新进行编译。
(5)Show Next Statement(快捷键:Alt+Num *):显示下一语句。
(6)Step Into(快捷键:F11):跟踪。如果是一语句,则单步执行;如果是一函数调用,则跟踪到函数第一条可执行语句。
(7)Step Over(快捷键F10):单步执行。如果是一语句,则单步执行;如果是一函数调用,将此函数一次执行完毕,运行到下一条可执行语句。
(8)Step Out(快捷键:Shift+F11):从函数体内运行到外,即从当前位置运行到调用该函数语句的下一条语句。
(9)Run To Cursor(快捷键:Ctrl+F10):从当前位置运行到编辑光标。
第3区有一个眼镜图标(Quick Watch,快捷键为Shift+F9)用于弹出一个对话框,观察当前编辑位置的变量的值。
第4区有6个图标,分别用于激活6个调试器窗口:
(1)观察窗口(Watch)用于观察指定变量或表达式的值。可任意添加要观察的变量或表达式,并可用标签的形式(Watch1,Watch2,Watch3 等)增加多组观察对象。
(2)变量窗口(Variables)用于观察断点处或其附近的变量的当前值。Variables有3个标签,Auto标签显示变量和函数返回值,Locals标签显示当前函数的局部变量,this标签显示this指针对象。在Variables窗口中,双击一个变量并输入新值会改变变量的值。与Watch窗口相同,Variables窗口也可任意添加要观察的变量或表达式。
(3)寄存器窗口(Register)用于观察在当前运行点的寄存器的内容。
(4)内存窗口(Memory)用于观察指定内存地址内容。
(5)调用栈窗口(Call Stack)用于观察调用栈中还未返回的被调用函数列表。调用栈给出从嵌套函数调用一直到断点位置的执行路径。
(6)汇编代码窗口(Disassmbly)用于显示被编译代码的汇编语言形式。
调试器窗口汇集了许多信息,但通常并不需要同时观察所有的信息。太多的观察窗口与编辑器窗口争夺屏幕空间,影响调试程序。缺省情况下,启动调试器时,Developer Studio自动打开Varialbles和Watch两个调试器窗口。
Developer Studio调试器有一个非常有用的特性,可以用来快速观察某个变量的值。即如果用鼠标在某个变量上停留片刻,就会出现一个小小的黄色Tip窗口,显示该变量当前数值。如果是指针,则显示指针数值;如果是字符串,就显示字符串内容。
利用调试器调试程序的过程,就是通过设置断点,观察断点的各种信息,单步跟踪有疑问的程序段进行的。尽管Develop Studio的调试器功能强大,但要取得好的效果,还要靠平时多练习,充分掌握Visual C++语言及其编程特点。优秀的调试器,只有在优秀的程序员手里才能充分发挥作用。
程序设计举例
[例5-6] 交换两个变量的值。
算 法:交换两个变量x和y的值一定要用到第三个变量t作为周转:
t = x;
x = y;
y = t;
程 序:
// Example 5-10:交换两个变量的值 (不成功)
#include <iostream.h>
// 函数 swap():交换两个变量的值(不成功)
void swap(int x,int y)
{
int t;
t = x;
x = y;
y = t;
}
// 测试函数 swap() 用的主函数
void main()
{
int a = 1,b = 2;
cout << "Before exchange:a= " << a << ",b= " << b << endl;
swap(a,b);
cout << "After exchange:a= " << a << ",b= " << b << endl;
}
输 出:Before exchange:a=1,b=2
After exchange:a=1,b=2
分 析:从输出结果来看,函数swap()并没有完成交换两个变量的任务。为什么? 如前所述,函数的参数实际上相当于在函数内部声明的变量,只是在调用时由实参变量a和b为其提供初值。因此,虽然在函数swap()中变量x和y的值确实被交换了,但它们对在主函数中作为调用函数swap()的实参的a和b却并无影响。考虑用如下语句调用swap()函数的情况:
swap(2,3+a);
这一点就更加明显了:常数2和表达式3+a用于向swap()函数的参数x和y传递初值,但无法想象常数2和表达式3+a交换的意义是什么。
[例5-7] 编写一个用于字符串比较的函数mystrcmp()。
算 法:字符串的比较应按字典序判断。例如,由于单词“word”在辞典中排在单词“work”的前面,所以我们讲单词“word”小于单词“work”。实际上进行两个字符串的比较时,要按字符串中的各个字符在ASCII码表中的次序进行比较,这是因为在字符串中不仅可以出现字母,还可能出现其他符号。只有当两个字符串中所有对应位置上的符号分别相同时,才能认为这两个字符串相等。
程 序:
// Example 5-11:比较两个字符串
int mystrcmp(char s1[],char s2[])
{
int i = 0;
while(s1[i]==s2[i] && s1[i]!=0 && s2[i]!=0)
i++;
return s1[i]-s2[i];
}
分 析:我们设计了一个循环,从两个字符串的第一个字符开始比较,直到出现不同的字符,或者有一个字符串已经结束为止。从程序中可以看出,这时如果两个字符串相等,则函数mystrcmp()返回0;如果字符串s1大于字符串s2,则返回一个正数; 如果字符串s1小于字符串s2,则函数返回一个负数。
单元上机练习题目
1.编写字符串查找函数mystrchr(),该函数的功能为在字符串(参数string)中查找指定字符(参数c),如果找到了则返回该字符在字符串中的位置,否则返回零。然后再编写主函数验证之。函数框架为
int mystrchr(char string[],int c)
{
,..,..
}
2.编写字符串反转函数mystrrev(),该函数的功能为将指定字符串中的字符顺序颠倒排列。然后再编写主函数验证之。(提示:求字符串长度可以直接调用库函数strlen(),但在程序首部应加上
#include <string.h>
函数框架为
void mystrrev(char string[])
{
,..,..
}
该函数无需返回值。
3.编写一组求数组中最大最小元素的函数。该组函数的格式为
int imax(int array[],int count); // 求整型数组的最大元素
int imin(int array[],int count); // 求整型数组的最小元素
long lmax(long array[],int count); // 求长整型数组的最大元素
long lmin(long array[],int count); // 求长整型数组的最小元素
float fmax(float array[],int count); // 求浮点数组的最大元素
float fmin(float array[],int count); // 求浮点数组的最小元素
double dmax(double array[],int count); // 求双精度数组的最大元素
double dmin(double array[],int count); // 求双精度数组的最小元素其中参数count为数组中的元素个数,函数的返回值即为求得的最大或最小元素之值。要求同时编写出主函数进行验证。
思考题
1,形如
void fun(void)
{
,..,..
}
的函数合理吗? 有用吗? 请举例说明。