《C语言编程初学者指南》一1.8 调试C程序

本节书摘来自异步社区《C语言编程初学者指南》一书中的第1章,第1.8节,作者【美】Keith Davenport(达文波特) , M1ichael Vine(维恩),更多章节内容可以访问云栖社区“异步社区”公众号查看

1.8 调试C程序

如果你的程序编译了,然后退出了或在执行中出现异常,程序中一定存在一个错误(一个bug)。我们将要花费很多的时间来找到并删除这些bug。本小节介绍了帮助你开始这一工作的一些技巧。然而,请记住,调试是计算机科学,同样也是一门艺术,当然,你的编程实践越多,调试也就变得越容易!往往一个程序编译和执行得很好,但总是产生你意料之外的或者不想要的结果。例如,如下的程序的编译和执行没有错误,但是输出却是无法读懂的,或者说不是我所期望的,其输出如图1.11所示。

include <stdio.h>

int main()
{
    printf("Chapter 1 - Getting Started with C Programming");
    printf("This is an example of a format bug."); 
    printf("The format issue can be corrected by using"); 
    printf(" the \n and \\ escape sequences");
    return 0;
}


《C语言编程初学者指南》一1.8 调试C程序

图1.11 错误的格式化

你能看到格式有什么问题吗?漏掉了什么,应该在哪里进行修改?下一段代码及其输出如图1.12所示,它通过放置相应的转义序列而改正了格式。


《C语言编程初学者指南》一1.8 调试C程序

图1.12 通过在需要的地方添加n和\转义序列,改正了格式

include <stdio.h>

int main()
{
    printf("Chapter 1 - Getting Started with C Programming\n");
    printf("This is an example of a format bug.\n"); 
    printf("The format issue can be corrected by using"); 
    printf(" the \\n and \\\\ escape sequences");
    return 0;
}

格式问题是编程初学者经常遇到的问题。通常,可以通过熟练使用printf()函数和各种转义序列来快速地解决这些问题。

另一类常见的bug是逻辑错误,包括循环在期望退出的时候没有退出、数学计算公式的错误或者可能是一次有瑕疵的相等性测试(条件)。调试逻辑错误的第一个步骤是,找到程序bug存在的第一行代码。做到这一点的一种方式是使用打印语句,也就是在整个代码中分散地使用printf()函数。例如,可以在源代码中做一些如下的事情:

anyFunction(int x, int y)
{
    printf("Entering anyFunction()\n"); fflush(stdout);
    —— lots of your code here ———
    printf("Exiting anyFunction()\n"); fflush(stdout);
}

fflush()函数确保了printf语句的结果会立即发送到屏幕,并且如果你想要使用printf()进行调试的话,应该使用fflush()函数。传递给fflush()函数的stdout参数是标准输出,这通常是计算机屏幕。

在将发生逻辑错误的地方的范围缩窄到代码行或函数之后,下一步就是搞清楚你的变量在彼时的值。还是可以使用printf()函数来打印出变量值,这对于确定非正常的程序行为的源头有很大的帮助。第2章将详细介绍使用printf()函数来显示变量值。

记住,当你修复了任何的bug之后,必须重新编译程序,运行它,并且如果必要的话,再次进行调试。

作为程序员新手,你经常会遇到的是编译错误,而不是逻辑错误,而编译错误通常是语法问题所导致的,例如漏掉了标识符和终结符,或者使用了无效的指令、转义序列或注释语句块等语法问题。

调试编译错误可能会令人沮丧,特别是当你在计算机屏幕上看到50条或者更多的错误的时候。需要记住的重要的一点是,程序开始处的一个错误,可能在编译的时候导致一系列层叠性的错误。因此,开始调试编译错误的最好的地方,就是错误列表中的第一个错误。在接下来的几个小节中,我们将介绍在刚开始编写C程序的时候的一些较为常见的编译错误。

1.8.1 常见错误之1:漏掉程序语句块标识符
如果忘记了插入一个开始或对应的结束程序语句块的标识符({和}),你将会看到如图1.13所示的错误消息。在下面的示例中,我们故意在main()函数名的后面,漏掉开始程序语句块标识符({}。

include <stdio.h>

int main()
    printf("Welcome to C Programming\n");
return 0;
}


