论坛交流
首页办公自动化| 网页制作| 平面设计| 动画制作| 数据库开发| 程序设计| 全部视频教程
应用视频: Windows | Word2007 | Excel2007 | PowerPoint2007 | Dreamweaver 8 | Fireworks 8 | Flash 8 | Photoshop cs | CorelDraw 12
编程视频: C语言视频教程 | HTML | Div+Css布局 | Javascript | Access数据库 | Asp | Sql Server数据库Asp.net  | Flash AS
当前位置 > 文字教程 > C语言程序设计教程
Tag:新手,函数,指针,数据类型,对象,Turbo,入门,运算符,数组,结构,二级,,tc,游戏,试题,问答,编译,视频教程

C语言编程常见问题解答之调试

文章类别:C语言程序设计 | 发表日期:2008-9-24 14:36:35

    调试(debugging)是指去掉程序中的错误(通常被称为bugs)的过程。一个错误可能非常简单,例如拼错一个单词或者漏掉一个分号;也可能比较复杂,例如使用一个指向并不存在的地址的指针。无论错误的复杂程度如何,把握正确的调试方法都能使程序员受益匪浅。

    11.1 假如我运行的程序挂起了,应该怎么办?
    当你运行一个程序时会有多种原因使它挂起,这些原因可以分为以下4种基本类型:
    (1)程序中有死循环;
    (2)程序运行的时间比所期望的长;
    (3)程序在等待某些输入信息,并且直到输入正确后才会继续运行;
    (4)程序设计的目的就是为了延迟一段时间,或者暂停执行。
    在讨论了因未知原因而挂起的程序的调试技巧后,将逐个分析上述的每种情况。
    调试那些因未知原因而挂起的程序是非常困难的。你可能花费了很长的时间编写一个程序,并努力确保每条代码都准确无误,你也可能只是在一个原来运行良好的程序上作了一个很小的修改,然而,当你运行程序时屏幕上却什么也没有显示。假如你能得到一个错误的结果,或者部分结果,你也许知道应该作些什么修改,而一个空白的屏幕实在令人沮丧,你根本不知道错在哪里。
    在开始调试这样一个程序时,你应该先检查一下程序结构,然后再按执行顺序依次查看程序的各个部分,看看它们是否能正确运行。
    例如,假如主程序只包含3个函数调用——A()、B()和C(),那么在调试时,你可以先检查函数A()是否把控制权返回给了主程序。为此,你可以在调用函数A()的语句后面加上exit()命令,也可以用注释符把对函数B()和C()的调用括起来,然后重新编译并运行这个程序。

    注重:通过调试程序(debugger)也可以做到这一点,然而上述方法是一种很传统的调试方法。调试程序是一个程序,它的作用是让程序员能够观察程序的运行情况、程序的当前运行行号、变量的值,等等。

    此时你将看到函数A()是否将控制权返回给了主程序——假如该程序运行并退出,你可以判定是程序的其它部分使程序挂起。你可以用这种方法测试程序的每一部分,直到发现使程序挂起的那一部分,然后集中精力修改相应的函数。
    有时,情况会更复杂一些。例如,使程序挂起的函数本身是完全正常的,问题可能出在该函数从别的地方得到了一些错误的数据。这时,你就要检查该函数所接受的所有的值,并找出是哪些值导致了错误操作。

    技巧:监视函数是调试程序的出色功能之一。
   
    分析下面这个简单的例子将帮助你把握这种技巧的使用方法:
#include <stdio. h>
#include <stdlib. h>
/*
   * Declare the functions that the main function is using
   */
int A(), B(int), C(int, int);
/*
   * The main program
   */
