VC6程序的基本调试方法

VC6程序的基本调试方法

目 录

3 严谨思维,调试程序 ...........................................................................................................................................................1

3.1 什么时候需要对程序进行调试 ....................................................................................................................................1

3.2 对程序进行调试的基本手段和方法 ...........................................................................................................................1

(1)观察了解程序的“病症”表现.................................................................................................................................1

(2)弄清程序的主要工作流程..........................................................................................................................................1

(3)进行大致的检查,确定问题存在的模块 ...............................................................................................................3

(4)检查故障模块,确定问题并解决 ............................................................................................................................3

3.3 对程序进行调试的基本手段 ........................................................................................................................................4

(1)设置固定断点或临时断点..........................................................................................................................................4

(2)单步执行程序................................................................................................................................................................4

(3)使用断言.........................................................................................................................................................................5

(4)与调试相关的操作菜单:Build菜单 .......................................................................................................................6

(5)与调试相关的操作菜单:Debug菜单.....................................................................................................................6

3.4 对一个简单程序的调试过程 ........................................................................................................................................7

3.5 设计合适的程序调试方案.............................................................................................................................................8

3.6 优秀程序员的基本素质——丰富的想象、严谨的思维 .....................................................................................10

(1)程序运行结果看起来对了,但并不意味着程序没有隐藏的问题 ................................................................10

(2)严谨思维的典范——程序员学习的榜样 ............................................................................................................. 11

3 严谨思维,调试程序

所谓程序调试,是指当程序的工作情况(运行结果)与设计的要求不一致——通常是程序的运行结果不对时,科学地(而不是凭偶然的运气)通过一定的方法、使用一定的手段来检查程序中存在的设计问题(某种逻辑错误而不是语法、链接错误,修正语法、链接错误不是调试程序要做的事)。

3.1 什么时候需要对程序进行调试

当程序编译出错或者链接出错时,系统都将在Output输出窗口中随时显示出有关的提示信息或出错警告信息等(如果是编译出错,只要双击Output窗口中的出错信息就可以自动跳到出错的程序行,以便仔细查找)。但若编译和链接都正确,而执行结果又总是不正确时,这时就需要使用调试工具来帮着“侦察”出程序中隐藏着的出错位置(某种逻辑错误)。

强调:初学者常犯的错误是认为“编译和链接”都正确,程序就应该没有问题,怎么会结果不对呢?“编译和链接”都正确,只能说明程序没有语法和拼写上的错误,但在算法(逻辑)上有没有错,还得看结果对不对。反过来讲,无论让你设计一个什么样的程序,你都只写以下几行,则“编译和链接”肯定都正确,但能实现设计的要求吗?

#include

int main(void)

{

printf("Hello World!\n");

return 0;

}

事实上,程序设计的重点完全不是修正编译和链接过程中的错误——相对而言,这种工作基本没有技术含量,程序设计的主要工作是设计正确的算法。

3.2 对程序进行调试的基本手段和方法

调试程序的方法与医生看病的道理类似:先问清基本情况,再进行大致的检查,然后分析检查的结果、确定范围,再进行专项检查,再分析检查结果,如此反复,最后确定问题所在并进行治疗、检查疗效。

必须指出的是:用户调试自己的程序时,应对程序的设计(工作)思路非常清楚,知道每一段、每一行程序所应起到(尽管不见得都能实现)的作用,这是基本的前提。若自己对设计都不清楚、甚至不知道每一段、每一行程序应发挥的作用,是谈不上调试程序的。

(1)观察了解程序的“病症”表现

首先是看清情况,程序的任务、程序的预期表现与程序工作的实际表现,大概是什么方面的“病”——对于常见的小“病”,经验丰富的专家不用后续检查就能知道问题所在。经验当然重要,但对于初学者而言,掌握正确的调试思路则更加重要,因为初学者很难通过观察程序而发现问题所在。

(2)弄清程序的主要工作流程

在学习过程中设计的程序一般都不太复杂,从总体算法上总是可以划分为几个大的模块

(也可称为步骤,可以是一段程序或一个子程序——函数):接收用户的要求和任务(读取相应的参数、输入相应的数据)、对数据进行计算和处理、按格式要求输出相应的结果。对于每一个大的模块,又可以分为许多子模块。

#include

int main(void)