《C语言编程初学者指南》一1.8 调试C程序

图1.13 由于漏掉了程序语句块标识符而导致的错误

由于忘记使用开始程序语句块标识符({),导致了很多的错误,如图1.13所示。当调试编译错误的时候,记住要从第1个错误开始,如下所示,它告诉你就在printf()函数之前有一个错误。你将会发现,解决了第1个错误之后,也就更正了很多甚至是所有剩下的错误。

hello.c:5:2: error: expected declaration specifiers before 'printf' 
  printf("Welcome to C Programming\n");
  ^

技巧
 

为了帮助你找到一个错误的位置,编译器试图向你显示捕获到错误的代码的行号。在前面的示例中,hello.c:5:是告诉你在hello.c源代码的第 5 行捕获到了错误。注意,编译器在统计行号的时候,将空行和代码行都算在内。
尽管编译器错误指向了printf开始处,但重要的是要知道,可能并不是printf而是在它之前的某处出现了错误,在这个例子中,是因为漏掉了语句块标识符。

1.8.2 常见错误之2:漏掉语句终结符
图1.13展示了由几个常见场景所导致的一条常见错误消息。这种类型的错误可能会由于几个原因而导致。除了漏掉程序语句块的缩进,漏掉了语句终结符(分号)也可能会导致解析错误。

图1.14展示了如下程序中的一个bug。你能看出这个bug隐藏在哪里吗?

include <stdio.h>

int main()
{
    printf("Welcome to C Programming\n")
    return 0;
}


《C语言编程初学者指南》一1.8 调试C程序

图1.14 漏掉终结符导致的错误

由于C编译器不能够确定一条程序语句(如一条打印语句)到哪里结束,导致了解析错误。在图1.14所示的例子中,C编译器(gcc)告诉你,它期望在第6行的return语句之前有一个分号(语句终结符)。

1.8.3 常见错误之3:无效的预处理器指令
如果输入了一条无效的预处理器指令,例如,把库的名称拼写错了,你将会接收到一条如图1.15所示的错误消息。


《C语言编程初学者指南》一1.8 调试C程序

图1.15 无效的预处理器指令导致错误

在如下的程序语句块中,预处理器指令中的库名称拼写错了,导致了如图1.15所示的错误消息。你能看出错误吗?

include <sdio.h>

int main()
{
    printf("Welcome to C Programming\n");
    return 0;
}

由于不存在库文件sdio.h,导致了这个错误。标准输入输出库的名称应该是stdio.h。

1.8.4 常见错误之4:无效的转义序列
在使用转义序列的时候,常常会使用无效的字符或者无效的字符序列。例如,图1.16展示了一个无效的转义序列所导致的错误。gcc 编译器更为具体地指出了这个错误,如图 1.16所示。特别是,它指出错误在第7行,并且指明这是一个未知的转义序列。


《C语言编程初学者指南》一1.8 调试C程序

图1.16 无效的转义序列导致的错误

你能够找出如下的程序中的无效的转义序列吗?

include <stdio.h>

int main()
{
    printf("Welcome to C Programming\m");
return 0;
}

用诸如n这样的一个有效的序列,来替代无效的转义序列m,就可以改正这个问题。

1.8.5 常见错误之5:无效的注释语句块
正如本章前面的1.3节所提到的,无效的注释语句块也会导致编译错误,如图1.17所示。

include <stdio.h>

int main()
{
    */ This demonstrates a common error with comment blocks /*
    printf("Welcome to C Programming\n");
    return 0;
}


《C语言编程初学者指南》一1.8 调试C程序

图1.17 无效的注释语句块导致错误

对于注释语句块进行一个简单的修改,如下所示,就可以解决这个问题,并能够让程序成功地编译。

/* This corrects the previous comment block error
上一篇:.NET数据库编程求索之路--5.使用ADO.NET实现(三层架构篇-使用List传递数据)(3)


下一篇:《IPv6精髓(第2版)》——3.11 必需的地址