C语言调试教程总结(以visual studio和Dev C++为例)_c语言怎么调试(1)

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

img

删除断点

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

img

代替暂停语句

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

四、查看和修改变量的值

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

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

#include <stdio.h>
int main(){
    int value_int, array_int[3];
    float value_float;
    char\* value_char_pointer;
    //在这里插入断点
    value_int = 1048576;
    value_float = 2.0;
    value_char_pointer = "Hello World";
    array_int[0] = 379; array_int[1] = 94;
    //在这里插入断点
    return 0;
}

在第7行和第12行插入断点。运行到第一个断点时,在局部变量窗口可以看到各个变量的值:

img

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

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

img

更加快捷的方式

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

img

如果是数组、指针、结构体等还可以展开,如下图所示:

img

这种查看变量的方式在实际开发中使用很多。

添加监视

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

img

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

img

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

五、单步调试(逐语句调试和逐过程调试)

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

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

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

#include <stdio.h>
int main(){
    int start, space, length, i, thisNum;
    long total = 0;
   
    printf("请输入等差数列的首项值:");
    scanf("%d", &start);
    printf("请输入等差数列的公差:");
    scanf("%d", &space);
    printf("请输入等差数列的项数:");
    scanf("%d", &length);
   
    for(i=0; i<length; i++){
        thisNum = start + space \* i;
        if( length-i > 1 ){
            printf("%d + ", thisNum);
        }else{
            printf("%d", thisNum);
        }
        total += thisNum;
    }
    printf(" = %ld\n", total);
   
    return 0;
}

在第6行设置一个断点,然后点击“逐过程调试”按钮,或者按F10键,程序就会运行到下一行并暂停:

img

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

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

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

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

img

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

逐过程(F10)逐语句(F11)都可以用来进行单步调试,但是它们有所区别:

  • 逐过程(F10)在遇到函数时,会把函数从整体上看做一条语句,不会进入函数内部;
  • 逐语句(F11)在遇到函数时,认为函数由多条语句构成,会进入函数内部。

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

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

修改代码运行位置

在VS中,调试器还允许我们直接跳过一段代码,不去执行它们。将下面的代码复制到源文件:

#include <stdio.h>
int main(){
    printf("111\n");
    printf("222\n");
    printf("333\n");
    printf("444\n");
    printf("555\n");
    printf("666\n");
    return 0;
}

在第3行设置断点,开始单步调试。假设我们不希望执行4~6行的代码,那么当程序执行到第4行时,可以将鼠标移动到黄色箭头处,直接向下拖动到第7行,如下图所示:

img

程序执行完成后,在控制台上会输出:
111
555
666

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

六、即时窗口的使用

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

img

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

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

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

调用函数

在即时窗口中除了可以使用代码中的变量,也可以调用代码中的函数。将下面的代码复制到源文件中:

int plus(int x, int y){
    return x + y;
}
int main(){
    return 0;
}

在第6行设置断点,并在即时窗口中输入plus(5, 6),如下图所示:

img

七、查看、修改运行时的内存

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

首先我们通过内存窗口查看变量的值,我们启动 Visual Studio,创建一个工程,输入如下代码:

#include <stdio.h>
int main()
{
    int testNumber = 5678;
    printf("testNumber 的内存地址为 0x00%x \n", &testNumber);
    //输出内存地址
    //TODO:在这里插入断点
    return 0;
}

我们在第七行设置好断点,然后按 F5 启动调试,等待触发断点。触发断点后,我们发现,IDE中并没有显示内存窗口(默认设置下),这时,我们点击菜单 -> 调试(D) -> 窗口 (W) -> 内存 (M) -> 内存1(1),就可以调出内存窗口了,如图:

img

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

img

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

img

没错,查看内存就是这么的简单。接下来我们就来查看与修改浮点数的内存数据,我们看下面这段代码:

#include <stdio.h>
int main()
{
    double pi = 3.141592653589;
    printf("pi 的内存地址为 %x \n", &pi);
    //输出内存地址
    //TODO:在这里插入断点
    return 0;
}

同样的,我们在第7行设置断点,按F5启动调试,等待断点被触发:

img

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

img

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

img

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

img

怎么样,内存的查看与修改是不是很简单呢?其实我们只要记住下面的几个对应关系,常用的数值数据类型的内存查看与修改都不在话下:

类型名变量类型内存查看窗口中应选择的数据格式
short16 位整形2 字节整数
int32 位整形4 字节整数
long32 位整形4 字节整数
long long64 位整形8 字节整数
float32 位(4字节)单精度浮点数32 位浮点
double64 位(8字节)双精度浮点数64 位浮点

内存新手常用的内存窗口操作就到这里了,其实关于内存操作还有很多高级用法,不过那不在我们初级调试教程的范围之内。我们在修改内存的时候要注意安全,防止随意修改导致的程序崩溃,甚至是无法结束进程!

八、有条件断点的设置

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

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

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

#include <stdio.h>
int main(){
    for ( int i=1 ; i <= 1000 ; i++ ) {
        //TODO:插入计次断点
        printf("我真行!\n");
    }
}

首先,我们在第4行插入断点,分析代码,我们不难得出它会输出 1000 行“我真行!”,那么我们思考一下,在不修改代码的情况下,如何才能让他输出 1499 行“我真行!”呢,其实很简单,我们只要在i 等于500的时候暂停程序,再将变量 i 的值修改为 1 即可,思路很简单,接下来我们就来实现这个命中条件的限制吧。

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

img

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

img

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

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

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

下面我们就来试试在 Visual Studio 中插入条件断点吧。我们首先创建一个工程,输入如下代码:

#include <stdio.h>
#include <stdlib.h>
#include <time.h> //time函数所在头文件
int main()
{
    int a, b;
    int randNumber;
    srand((unsigned)time(NULL));
    //设置随机数种子,以产生不同随机数
    for (int i = 0; i<50; i++)
    {
        a = rand() % 7; //产生0-6的随机数
        b = rand() % 7; //产生0-6的随机数
        //TODO:在这里插入条件断点: a == b
    }
    return 0;
}

我们让程序运行过程中 a 等于 b 的时候触发断点,首先,我们在第十四行插入断点,然后我们鼠标右键单击左侧的断点图标,在弹出的菜单中选择条件©,IDE会弹出如下对话框,我们在条件输入框中输入 a==b ,然后在下面选择 为 true ,然后点击确定即可。

九、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 中创建一个工程,输入下面这段非常简单的代码:

#include <stdio.h>
#include <assert.h>
int main()
{
    printf("assert 函数测试:");
    assert(true); //表达式为真
    assert(1 >= 2); //表达式为假
    return 0;
}

我们按 F5 启动调试:

img

我们看到,我们的输出窗口打印出了断言失败的信息,并且 Visual Studio 弹出了一个对话框询问我们是否继续执行。但是如果我们不绑定调试器,构建发布版程序,按 Ctrl + F5 直接运行呢?是的,这个 assert 语句就无效了,原因其实很简单,我们看看 assert.h 头文件中的这部分代码:

#ifdef NDEBUG
#define assert(\_Expression) ((void)0)
#else /\* NDEBUG \*/

我们看到,只要我们定义了 NDEBUG 宏,assert 就会失效,而 Visual Studio 的默认的发布版程序编译参数中定义了 NDEBUG 宏,所以我们不用额外定义,但是在其他编译器中,我们在发布程序的时候就必须在包含 assert.h 头文件前定义 NDEBUG 宏,避免 assert 生效,否则总是让用户看到“程序已经停止运行,正在寻找解决方案 . . .”的 Windows 系统对话框可就不妙了。

下面我们来了解一下 assert 的常用情境以及一些注意事项。举个C语言文件操作的例子:

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
int main(void)
{
    FILE \*fpoint;
    fpoint = fopen("存在的文件.txt", "w");
    //以可读写的方式打开一个文件
    //如果文件不存在就自动创建一个同名文件
    assert(fpoint);                        
    //(第一次断言)所以这里断言成功
    fclose(fpoint);
    fpoint = fopen("不存在的文件.txt", "r");
    //以只读的方式打开一个文件,如果不存在就打开文件失败
    assert(fpoint);
    //(第二次断言)所以断言失败
    printf("文件打开成功");
    //程序永远都执行不到这里来
    fclose(fpoint);                          
    return 0;
}

阅读代码,我们可以知道,如果我们错误的设置了读写权限,或者没有考虑 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 的另外一个注意意事项,我们新建一个工程,输入如下代码:

#include <stdio.h>
#include <assert.h>
int main(void)
{
    int i = 0;
    for ( ; ; )
    {
        assert(i++ <= 100);
        printf("我是第%d行\n",i);
    }
    return 0;
}

我们按 F5 运行调试器,我们会看到这样的情景:

img

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

img

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

img

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

assert(i++ <= 100);

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

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

九、调试信息的输出

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

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

#ifdef UNICODE
#define OutputDebugString OutputDebugStringW
#else
#define OutputDebugString OutputDebugStringA
#endif // !UNICODE

我们可以从代码高亮上看到,OutputDebugString 实际上等价于 OutputDebugStringW,这就意味着我们必须传入宽字符串(事实上只要定义了 UNICODE ,调用所有 Windows 提供的函数都需要使用宽字符),或者使用 TEXT 或 _T 宏,并且这是最好的方法,这个宏会自动识别编译器是否处于默认宽字符的状态并对传入字符串做些处理,使用这个宏可以加强代码对不同编译器不同编译参数的兼容性。下面我们就来看一段示例代码:

#include <windows.h> //使用 OutputDebugString 包含此文件
#include <tchar.h> //使用 TEXT 宏需要包含此文件
int main(){
    OutputDebugString(TEXT("你好,C语言中文网。"));
    OutputDebugString(\_T("大家好才是真的好。"));
    //也可以:OutputDebugStringA("大家好才是真的好。");
    //也可以:OutputDebugStringW(L"大家好才是真的好。");
    //使用自动字符宏 TEXT 或者 \_T 可以自动判断是否使用宽字符
    system("pause"); //使程序暂停一下
    return 0;
}

在程序执行 system(“pause”); 暂停的时候我们来观察一下我们的,调试输出窗口:

img

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

直接使用这个调试信息输出函数有个弊端,那就是它不能直接输出含参数的字符串。但是我们可以通过 sprintf / wsprintf 等缓冲区写入函数来间接实现输出含参数字符串的功能。下面是一段示例代码:

#include <stdio.h>
#include <windows.h>
int main(){
    //注意!这段代码我们指定使用ANSI字符!
    char szBuffer[200];
    int number = 100;
    sprintf\_s(szBuffer, "变量 number 的值是 %d \r\n", number); //写入缓冲区,注意不要溢出
    OutputDebugStringA(szBuffer);
    sprintf\_s(szBuffer, "变量 number 的地址是 %x \r\n", &number);
    OutputDebugStringA(szBuffer);
    //我门指定使用 ANSI 版本的 OutputDebugString 函数
    return 0;
}

我们按 F5 开始调试:

img

我们看到了输出的结果,怎么样,大家是不是觉得这样调用这个函数很麻烦?为解决此问题,这里C语言中文网为大家提供了一个更好的解决方案,我们可以自己写一个前端函数,然后保存到头文件中(编译生成 dll 也可以,有兴趣的同学可以试试)。为了方便,我们已经编写好了这么一套函数。代码如下:

#include <stdio.h>
#include <windows.h>
#ifndef \_DEBUG\_INFO\_HEADER\_
//防止头文件被重复载入出错
#define \_DEBUG\_INFO\_HEADER\_
#if (defined UNICODE)||(defined \_UNICODE)
#define DebugInfo DebugInfoW
#else
#define DebugInfo DebugInfoA
#endif
// 函数: DebugInfoA(char\*, int, ...)
//
// 目的: 以窄字符的形式输出调试信息
//
// char\* str - 格式化 ANSI 字符串
// ... - 任意不定长参数
//
void DebugInfoA(char\* str, ...){
    char szBuffer[500]; //注意不要让缓冲区溢出!
    va_list Argv;
    va\_start(Argv, str);
    \_vsnprintf\_s(szBuffer, 500, str, Argv);
    va\_end(Argv);
    OutputDebugStringA(szBuffer);
}
// 函数: DebugInfoW(char\*, int, ...)
//
// 目的: 以宽字符的形式输出调试信息
//
// char\* str - 格式化 UNICODE 字符串
// ... - 任意不定长参数
//
void DebugInfoW(wchar\_t\* str, ...){
    wchar\_t szBuffer[1000];
    va_list Argv;
    va\_start(Argv, str);
    \_vsnwprintf\_s(szBuffer, 500, str, Argv);


![img](https://img-blog.csdnimg.cn/img_convert/c423f353571d7e4b99543ecb3becd10e.png)
![img](https://img-blog.csdnimg.cn/img_convert/9cd57723f8ff4168c89e3763044f8ad7.png)
![img](https://img-blog.csdnimg.cn/img_convert/d88adb89c08fca5312d694a3734a26ee.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

str);
    \_vsnprintf\_s(szBuffer, 500, str, Argv);
    va\_end(Argv);
    OutputDebugStringA(szBuffer);
}
// 函数: DebugInfoW(char\*, int, ...)
//
// 目的: 以宽字符的形式输出调试信息
//
// char\* str - 格式化 UNICODE 字符串
// ... - 任意不定长参数
//
void DebugInfoW(wchar\_t\* str, ...){
    wchar\_t szBuffer[1000];
    va_list Argv;
    va\_start(Argv, str);
    \_vsnwprintf\_s(szBuffer, 500, str, Argv);


[外链图片转存中...(img-hVHJ0KNb-1715493637934)]
[外链图片转存中...(img-EsgLdnXe-1715493637935)]
[外链图片转存中...(img-Q2o4BvQf-1715493637935)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数字签名是一种常用的身份验证技术,可以用于验证文件的完整性、真实性和来源。在C语言中,可以使用OpenSSL库来实现数字签名和验证。 以下是在Visual Studio Code中编写和调试C语言代码的简单步骤: 1. 安装Visual Studio Code和C语言插件。可以在Visual Studio Code的扩展商店中搜索并安装C/C++插件。 2. 打开Visual Studio Code并创建一个新的C语言文件。可以使用Ctrl + N快捷键或选择“文件”>“新建文件”来创建新文件。 3. 添加OpenSSL库。可以使用以下命令在Ubuntu上安装OpenSSL库: ``` sudo apt-get install libssl-dev ``` 在Windows上,可以从OpenSSL官网下载预编译的库并将其添加到项目中。 4. 编写代码。以下是一个简单的数字签名示: ``` #include <stdio.h> #include <string.h> #include <openssl/evp.h> void sign_data(unsigned char *data, int len, unsigned char *key, unsigned char *sig, unsigned int *sig_len) { EVP_MD_CTX *mdctx; const EVP_MD *md; md = EVP_get_digestbyname("sha256"); mdctx = EVP_MD_CTX_new(); EVP_DigestInit_ex(mdctx, md, NULL); EVP_DigestSignInit(mdctx, NULL, md, NULL, key); EVP_DigestSignUpdate(mdctx, data, len); EVP_DigestSignFinal(mdctx, sig, sig_len); EVP_MD_CTX_free(mdctx); } int verify_data(unsigned char *data, int len, unsigned char *key, unsigned char *sig, unsigned int sig_len) { EVP_MD_CTX *mdctx; const EVP_MD *md; int ret; md = EVP_get_digestbyname("sha256"); mdctx = EVP_MD_CTX_new(); EVP_DigestInit_ex(mdctx, md, NULL); EVP_DigestVerifyInit(mdctx, NULL, md, NULL, key); EVP_DigestVerifyUpdate(mdctx, data, len); ret = EVP_DigestVerifyFinal(mdctx, sig, sig_len); EVP_MD_CTX_free(mdctx); return ret; } int main() { unsigned char data[] = "hello world"; unsigned char key[] = "secret key"; unsigned char sig[1024]; unsigned int sig_len; sign_data(data, strlen(data), key, sig, &sig_len); printf("Signature: %s\n", sig); printf("Verification: %d\n", verify_data(data, strlen(data), key, sig, sig_len)); return 0; } ``` 5. 编译和运行代码。可以使用以下命令在Ubuntu上编译代码: ``` gcc -o main main.c -lcrypto ``` 在Windows上,可以使用MinGW或Cygwin等工具编译代码。 6. 在Visual Studio Code中调试代码。可以使用Visual Studio Code的调试器来设置断点、单步执行和查看变量等。可以使用以下配置文件来设置Visual Studio Code的调试器: ``` { "version": "0.2.0", "configurations": [ { "name": "Debug", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/main", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": true, "MIMode": "gdb", "miDebuggerPath": "/usr/bin/gdb" } ] } ``` 可以在Visual Studio Code的调试视图中选择“启动调试”来启动调试器。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值