int A(), B(), C(); /*These are functions in some other
                      module * /
int main()
{
    int v1,  v2, v3;
    v1  = A();
    v2  = B(v1);
    v3  = C(v1, v2);
    printf ("The Result is %d. \n" , v3);
    return(0) ;
}

    你可以在调用函数A()的语句后输出变量v1的值,以确认它是否在函数B()所能接受的值的范围之内,因为即使是函数B()使程序挂起,它本身并不一定就有错,而可能是因为函数A()给了函数B()一个并非它所期望的值。
  现在,已经分析了调试“挂起”的程序的基本方法,下面来看看一些使程序挂起的常见错误。

    死循环
    当你的程序出现了死循环时,机器将无数次地执行同一段代码,这种操作当然是程序员所不希望的。出现死循环的原因是程序员使程序进行循环的判定条件永远为真,或者使程序退出循环的判定条件永远为假。下面是一个死循环的例子:
/* initialize a double dimension array */
for (a = 0 ;  a < 10; ++a )
{
    for(b =  0; b<10; ++a)
    {
         array[a][b]==0;
    }
}

    这里的问题是程序员犯了一个错误(事实上可能是键入字母的错误),第二个循环本应在变量b增加到10后结束,但是却从未让变量b的值增加!第二个for循环的第三部分增加变量a的值,而程序员的本意是要增加变量b的值。因为b的值将总是小于10,所以第二个for循环会一直运行下去。
    怎样才能发现这个错误呢?除非你重新阅读该程序并注重到变量b的值没有增加,否则你不可能发现这个错误。当你试图调试该程序时,你可以在第二个for循环的循环体中加入这样一条语句:
    printf(" %d %d %d\n" ,  a  , b , array[a][b]) ;
这条语句的正确输出应该是:
    0 0 0
    0 1 0
    (and eventually reaching)
    9 9 0
但你实际上看到的输出却是:
    0 0 0
    1 0 0
    2 0 0
    ...
你所得到是一个数字序列,它的第一项不断增加,但它本身永远不会结束。用这种方法输出变量不仅可以找出错误,而且还能知道数组是否由所期望的值组成。这个错误用其它方法似乎很难发现!这种输出变量内容的技巧以后还会用到。
    产生死循环的其它原因还有一些其它的原因也会导致死循环。请看下述程序段:
int main()
{
     int a = 7;
     while ( a  <  10)
    {
      + +a;
      a /= 2;
    }
    return (0);
}
    尽管每次循环中变量a的值都要增加,但与此同时它又被减小了一半。变量a的初始值为7,它先增加到8,然后减半到4。因此,变量a永远也不会增加到10,循环也永远不会结束。

   运行时间比期望的时间长

    在有些情况下,你会发现程序并没有被完全“锁死”,只不过它的运行时间比你所期望的时间长,这种情况是令人讨厌的。假如你所使用的计算机运算速度很快,能在极短的时间内完成很复杂的运算,那么这种情况就更令人讨厌了。下面举几个这样的例子:

/*
   * A subroutine to calculate Fibonacci numbers
   */
int fib ( int i)
{
    if  (i <3)
         return 1;
    else
         return fib( i - 1)+fib( i - 2);
}

    一个菲波那契(Fibonacci)数是这样生成的:任意一个菲波那契数都是在它之前的两个菲波那契数之和;第一个和第二个菲波那契数是例外,它们都被定义为1。菲波那契数在数学中很有意思,而且在实际中也有许多应用。

    注重:在向日葵的种子中可以找到菲波那契数的例子——向日葵有两组螺旋形排列的种子,一组含21颗种子,另一组含34颗种子,这两个数恰好都是菲波那契数。

    从表面上看,上述程序段是定义菲波那契数的一种很简单的方法。这段程序简洁短小,看上去执行时间不会太长。但事实上,哪怕是用计算机计算出较小的菲波那契数,例如第100个,都会花去很长的时间,下文中将分析其中的原因。
    假如你要计算第40个菲波那契数的值,就要把第39个和第38个菲波那契数的值相加,因此需要先 计算出这两个数,而为此又要分别计算出另外两组更小的菲波那契数的和。不难看出,第一步是2个子问题,第二步是4个子问题,第三步是8个子问题,如此继续下去,结果是子问题的数目以步数为指数不断增长。例如,在计算第40个菲波那契数的过程中,函数fib()将被调用2亿多次!即便在一台速度相当快的计算机上,这一过程也要持续好几分钟。
    数字的排序所花的时间有时也会超出你的预料:
  /*
   *  Routine to sort an array of integers.
   *  Takes two parameters:
   *     ar---The array of numbers to be sorted, and
   *     size---the size of the array.
   */
