精华内容
下载资源
问答
  • 循环运行 在设定条件后,代码反复运行某一个或多个部分,直到达到某些条件后终止 条件运行 代码会先推断某些条件,假设满足则运行部分代码,假设不满足则运行还有一部分代码 实际其中,这三种运行顺序是交错出现...

    软件开发中。代码有三种基本运行顺序:

    顺序运行 代码从入口開始一条一条运行。直到返回或者结束

    循环运行 在设定条件后,代码反复运行某一个或多个部分,直到达到某些条件后终止

    条件运行 代码会先推断某些条件,假设满足则运行部分代码,假设不满足则运行还有一部分代码

    实际其中,这三种运行顺序是交错出现的。你中有我,我中有你。

    以下是上节课的代码,我们来一句一句的分析看。你就会明确我上面说的是什么了:

    #include <stdio.h>
    #include "public.h"
    
    int main(int argc, char* argv[], char* env[])
    {
    	Fibonacci(10);
    	return 0;
    }
    
    void Fibonacci(unsigned int nTotal)
    {
    	int nFirst = 1;
    	int nSecond = 1,nThird;
    	unsigned int i;
    	if (nTotal == 0){
    		return;//假设用户要求输出的数量为0。则什么都不输出
    	}
    	printf("%d\n", nFirst);
    	if (nTotal == 1)
    	{//假设用户要求输出的数量为1。则到此为止
    		return;
    	}
    	//先把头两个数输出出来
    	printf("%d\n",nSecond);
    	if (nTotal <= 2)//假设用户指定输出数量不超过2个,则到此为止
    		return;
    	for (i = 2; i < nTotal;i++)
    	{
    		nThird = nFirst + nSecond;
    		printf("%d\n", nThird);
    		nFirst = nSecond;
    		nSecond = nThird;
    	}
    }
    首先是main函数。整个代码的4-8行

    这是一段典型的顺序运行代码。从函数入口開始。一步一步运行。

    顺序运行的代码逻辑比較清晰,没有什么歧义。

    然后是比較复杂的Fibonacci函数(10-34行)

    12-14行是明显的顺序运行。

    主要是声明并初始化变量。

    15-17行就是一个典型的条件运行顺序。

    仅仅有当nTotal为0的时候才会运行大括号中面的代码。否则直接跳过

    注意=号表示赋值。而==表示逻辑等推断。

    然而C/C++中一个比較坑爹的设定是。即使是赋值表达式。也是有返回值的。

    比方:nFirst=1这个表达式

    假设你这么推断

    if(nFirst=1){

        printf("done!\n");

    }

    那么非常好,那条输出函数printf("done!\n");将总是被运行。

    由于赋值表达式的值,总是和它被赋的值相等

    也就是nFirst=0这个表达的值永远等于0

    所以当你是要进行nFirst是否等于1的条件推断时,一定要用==而不是=

    另外 表达式的含义是没有; 有;的是语句

    if这个逻辑推断keyword后面的小括号,仅仅能写入表达式,不能写入语句

    可是请注意27-33行这个循环模块

    for后面的括号中面是有两个语句和一个表达式。

    和你看到的不一样(i = 2; i < nTotal;i++)。这三条中。仅仅有第一条i = 2和最后一条i++是语句,中间那条是表达式

    这个语法特征是forkeyword独有的,别无分号。

    当然。你能够把代码写成这样:

            for (i = 2;
    		i < nTotal;
    		i++)
    	{
    		nThird = nFirst + nSecond;
    		printf("%d\n", nThird);
    		nFirst = nSecond;
    		nSecond = nThird;
    	}
    我们实际调试一下,看看代码会怎样运行

    详细操作例如以下:

    选中上面代码的第一行。然后选择 调试-新建断点-在函数处中断 例如以下图:

    当然。也能够在代码行号前面单机鼠标左键

    或者选中行之后。按下Ctrl+B按键来建立断点

    断点建立好之后,按下F5来启动调试。注意是F5不是Ctrl+F5

    一切顺利的话。你就会看到例如以下内容:

    黄色箭头表示如今程序将要运行的代码行

    这个时候你能够按下F10来一条一条运行代码

    然后你能够观察for语句中的代码是怎样运行的

    假设你想释放调试。仅仅须要在设置断点的行。点击红色的点清除断点,然后按下F5键就可以

    只是如今你能够先单步看看,代码在for模块中是怎样运行的

    没有意外的话,你会发现i=2这条。仅仅运行了一次;而i++则从第二次循环才開始运行;i<nTotal则每次都运行

    在调试过程中,假设你的鼠标停止在某个变量上面,则会显示变量当前的值

    通过这个方式你能够观察i是怎样变化的

    好了,代码的三种运行顺序已经讲完了

    各位能够好好练习一下调试代码,以后这个技能会伴随你的整个开发生涯

    展开全文
  • C语言的变量类型与存储(常量、自动变量、静态局部变量、全部变量) 单片机中FLASH、SRAM 单片机程序编译时的Code、RO-data、RW-data、ZI-data大小 等结合的到一起考虑。基本知识摘自书籍、网络,主要提供交叉...

    本文将

    进程的地址空间(数据段、代码段、堆、栈等)

    C语言的变量类型与存储(常量、自动变量、静态局部变量、全部变量)

    单片机中FLASH、SRAM

    单片机程序编译时的Code、RO-data、RW-data、ZI-data大小

    等结合的到一起考虑。基本知识摘自书籍、网络,主要提供交叉部分的提示和反复修改示例代码的演示。

     

     

    在操作系统中运行C程序还涉及到命令行参数和环境变量,单片机上主要如下:

    1、栈区(stack),栈是由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。栈的申请是由系统自动分配,如在函数内部申请一个局部变量 int h,同时判别所申请空间是否小于栈的剩余空间,如若小于的话,在堆栈中为其开辟空间,为程序提供内存,否则将报异常提示栈溢出(单片机上没提示,数据乱了程序会有莫名其妙的错误并进入Hard Fault)。    

    2、堆(heap)区,堆一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。堆的申请是由程序员自己来操作的,在C中使用malloc函数,而C++中使用new运算符,但是堆的申请过程比较复杂:当系统收到程序的申请时,会遍历记录空闲内存地址的链表,以求寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,此处应该注意的是有些情况下,新申请的内存块的首地址记录本次分配的内存块大小,这样在delete尤其是 delete[]时就能正确的释放内存空间。

    3、数据区(静态区) (static),全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。另外文字常量区,常量字符串就是放在这里,程序结束后有系统释放。

    4、程序代码区,放着函数体的二进制代码。

    写入ROM的是数据和机器指令,机器指令由操作码+操作数构成(有的没有操作数)。此时不再有任何C语言关键字、运算符、变量,汇编语言指令助记符、伪指令,编译器指令等。编译器先完成翻译工作,其中包括优化(C代码与汇编代码并不容易对应上),一个.c文件转换成一个.o文件,链接的过程进行地址分配,.o文件中的地址还是相对地址,链接后将是实际地址,操作数主要是立即数、寄存器、内存地址(采用精简指令集的寻址方式少),C语言中变量都是对应内存地址。

    在PC中各区的地址是虚拟内存的地址,利用程序的局部性原理可以运行比内存空间大的程序,采用段页式存储,完整程序是储存在硬盘(外设)上的,根据需要复制其中一部分到内存运行,运行中可能需要进行段页切换。

    单片机中使用实际物理地址(对单片机主程序来说只有一个进程),写入ROM(FLASH)的包括全部数据和代码,单片机启动过程进行加载,将可读可写的数据复制到SRAM中。指令读取从FLASH,数据读写从SRAM。(这种指令和数据储存在不同位置的叫哈弗结构,但有些只是存储位置不同访问时使用同一总线,部分单片机访问总线也是分开的使访问数据和访问指令可以并行处理,提高运行效率)

    单片机的CPU内核不能任意直接写FLASH某个存储单元的内容,所以对CPU它是只读存储器ROM,SRAM可以任意读写存储单元是随机存储器RAM。一个型号的单片机通常提供不同的FLASH+SRAM组合。

    编译出来的Code、RO、RW及加载代码储存在FLASH,RW和ZI需要SRAM空间。代码量、常量、全局变量之和过多FLASH不足,全部变量、静态变量之和过多SRAM不足,自动变量过多、函数嵌套调用过多栈空间不足运行中才会发现。

    int main(void)

    {

    }

    int main(void)

    {

    int auto_a=999;

    auto_a=auto_a;//这里是为了消除编译器告警

    }

    自动变量是在栈空间的,且由于编译器的优化,增加的代码对代码段、数据段都没有影响。

    int global_a=0;

    int main(void)

    {

    int auto_a=999;

    auto_a=auto_a;//这里是为了消除编译器告警

    }

     

     

    汇编代码中为全部变量生成了标号,然而数据区大小还是没变。

     

    int global_a=0;

    int main(void)

    {

    int auto_a=999;

    auto_a=auto_a;//这里是为了消除编译器告警

    global_a=auto_a;

    }

     

     

    CODE、RO、RW均有增加,汇编中增加了代码,CODE增加不难理解,RW增加也好理解,RO的增加应该来自常量区,但增加的值不好理解。

    int global_a=0;

    int main(void)

    {

    static int auto_a=999;

    auto_a=auto_a;//这里是为了消除编译器告警

    global_a=auto_a;

    }

    RW增加,ZI减少,ZI应该是算上了局部变量,静态变量是初始化的。

     

    static int global_a=0;

    int main(void)

    {

    static int auto_a=999;

    auto_a=auto_a;//这里是为了消除编译器告警

    global_a=auto_a;

    }

     

    全部变量加static后存储无变化,但不能被外部文件访问了,从汇编代码可以看出来。

     

    static int global_a=0;

    int global_b=0;

     

    int add(int a,int b)

    {

    int temp;

    temp=a+b;

    temp++;

    return temp;

    }

     

    int main(void)

    {

    //int auto_a=999;

    static int auto_a=999;

    auto_a=auto_a;//这里是为了消除编译器告警

    global_a=auto_a;

    global_b++;

    //global_a=add(auto_a,global_b);

    }

    static int global_a=0;

    int global_b=0;

     

    int add(int a,int b)

    {

    int temp;

    temp=a+b;

    temp++;

    return temp;

    }

     

    int main(void)

    {

    //int auto_a=999;

    static int auto_a=999;

    auto_a=auto_a;//这里是为了消除编译器告警

    global_a=auto_a;

    global_b++;

    global_a=add(auto_a,global_b);

    }

    同一文件内函数调用被优化

     

    extern int add2(int a,int b);

     

    static int global_a=0;

    int global_b=0;

     

    int add(int a,int b)

    {

    int temp;

    temp=a+b;

    temp++;

    return temp;

    }

     

    int main(void)

    {

    //int auto_a=999;

    static int auto_a=999;

    auto_a=auto_a;//这里是为了消除编译器告警

    global_a=auto_a;

    global_b++;

    global_a=add(auto_a,global_b);

    global_b=add2(auto_a,global_a);

    while(1);

    }

    跳转到其它文件函数

     

     

    展开全文
  • C语言学习教程

    2019-02-22 11:02:55
    在实际教学过程中,有不少同学觉得C语言学习比较困难,就我的理解,主要有两点...C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言...

    在实际教学过程中,有不少同学觉得C语言学习比较困难,就我的理解,主要有两点原因。第一,C语言的语法知识太多、太琐碎;第二,反复地做算法类的编程练习,不感兴趣,不愿写程序,进而觉得入门困难。

    想学习C语言,首先就要了解什么是C语言!

    C语言是一门通用计算机编程语言,应用广泛。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。

    尽管C语言提供了许多低级处理的功能,但仍然保持着良好跨平台的特性,以一个标准规格写出的C语言程序可在许多电脑平台上进行编译,甚至包含一些嵌入式处理器(单片机或称MCU)以及超级电脑等作业平台。

    二十世纪八十年代,为了避免各开发厂商用的C语言语法产生差异,由美国国家标准局为C语言订定了一套完整的国际标准语法,称为ANSI C,作为C语言最初的标准。

    C语言的基本特性

    1、高级语言:它是把高级语言的基本结构和语句与低级语言的实用性结合起来的工作单元。

    2、结构式语言:结构式语言的显著特点是代码及数据的分隔化,即程序的各个部分除了必要的信息交流外彼此独立。这种结构化方式可使程序层次清晰,便于使用、维护以及调试。C 语言是以函数形式提供给用户的,这些函数可方便的调用,并具有多种循环、条件语句控制程序流向,从而使程序完全结构化。

    4、代码级别的跨平台:由于标准的存在,使得几乎同样的C代码可用于多种操作系统,如Windows、DOS、UNIX等等;也适用于多种机型。C语言对编写需要进行硬件操作的场合,优于其它高级语言。

    5、使用指针:可以直接进行靠近硬件的操作,但是C的指针操作不做保护,也给它带来了很多不安全的因素。C++在这方面做了改进,在保留了指针操作的同时又增强了安全性,受到了一些用户的支持,但是,由于这些改进增加语言的复杂度,也为另一部分所诟病。Java则吸取了C++的教训,取消了指针操作,也取消了C++改进中一些备受争议的地方,在安全性和适合性方面均取得良好的效果,但其本身解释在虚拟机中运行,运行效率低于C++/C。一般而言,C,C++,java被视为同一系的语言,它们长期占据着程序使用榜的前三名。

    C语言免费教学课程推荐:零基础C语言学习教程

    (本课程主要讲解C语言的语法以及项目实战,以掌握C语言的语法和编程应用为目标,适合开发人群以及零基础小白)

    更多精品课程:

    阿里云云计算助理工程师认证(ACA)

    阿里云大数据助理工程师认证(ACA)

    阿里云云安全助理工程师认证(ACA)

    展开全文
  • C语言调试

    万次阅读 多人点赞 2016-05-10 07:33:40
    在编写代码的过程中,相信大家肯定遇到过这样的情况:代码能够编译通过,没有语法错误,但是运行结果却不对,反复检查了很多遍,依然不知道哪里出了问题。这个时候,就需要调试程序了。 所谓调试(Debug),就是...

    1.调试的概念以及调试器的选择

    在编写代码的过程中,相信大家肯定遇到过这样的情况:代码能够编译通过,没有语法错误,但是运行结果却不对,反复检查了很多遍,依然不知道哪里出了问题。这个时候,就需要调试程序了。

    所谓调试(Debug),就是让代码一步一步慢慢执行,跟踪程序的运行过程。比如,可以让程序停在某个地方,查看当前所有变量的值,或者内存中的数据;也可以让程序一次只执行一条或者几条语句,看看程序到底执行了哪些代码。

    在调试的过程中,我们可以监控程序的每一个细节,包括变量的值、函数的调用过程、内存中数据、线程的调度等,从而发现隐藏的错误或者低效的代码。

    编译器可以发现程序的语法错误,调试可以发现程序的逻辑错误。所谓逻辑错误,是指代码思路或者设计上的缺陷。

    对于初学者来说,学习调试也可以增加编程的功力,它能让我们更加了解自己自己的程序,比如变量是什么时候赋值的、内存是什么时候分配的,从而弥补学习的纰漏。

    调试是每个程序员必须掌握的技能,没有选择的余地!

    调试器(Debugger)

    调试需要借助专业的辅助软件——调试器(Debugger)。现在主流C/C++调试器有下面几种:

    1) Remote Debugger

    Remote Debugger 是 VC/VS 自带的调试器,与整个IDE无缝衔接,使用非常方便,初学者建议使用该调试器,本教程也以 VS2010 为例讲解调试技巧。

    2) WinDbg

    大名鼎鼎的 Windows 下的调试器,它的功能甚至超越了 Remote Debugger,它还有一个命令行版本(cdb.exe),但是这个命令行版本的调试器指令比较复杂,不建议初学者使用。

    3) LLDB

    XCode 自带的调试器,Mac OS X 下开发必备调试器。

    4) GDB

    Linux 下使用最多的一款调试器,也有 Windows 的移植版,如果你不使用 VC/VS,GDB 将是一个不错的选择。

    考虑到大部分读者使用Windows,本教程以 VS2010 为例讲解调试技巧,也即使用 Remote Debugger 调试器。当然你也可以使用 VS2012、VS2013、VS2015 等,它们之间的差别很小。


    2.设置断点,开始调试

    默认情况下,程序不会进入调试模式,代码会瞬间从开头执行到末尾。要想观察程序的内部细节,就得让程序在某个地方停下来,我们可以在这个地方设置断点。

    所谓断点(BreakPoint),可以理解为障碍物,人遇到障碍物不能行走,程序遇到断点就暂停执行。


    上图中,我们希望让程序在第4行代码处暂停执行,那么在第4行代码左侧的灰色部分单击鼠标即可插入断点。你也可以将光标定位到要暂停的代码行,然后按F9键插入断点。也可以在要暂停的位置单击鼠标右键,在弹出菜单中插入断点,如下图所示:

    插入断点后,点击上方的“运行”按钮,或者按F5键,即可进入调试模式,如下图所示:

    可以看到,程序虽然运行了,但并未输出任何数据,这是因为在输出数据之前就暂停了。同时,在IDE的上方出现了与调试相关的工具条,下方也多出了几个与调试相关的窗口:
    • 调用堆栈可以看到当前函数的调用关系。
    • 断点窗口可以看到当前设置的所有断点。
    • 即时窗口可以让我们临时运行一段代码,后续我们会重点讲解。
    • 输出窗口和我们之前看到的没有,用来显示程序的运行过程,给出错误信息和警告信息。
    • 自动窗口会显示当前代码行和上一代码行中所使用到的变量。
    • 局部变量窗口会显示当前函数中的所有局部变量。
    • 线程模块窗口暂时无需理会。

    如果你的VS缺少某个窗口,可以通过VS上方的“调试”菜单调出,如下图所示:
    注意:必须在调试状态下才能看到图中的菜单。
    如果你希望关闭某个窗口,可以在窗口标题处单击鼠标右键,在弹出菜单中隐藏,如下图所示:

    断点的真正含义

    严格来说,调试器遇到断点时会把程序暂时挂起,让程序进入一种特殊的状态——中断状态,这种状态下操作系统不会终止程序的执行,也不会清除与程序相关的元素,比如变量、函数等,它们在内存中的位置不会发生变化。

    关键是,处于中断状态下的程序允许用户查看和修改它的运行状态,比如查看和修改变量的值、查看和修改内存中的数据、查看函数调用关系等,这就是调试的奥秘。

    继续执行程序

    点击“运行”按钮或者按F5键即可跳过断点,让程序恢复正常状态,继续执行后面的代码,直到程序结束或者遇到下一个断点。

    在调试过程中,按照上面的方法可以设置多个断点,程序在执行过程中每次遇到断点都会暂停,如下图所示:

    删除断点

    如果不希望程序暂停,可以删除断点。删除断点也很简单,在原有断点处再次单击鼠标即可,也可以将光标定位到要删除断点的代码行,再次按F9键,或者在右键菜单中删除,如下图所示

    代替暂停语句

    在VS下,程序运行结束后不会自动暂停(一闪而退),要手动添加暂停语句system("pause");,如果大家觉得麻烦,也可以在代码最后插入断点,强制程序暂停。


    3.查看和修改变量的值

    设置了断点,就可以观察程序的运行情况了,其中很重要的一点就是查看相关变量的值,这足以发现大部分逻辑错误。

    将下面的代码复制到源文件中:

    1. #include <stdio.h>
    2. int main(){
    3. int value_int, array_int[3];
    4. float value_float;
    5. char* value_char_pointer;
    6. //在这里插入断点
    7. value_int = 1048576;
    8. value_float = 2.0;
    9. value_char_pointer = "Hello World";
    10. array_int[0] = 379; array_int[1] = 94;
    11. //在这里插入断点
    12. return 0;
    13. }
    在第7行和第12行插入断点。运行到第一个断点时,在局部变量窗口可以看到各个变量的值:

    可以看到,未经初始化的局部变量和数组的值都是垃圾值,是随机的,没有意义。双击变量的值,可以进行修改。

    点击“运行”按钮或按F5键,程序会运行到下一个断点位置,在局部变量窗口可以看到各个值的变化:

    更加快捷的方式

    除了在窗口中查看变量,还有一种更加便捷的方法:在调试模式下,把鼠标移动到要查看的变量的上方,即可看他它的值。如下图所示:

    如果是数组、指针、结构体等还可以展开,如下图所示:
    这种查看变量的方式在实际开发中使用很多。

    添加监视

    如果你希望长时间观测某个变量,还可以将该变量添加到监视窗口。在要监视的变量处单击鼠标右键,弹出如下菜单:

    选择“添加监视”,在VS下方的监视窗口就可以看到当前变量:

    这样,每次变量的值被改变都会反映到该窗口中,无需再将鼠标移动到变量上方查看其值。尤其是当程序稍大时,往往需要同时观测多个变量的值,添加监视的方式就会显得非常方便。


    4.单步调试(逐语句调试和逐过程调试)

    在实际开发中,常常会出现这样的情况,我们可以大致把出现问题的代码锁定在一定范围内,但无法确定到底是哪条语句出现了问题,该怎么办呢?按照前面的思路,必须要在所有代码行前面设置断点,让代码一个断点一个断点地执行。

    这种方案确实可行,但很麻烦,也不专业,这节我们就来介绍一种更加便捷的调试技巧——单步调试。所谓单步调试,就是让代码一步一步地执行。

    下面的代码用来求一个等差数列的和,我们以该代码来演示单步调试:

    1. #include <stdio.h>
    2. int main(){
    3. int start, space, length, i, thisNum;
    4. long total = 0;
    5. printf("请输入等差数列的首项值:");
    6. scanf("%d", &start);
    7. printf("请输入等差数列的公差:");
    8. scanf("%d", &space);
    9. printf("请输入等差数列的项数:");
    10. scanf("%d", &length);
    11. for(i=0; i<length; i++){
    12. thisNum = start + space * i;
    13. if( length-i > 1 ){
    14. printf("%d + ", thisNum);
    15. }else{
    16. printf("%d", thisNum);
    17. }
    18. total += thisNum;
    19. }
    20. printf(" = %ld\n", total);
    21. return 0;
    22. }
    在第6行设置一个断点,然后点击“逐过程调试”按钮,或者按F10键,程序就会运行到下一行并暂停:

    再次点击“逐过程”按钮或按F11键,就会执行第7行代码,要求用户输入数据。用户输入结束后,黄色箭头就会指向第8行,并暂停程序。如此重复执行上面的操作,就可以让程序一条语句一条语句地执行,以观察程序的内部细节,这就称为单步调试

    逐过程调试(F10)和逐语句调试(F11)

    刚才我们在第6行设置了断点,按下“逐过程”按钮或F10键,程序就会执行 printf(),并暂停在下一行代码处。

    printf() 是一个函数,它本身由多条语句构成,如果你希望进入 printf() 函数内部,查看整个函数的执行过程,可以点击“逐语句”按钮,或者按F11键,这样程序就会进入 printf() 所在的源文件,如下图所示:

    当需要跳出函数时,可以点击“跳出”按钮,或者按Shift+F11键,就会返回刚才的代码。

    逐过程(F10)逐语句(F11)都可以用来进行单步调试,但是它们有所区别:
    • 逐过程(F10)在遇到函数时,会把函数从整体上看做一条语句,不会进入函数内部;
    • 逐语句(F11)在遇到函数时,认为函数由多条语句构成,会进入函数内部。

    逐语句(F10)不仅可以进入库函数的内部,还可以进入自定义函数内部。在实际的调试过程中,两者结合可以发挥更大的威力。

    断点 + 查看/修改变量 + 逐过程调试 + 逐语句调试,这足以解决绝大多数逻辑问题,到此,初学者就已经学到了调试的基本技能。

    修改代码运行位置

    在VS中,调试器还允许我们直接跳过一段代码,不去执行它们。将下面的代码复制到源文件:
    1. #include <stdio.h>
    2. int main(){
    3. printf("111\n");
    4. printf("222\n");
    5. printf("333\n");
    6. printf("444\n");
    7. printf("555\n");
    8. printf("666\n");
    9. return 0;
    10. }
    在第3行设置断点,开始单步调试。假设我们不希望执行4~6行的代码,那么当程序执行到第4行时,可以将鼠标移动到黄色箭头处,直接向下拖动到第7行,如下图所示:
    程序执行完成后,在控制台上会输出:
    111
    555
    666

    注意:随意修改程序运行位置是非常危险的行为,假设我们定义了一个指针,在第N行代码中让它指向了一个数组,如果我们在修改程序运行位置的时候跳过了第N行代码,并且后面也使用到了该指针,那么就极有可能导致程序崩溃。


    5.即时窗口的使用

    “即时窗口”是VS提供的一项非常强大的功能,在调试模式下,我们可以在即时窗口中输入C语言代码并立即运行,如下图所示:


    在即时窗口中可以使用代码中的变量,可以输出变量或表达式的值(无需使用printf()函数),也可以修改变量的值。

    即时窗口本质上是一个命令解释器,它负责解释我们输入的代码,再由VS中的对应模块执行,最后将输出结果呈现到即时窗口。

    需要注意的是,在即时窗口中不能定义新的变量,因为程序运行时 Windows 已经为它分配好了只够刚好使用的内存,定义变量是需要额外分配内存的,所以调试器不允许在程序运行的过程中定义变量,因为这可能会导致不可预知的后果。

    调用函数

    在即时窗口中除了可以使用代码中的变量,也可以调用代码中的函数。将下面的代码复制到源文件中:
    1. int plus(int x, int y){
    2. return x + y;
    3. }
    4. int main(){
    5. return 0;
    6. }
    在第6行设置断点,并在即时窗口中输入plus(5, 6),如下图所示:


    6.查看、修改运行时的内存
    在 Visual Studio 的调试过程中,有时候我们可能需要对程序的运行内存进行查看,修改该怎么办?Visual Studio 也为我们提供了很好的解决方案。那就是使用 Visual Studio 自带的内存查看窗口。

    首先我们通过内存窗口查看变量的值,我们启动 Visual Studio,创建一个工程,输入如下代码:
    1. #include <stdio.h>
    2. int main()
    3. {
    4. int testNumber = 5678;
    5. printf("testNumber 的内存地址为 0x00%x \n", &testNumber);
    6. //输出内存地址
    7. //TODO:在这里插入断点
    8. return 0;
    9. }
    我们在第七行设置好断点,然后按 F5 启动调试,等待触发断点。触发断点后,我们发现,IDE中并没有显示内存窗口(默认设置下),这时,我们点击菜单 -> 调试(D) -> 窗口 (W) -> 内存 (M) -> 内存1(1),就可以调出内存窗口了,如图:

    我们看到,内存窗口里面显示着一大堆乱七八糟的数据,这里面的这些就是我们内存中的数据啦,我们可以通过变量 testNumber 的内存地址跳转到器对应的内存空间,我们看到 testNumber 的地址为 0x0018f830 (在不同的进程是不一样的),我们把这个地址输入到我们的内存窗口的地址栏。如图:

    我们看到,尽管我们已经输入了正确地地址,但是我们还是没有看到正确的数据显示,其实原因非常简单,我们来回顾一下 C 语言的一些入门知识:我们知道,在我们的源代码中,我们的 testNumber 变量被定义为 int 整形,我们再想想 int 整形在内存中占几个字节?没错,是4个字节。所以我们应该以四字节的形式格式化这些内存数据,这是我们在内存窗口中单击我们的鼠标右键,在弹出的菜单中选择“4字节整数(4)”,然后就能正确地显示相关的数据了,如图:

    没错,查看内存就是这么的简单。接下来我们就来查看与修改浮点数的内存数据,我们看下面这段代码:
    1. #include <stdio.h>
    2. int main()
    3. {
    4. double pi = 3.141592653589;
    5. printf("pi 的内存地址为 %x \n", &pi);
    6. //输出内存地址
    7. //TODO:在这里插入断点
    8. return 0;
    9. }
    同样的,我们在第7行设置断点,按F5启动调试,等待断点被触发:

    这时我们看到的内存地址是这样的,与我们在内存窗口看到的不同,我们需要将其补齐,在我们现阶段编写的小程序中,显示的内存地址基本上都是六位的,我们在前面加上 “0x00”,将其补到八位(内存窗口上的地址栏里有几位就补到几位)。然后我们将其输入到内存窗口上的地址栏。

    我们发现,现在显示的数据依然是错误的,因为查看器现在还是在使用我们之前设置的 4位整形格式 格式化我们的内存数据呢,我们知道,我们的 double 属于64位浮点数,所以我们在查看窗口点击鼠标右键,在弹出的菜单中选择“64位浮点(6)”,然后我们就能看到它正确地输出数据了。

    我们注意到,在我们设置的变量pi的值的基础上,内存窗口里显示的数据多了几个0,这些0所代表的就是 double 型数据的最高精度。接下来我们尝试在内存窗口修改变量 pi 的值,为其补齐精度。现在我们用鼠标右键点击我们的pi的内存数据,在弹出的菜单中选择编辑值(E),在显示的输入框中我们输入 3.1415926535897931,按回车即可保存。我们看看效果:

    怎么样,内存的查看与修改是不是很简单呢?其实我们只要记住下面的几个对应关系,常用的数值数据类型的内存查看与修改都不在话下:
    类型名 变量类型 内存查看窗口中应选择的数据格式
    short 16 位整形 2 字节整数
    int 32 位整形 4 字节整数
    long 32 位整形 4 字节整数
    long long 64 位整形 8 字节整数
    float 32 位(4字节)单精度浮点数 32 位浮点
    double 64 位(8字节)双精度浮点数 64 位浮点
     
    内存新手常用的内存窗口操作就到这里了,其实关于内存操作还有很多高级用法,不过那不在我们初级调试教程的范围之内。我们在修改内存的时候要注意安全,防止随意修改导致的程序崩溃,甚至是无法结束进程!

    7.有条件断点的设置

    在此之前,我们已经了解了无条件断点、跟踪点这两种断点,事实上在 Visual Studio 中还有几种常用的断点,在本节我们将一一为大家介绍。

    大家有没有碰到这样的情况,在一个循环体中设置断点,假设有一千次循环,我们想在第五百次循环中设置断点,这该怎么办?反正设置断点不断按 F5 继续运行四百九十九次是不可能的。那该怎么办呢?其实我们的断点是可以设置各种各样的条件的,对于上述情况,我们可以对断点的命中次数做一个限制。

    我们首先在 Visual Studio 中创建一个工程,并且输入如下代码:

    1. #include <stdio.h>
    2. int main(){
    3. for ( int i=1 ; i <= 1000 ; i++ ) {
    4. //TODO:插入计次断点
    5. printf("我真行!\n");
    6. }
    7. }
    首先,我们在第4行插入断点,分析代码,我们不难得出它会输出 1000 行“我真行!”,那么我们思考一下,在不修改代码的情况下,如何才能让他输出 1499 行“我真行!”呢,其实很简单,我们只要在i 等于500的时候暂停程序,再将变量 i 的值修改为 1 即可,思路很简单,接下来我们就来实现这个命中条件的限制吧。

    首先我们用鼠标右键单击第4行的断点图标,在弹出的菜单中选择 命中次数(H) ,接下来会弹出如下图的一个对话框,我们在中间的选择框中选择 “中断,条件是命中次数等于”,我们在右边的编辑框输入 500。

    我们点击确定,断点就设置到位了,接下来我们按 F5 运行调试。

    我们看到,在输出四百九十九行“我真行!”后,程序进入了中断状态,这是我们注意到自动窗口中的变量 i 的值为 500,接下来我们把这个 i 的值改为 1,点击 继续(C) 继续程序的运行,这样程序就再输出了一千行“我真行!”,然后退出。没错,命中次数限制的使用就是这么简单。

    我们再次用鼠标右键单击第4行的断点图标,在弹出的菜单中选择 命中次数(H) ,大家如果有兴趣的话,可以试试中间的选择框中其他的条件选项,使用方法基本一致,这里不再赘述。

    接下来我们来了解一下断点触发条件的使用,在 Visual Studio 的调试器中,我们可以对断点设置断点触发条件,这个条件可以引用我们程序中的变量,比如我们程序中有两个变量 a、b ,我们的命中条件可以是 a == b 、 a >= b 、 a != b 甚至是 (a - b)*(a*2 - b) > 0 这样的复杂条件。

    下面我们就来试试在 Visual Studio 中插入条件断点吧。我们首先创建一个工程,输入如下代码:
    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <time.h> //time函数所在头文件
    4. int main()
    5. {
    6. int a, b;
    7. int randNumber;
    8. srand((unsigned)time(NULL));
    9. //设置随机数种子,以产生不同随机数
    10. for (int i = 0; i<50; i++)
    11. {
    12. a = rand() % 7; //产生0-6的随机数
    13. b = rand() % 7; //产生0-6的随机数
    14. //TODO:在这里插入条件断点: a == b
    15. }
    16. return 0;
    17. }
    我们让程序运行过程中 a 等于 b 的时候触发断点,首先,我们在第十四行插入断点,然后我们鼠标右键单击左侧的断点图标,在弹出的菜单中选择条件(C),IDE会弹出如下对话框,我们在条件输入框中输入 a==b ,然后在下面选择 为 true ,然后点击确定即可。


    8.assert断言函数

    在我们的实际开发过程之中,常常会出现一些隐藏得很深的BUG,或者是一些概率性发生的BUG,通常这些BUG在我们调试的过程中不会出现很明显的问题,但是如果我们将其发布,在用户的各种运行环境下,这些程序可能就会露出马脚了。那么,如何让我们的程序更明显的暴露出问题呢?这种情况下,我们一般都会使用 assert 断言函数,这是C语言标准库提供的一个函数,也就是说,它的使用与操作系统平台,调试器种类无关。我们只要学会了它的使用,便可一次使用,处处开花。

    接下来我们来了解一下 assert 函数的用法,这个函数在 assert.h 头文件中被定义,在微软的 cl 编译器中它的原型是这样的:

    #define assert(_Expression) (void)( (!!(_Expression)) || (_wassert(_CRT_WIDE(#_Expression), _CRT_WIDE(__FILE__), __LINE__), 0) )

    我们看到 assert 在 cl 编译器中被包装成了一个宏,实际调用的函数是 _wassert ,不过在一些编译器中,assert 就是一个函数。为了避免这些编译器差异带来的麻烦,我们就统一将 assert 当成一个函数使用。

    我们来了解一下 assert 函数的用法和运行机制,assert 函数的用法很简单,我们只要传入一个表达式即可,它会计算我们传入的表达式的结果,如果为真,则不会有任何操作,但是如果我们传入的表达式的计算结果为假,它就会像  stderr (标准错误输出)打印一条错误信息,然后调用 abort 函数直接终止程序的运行。

    现在我们在 Visual Studio 中创建一个工程,输入下面这段非常简单的代码:
    1. #include <stdio.h>
    2. #include <assert.h>
    3. int main()
    4. {
    5. printf("assert 函数测试:");
    6. assert(true); //表达式为真
    7. assert(1 >= 2); //表达式为假
    8. return 0;
    9. }
    我们按 F5 启动调试:

    我们看到,我们的输出窗口打印出了断言失败的信息,并且 Visual Studio 弹出了一个对话框询问我们是否继续执行。但是如果我们不绑定调试器,构建发布版程序,按 Ctrl + F5 直接运行呢?是的,这个 assert 语句就无效了,原因其实很简单,我们看看 assert.h 头文件中的这部分代码:
    1. #ifdef NDEBUG
    2. #define assert(_Expression) ((void)0)
    3. #else /* NDEBUG */
    我们看到,只要我们定义了 NDEBUG 宏,assert 就会失效,而 Visual Studio 的默认的发布版程序编译参数中定义了 NDEBUG 宏,所以我们不用额外定义,但是在其他编译器中,我们在发布程序的时候就必须在包含 assert.h 头文件前定义 NDEBUG 宏,避免 assert 生效,否则总是让用户看到“程序已经停止运行,正在寻找解决方案 . . .”的 Windows 系统对话框可就不妙了。

    下面我们来了解一下 assert 的常用情境以及一些注意事项。举个C语言文件操作的例子:
    1. #include <stdio.h>
    2. #include <assert.h>
    3. #include <stdlib.h>
    4. int main(void)
    5. {
    6. FILE *fpoint;
    7. fpoint = fopen("存在的文件.txt", "w");
    8. //以可读写的方式打开一个文件
    9. //如果文件不存在就自动创建一个同名文件
    10. assert(fpoint);
    11. //(第一次断言)所以这里断言成功
    12. fclose(fpoint);
    13. fpoint = fopen("不存在的文件.txt", "r");
    14. //以只读的方式打开一个文件,如果不存在就打开文件失败
    15. assert(fpoint);
    16. //(第二次断言)所以断言失败
    17. printf("文件打开成功");
    18. //程序永远都执行不到这里来
    19. fclose(fpoint);
    20. return 0;
    21. }
    阅读代码,我们可以知道,如果我们错误的设置了读写权限,或者没有考虑 Windows 7(或更高版本) 的 UAC(用户账户权限控制) 问题的话,我们的程序出现的问题就很容易在我们的测试中暴露出来。事实上,上面的代码也不是一定能通过第一次断言的,我们把构建生成的(Debug 模式的)可执行文件复制到我们的系统盘的 Program Files 文件夹再执行,那么 Win7 及以上的操作系统的UAC都会导致程序无法通过第一次断言的。所以在这种情况下我们就需要在必要的情况申请管理员权限避免程序BUG的发生。

    在我们的实际使用过程中,我们需要注意一些使用 assert 的问题。首先我们看看下面的这个断言语句:
    //...
    assert( c1 /*条件1*/ && c2 /*条件2*/ );
    //...
    我们思考一下:如果我们的程序在这里断言失败了,我们如何知道是 c1 断言失败还是 c2 断言失败呢,答案是:没有办法。在这里我们应该遵循使用 assert 函数的第一个原则:每次断言只能检验一个条件,所以上面的代码应该改成这样:
    //...
    assert(c1 /*条件1*/);
    assert(c2 /*条件2*/);
    //...
    这样,一旦出现问题,我们就可以通过行号知道是哪个条件断言失败了。

    接下来我们来思考一个问题:我们可以使用 assert 语句代替我们的条件过滤语句么?我们来举个例子,我们写了一个视频格式转换器,我们本应输出标准MP4格式的视频,但是由于编码解码库的问题,我们在转换过程中出现了错误,像这种情况下,我们能否用 assert 代替掉我们的 if 语句呢(假设这里忽略了 Visual Studio 发布版程序编译过程中自动定义的 NDEBUG 宏)?很明显,这样做是不可以的。这是为什么呢?

    现在我们来区分一下程序中的错误与异常,所谓错误,是代码编写途中的缺陷导致的,是程序运行中可以用 if 语句检测出来的。而异常在我们的 C 语言中,一般也可以使用 if 语句判断,并进行对应的处理。而 assert 是用来判断我们程序中的代码级别的错误的。像用户输入错误,调用其他函数库错误,这些问题我们都是可以用 if 语句检测处理的。另一方面,使用 if 语句的程序对用户更友好。

    下面我们通过代码展示使用 assert 的另外一个注意意事项,我们新建一个工程,输入如下代码:
    1. #include <stdio.h>
    2. #include <assert.h>
    3. int main(void)
    4. {
    5. int i = 0;
    6. for ( ; ; )
    7. {
    8. assert(i++ <= 100);
    9. printf("我是第%d行\n",i);
    10. }
    11. return 0;
    12. }
    我们按 F5 运行调试器,我们会看到这样的情景:

    这是正常的,我们按 Shift + F5 终止调试,接下来,我们切换一下编译模式到发布模式:

    接下来我们按 Ctrl+F5 不绑定调试器直接运行:

    我们看到了一个完全不相同的运行结果,这是为什么呢?其实原因很简单,我们注意这段代码:

    assert(i++ <= 100);

    我们的条件表达式为 i++ <= 100,这个表达式会更改我们的运行环境(变量i的值),在发布版程序中,所有的 assert 语句都会失效,那么这条语句也就被忽略了,但是我们可以把它改为 i++ ; assert(i <= 100); ,这样程序就能正常运行了。所以请记住:不要使用会改变环境的语句作为断言函数的参数,这可能导致实际运行中出现问题。

    最后,我们再来探讨一下,什么时候应该用 assert 语句?一个健壮的程序,都会有30%~50%的错误处理代码,几乎用不上 assert 断言函数,我们应该将 assert 用到那些极少发生的问题下,比如Object* pObject = new Object,返回空指针,这一般都是指针内存分配出错导致的,不是我们可以控制的。这时即使你使用了容错语句,后面的代码也不一定能够正常运行,所以我们也就只能停止运行报错了。


    9.调试信息输出

    上一节,我们讲解了 assert 断言函数的使用,本节我们来学习在调试器的调试窗口上输出我们自己的调试信息,在这里,我们将用到一个 Windows 操作系统提供的函数 —— OutputDebugString,这个函数非常常用,他可以向调试输出窗口输出信息(无需设置断点,执行就会输出调试信息),并且一般只在绑定了调试器的情况下才会生效,否则会被 Windows 直接忽略。接下来我们了解一下这个函数的使用方法。

    首先,这个函数在 windows.h 中被定义,所以我们需要包含 windows.h 这个头文件才能使用 OutputDebugString 函数。这个函数的使用方式非常的简单,它只有简单的一个参数——我们要输出的调试信息。但是有一点值得注意:准确来说 OutputDebugString 并不是一个函数,他是一个宏。在高版本的 Visual Studio 中,因为编译的时候 Visual Studio 默认定义了 UNICODE 宏,所以我们查找 OutputDebugString 的定义会看到如下代码:

    1. #ifdef UNICODE
    2. #define OutputDebugString OutputDebugStringW
    3. #else
    4. #define OutputDebugString OutputDebugStringA
    5. #endif // !UNICODE
    我们可以从代码高亮上看到,OutputDebugString 实际上等价于 OutputDebugStringW,这就意味着我们必须传入宽字符串(事实上只要定义了 UNICODE ,调用所有 Windows 提供的函数都需要使用宽字符),或者使用 TEXT 或 _T 宏,并且这是最好的方法,这个宏会自动识别编译器是否处于默认宽字符的状态并对传入字符串做些处理,使用这个宏可以加强代码对不同编译器不同编译参数的兼容性。下面我们就来看一段示例代码:
    1. #include <windows.h> //使用 OutputDebugString 包含此文件
    2. #include <tchar.h> //使用 TEXT 宏需要包含此文件
    3. int main(){
    4. OutputDebugString(TEXT("你好,C语言中文网。"));
    5. OutputDebugString(_T("大家好才是真的好。"));
    6. //也可以:OutputDebugStringA("大家好才是真的好。");
    7. //也可以:OutputDebugStringW(L"大家好才是真的好。");
    8. //使用自动字符宏 TEXT 或者 _T 可以自动判断是否使用宽字符
    9. system("pause"); //使程序暂停一下
    10. return 0;
    11. }
    在程序执行 system("pause"); 暂停的时候我们来观察一下我们的,调试输出窗口:

    怎么样,是不是输出了“你好,C语言中文网。大家好才是真的好。”呢?这个函数与 printf 等函数一样,需要我们自己插入换行符,但在 Windows 下,我们一般使用 \r\n 作为完整的换行符。

    直接使用这个调试信息输出函数有个弊端,那就是它不能直接输出含参数的字符串。但是我们可以通过 sprintf / wsprintf 等缓冲区写入函数来间接实现输出含参数字符串的功能。下面是一段示例代码:
    1. #include <stdio.h>
    2. #include <windows.h>
    3. int main(){
    4. //注意!这段代码我们指定使用ANSI字符!
    5. char szBuffer[200];
    6. int number = 100;
    7. sprintf_s(szBuffer, "变量 number 的值是 %d \r\n", number); //写入缓冲区,注意不要溢出
    8. OutputDebugStringA(szBuffer);
    9. sprintf_s(szBuffer, "变量 number 的地址是 %x \r\n", &number);
    10. OutputDebugStringA(szBuffer);
    11. //我门指定使用 ANSI 版本的 OutputDebugString 函数
    12. return 0;
    13. }
    我们按 F5 开始调试:

    我们看到了输出的结果,怎么样,大家是不是觉得这样调用这个函数很麻烦?为解决此问题,这里C语言中文网为大家提供了一个更好的解决方案,我们可以自己写一个前端函数,然后保存到头文件中(编译生成 dll 也可以,有兴趣的同学可以试试)。为了方便,我们已经编写好了这么一套函数。代码如下:
    1. #include <stdio.h>
    2. #include <windows.h>
    3. #ifndef _DEBUG_INFO_HEADER_
    4. //防止头文件被重复载入出错
    5. #define _DEBUG_INFO_HEADER_
    6. #if (defined UNICODE)||(defined _UNICODE)
    7. #define DebugInfo DebugInfoW
    8. #else
    9. #define DebugInfo DebugInfoA
    10. #endif
    11. // 函数: DebugInfoA(char*, int, ...)
    12. //
    13. // 目的: 以窄字符的形式输出调试信息
    14. //
    15. // char* str - 格式化 ANSI 字符串
    16. // ... - 任意不定长参数
    17. //
    18. void DebugInfoA(char* str, ...){
    19. char szBuffer[500]; //注意不要让缓冲区溢出!
    20. va_list Argv;
    21. va_start(Argv, str);
    22. _vsnprintf_s(szBuffer, 500, str, Argv);
    23. va_end(Argv);
    24. OutputDebugStringA(szBuffer);
    25. }
    26. // 函数: DebugInfoW(char*, int, ...)
    27. //
    28. // 目的: 以宽字符的形式输出调试信息
    29. //
    30. // char* str - 格式化 UNICODE 字符串
    31. // ... - 任意不定长参数
    32. //
    33. void DebugInfoW(wchar_t* str, ...){
    34. wchar_t szBuffer[1000];
    35. va_list Argv;
    36. va_start(Argv, str);
    37. _vsnwprintf_s(szBuffer, 500, str, Argv);
    38. va_end(Argv);
    39. OutputDebugStringW(szBuffer);
    40. }
    41. #endif
    上面的这段代码会自动识别编译器是否默认使用了宽字符并且使用对应版本的输出函数,其中注释为 Visual Studio 的智能提示信息,我们把上面的代码保存到 debuginfo.h 并添加到当前工程中,就可以直接通过如下代码调用:
    1. #include <stdio.h>
    2. #include <tchar.h>
    3. #include <windows.h>
    4. #include "debuginfo.h"
    5. int main(){
    6. int num;
    7. //这里我们使用微软提供的 xxxx_s 安全函数
    8. printf_s("请输入数值:\n");
    9. scanf_s("%d", &num);
    10. DebugInfo(TEXT("用户输入的数是 %d\n"), num);
    11. return 0;
    12. }
    我们按 F5 开始调试,我们输入666,观察 IDE 的输出窗口:

    我们可以看到,它正确地输出了 “用户输入的数是 666” ,这就说明我们的代码执行成功了,上面的代码大家可以随意修改我们上面提供的代码以符合自己的实际需要。但是请注意不要让缓冲区溢出,否则会造成不可估计的后果。

    我们除了在调试器中可以看到调试字符串的输出,我们还可以借助 Sysinternals 软件公司研发的一个相当高级的工具 —— DebugView 调试信息捕捉工具,这个工具可以在随时随地捕捉 OutputDebugString 调试字符串的输出(包括发布模式构建的程序),可以说这是个神器,大家可以在微软 MSDN 库上搜索下载。接下来我们运行 DebugView 调试信息捕捉工具。

    首先我们打开 DebugView:

    我们可以看到随时有程序在输出调试信息,接下来我们运行(按Ctrl+F5 运行,不要启动调试器)刚才的程序,输入 222,看看 DebugView 有什么反应:

    这个调试信息查看工具显示了正确的调试信息。注意!如果挂载了 Visual Studio 的调试器,这个工具就会失效。

    使用这个工具可以捕获发布版程序的输出,我们一般可以用来跟踪测试产品的运行状态。以趁早发现我们程序中存在的问题,避免发布后出现的容易被检测到的BUG。


    10.VS调试的总结以及技巧

    我们已经对 Visual Studio 的调试有了一些了解,在这一节,我们将向大家展示我们在调试过程中可以使用的一些实用技巧。使大家的调试操作更加轻松愉快。

    首先我们再了解一下Visual Studio 中,Release构建模式和Debug 构建模式的区别。我们在可以切换构建模式。Release构建模式下构建的程序为发行版,而Debug构建模式下构建的程序为调试版。在 Visual Studio 中调试模式还会定义两个宏 _DEBUG 和 DEBUG,后文我们将介绍它们的一些妙用。在 Visual Studio 中,如果我们要更改编译参数的话,可以点击菜单 -> 项目(P) -> <项目名>属性(P),我们在弹出的页面左侧选择配置属性即可对编译参数进行修改。

    接下来,我们来了解一下调试标记。不知道大家有没有遇到这样的情况,我们需要在调试的时候额外运行一段代码,但是实际发布的时候却不需要这段代码呢。那该怎么办,绝大多数数的初学者会选择使用注释,即在发布的时候将无用的测试代码注释掉。但是这样很麻烦,下面我们就为大家介绍一种全新的方法——使用调试标记。事实上这种方法我们在前面使用过,但是没有详细讲解。

    这种方法借助了预处理指令,方法很简单,我们首先定义一个宏作为处于调试状态的标记,后面的代码我们用 #ifdef 和 #endif 预处理指令检测宏是否被定义,然后由编译器判断是否编译其中的代码。这么做的好处就是可以减少发布程序的体积,另一方面可以提高发布程序的运行效率。下面是一段示范代码:

    1. #include <stdio.h>
    2. #define _DEBUGNOW
    3. int main(){
    4. #ifdef _DEBUGNOW
    5. printf("正在为调试做准备...");
    6. #endif // _DEBUGNOW
    7. printf("程序正在运行...");
    8. return 0;
    9. }
    当我们要发布上面的这个程序的时候,我们只要将 #define _DEBUGNOW 注释掉即可,无需进行任何额外的操作。怎么样?是不是很方便呢?善用调试标记可以大大地提高我们的调试效率,但是有一点记住,调试标记名不要过于简单,否则可能和程序中的变量/常量产生冲突。

    在我们的调试过程中,我们常常会需要在不触发断点的情况下输出一些数值,这时我们一般会这么做:
    printf("%d\n",Value/*要输出的数值*/);
    但是像这种代码写多了我们可能会对它产生一种厌恶之情,这是我们的预处理器又可以派上用场了,我们可以定义一个宏解决这个问题,定义宏的代码如下:
    #define _PUTINT(NUM) printf("%d\n",NUM)
    然后我们只要这样调用我们的宏:
    _PUTINT(45/*要输出的数值*/);
    怎么样?是不是很方便呢?那如果我们想让这个宏在我们发布程序的时候失效呢,我们该怎么做?其实很简单,我们依然可以使用预处理指令完成这项操作,下面我们来看一套完整的代码(同时使用调试标记和宏):
    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #define _PAUSE() system("pause");
    4. #if (defined DEBUG) || (defined _DEBUG) //检测构建模式是否为调试模式
    5. //如果构建模式为调试模式,这里定义几个宏
    6. #define _DEBUGNOW
    7. #define _PUTSIL(NUM) printf("%d\n",NUM) //输出整数
    8. #define _PUTFD(NUM) printf("%f\n",NUM) //输出浮点数
    9. #else
    10. //如果构建模式为发布模式,自动忽略这些宏的存在
    11. #define _PUTSIL(NUM) ((void)0)
    12. #define _PUTFD(NUM) ((void)0)
    13. #endif
    14. int main(){
    15. #ifdef _DEBUGNOW
    16. printf("正在为调试做准备...\n");
    17. #endif // _DEBUGNOW
    18. printf("程序正在运行...\n");
    19. _PUTSIL(12666);
    20. _PUTFD(3.1415926535898);
    21. printf("程序运行完毕...\n");
    22. _PAUSE(); // 暂停程序
    23. return 0;
    24. }
    我们看到,宏的定义和函数差不多,有的简单的函数可以直接用宏实现,这样做带来的好处就是,我们可以不用频繁的判断是否处于调试状态,一次定义,一直有效。

    为了方便我们的调试(检查)操作以及日后的团队合作,我们在编写函数的时候应该为其加上 Visual Studio 的智能提示,方法比较简单,我们只要在函数定义前面加上提示注释即可(格式自由),Visual Studio 便会自动分析我们的代码并加入其智能提示列表,下面我们举一个定义函数设置智能提示的例子:
    1. //
    2. // 函数: Convert2Jpeg(wchar_t*, wchar_t, int)
    3. //
    4. // 目的: 转换图片到 Jpeg 格式
    5. //
    6. // orgiPath : 源文件路径
    7. // destPath : 目标路径
    8. // quality : 图像质量
    9. //
    10. bool Convert2Jpeg(wchar_t* orgiPath, wchar_t* destPath, int quality){
    11. return true;
    12. }
    我们在自动完成将或者鼠标移动到编辑器内函数名上,就有了智能提示。


    这种智能提示不但可以减轻我们编写代码时的负担,在调试情境下也可以增加我们检查代码的效率。如此一来,岂不美哉!

    接下来我们为大家总结了一些调试的经验以及调试思路:
    当我们运行我们编写的程序发现运行结果与我们预想的不同的时候,我们可以先用即时窗口,使用一些比较简单的数据来测试我们的各个函数运行结果是否符合我们的预期,如果都符合的话,我们可以使用程序中产生的一些比较复杂的数据来进一步测试我们的各个函数,直至找到可能导致错误的函数。

    找到可能导致错误的函数之后,我们就可以使用逐语句调试来一步步跟踪运行程序了,渐渐的的我们就可以缩小范围直至定位错误(无关代码可以考虑暂时注释掉),在这期间,我们要仔细观察程序运行过程中各个数据的变化情况,观察的仔细与否直接与我们能否找到错误直接挂钩。

    如果上一步运行的数据一直是正常的,我们就可以排除这个函数的嫌疑了(减少对他的调试次数)。此时,我们就应该考虑问题是否出现在之前的函数上了,可能因为偶然性,我们第一次测试函数的时候并没有发现其错误,导致范围锁定产生偏差,此时我们需要再次耐心的对所有未排除嫌疑的进行调试,直至再次找到出错的函数。再重复上一步,直至找到错误。

    可以看到,调试其实是一项比较复杂的活,需要大量的操作,所以在我们编写代码的时候要万分谨慎!因为很多时候,BUG都是因为我们的粗心大意导致的笔误引起的!

    展开全文
  • 在实际教学过程中,有不少同学觉得C语言学习比较困难,就我的理解,主要有两点...C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。...
  • 导读 这一章节作为C语言程序设计的基础内容,涉及的内容比较多,主要包含了以下内容: C语言程序的基本结构,这个在前面章节中已经反复强调,C语言程序将围绕这个框架设计。 C语言的编写的基本步骤和运行方式,通过...
  • 在实际教学过程中,有不少同学觉得C语言学习比较困难,就我的理解,主要有两点原因。...C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行...
  • C语言题库

    千次阅读 2014-04-14 10:59:54
    从编号为1的猴子开始顺时针报数,每数到m(m2:编写猜数游戏程序,该程序运行时产生一个随机整数(随机整数的范围为1--100)作为游戏者需要猜测的数字,游戏者从键盘反复输入数据进行猜测。如果未猜中,程序提示输入...
  • C语言允许函数的递归调用。在递归调用中,主调函数又是被调函数。执行递归函数将反复调用其自身,每调用一次就进入新的一层。例如有函数f如下: int f(int x) { int y; z=f(y); return z;}这个函数是一个递归函数。...
  • C语言程序设计

    2021-01-16 21:40:45
    (5)反复上机调试程序,直到改正所有编译和运行错误 (6)运行 注:编译器:把源代码转化为可被计算机理解的机器代码,把机器代码以可执行文件的形式保存在磁盘上,一种程序设计语言对应一种编辑器。 //写出成为...
  • 导读 这一章节作为C语言程序设计的基础内容,涉及的内容比较多,主要包含了以下内容: C语言程序的基本结构,这个在前面章节中已经反复强调,C语言程序将围绕这个框架设计。 C语言的编写的基本步骤和运行方式,通过...
  • c语言】递归算法解析

    万次阅读 多人点赞 2018-07-25 10:54:56
    c语言通过运行时堆栈来支持递归的实现的。递归函数就是直接或者间接调用自身的函数。 这里有一个简单的程序,可用来说明递归。程序的目的是将一个整数从二进制形式转化为可打印的字符形式,例如给出一个值4267,...
  • 介绍了编程是一个实践性很强的工作,最好的学习方法就是边看书边调试代码,把书上的例子程序反复的修改调试运行,从中得到自己的体会。在自己的学习或工作中写的一些测试的程序,要分门别类的保存下来,在以后的实际...
  • C语言内存类型

    2016-07-21 22:45:20
    运行时自动分配&自动回收:栈是自动管理的,程序员不需要手工干预。方便简单。 反复使用:栈内存在程序中其实就是那一块空间,程序反复使用这一块空间。 脏内存:栈内存由于反复使用,每次使用后程序不会去清理,...
  • C语言复习笔记 7

    2017-04-10 23:46:53
    C语言调试调试概念的引入在编写代码的过程中,相信大家肯定遇到过这样的情况:代码能够编译通过,没有语法错误,但是运行结果却不对,反复检查了很多遍,依然不知道哪里出了问题。这个时候,就需要调试程序了。所谓...
  • C语言-排序名单

    2020-09-06 15:14:05
    测评机会反复运行你的程序。每次程序运行时,你的程序会被输入 1010 行不含有空格的字符串,分别对应十个学生的姓名(字符串长度均大于 00 且小于 2020)。 输出格式 输出为 1010 行,为排序后的 1010 个学生姓名,...
  • C语言矩阵翻转输出

    2021-01-04 22:17:39
    测评机会反复运行你写的程序。每次程序运行时,首先在第一行输入 2 个整数,分别对应题目描述中的 m 和 nnn(1≤m,n≤100),两个整数之间用一个空格分隔。 接下来输入 m 行,每行包含 n 个整数,每两个整数之间用一...
  • 是新朋友吗?记得先点蓝字关注我哦~ 今日课程菜单Java全栈开发 |Web前端+H5大数据开发 | 数据...先问清基本情况,再进行大致的检查,然后分析检查的结果、确定范围,再进行专项检查,再分析检查结果,如此反复,最...
  • 程序中内存从哪里来在一个c语言程序中,能够获取内存的三种情况:栈(stack)、堆(heap)、数据区(.data)。 栈:  运行时自动分配&自动回收  反复使用:栈内存在程序中其实就是一块空间,程序反复使用这块...
  • C语言优先级的问题

    2011-05-07 11:20:00
    if((rIISCON & (1一个C语言优先级的问题导致我弄了好几天的程序,以后一定要坚信一个小小的C语言错误都会导致你程序无法运行,一定不要小看这个好像不起眼的错误,他们会让你付出代价的,正想现在我经历的一样,一个...
  • C语言函数的递归调用

    2014-03-22 09:54:50
    C语言函数的递归调用 ...执行递归函数将反复调用其自身,每调用一次就进入新的一层。例如有函数f如下: int f(int x){ int y; z=f(y); return z;} 这个函数是一个递归函数。但是运行该函数将无休
  • C语言和内存结合漫谈

    2017-11-05 12:25:35
    内存管理最终是操作系统完成...C语言获取内存的三种情况:栈,堆,数据区(.data),代码段,bss段栈的详解: 局部变量int a 运行时自动分配,自动回收,程序员不需要手工干预。 反复使用:栈内存在程序中就是一块空
  • //前言 1,默认运行流程:按书写顺序执行...循环结构:在给定条件成立下,反复执行某一段代码。 //一,顺序结构 //二选择结构 C语言的选择结构有两种:if语句 和 switch语句。 1,if语句 1,形式 if(条件) { ...
  • c语言编写单片机技巧

    2009-04-19 12:15:17
    1. C语言和汇编语言在开发单片机时各有哪些优缺点? 答:汇编语言是一种用文字助记符来表示机器指令的符号语言,是最接近机器码的一种语言。其主要优点是占用资源少、程序执行效率高。但是不同的CPU,其汇编语言...
  • C语言复合字面量

    2018-09-01 20:48:05
      在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)(字面量是相对变量常量等定义的,无论是常量还是变量,其值在某一时刻总是确定的,只是变量可以反复赋值、刷新等,然而在运行时...
  • 摘要:在实际学习过程中,有不少同学觉得C语言学习比较困难,就我的理解,主要有...C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言...
  • 指针学习C语言Day03

    2021-04-19 16:19:19
    一般使用一个变量来引导它的运行,这个变量叫做循环变量i,index for([i];[2];[3]) { [4] } 1.给循环变量赋初值,c99标准可以定义循环变量-std=gnu99 在此处定义的循环变量,只能在for循环中使用 2.判断循环变量...

空空如也

空空如也

1 2 3 4 5 ... 8
收藏数 146
精华内容 58
关键字:

c语言反复运行

c语言 订阅