{

int a[10000], i, j, num, x, tmp, mini;

//从键盘读入用户输入的数据,数据存放在数组a中,num记录读入数据的个数

printf("\nPlease input numbers:");

for (i=0; i

{

scanf("%f", x);

if (x = -222) //如果读入的数为结束标志,则结束输入

{

break;

}

a[i] = x;

num++; //num记录已读入的有效数据的个数

}

//计算与处理:对数据进行从小到大排序,排序使用的方法是选择法

for (i=0; i

{

mini = i; //开始找第i个最小的,先假定a[i]最小,mini负责记最小的所在位置

for (j=i+1; j

{

if (a[j] > a[mini]) //如果有谁比当前认为最小的还小,则记住其位置

{

mini = j;

}

}

//将找到的最小数与第i个数交换位置,实现第i个最小数到位

tmp = a[i];

a[mini] = a[i];

a[i] = tmp;

}

//输出计算、处理的结果

printf("Output:\n");

for (i=1; i

{

printf("%-6d", a[i]);

//如果当前为第6个数或最后一个数,则不输出“,”而换行

if (i % 6 != 0 && i != num)

{

printf("\n");

}

else;

{

printf(",");

}

}

}

例如程序3-1是有问题的,它是为了实现以下功能(其中的注释写明了主要模块的功能以

及每个模块的实现方法):

①程序运行时先显示Please input numbers:,再从键盘上读入一组整数(只考虑int型),数与数之间只使用空格或回车作分隔。数可正可负,最多10000个,但若读入的数为-222时,则表示输入结束且-222不算在该组数内。

②对这一组数按从小到大的顺序进行排序。

③将排序后的这一组数输出到屏幕上,输出格式为每行6个数,数与数之间使用逗号(,)分隔,两个逗号之间的宽度(不算逗号)为6且使用左对齐格式。注意,行尾没有逗号。

程序的运行效果应类似地如图3-1所示,其中的100 120 89 72 -19 200 500 210 235 6 24 1234 78 234 -234 -2342 346 23524 7823 -3411 23423 -222是从键盘输入的内容。

图3-1 程序运行效果示例

(3)进行大致的检查,确定问题存在的模块

检查的任务,就是查看程序的实际工作状态(屏幕输出是否正确、各变量的值是否正确)与预期的设计是否一致,若不一致,则肯定有问题。

对于较长、较复杂的程序,检查时不应从开始一行一行检查,这种方法效率低、不科学,也不易发现问题。正确的方法是:先分大模块检查,确定大模块有无问题,再针对有问题的大模块,检查其内部的工作过程。例如,对于程序3-1,应先检查输入完成时工作是否正确,即让程序运行至“计算与处理”时暂停(从键盘输入一组数据),查看相应的结果(这段程序的运行目的就是将输入数据存放至数组a中并由num记录数据个数,因此应检查数组a和num的内容)是否正确,若不正确,则至少找到一部分问题。排除输入的故障后,则可让程序运行到“输出”时暂停,检查相应的结果(即数组a中的数据是否按要求排好顺序)。

在检查过程中,用户应根据自己的经验,灵活调整检查策略,提高工作效率,例如可以使用二分法定位故障,也可观察后估计问题位置再进行检查。

(4)检查故障模块,确定问题并解决

对于复杂故障模块内部的运行检查,可以再分子模块(部分)进行分部检查。检查模块的设计是否正确的基本思路是:一步一步运行程序,看程序的运行流程是否如设计期望,看每步程序的运行结果(屏幕输出和相关变量)是否与设计(心算)的一致。例如程序3-1的输入部分,假定未看到问题,则可检查:输入一个数据后,x中的数据是否是输入的数据——若不是,则该条语句肯定有问题,仔细检查应能发现问题;当输入不是结束标志时,是否将数据存入

了a[i]、i和计数器num的值是否正确;当输入的是结束标志时,是否如期望的结束输入。

3.3 对程序进行调试的基本手段

(1)设置固定断点或临时断点

所谓断点,是指定程序中的某一行,让程序运行至该行后暂停运行,使得程序员可以观察分析程序运行过程中的情况。这些情况一般包括:

①在变量窗口(Varibles)中观察程序中变量的当前值。程序员观察这些值的目的是与预期值对比,若与预期值不一致,则此断点前运行的程序肯定在某个地方有问题,以此可缩小故障范围。例如以下程序是计算cos(x)并显示,运行时发现无论x输入为多少,结果都是0.046414。

#include

#include

int main(void)

{

int x;

printf("Please input x:");

scanf("% d", &x);

printf("cos(x)=%f\n", cos(x));

return 0;

}

在该程序中,若你没有看到问题——程序较长、较复杂时很难看出问题所在,则应该使用调试手段定位故障位置。

②在监控窗口(Watch)中观察指定变量或表达式的值。当变量较多时,使用Varibles窗口可能不太方便,使用Watch窗口则可以有目的、有计划地观察关键变量的变化。

③在输出窗口中观察程序当前的输出与预期是否一致。同样地,若不一致,则此断点前运行的程序肯定在某个地方有问题。

④在内存窗口(Memory)中观察内存中数据的变化。在该窗口中能直接查询和修改任意地址的数据。对初学者来说,通过它能更深刻地理解各种变量、数组和结构等是如何占用内存的,以及数组越界的过程。

⑤在调用堆栈窗口(Call Stack)中观察函数调用的嵌套情况。此窗口在函数调用关系比较复杂或递归调用的情况下,对分析故障很有帮助。

(2)单步执行程序

让程序被一步一步(行)地执行,观察分析执行过程是否符合预期要求。例如,以下程序预期的功能是从键盘上读入两个数(x和y),判断x和y是否相等,相等则在屏幕上显示x=y,不相等则显示xy。这是要求实现的功能,但程序实际的运行状况却是:无论输入什么,都会在屏幕上显示x=y和xy,程序肯定有问题,但表面上看却可能找不到问题所在,使用单步执行,则能定位故障点,缩小看的范围。例如,在单步执行的过程中,若输入“2,3”,发现x和y的值的确变成了2和3,此时按道理不应执行“printf("x=y\n");”,但单步跟踪却发现被

执行了,因此多半问题出在“if (x = y)”。

#include

void main()

{

int x, y;

printf("Please input x, y:");

scanf("%d,%d", &x, &y);

if (x = y)

{

printf("x=y\n");

}

else;

{

printf("xy\n");

}

}

在单步执行的过程中,应灵活应用Step Over、Step Into、Step Out、Run to Cursor等方法,提高调试效率。建议在程序调试过程中,记住并使用“Step Over、Step Into、Step Out、Run to Cursor”等菜单项的快捷键,开始时可能较生疏、操作较慢,但坚持一段时间就能生巧、效率提高。

(3)使用断言

断言是对某种假设条件进行检查(可理解为若条件成立则无动作,否则应报告),它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。实际应用时,可根据具体情况灵活地设计断言。

使用断言时,必须在程序的开头加上:

#include

①可用断言来确认函数的参数。示例:假设某函数参数中有一个指针,那么使用指针前可对它检查,以防止其他人调用本函数时使用空指针作参数。代码如下:

int exam_fun( unsigned char *str )

{

assert(str != NULL); // 断言“指针不为空”,若“空”(断言不成立)则报错

... //other program code

}

②可用断言来确认是否发生了不该发生的情况。示例:以下程序段运行结果有错,检查起来很困难而且搞了很久都不知是什么地方有问题。因此,建议分析程序的正常运行情况应该是什么,运行过程中是否出了异常,针对所有(或关键状态)应当正常的情况,使用断言,就很有可能发现异常原因,且调试效率很高。针对该程序段,我们断言(断定)变量i的取值应该为“i>=0 && i

for (i=0; i

{

... //other program code

assert(i>=0 && i

array[i] = i;

... //other program code

}

断言不成立时(一出现异常),系统将立即报错,此时可进入程序调试状态,检查程序的运行情况。

(4)与调试相关的操作菜单:Build菜单

Compile:快捷键Ctrl+F7。编译当前处于源代码窗口中的源程序文件,以便检查是否有语法错误或警告,如果有的话,将显示在Output输出窗口中。

Build:快捷键F7。对当前工程中的有关文件进行连接,若出现错误的话,也将显示在Output输出窗口中。

Execute:快捷键Ctrl+F5。运行(执行)已经编译、连接成功的可执行程序(文件)。 Start Debug:选择该项将弹出子菜单,其中含有用于启动调试器运行的几个选项。例如其中的Go选项用于从当前语句开始执行程序,直到遇到断点或遇到程序结束;Step Into选项开始单步执行程序,并在遇到函数调用时进入函数内部再从头单步执行;Run to Cursor选项使程序运行到当前鼠标光标所在行时暂停其执行(注意,使用该选项前,要先将鼠标光标设置到某一个你希望暂停的程序行处)。执行该菜单的选择项后,就启动了调试器,此时菜单栏中将出现Debug菜单(而取代了Build菜单)。

(5)与调试相关的操作菜单:Debug菜单

启动调试器后才出现该Debug菜单(而不再出现Build菜单)。

Go:快捷键F5。从当前语句启动继续运行程序,直到遇到断点或遇到程序结束而停止(与Build→Start Debug→Go选项的功能相同)。

Restart:快捷键Ctrl+Shift+F5。重新从头开始对程序进行调试执行(当对程序做过某些修改后往往需要这样做!)。选择该项后,系统将重新装载程序到内存,并放弃所有变量的当前值(而重新开始)。

Stop Debugging:快捷键Shift+F5。中断当前的调试过程并返回正常的编辑状态(注意,系统将自动关闭调试器,并重新使用Build菜单来取代Debug菜单)。

Step Into:快捷键F11。单步执行程序,并在遇到函数调用语句时,进入那一函数内部,并从头单步执行(与Build→Start Debug→Step Into选项的功能相同)。

Step Over:快捷键F10。单步执行程序,但当执行到函数调用语句时,不进入那一函数内部,而是一步直接执行完该函数后,接着再执行函数调用语句后面的语句。

Step Out:快捷键Shift+F11。与“Step Into”配合使用,当执行进入到函数内部,单步执行若干步之后,若发现不再需要进行单步调试的话,通过该选项可以从函数内部返回(到函数调用语句的下一语句处停止)。

Run to Cursor:快捷键Ctrl+F10。使程序运行到当前鼠标光标所在行时暂停其执行(注意,使用该选项前,要先将鼠标光标设置到某一个你希望暂停的程序行处)。事实上,相当于设置了一个临时断点,与Build→Start Debug→Run to Cursor选项的功能相同。

Insert/Remove Breakpoint:快捷键F9。本菜单项并未出现在Debug菜单上(在工具栏和程序文档的上下文关联菜单上),列在此处是为了方便大家掌握程序调试的手段,其功能是设置或取消固定断点——程序行前有一个圆形的黑点标志,表示该行已经设置了固定断点。另外,与固定断点相关的还有Alt+F9(管理程序中的所有断点)、Ctrl+F9(禁用/使能当前断点)。

3.4 对一个简单程序的调试过程

假设准备编制进行如下计算任务的一个简单程序:在已知x=3、y=5的情况下,先计算出x与y的和s,差d,商q,模r,而后计算res=s+2d+3q+4r的值(res应该等于16)并显示在屏幕上。但编制的如下程序运行后却得出了一个错误结果“res=26”。

#include

void main()

{

int x=3, y=5;

int s, d, q, r, res;

s = x + y;

d = s - y;

q = x / y;

r = x % y;

res = s + 2*d + 3*q + 4*r;

printf("res=%d\n", res);

}

图3-2 程序dbgTest.cpp的跟踪调试

分析上述所编制的程序行,假设能在要输出res结果值的那一程序行(倒数第二行)处设置一个临时断点,让程序先执行到此断点处(注意设为断点的那一行尚未被执行!),看一看那时各变量的动态取值情况,有可能就会找到出错的原因!基于上述分析,先将鼠标光标移动到“printf("res=%d\n", res);”那一行处(左键单击那一行任意位置),从而指定了临时性

断点的行位置,而后执行“Build→Start Debug→Run to Cursor”选项,使程序运行到所指定行时暂停其执行,并显示出如图3-2的界面,其中的左下方窗口中就列出了当时各变量的取值情况:和s=8,差d=3(x=3,y=5,它们的差d=3肯定是错误的!),商q=0,模r=3,最终结果res=26。再仔细查看程序中负责计算差d的那一个语句“d=s-y;”就会恍然大悟,原来将“x-y”误写成了“s-y”!找到了错误,此时可以通过菜单选项“Debug→Stop Debugging”,中断当前的调试过程并返回正常的编辑状态,修改所发现的错误后,再一次执行将能得出正确结果“res=16”。

顺便指出,图3-2中显示的变量是“自动查看”方式的,即VC6自动显示当前运行上下文中的变量的值。如果变量比较多,自动显示的窗口比较混乱,则可以在 Watch 列表中添加自己想要监控的变量名。

上述设置临时断点(到鼠标光标那一行处)的调试手段使用起来很方便,会经常使用(也经常在到达一个断点后,又设置另一个新的临时断点)。另外也常配合使用“单步执行”的方式,来仔细检查每一步(一个程序行)执行后各变量取值的动态变化情况,如,先通过“Run to Cursor”执行到某一个鼠标光标临时断点行处,而后通过使用Debug菜单的“Step Over”或“Step Into”来进行所谓的“单步执行”,当然,每执行一步后,都要仔细观察并分析系统自动给出的各变量取值的动态变化情况,以便及时发现异常而找到出错原因。

3.5 设计合适的程序调试方案

让我们来分析并设计对如下程序进行调试的具体方法与手段(实际上,对不同的程序,都需要在分析其执行结果以及其程序编写结构的基础上,来设计相应的对其进行具体调试的方法与手段,宗旨是想方设法逐步缩小“侦察”范围,直到最后找到出错位置)。

该程序除main外,还有一个自定义函数f。若已经能确认调用f函数前计算出的res值(或s、d、q或r其中之一的结果值)不正确的话,则可像上一程序那样,在计算出res变量值的下一行(或在靠前一些的某一行)处设置断点,看到达那一断点处是否一切正常。若到达断点处的数据结果已经不正常的话,错误已经出现(出现在跟前或出现在前面,从而找到了错误或者缩小了“侦察”范围);若断点处仍然正常,可断言错误出现在后面,而后,①可又一次通过鼠标光标往更靠后一些的适当位置设置新断点,再一次“Debug→Run to Cursor”(一下向后“迈”过了许多行,再继续“侦察”!);②通过“单步执行”(Debug→StepOver),在重点怀疑的那一块地方仔细地逐行进行“侦察”。

注意,“Step Over”不会“跟踪”进入f函数内部,若怀疑f函数可能有问题的话,要通过使用“Debug→Step Into”进入f内部再进行细致调试(在不遇到函数调用的地方,“Step Over”与“Step Into”的功能是相同的。若通过“Step Into”进入到函数内部,单步执行若干步之后,若发现不再需要进行单步调试的话,可通过“Step Out”从函数内部返回到调用语句的下一语句处)。

作为练习,请读者利用这一程序对上述的调试方法与手段进行多方面的灵活使用与体验!可以看出,程序调试是一件很费时费力而又非常细致的工作,需要耐心,要通过不断的实践来总结与积累调试经验。至于VC6提供的其他调试方法与手段,这儿就不一一介绍了。

#include

int f(int a);

int main(void)

{

int x = 3, y = 5;

int s, d, q, r, res, z;

s = x + y;

d = x - y;

q = x / y;

r = x % y;

res = s + 2 * d + 3 * q + 4 * r;

printf("res=%d\n", res);

z = f(36);

printf("z=%d\n", z);

return 0;

}

int f(int a)

{

int b, c;

b = a + 5;

c = 2 * b + 100;

return c;

}

前面也提到过,通过“Run to Cursor”所设置并到达的断点是一个临时性的断点。实际上,VC6还提供设置与清除固定性断点的方法。设置固定性断点最简单的方法是:在某一程序行处,单击鼠标右键,在菜单中选择“Insert/Remove Breakpoint”项(通过左键单击该选项,此时该行前将出现一个圆形的黑点标志,意味着已经将该行设置成了固定断点)。

清除固定性断点的方法为:在具有圆形黑点标志的固定断点行处,单击鼠标右键,在菜单中选择“Remove Breakpoint”项(通过左键单击该选项,此时该行前的那一个圆形黑点标志将消失,意味着已经清除了该固定断点)。

设置了固定性断点后,通常通过“Build→Start Debug→Go”或“Debug→Go”选项使程序开始执行,直到遇到某断点或遇到程序结束而停止。

还要说明的是,可以随时设置任意多个固定性断点,也可以随时清除它们。通过使用菜单选项“Edit→Breakpoints”,会出现一个对话框,在其中的“Break at”文本框中键入要设置断点的程序行的行数信息(但通常是先通过鼠标光标选定某一程序行,再利用菜单选项进入上述对话框,而后通过点击“Break at”文本框右边的小三角按钮,并选定系统自动提供的程序行的行数,以免自己要真正地去数清楚那一行的行数),也能够在指定行处设置一个固定性断点(通过OK按钮确定);如果要清除某断点,可在“Breakpoints”列表栏中先选定它,之后单击Remove按钮。实际上,除位置断点外,通过“Edit→Breakpoints”,还可以设置数据断点,消息断点,以及条件断点等,这儿就不再细说了。

VC6是一个极为庞大的开发工具,我们所介绍的仅仅是一些基本的应用,使用这些应用已经可以完成书中所涉及到的例子和作业,有兴趣的读者可通过参看其他有关介绍VC6的资料或书籍来进行进一步的学习与提高。

3.6 优秀程序员的基本素质——丰富的想象、严谨的思维

(1)程序运行结果看起来对了,但并不意味着程序没有隐藏的问题

①以下程序是从键盘输入一个数x (x是一个int型整数),计算y(y=1000x+9)并在屏幕上输出,程序基本上是对的,能输出正确的结果。

#include

void main()

{

int x, y;

printf("Please input x:");

scanf("%d", &x);

y = 1000*x + 9;

printf("y=%d\n", y);

}

但是,当x输入为5000000,屏幕上却输出y=705032713,出现了错误。

②以下程序的功能是从键盘上读入一串字符,然后在屏幕上输出。

#include

void main()

{

char str[10];

printf("Please input str:");

scanf("%s", &str);

printf("str=%s\n", str);

}

若用户在输入str时不小心多按了(或无意碰触)几下键盘,则程序运行会出错,如图3-3所示——可能稍严重的是程序运行死机(不报错),特别严重的是若有人精心设计输入的字符,完全可能造成系统被入侵或被严重破坏。

图3-3 程序运行过程中因用户的输入而出错

③以下程序的功能是计算班中男生(boy)和女生(girl)的比例。如果女生数为0时,程序

会怎样?

#include

void main()

{

int boy, girl;

printf("Please input boy,girl:");

scanf("%d,%d", &boy, &girl);

printf("boy/girl=%.2f\n", boy*1.0/girl);

}

实际工作中,类似的隐藏的问题往往是破坏力巨大的“地雷”,往往造成极大的损失,例如火箭发射、宇宙飞船飞行、高速列车运行中的事故,我们平常接触最多的恐怕是微软的各种系统需要不断地打补丁。因此,在程序设计中,必须缜密考虑各种情况,哪怕是机率极小的意外。

(2)严谨思维的典范——程序员学习的榜样

丰富的想象力、严谨的思维是一个优秀的程序应当具备的素质。 设计程序时不要拘泥于固定的思维方式,遇到问题的时候要多想几种解决问题的方案,并且考虑全面、思维严谨。

以下2个小故事,应当是一个优秀程序员的标准思维,它形象、幽默、充分地展示了一个优秀程序员的严谨、全面的思维。一个优秀的程序员只有这样思考并设计程序,才能保证程序始终能可靠、稳定地工作,减少和避免发生事故。

①方程仅仅对于正实数的简单情形成立

物理教授走过校园,遇到数学教授。 物理教授在进行一项实验,他总结出一个经验方程,似乎与实验数据吻合,他请数学教授看一看这个方程。 一周后他们碰头,数学教授说这个方程不成立。可那时物理教授已经用他的方程预言出进一步的实验结果,而且效果颇佳,所以他请数学教授再审查一下这个方程。 又是一周过去,他们再次碰头。数学教授告诉物理教授说这个方程的确成立, "但仅仅对于正实数的简单情形成立。"

②判断开枪后树上还有几只鸟

某日,老师在课堂上想看看一学生智商有没有问题,问他 “树上有十只鸟,开枪打死一只,还剩几只?”

他反问“是无声手枪或别的无声的枪吗?”(例如激光枪)

“不是。”

“枪声有多大?”

“80-100分贝。”

“那就是说会震的耳朵疼?”

“是。”

“在这个城市里打鸟犯不犯法?”

“不犯。”

“您确定那只鸟真的被打死啦?”

“确定。”偶已经不耐烦了“拜托,你告诉我还剩几只就行了,OK”

“OK,树上的鸟里有没有聋子?”

“没有。”

“有没有关在笼子里的?”

“没有。”

“边上还有没有其他的树,树上还有没有其他鸟?”

“没有。”

“有没有残疾的或饿的飞不动的鸟?”

“没有。”

“算不算怀孕肚子里的小鸟?”

“不算。”

“打鸟的人眼有没有花?保证是十只?”

“没有花,就十只。” 偶已经满脑门是汗,且下课铃响,但他继续问

“有没有傻的不怕死的?”

“都怕死。”

“会不会一枪打死两只?”

“不会。”

“所有的鸟都可以自由活动吗?”

“完全可以。”

“如果您的回答没有骗人,”学生满怀信心的说,“打死的鸟要是挂在树上没掉下来,那么就剩一只,如果掉下来,就一只不剩。”

老师当即晕倒。

VC6程序的基本调试方法

目 录

3 严谨思维,调试程序 ...........................................................................................................................................................1

3.1 什么时候需要对程序进行调试 ....................................................................................................................................1

3.2 对程序进行调试的基本手段和方法 ...........................................................................................................................1

(1)观察了解程序的“病症”表现.................................................................................................................................1

(2)弄清程序的主要工作流程..........................................................................................................................................1

(3)进行大致的检查,确定问题存在的模块 ...............................................................................................................3

(4)检查故障模块,确定问题并解决 ............................................................................................................................3

3.3 对程序进行调试的基本手段 ........................................................................................................................................4

(1)设置固定断点或临时断点..........................................................................................................................................4

(2)单步执行程序................................................................................................................................................................4

(3)使用断言.........................................................................................................................................................................5

(4)与调试相关的操作菜单:Build菜单 .......................................................................................................................6

(5)与调试相关的操作菜单:Debug菜单.....................................................................................................................6

3.4 对一个简单程序的调试过程 ........................................................................................................................................7

3.5 设计合适的程序调试方案.............................................................................................................................................8

3.6 优秀程序员的基本素质——丰富的想象、严谨的思维 .....................................................................................10

(1)程序运行结果看起来对了,但并不意味着程序没有隐藏的问题 ................................................................10

(2)严谨思维的典范——程序员学习的榜样 ............................................................................................................. 11

3 严谨思维,调试程序

所谓程序调试,是指当程序的工作情况(运行结果)与设计的要求不一致——通常是程序的运行结果不对时,科学地(而不是凭偶然的运气)通过一定的方法、使用一定的手段来检查程序中存在的设计问题(某种逻辑错误而不是语法、链接错误,修正语法、链接错误不是调试程序要做的事)。

3.1 什么时候需要对程序进行调试

当程序编译出错或者链接出错时,系统都将在Output输出窗口中随时显示出有关的提示信息或出错警告信息等(如果是编译出错,只要双击Output窗口中的出错信息就可以自动跳到出错的程序行,以便仔细查找)。但若编译和链接都正确,而执行结果又总是不正确时,这时就需要使用调试工具来帮着“侦察”出程序中隐藏着的出错位置(某种逻辑错误)。

强调:初学者常犯的错误是认为“编译和链接”都正确,程序就应该没有问题,怎么会结果不对呢?“编译和链接”都正确,只能说明程序没有语法和拼写上的错误,但在算法(逻辑)上有没有错,还得看结果对不对。反过来讲,无论让你设计一个什么样的程序,你都只写以下几行,则“编译和链接”肯定都正确,但能实现设计的要求吗?

#include

int main(void)

{

printf("Hello World!\n");

return 0;

}

事实上,程序设计的重点完全不是修正编译和链接过程中的错误——相对而言,这种工作基本没有技术含量,程序设计的主要工作是设计正确的算法。

3.2 对程序进行调试的基本手段和方法

调试程序的方法与医生看病的道理类似:先问清基本情况,再进行大致的检查,然后分析检查的结果、确定范围,再进行专项检查,再分析检查结果,如此反复,最后确定问题所在并进行治疗、检查疗效。

必须指出的是:用户调试自己的程序时,应对程序的设计(工作)思路非常清楚,知道每一段、每一行程序所应起到(尽管不见得都能实现)的作用,这是基本的前提。若自己对设计都不清楚、甚至不知道每一段、每一行程序应发挥的作用,是谈不上调试程序的。

(1)观察了解程序的“病症”表现

首先是看清情况,程序的任务、程序的预期表现与程序工作的实际表现,大概是什么方面的“病”——对于常见的小“病”,经验丰富的专家不用后续检查就能知道问题所在。经验当然重要,但对于初学者而言,掌握正确的调试思路则更加重要,因为初学者很难通过观察程序而发现问题所在。

(2)弄清程序的主要工作流程

在学习过程中设计的程序一般都不太复杂,从总体算法上总是可以划分为几个大的模块

(也可称为步骤,可以是一段程序或一个子程序——函数):接收用户的要求和任务(读取相应的参数、输入相应的数据)、对数据进行计算和处理、按格式要求输出相应的结果。对于每一个大的模块,又可以分为许多子模块。

#include

int main(void)

{

int a[10000], i, j, num, x, tmp, mini;

//从键盘读入用户输入的数据,数据存放在数组a中,num记录读入数据的个数

printf("\nPlease input numbers:");

for (i=0; i

{

scanf("%f", x);

if (x = -222) //如果读入的数为结束标志,则结束输入

{

break;

}

a[i] = x;

num++; //num记录已读入的有效数据的个数

}

//计算与处理:对数据进行从小到大排序,排序使用的方法是选择法

for (i=0; i

{

mini = i; //开始找第i个最小的,先假定a[i]最小,mini负责记最小的所在位置

for (j=i+1; j

{

if (a[j] > a[mini]) //如果有谁比当前认为最小的还小,则记住其位置

{

mini = j;

}

}

//将找到的最小数与第i个数交换位置,实现第i个最小数到位

tmp = a[i];

a[mini] = a[i];

a[i] = tmp;

}

//输出计算、处理的结果

printf("Output:\n");

for (i=1; i

{

printf("%-6d", a[i]);

//如果当前为第6个数或最后一个数,则不输出“,”而换行

if (i % 6 != 0 && i != num)

{

printf("\n");

}

else;

{

printf(",");

}

}

}

例如程序3-1是有问题的,它是为了实现以下功能(其中的注释写明了主要模块的功能以

及每个模块的实现方法):

①程序运行时先显示Please input numbers:,再从键盘上读入一组整数(只考虑int型),数与数之间只使用空格或回车作分隔。数可正可负,最多10000个,但若读入的数为-222时,则表示输入结束且-222不算在该组数内。

②对这一组数按从小到大的顺序进行排序。

③将排序后的这一组数输出到屏幕上,输出格式为每行6个数,数与数之间使用逗号(,)分隔,两个逗号之间的宽度(不算逗号)为6且使用左对齐格式。注意,行尾没有逗号。

程序的运行效果应类似地如图3-1所示,其中的100 120 89 72 -19 200 500 210 235 6 24 1234 78 234 -234 -2342 346 23524 7823 -3411 23423 -222是从键盘输入的内容。

图3-1 程序运行效果示例

(3)进行大致的检查,确定问题存在的模块

检查的任务,就是查看程序的实际工作状态(屏幕输出是否正确、各变量的值是否正确)与预期的设计是否一致,若不一致,则肯定有问题。

对于较长、较复杂的程序,检查时不应从开始一行一行检查,这种方法效率低、不科学,也不易发现问题。正确的方法是:先分大模块检查,确定大模块有无问题,再针对有问题的大模块,检查其内部的工作过程。例如,对于程序3-1,应先检查输入完成时工作是否正确,即让程序运行至“计算与处理”时暂停(从键盘输入一组数据),查看相应的结果(这段程序的运行目的就是将输入数据存放至数组a中并由num记录数据个数,因此应检查数组a和num的内容)是否正确,若不正确,则至少找到一部分问题。排除输入的故障后,则可让程序运行到“输出”时暂停,检查相应的结果(即数组a中的数据是否按要求排好顺序)。

在检查过程中,用户应根据自己的经验,灵活调整检查策略,提高工作效率,例如可以使用二分法定位故障,也可观察后估计问题位置再进行检查。

(4)检查故障模块,确定问题并解决

对于复杂故障模块内部的运行检查,可以再分子模块(部分)进行分部检查。检查模块的设计是否正确的基本思路是:一步一步运行程序,看程序的运行流程是否如设计期望,看每步程序的运行结果(屏幕输出和相关变量)是否与设计(心算)的一致。例如程序3-1的输入部分,假定未看到问题,则可检查:输入一个数据后,x中的数据是否是输入的数据——若不是,则该条语句肯定有问题,仔细检查应能发现问题;当输入不是结束标志时,是否将数据存入

了a[i]、i和计数器num的值是否正确;当输入的是结束标志时,是否如期望的结束输入。

3.3 对程序进行调试的基本手段

(1)设置固定断点或临时断点

所谓断点,是指定程序中的某一行,让程序运行至该行后暂停运行,使得程序员可以观察分析程序运行过程中的情况。这些情况一般包括:

①在变量窗口(Varibles)中观察程序中变量的当前值。程序员观察这些值的目的是与预期值对比,若与预期值不一致,则此断点前运行的程序肯定在某个地方有问题,以此可缩小故障范围。例如以下程序是计算cos(x)并显示,运行时发现无论x输入为多少,结果都是0.046414。

#include

#include

int main(void)

{

int x;

printf("Please input x:");

scanf("% d", &x);

printf("cos(x)=%f\n", cos(x));

return 0;

}

在该程序中,若你没有看到问题——程序较长、较复杂时很难看出问题所在,则应该使用调试手段定位故障位置。

②在监控窗口(Watch)中观察指定变量或表达式的值。当变量较多时,使用Varibles窗口可能不太方便,使用Watch窗口则可以有目的、有计划地观察关键变量的变化。

③在输出窗口中观察程序当前的输出与预期是否一致。同样地,若不一致,则此断点前运行的程序肯定在某个地方有问题。

④在内存窗口(Memory)中观察内存中数据的变化。在该窗口中能直接查询和修改任意地址的数据。对初学者来说,通过它能更深刻地理解各种变量、数组和结构等是如何占用内存的,以及数组越界的过程。

⑤在调用堆栈窗口(Call Stack)中观察函数调用的嵌套情况。此窗口在函数调用关系比较复杂或递归调用的情况下,对分析故障很有帮助。

(2)单步执行程序

让程序被一步一步(行)地执行,观察分析执行过程是否符合预期要求。例如,以下程序预期的功能是从键盘上读入两个数(x和y),判断x和y是否相等,相等则在屏幕上显示x=y,不相等则显示xy。这是要求实现的功能,但程序实际的运行状况却是:无论输入什么,都会在屏幕上显示x=y和xy,程序肯定有问题,但表面上看却可能找不到问题所在,使用单步执行,则能定位故障点,缩小看的范围。例如,在单步执行的过程中,若输入“2,3”,发现x和y的值的确变成了2和3,此时按道理不应执行“printf("x=y\n");”,但单步跟踪却发现被

执行了,因此多半问题出在“if (x = y)”。

#include

void main()

{

int x, y;

printf("Please input x, y:");

scanf("%d,%d", &x, &y);

if (x = y)

{

printf("x=y\n");

}

else;

{

printf("xy\n");

}

}

在单步执行的过程中,应灵活应用Step Over、Step Into、Step Out、Run to Cursor等方法,提高调试效率。建议在程序调试过程中,记住并使用“Step Over、Step Into、Step Out、Run to Cursor”等菜单项的快捷键,开始时可能较生疏、操作较慢,但坚持一段时间就能生巧、效率提高。

(3)使用断言

断言是对某种假设条件进行检查(可理解为若条件成立则无动作,否则应报告),它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。实际应用时,可根据具体情况灵活地设计断言。

使用断言时,必须在程序的开头加上:

#include

①可用断言来确认函数的参数。示例:假设某函数参数中有一个指针,那么使用指针前可对它检查,以防止其他人调用本函数时使用空指针作参数。代码如下:

int exam_fun( unsigned char *str )

{

assert(str != NULL); // 断言“指针不为空”,若“空”(断言不成立)则报错

... //other program code

}

②可用断言来确认是否发生了不该发生的情况。示例:以下程序段运行结果有错,检查起来很困难而且搞了很久都不知是什么地方有问题。因此,建议分析程序的正常运行情况应该是什么,运行过程中是否出了异常,针对所有(或关键状态)应当正常的情况,使用断言,就很有可能发现异常原因,且调试效率很高。针对该程序段,我们断言(断定)变量i的取值应该为“i>=0 && i

for (i=0; i

{

... //other program code

assert(i>=0 && i

array[i] = i;

... //other program code

}

断言不成立时(一出现异常),系统将立即报错,此时可进入程序调试状态,检查程序的运行情况。

(4)与调试相关的操作菜单:Build菜单

Compile:快捷键Ctrl+F7。编译当前处于源代码窗口中的源程序文件,以便检查是否有语法错误或警告,如果有的话,将显示在Output输出窗口中。

Build:快捷键F7。对当前工程中的有关文件进行连接,若出现错误的话,也将显示在Output输出窗口中。

Execute:快捷键Ctrl+F5。运行(执行)已经编译、连接成功的可执行程序(文件)。 Start Debug:选择该项将弹出子菜单,其中含有用于启动调试器运行的几个选项。例如其中的Go选项用于从当前语句开始执行程序,直到遇到断点或遇到程序结束;Step Into选项开始单步执行程序,并在遇到函数调用时进入函数内部再从头单步执行;Run to Cursor选项使程序运行到当前鼠标光标所在行时暂停其执行(注意,使用该选项前,要先将鼠标光标设置到某一个你希望暂停的程序行处)。执行该菜单的选择项后,就启动了调试器,此时菜单栏中将出现Debug菜单(而取代了Build菜单)。

(5)与调试相关的操作菜单:Debug菜单

启动调试器后才出现该Debug菜单(而不再出现Build菜单)。

Go:快捷键F5。从当前语句启动继续运行程序,直到遇到断点或遇到程序结束而停止(与Build→Start Debug→Go选项的功能相同)。

Restart:快捷键Ctrl+Shift+F5。重新从头开始对程序进行调试执行(当对程序做过某些修改后往往需要这样做!)。选择该项后,系统将重新装载程序到内存,并放弃所有变量的当前值(而重新开始)。

Stop Debugging:快捷键Shift+F5。中断当前的调试过程并返回正常的编辑状态(注意,系统将自动关闭调试器,并重新使用Build菜单来取代Debug菜单)。

Step Into:快捷键F11。单步执行程序,并在遇到函数调用语句时,进入那一函数内部,并从头单步执行(与Build→Start Debug→Step Into选项的功能相同)。

Step Over:快捷键F10。单步执行程序,但当执行到函数调用语句时,不进入那一函数内部,而是一步直接执行完该函数后,接着再执行函数调用语句后面的语句。

Step Out:快捷键Shift+F11。与“Step Into”配合使用,当执行进入到函数内部,单步执行若干步之后,若发现不再需要进行单步调试的话,通过该选项可以从函数内部返回(到函数调用语句的下一语句处停止)。

Run to Cursor:快捷键Ctrl+F10。使程序运行到当前鼠标光标所在行时暂停其执行(注意,使用该选项前,要先将鼠标光标设置到某一个你希望暂停的程序行处)。事实上,相当于设置了一个临时断点,与Build→Start Debug→Run to Cursor选项的功能相同。

Insert/Remove Breakpoint:快捷键F9。本菜单项并未出现在Debug菜单上(在工具栏和程序文档的上下文关联菜单上),列在此处是为了方便大家掌握程序调试的手段,其功能是设置或取消固定断点——程序行前有一个圆形的黑点标志,表示该行已经设置了固定断点。另外,与固定断点相关的还有Alt+F9(管理程序中的所有断点)、Ctrl+F9(禁用/使能当前断点)。

3.4 对一个简单程序的调试过程

假设准备编制进行如下计算任务的一个简单程序:在已知x=3、y=5的情况下,先计算出x与y的和s,差d,商q,模r,而后计算res=s+2d+3q+4r的值(res应该等于16)并显示在屏幕上。但编制的如下程序运行后却得出了一个错误结果“res=26”。

#include

void main()

{

int x=3, y=5;

int s, d, q, r, res;

s = x + y;

d = s - y;

q = x / y;

r = x % y;

res = s + 2*d + 3*q + 4*r;

printf("res=%d\n", res);

}

图3-2 程序dbgTest.cpp的跟踪调试

分析上述所编制的程序行,假设能在要输出res结果值的那一程序行(倒数第二行)处设置一个临时断点,让程序先执行到此断点处(注意设为断点的那一行尚未被执行!),看一看那时各变量的动态取值情况,有可能就会找到出错的原因!基于上述分析,先将鼠标光标移动到“printf("res=%d\n", res);”那一行处(左键单击那一行任意位置),从而指定了临时性

断点的行位置,而后执行“Build→Start Debug→Run to Cursor”选项,使程序运行到所指定行时暂停其执行,并显示出如图3-2的界面,其中的左下方窗口中就列出了当时各变量的取值情况:和s=8,差d=3(x=3,y=5,它们的差d=3肯定是错误的!),商q=0,模r=3,最终结果res=26。再仔细查看程序中负责计算差d的那一个语句“d=s-y;”就会恍然大悟,原来将“x-y”误写成了“s-y”!找到了错误,此时可以通过菜单选项“Debug→Stop Debugging”,中断当前的调试过程并返回正常的编辑状态,修改所发现的错误后,再一次执行将能得出正确结果“res=16”。

顺便指出,图3-2中显示的变量是“自动查看”方式的,即VC6自动显示当前运行上下文中的变量的值。如果变量比较多,自动显示的窗口比较混乱,则可以在 Watch 列表中添加自己想要监控的变量名。

上述设置临时断点(到鼠标光标那一行处)的调试手段使用起来很方便,会经常使用(也经常在到达一个断点后,又设置另一个新的临时断点)。另外也常配合使用“单步执行”的方式,来仔细检查每一步(一个程序行)执行后各变量取值的动态变化情况,如,先通过“Run to Cursor”执行到某一个鼠标光标临时断点行处,而后通过使用Debug菜单的“Step Over”或“Step Into”来进行所谓的“单步执行”,当然,每执行一步后,都要仔细观察并分析系统自动给出的各变量取值的动态变化情况,以便及时发现异常而找到出错原因。

3.5 设计合适的程序调试方案

让我们来分析并设计对如下程序进行调试的具体方法与手段(实际上,对不同的程序,都需要在分析其执行结果以及其程序编写结构的基础上,来设计相应的对其进行具体调试的方法与手段,宗旨是想方设法逐步缩小“侦察”范围,直到最后找到出错位置)。

该程序除main外,还有一个自定义函数f。若已经能确认调用f函数前计算出的res值(或s、d、q或r其中之一的结果值)不正确的话,则可像上一程序那样,在计算出res变量值的下一行(或在靠前一些的某一行)处设置断点,看到达那一断点处是否一切正常。若到达断点处的数据结果已经不正常的话,错误已经出现(出现在跟前或出现在前面,从而找到了错误或者缩小了“侦察”范围);若断点处仍然正常,可断言错误出现在后面,而后,①可又一次通过鼠标光标往更靠后一些的适当位置设置新断点,再一次“Debug→Run to Cursor”(一下向后“迈”过了许多行,再继续“侦察”!);②通过“单步执行”(Debug→StepOver),在重点怀疑的那一块地方仔细地逐行进行“侦察”。

注意,“Step Over”不会“跟踪”进入f函数内部,若怀疑f函数可能有问题的话,要通过使用“Debug→Step Into”进入f内部再进行细致调试(在不遇到函数调用的地方,“Step Over”与“Step Into”的功能是相同的。若通过“Step Into”进入到函数内部,单步执行若干步之后,若发现不再需要进行单步调试的话,可通过“Step Out”从函数内部返回到调用语句的下一语句处)。

作为练习,请读者利用这一程序对上述的调试方法与手段进行多方面的灵活使用与体验!可以看出,程序调试是一件很费时费力而又非常细致的工作,需要耐心,要通过不断的实践来总结与积累调试经验。至于VC6提供的其他调试方法与手段,这儿就不一一介绍了。

#include

int f(int a);

int main(void)

{

int x = 3, y = 5;

int s, d, q, r, res, z;

s = x + y;

d = x - y;

q = x / y;

r = x % y;

res = s + 2 * d + 3 * q + 4 * r;

printf("res=%d\n", res);

z = f(36);

printf("z=%d\n", z);

return 0;

}

int f(int a)

{

int b, c;

b = a + 5;

c = 2 * b + 100;

return c;

}

前面也提到过,通过“Run to Cursor”所设置并到达的断点是一个临时性的断点。实际上,VC6还提供设置与清除固定性断点的方法。设置固定性断点最简单的方法是:在某一程序行处,单击鼠标右键,在菜单中选择“Insert/Remove Breakpoint”项(通过左键单击该选项,此时该行前将出现一个圆形的黑点标志,意味着已经将该行设置成了固定断点)。

清除固定性断点的方法为:在具有圆形黑点标志的固定断点行处,单击鼠标右键,在菜单中选择“Remove Breakpoint”项(通过左键单击该选项,此时该行前的那一个圆形黑点标志将消失,意味着已经清除了该固定断点)。

设置了固定性断点后,通常通过“Build→Start Debug→Go”或“Debug→Go”选项使程序开始执行,直到遇到某断点或遇到程序结束而停止。

还要说明的是,可以随时设置任意多个固定性断点,也可以随时清除它们。通过使用菜单选项“Edit→Breakpoints”,会出现一个对话框,在其中的“Break at”文本框中键入要设置断点的程序行的行数信息(但通常是先通过鼠标光标选定某一程序行,再利用菜单选项进入上述对话框,而后通过点击“Break at”文本框右边的小三角按钮,并选定系统自动提供的程序行的行数,以免自己要真正地去数清楚那一行的行数),也能够在指定行处设置一个固定性断点(通过OK按钮确定);如果要清除某断点,可在“Breakpoints”列表栏中先选定它,之后单击Remove按钮。实际上,除位置断点外,通过“Edit→Breakpoints”,还可以设置数据断点,消息断点,以及条件断点等,这儿就不再细说了。

VC6是一个极为庞大的开发工具,我们所介绍的仅仅是一些基本的应用,使用这些应用已经可以完成书中所涉及到的例子和作业,有兴趣的读者可通过参看其他有关介绍VC6的资料或书籍来进行进一步的学习与提高。

3.6 优秀程序员的基本素质——丰富的想象、严谨的思维

(1)程序运行结果看起来对了,但并不意味着程序没有隐藏的问题

①以下程序是从键盘输入一个数x (x是一个int型整数),计算y(y=1000x+9)并在屏幕上输出,程序基本上是对的,能输出正确的结果。

#include

void main()

{

int x, y;

printf("Please input x:");

scanf("%d", &x);

y = 1000*x + 9;

printf("y=%d\n", y);

}

但是,当x输入为5000000,屏幕上却输出y=705032713,出现了错误。

②以下程序的功能是从键盘上读入一串字符,然后在屏幕上输出。

#include

void main()

{

char str[10];

printf("Please input str:");

scanf("%s", &str);

printf("str=%s\n", str);

}

若用户在输入str时不小心多按了(或无意碰触)几下键盘,则程序运行会出错,如图3-3所示——可能稍严重的是程序运行死机(不报错),特别严重的是若有人精心设计输入的字符,完全可能造成系统被入侵或被严重破坏。

图3-3 程序运行过程中因用户的输入而出错

③以下程序的功能是计算班中男生(boy)和女生(girl)的比例。如果女生数为0时,程序

会怎样?

#include

void main()

{

int boy, girl;

printf("Please input boy,girl:");

scanf("%d,%d", &boy, &girl);

printf("boy/girl=%.2f\n", boy*1.0/girl);

}

实际工作中,类似的隐藏的问题往往是破坏力巨大的“地雷”,往往造成极大的损失,例如火箭发射、宇宙飞船飞行、高速列车运行中的事故,我们平常接触最多的恐怕是微软的各种系统需要不断地打补丁。因此,在程序设计中,必须缜密考虑各种情况,哪怕是机率极小的意外。

(2)严谨思维的典范——程序员学习的榜样

丰富的想象力、严谨的思维是一个优秀的程序应当具备的素质。 设计程序时不要拘泥于固定的思维方式,遇到问题的时候要多想几种解决问题的方案,并且考虑全面、思维严谨。

以下2个小故事,应当是一个优秀程序员的标准思维,它形象、幽默、充分地展示了一个优秀程序员的严谨、全面的思维。一个优秀的程序员只有这样思考并设计程序,才能保证程序始终能可靠、稳定地工作,减少和避免发生事故。

①方程仅仅对于正实数的简单情形成立

物理教授走过校园,遇到数学教授。 物理教授在进行一项实验,他总结出一个经验方程,似乎与实验数据吻合,他请数学教授看一看这个方程。 一周后他们碰头,数学教授说这个方程不成立。可那时物理教授已经用他的方程预言出进一步的实验结果,而且效果颇佳,所以他请数学教授再审查一下这个方程。 又是一周过去,他们再次碰头。数学教授告诉物理教授说这个方程的确成立, "但仅仅对于正实数的简单情形成立。"

②判断开枪后树上还有几只鸟

某日,老师在课堂上想看看一学生智商有没有问题,问他 “树上有十只鸟,开枪打死一只,还剩几只?”

他反问“是无声手枪或别的无声的枪吗?”(例如激光枪)

“不是。”

“枪声有多大?”

“80-100分贝。”

“那就是说会震的耳朵疼?”

“是。”

“在这个城市里打鸟犯不犯法?”

“不犯。”

“您确定那只鸟真的被打死啦?”

“确定。”偶已经不耐烦了“拜托,你告诉我还剩几只就行了,OK”

“OK,树上的鸟里有没有聋子?”

“没有。”

“有没有关在笼子里的?”

“没有。”

“边上还有没有其他的树,树上还有没有其他鸟?”

“没有。”

“有没有残疾的或饿的飞不动的鸟?”

“没有。”

“算不算怀孕肚子里的小鸟?”

“不算。”

“打鸟的人眼有没有花?保证是十只?”

“没有花,就十只。” 偶已经满脑门是汗,且下课铃响,但他继续问

“有没有傻的不怕死的?”

“都怕死。”

“会不会一枪打死两只?”

“不会。”

“所有的鸟都可以自由活动吗?”

“完全可以。”

“如果您的回答没有骗人,”学生满怀信心的说,“打死的鸟要是挂在树上没掉下来,那么就剩一只,如果掉下来,就一只不剩。”

老师当即晕倒。


相关内容

  • [嵌入式系统设计]实验指导书-
  • 电子科技大学 微机课程组 嵌入式系统设计 实验指导书 2015年9月 目录 第一部分嵌入式系统仿真实验系统简介 1 第一节.开发平台软件资源及文档 1 第二节.开发平台软件安装 第二部分 实验项目说明 第一节.实验项目列表 第二节.实验项目使用建议 第三部分 嵌入式开发系统实验案例指导 实验一.多任 ...

  • C语言结构化程序设计案例分析开题报告会记录单
  • 重庆文理学院 电子电气工程学院毕业设计(论文)开题报告会记录单 学生姓名 学生学号 所在专业 电子信息科学与技 术 论文题目:C 语言结构化案例分析 通过对结构化程序设计案例的分析,掌握从系统结构到各个 模块.各个函数的设计和实现手段,并进一步规范编程方法,使 选题意义及预期目标 程序设计有章可循, ...

  • [VC]内存泄露检测
  • VC++ IDE 的默认状态(VC6)是没有启用内存泄漏检测机制的,也就是说即使某段代码有内存泄漏,调试会话的 Output 窗口的 Debug 页不会输出有关内存泄漏信息.你必须设定以启用内存泄漏检测机制. 按下面的方法使用调试堆函数,你再看看输出结果,是不是有很多的内存泄漏? #define _ ...

  • IT个人简历中英文模板
  • 个 人 简 历 个人基本信息: 姓 名:××× 民 族:汉 性 别:男 出生年月:××× 政治面貌:党员 户 籍:××× 学 历:×× 身体状况:良好 毕业院校:×××× 专 业:×××××× 联系电话:××××× E-mail:××××@126.com 联系地址:深圳市宝安区 邮政编码:51800 ...

  • VC知识库文章 - COM 组件设计与应用(七)--编译.注册.调用
  • 一.前言 上两回中,咱们用 ATL 写了第一个 COM 组件程序,这回中,主要介绍编译.注册和调用方法.示例程序你已经下载了吗?如果还没有下载,vc6.0 的用户点这里,vc.net 的用户点这里. 二.关于编译 2-1 最小依赖 "最小依赖",表示编译器会把 ATL 中必须使用 ...

  • C++二叉树遍历实验报告
  • 华大计科学院 课程设计说明书 题目: 二叉树的递归和非递归遍历 专业: 计算机科学与技术 班级: 网络工程1班 姓名: 刘群 学号: 1125111023 完成日期: 2012-11 一.设计题目与要求 设计题目:二叉树的遍历 实验要求: 以二叉树表为存储结构,实现二叉树的先.中.后三种次序的递归和 ...

  • PCI总线集成电路测试仪接口电路设计
  • 目前广泛用于集成电路封装测试的设备是由计算机软件控制,通过接口总线与硬件设备通信,能够代替测试人员的大部分劳动,也称为自动化测试系统(ATE).其工作原理是:在计算机中使用测试软件编写待测芯片的测试程序,编写测试程序的过程就是利用程序语言实现对测试系统硬件资源的调度,将测试图形应用于被测集成电路的管 ...

  • 二级考试一个漏洞
  • --Visual C++6.0 的使用方法 教育部考试中心于2007年颁布了新的计算机等级考试大纲,新大纲中规定自第27次全国计算机等级考试(即2008年4月考试)开始,二级C 语言.三级网络技术.数据库技术和信息管理技术上机考试环境由原来的Turbo C2.0变成Visual C++6.0.关于V ...

  • 实验五 模块化的程序设计
  • 实验五 模块化的程序设计 实验目的 (1)理解自顶向下,逐步细化的模块化设计思想划分子模块:知道模块化程序设计方法. (2)熟悉函数的定义.函数的声明和函数的调用,理解函数调用的实现过程. (3)理解函数调用时的数据传送机制,通过参数将主调函数的数据传递到被调函数,运用 return语句将被调函数的 ...