void sort( int ar[], int size )
{
      int i,j;
      for( i = 0; i<size - 1;  ++ i)
       {
           for( j = 0; j< size - 1; ++j )
           {
               if (ar[j]>ar[j + 1])
                {
                    int temp;
                    temp = ar[j];
                    ar[j] = ar[j + 1];
                    ar[j + 1] = temp;
                }
            }
       }
 }

    假如你用几个较短的数列去检验上述程序段,你会感到十分满足,因为这段程序能很快地将较短的数列排好序。假如你用一个很长的数列来检验这段程序,那么程序看上去就停滞了,因为程序需要执行很长时间。为什么会这样呢?
    为了回答上述问题,先来看看嵌套的for循环。这里有两重for循环,其中一个循环嵌套在另一个循环中。这两个循环的循环变量都是从O到size-1,也就是说,处于两重循环之间的程序段将被执行size*size次,即size的平方次。对含10个数据项的数列进行排序时,这段程序还是令人满足的,因为10的平方只有100。但是,当你对含5000个数据项的数列进行排序时,循环中的那段程序将被执行2500万次;当你对含100万个数据项的数列进行排序时,循环中的那段程序将被执行1万亿次。
    在上述这些情况下,你应该比较准确地估计程序的工作量。这种估计属于算法分析的范畴,把握它对每个程序员来说都是很重要的。

  等待正确的输入

  有时程序停止运行是因为它在等待正确的输入信息。最简单的情况就是程序在等待用户输入信息,而程序却没有输出相应的提示信息,因而用户不知道要输入信息,程序看上去就好象锁住了。更令人讨厌的是由输出缓冲造成的这种结果,这个问题将在17.1中深入讨论。
    请看下述程序段:
   /*
    *This program reads all the numbers from a file.
   *  sums them, and prints them.
   */
   # include <stdio. h>
   main()
   {
         FILE  *in = fopen("numbers.dat", "r");
         int total =  0, n;
         while( fscanf( in, " %d" , &n )! =EOF)
         {
             total + = n;
         }  
        printf( "The total is  %d\n" , total);
        fclose ( in ) ;
    }

    假如文件NUMBERS.DAT中只包含整数,这段程序会正常运行。但是,假如该文件中包含有效整数以外的数据,那么这段程序的运行结果将是令人惊异的。当该程序碰到一个不恰当的值时,它会发现这不是一个整数值,所以它不会读入这个值,而是返回一个错误代码。但此时程序并未读到文件尾部,因此与EOF比较的值为假。这样,循环将继续进行,而n将取某个未定义的值,程序会试图再次读文件,而这一次又碰到了刚才那个错误数据。请记住,因为数据不正确,所以程序并不读入该数据。这样,程序就会无休止地执行下去,并一直试图读入那个错误的数据。解决这个问题的办法是让while循环去测试读入的数据是否正确。
  还有许多其它原因会使程序挂起,但总的来说,它们都属于上述三种类型中的某一种。

    请参见:
    11.2如何检测内存漏洞(1eak)?

   11.2 如何检测内存漏洞(leak)?
   在动态分配的内存单元(即由函数malloc()或ealloc()分配的内存单元)不再使用却没有被释放的情况下,会出现内存漏洞。未释放内存单元本身并不是一种错误,编译程序不会因此报告出错,程序也不会因此而立即崩溃。但是,假如不再使用而又没有被释放的内存单元越来越多,程序所能使用的内存空间就越来越小。最终,当程序试图要求分配内存时,就会发现已经没有可用的内存空间。这时,尤其是当程序员没有考虑到内存分配失败的可能性时,程序的运行就会出现异常现象。
    内存漏洞是最难检测的错误之一,同时也是最危险的错误。导致这个问题的编程错误很可能出现在程序的开始部分,但只有当程序奠名其妙地使用完内存后,这个问题才会暴露出来。
此时去检查当前那条导致内存分配失败的语句是无济于事的,因为那些分配了内存却未能按时释放内存的代码可能在程序的其它地方。
    遗憾的是C语言并没有为检测或修复内存漏洞提供现成的方法。除非使用提供这种功能的商业软件包,否则,程序员就需要以很大的耐心和精力去检测和修复内存漏洞。最好的办法是在编写程序时就充分考虑到内存漏洞的可能性,并小心谨慎地处理这种可能性。
    导致内存漏洞的最简单的也是最常见的原因是忘记释放分配给临时缓冲区的内存空间,请看下述程序段: 
# include <stdio. h>
# include <stdlib. h>
  /*
   *  Say hello to the user's  and put the user's name in UPPERCASE.
   */
void SayHi( char *name )
{
      char * UpName;
      int a;
      UpName = malloc(  strlen( name ) +1);
                            / * Allocate space for the name * /
  for( a  =0; a<strlen( name ); ++a)
      UpName[a] = toupper( name[a]) ;
      UpName [a] = '\0'i
      printf("Hello, %si\n", UpName );
}
int main()
{
      SayHi( "Dave" );
      return( 0 );
}

    这段程序中的问题是显而易见的——它为存储使用大写字母的名字分配了临时空间,但从未释放这些空间。为了保证永远不发生类似的情况,你可以采用这样的方法:在分配内存的每条语句后加上相应的free语句,然后把使用这些临时内存的语句插到这两条语句之间。只要在程序中分配和释放内存的语句之间没有break,continue或goto语句,这种方法就能保证每次分配的空间在使用完后就被释放掉。
    上述方法相当繁琐,并且不能完全避免内存漏洞的出现,因为在实际编程中,所分配的内存空间的使用时间往往是不能猜测的。此外,假如操作或删除内存空间的程序段有错误,也会出现内存漏洞。例如,在删除链表的过程中,最后一个结点可能会丢失,或者一个指向内存空间的指针可能会被改写。解决这类问题的办法只能是小心谨慎地编写程序,或者象前面提到的那样使用相应的软件包,或者利用语言的扩展功能。

    请参见:
    11.1假如我运行的程序挂起了,应该怎么办?

    11. 3 调试程序的最好方法是什么?
    要了解调试程序的最好方法,首先要分析一下调试过程的三个要素:
    ·应该用什 么工具调试一个程序?
    ·用什么办法才能找出程序中的错误?
    ·怎样才能从一开始就避免错误?

    应该用什么工具调试一个程序?

    有经验的程序员会使用许多工具来帮助调试程序,包括一组调试程序和一些"lint”程序,当然,编译程序本身也是一种调试工具。
    在检查程序中的逻辑错误时,调试程序是非凡有用的,因此许多程序员都把调试程序作为基本的调试工具。一般来说,调试程序能帮助程序员完成以下工作:
  (1)观察程序的运行情况
  仅这项功能就使一个典型的调试程序具备了不可估量的价值。即使你花了几个月的时间精心编写了一个程序,你也不一定完全清楚这个程序每一步的运行情况。假如程序员忘记了某些if语句、函数调用或分支程序,可能会导致某些程序段被跳过或执行,而这种结果并不是程序员所期望的。不管怎样,在程序的执行过程中,尤其是当程序有异常表现时,假如程序员能随时查看当前被执行的是那几行代码,那么他就能很好地了解程序正在做什么以及错误发生在
什么地方。
    (2)设置断点
    通过设置断点可以使程序在执行到某一点时暂时停住。当你知道错误发生在程序的哪一部分时,这种方法是非凡有用的。你可以把断点设置在有问题的程序段的前面、中间或后面。当程序执行到断点时,就会暂时停住,此时你可以检查所有局部变量、参数和全局变量的值。假如一切正常,可以继续执行程序,直到碰到另一个断点,或者直到引起问题的原因暴露出来。
    (3)设置监视
    程序员可以通过调试程序监视一个变量,即连续地监视一个变量的值或内容。假如你清楚一个变量的取值范围或有效内容,那么通过这种方法就能很快地找出错误的原因。此外,你可以让调试程序替你监视变量,并且在某个变量超出预先定义的取值范围或某个条件满足时使程序暂停执行。假如你知道变量的所有行为,那么这么做是很方便的。
    好的调试程序通常还提供一些其它功能来简化调试工作。然而,调试程序并不是唯一的调试工具,lint程序和编译程序本身也能提供很有价值的手段来分析程序的运行情况。

    注重:lint程序能分辨数百种常见的编程错误,并且能报告这些错误发生在程序的哪一部分。尽管其中有一些并不是真正的错误,但大部分还是有价值的。

    lint程序和编译程序所提供的一种典型功能是编译时检查(compile—time checks),这种功能是调试程序所不具备的。当用这些工具编译你的程序时,它们会找出程序中有问题的程序段,可能产生意想不到的效果的程序段,以及常见的错误。下面将分析几个这种检查方式的应用例子,相信对你会有所帮助。

    等于运算符的误用   

    编译时检查有助于发现等于运算符的误用。请看下述程序段:
    void foo(int a,int b)
    {
      if ( a = b )
      {
          / * some code here * /
      }
    }

    这种类型的错误一般很难发现!程序并没有比较两个变量,而是把b的值赋给了a,并且在b不为零的条件下执行if体。一般来说,这并不是程序员所希望的(尽管有可能)。这样一来,不仅有关的程序段将被执行错误的次数,并且在以后用到变量a时其值也是错误的。

    未初始化的变量

    编译时检查有助于发现未初始化的变量。请看下面的函数:
void average ( float ar[], int size )
{
       float total;
       int a;
       for( a = 0;a<size; ++a)
       {
            total+=ar[a];
       }
       printf(" %f\n", total /  (float) size );
}
    这里的问题是变量total没有被初始化,因此它很可能是一个随机的无用的数。数组所有元素的值的和将与这个随机数的值相加(这部分程序是正确的),然后输出包括这个随机数在内的一组数的平均值。

    变量的隐式类型转换

    在有些情况下,C语言会自动将一种类型的变量转换为另一种类型。这可能是一件好事(程序员不用再做这项工作),但是也可能会产生意想不到的效果。把指针类型隐式转换成整型恐怕是最糟糕的隐式类型转换。
void sort( int ar[],int size )
{
       /* code to sort goes here * /
}
int main()
{
       int arrgy[10];
       sort(  10, array );
}
    上述程序显然不是程序员所期望的,虽然它的实际运行结果难以猜测,但无疑是灾难性的。

    用什么办法才能找出程序中的错误?

    在调试程序的过程中,程序员应该记住以下几种技巧:
        先调试程序中较小的组成部分,然后调试较大的组成部分

    假如你的程序编写得很好,那么它将包含一些较小的组成部分,最好先证实程序的这些部分是正确的。尽管程序中的错误并不一定发生在这些部分中,但是先调试它们有助于你理解程序的总体结构,并且证实程序的哪些部分不存在错误。进一步地,当你调试程序中较大的组成部分时,你就可以确信那些较小的组成部分是正常工作的。

    彻底调试好程序的一个组成部分后,再调试下一个组成部分

    这一点非常重要。假如证实了程序的一个组成部分是正确的,不仅能缩小可能存在错误的范围,而且程序的其它组成部分就能安全地使用这部分程序了。这里应用了一种很好的经验性原则,简单地说就是调试一段代码的难度与这段代码长度的平方成正比,因此,调试一段20行的代码比调试一段10行的代码要难4倍。因此,在调试过程中每次只把精力集中在一小段代码上是很有帮助的。当然,这仅仅是一个总的原则,具体使用时还要视具体情况而定。

    连续地观察程序流(flow)和数据的变化

    这一点也很重要!假如你小心仔细地设计和编写程序,那么通过监视程序的输出你就能准确地知道正在执行的是哪部分代码以及各个变量的内容都是什么。当然,假如程序表现不正常,你就无法做到这一点。为了做到这一点,通常只能借助于调试程序或者在程序中加入大量的print语句来观察控制流和重要变量的内容。

    始终打开编译程序警告选项  并试图消除所有警告

    在开发程序的过程中,你自始至终都要做到这一点,否则,你就会面临一项十分繁重的工作。尽管许多程序员认为消除编译程序警告是一项繁琐的工作,但它是很有价值的。编译程序给出警告的大部分代码至少都是有问题的,因此用一些时间把它们变成正确的代码是值得的;而且,通过消除这些警告,你往往会找到程序中真正发生错误的地方。

   正确地缩小存在错误的范围

    假如你能一下子确定存在错误的那部分程序并在其中找到错误,那就会节省许多调试时间,并且你能成为一个收入相当高的专业调试员。但事实上,我们并不能总是一下子就命中要害,因此,通常的做法是逐步缩小可能存在错误的程序范围,并通过这种过程找出真正存在错误的那部分 程序。不管错误是多么难于发现,这种做法总是有效的。当你找到这部分程序后,就可以把所有的调试工作集中到这部分程序上了。不言而喻,准确地缩小范围是很重要的,否则,最终集中精力调试的那部分程序很可能是完全正确的。

    如何从一开始就避免错误?

    有这样一句谚语——“防患于未然”,它的意思是避免问题的出现比出现问题后再想办法弥补要好得多。这在计算机编程中也是千真万确的!在编写程序时,一个经验丰富的程序员所花的时间和精力要比一个缺乏经验的程序员多得,但正是这种耐心和严谨的编程风格使经验丰富的程序员往往只需花很少的时间来调试程序,而且,假如此后程序要解决某个问题或做某种改动,他便能很快地修正错误并加入相应的代码。相反,对于一个粗制滥造的程序,即使它总的来说还算正确,那么改动它或者修正其中一个很快就暴露出来的错误,都会是一场恶梦。
    一般来说,按结构化程序设计原则编写的程序是易于调试和修改的,下面将介绍其中的一些原则。

    程序中应有足够的注释

    有些程序员认为注释程序是一项繁琐的工作,但即使你从来没想过让别人来读你的程序,你也应该在程序中加入足够的注释,因为即使你现在认为清楚明了的语句,在几个月以后往往也会变得晦涩难懂。这并不是说注释越多越好,过多的注释有时反而会混淆代码的原意。但是,在每个函数中以及在执行重要功能或并非一目了然的代码前加上几行注释是必要的。下面就是一段注释得较好的代码:
  /*  
   *   Compute an integer factorial value using recursion.
   *   Input   an integer number.
   *   Output  : another integer
   *   Side effects : may blow up stack if input value is  * Huge *
   */
int factorial ( int number)
{
       if ( number < = 1)
           return 1;  /* The factorial of one is one; QED * /
       else
            return n * factorial( n - 1 );
       / * The magic! This is possible because the factorial of a
         number is the number itself times the factorial  of the
         number minus one.  Neat!  * /
}


    函数应当简洁

    按照前文中曾提到的这样一条原则——调试一段代码的难度和这段代码长度的平方成正比——函数编写得简洁无疑是有益的。但是,需要补充的是,假如一个函数很简洁,你就应该多花一点时间去仔细地分析和检查它,以确保它准确无误。此后你可以继续编写程序的其余部分,并且可以对刚才编写的函数的正确性布满信心,你再也不需要检查它了。对于一段又长又复杂的例程,你往往是不会有这样的信心的。
    编写短小简洁的函数的另一个好处是,在编写了一个短小的函数之后,在程序的其它部分就可以使用这个函数了。例如,假如你在编写一个财务处理程序,那么你在程序的不同部分可能都需要按季、按月、按周或者按一月中的某一天等方式来计算利息。假如按非结构化原则编写程序,那么在计算利息的每一处都需要一段独立的代码,这些重复的代码将使程序变得冗长而难读。然而,你可以把这些任务的实现简化为下面这样的一个函数:
  /*
   *    ComDllte what the "real" rate of interest would be
   *     for a given flat interest rate, divided into N segments
   */
double Compute Interest( double Rate, int Segments )
{
       int  a;
       double Result = 1.0;
       Rate /= (double) Segments;
       for( a = 0; a< Segments  ; ++a )
           Result * =Rate;
       return Result;
}

   在编写了上述函数之后,你就可以在计算利息的每一处调用这个函数了。这样一来,你不仅能有效地消除每一段复制的代码中的错误,而且大大缩短了程序的长度,简化了程序的结构。这种技术往往还会使程序中的其它错误更轻易被发现。
    当你习惯了用这种方法把程序分解为可控制的模块后,你就会发现它还有更多的妙用。

   程序流应该清楚,避免使用goto语句和其它跳转语句

    这条原则在计算机技术领域内已被广泛接受,但在某些圈子中对此还很有争议。然而,人们也一致认为那些通过少数语句使程序流无条件地跳过部分代码的程序调试起来要轻易得多,因为这样的程序通常更加清楚易懂。许多程序员不知道如何用结构化的程序结构来代替那些“非结构化的跳转”,下面的一些例子说明了应该如何完成这项工作:
for( a = 0; a<100s ++a)
{
     Func1( a );
     if (a  = = 2 ) continue;
     Func2( a );
}

    当a等于2时,这段程序就通过continue语句跳过循环中的某余部分。它可以被改写成如下的形式:
for( a = 0; a<100; ++a)
{
     Func1 (a);
     if (a !=2 )
        Func2(a) ;
}

    这段程序更易于调试,因为花括号内的代码清楚地显示了应该执行和不应该执行什么。那么,它是怎样使你的代码更易于修改和调试的呢?假设现在要加入一些在每次循环的最后都要被执行的代码,在第一个例子中,假如你注重到了continue语句,你就不得不对这段程序做复杂的修改(不妨试一下,因为这并非是显而易见的!);假如你没有注重到continue语句,那么你恐怕就要犯一个难以发现的错误了。在第二个例子中,要做的修改很简单,你只需把新的代
码加到循环体的末尾。
    当你使用break语句时,可能会发生另外一种错误。假设你编写了下面这样一段程序:
for (a =0) a<100;  ++a)
{
     if (Func1 (a) ==2 )
        break;
     Func2 (a)  ;
}

    假设函数Funcl()的返回值永远不会等于2,上述循环就会从1进行到100;反之,循环在到达100以前就会结束。假如你要在循环体中加入代码,看到这样的循环体,你很可能就会认为它确实能从0循环到99,而这种假设很可能会使你犯一个危险的错误。另一种危险可能来自对a值的使用,因为当循环结束后,a的值并不一定就是100。
    c语言能帮助你解决这样的问题,你可以按如下形式编写这个for循环:
    for(a=O;a<100&&Func1(a)!=2;++a)
    上述循环清楚地告诉程序员:“从0循环到99,但一旦Func1()等于2就停止循环”。因为整个退出条件非常清楚,所以程序员此后就很难犯前面提到的那些错误了。

    函数名和变量名应具有描述性

    使用具有描述性的函数和变量名能更清楚地表达代码的意思——并且在某种程度上这本身就是一种注释。以下几个例子就是最好的说明:
    y=p+i-c;

    YearlySum=Principal+Interest-Charges:
哪一个更清楚呢?
    p=*(l+o);

    page=&List[offset];
哪一个更清楚呢?

    11.4 怎样调试TSR程序?
    TSR(terminate and stay resident)程序是一种执行完毕后仍然驻留在计算机内存中并 继续完成某些任务的程序,这一点是通过使操作系统的某些部分定期调用由TSR程序驻留在计算机内存中的代码来实现的。
    TSR程序的工作方式使TSR程序调试起来非常困难!因为对调试者来说,TSR程序真正的执行时间非常短,调试者根本无法知道TSR程序正在做什么,也无法知道TSR程序是否结束了或仍在运行。正是这种“不可见性”使得TSR程序作用非凡,然而它带来了很多问题。
    此外,TSR程序是通过改变向量地址,改变可用内存的大小以及其它方法使自身驻留到内存中的,这一过程会与调试工具的执行发生灾难性的冲突,而调试程序也可能会破坏TSR所作的这些改变。
    无论如何,用调试程序调试TSR程序几乎是不可能的,除非有了专门为TSR程序设计的调试程序。然而,你可以用其它方法调试TSR程序。
    首先,你可以再次使用前文中提到过的一种方法,即利用print语句监视程序的运行情况。这里要对这种方法稍加改动:每当TSR程序被系统调用时,无论系统采用什么方式(击键,时钟中断,等等),你都可以用append模式打开一个记录文件,并将能告知程序员程序运行情况的有关信息打印到这个文件中。这些信息可以包括执行过程中碰到的函数,变量的值以及其它信息。在TSR程序执行完毕(或崩溃)后,你可以分析这个记录文件,并从中获得一些有价值的信息。
    另一种方法是创建一个"假的"TSR程序。换句话说,创建一个与TSR程序功能相似的程序,但并不是一个真正的TSR程序!相反,还要使这个程序成为一个测试程序的子程序。你可以很轻易地把原来接受系统中断的功能改为从主程序中接受函数调用。你可以把输送给TSR程序的输入信息“封装”在主程序中,也可以让主程序动态地从程序员那里接受这些输入信息。
这个功能和TSR程序相似的程序永远不会把自身装到计算机内存中,也不会改变操作系统的向量地址。
    第二种方法有几点明显的好处:它使程序员能够使用自己惯用的调试技术,调试方法和调试程序,它也为程序员提供了一种更好的观察程序内部操作的方法。此外,真正的TSR程序会驻留在内存中,假如不将它们移去,它们会一直占用一部分计算机内存。假如你的程序还未经调试,那么它完全有可能不能正确地把自身从计算机内存中移去,而这很可能会导致耗尽计算机的内存(这一点很象内存漏洞)。

    11.5 怎样获得一个能报告条件失败的程序?
    在任何一个程序中,有些情况是永远不应该出现的,例如除数为O,给空指针赋值,等等。当这些情况出现时,程序应该能立即给出报告,并且还应该能报告发生错误的位置。
    c语言提供了assert()命令,它能帮助你解决这个问题。assert()命令会检查它的括号中的条件,假如该条件失败,它会执行以下几步:
    (1)打印失败条件的内容;
    (2)打印发生错误的行号;
    (3)打印错误所在的源文件名;
    (4)使程序以出错状态结束。
    简单地说,assert()命令的作用就是保证那些不应出现的情况不会出现。下面将给出一些这样的条件的例子。
    内存分配失败是最常见的问题之一,在确实需要内存空间却又无法得到的情况下,程序只好退出。利用assert()命令可以很好地解决这个问题:
foo()
{
      char  * buffer;
      buffer = malloc( 10000 );
      assert ( buffer |= NULL );
}

    假如buffer等于NULL,程序将停止执行,并且报告这个错误以及发生错误的位置;否则程序将继续执行。
        下面是assert()命令的另一种用法:
float IntFracC int Num, int Denom )
{
     assert( Denom ! = 0 ) ;
     return  ( ( float ) Num  ) / ( ( float ) Denom );
}

    这里用assert()命令防止在程序中把O作为除数。
    应该强调的是,只有在条件失败会导致灾难性的后果时,才需要使用assert()命令。假如有可能,程序员应该用更好的方法来处理这类错误。在前面的例子中,除数为O的可能性可能很小,但这并不意味着assert()命令就没有用。一个设计良好的程序应该有很多assert()命令,因为知道将要发生灾难性结果究竟比一无所知(或不愿知道)要好得多。
    使用assert()命令的另一个好处是通过在程序的头部加入宏NDEBUG(不调试),就可以使所有的assert()命令在编译时被忽略掉。当所有的错误都被修正后,这一点对于生成不同版本的程序是非常重要的。假如加入NDEBUG宏定义,就能生成不含调试代码的可执行程序,你可以向用户发放这种版本;而删去NDEBUG宏定义后又能生成含调试代码的可执行程序,你可以保留这种版本供自己使用。不含assert()命令的代码运行起来要快得多,并且程序也不会因为某个变量稍微越界而忽然停止运行。

    请参见:
    11.1 假如我运行的程序挂起了,应该怎么办?
    11.3 调试程序的最好方法是什么?

视频教程列表
文章教程搜索
 
C语言程序设计推荐教程
C语言程序设计热门教程
看全部视频教程
购买方式/价格
购买视频教程: 咨询客服
tel:15972130058