八、调试技巧

本文详细介绍了调试的基础知识,包括Debug和Release模式的区别,以及Windows环境下如何进行调试,如设置断点、逐过程和逐语句等。通过实例讲解了调试技巧,并模拟实现了strcpy和strlen函数,强调了写出易于调试的代码的重要性。同时,列举了编程中常见的错误类型及其处理方法。
摘要由CSDN通过智能技术生成

目录

一、调试(Debug)

0x00 何为调试

0x01调试的基本步骤

二、Debug和Release的介绍

0x00对比

0x01Release的优化

三、Windows环境调试介绍

0x00调试环境准备

0x01开始调试(F5)

0x02 断点(F9)

0x03 逐过程(F10)

0x04 逐语句(F11)

0x05 开始执行不调试(Ctrl + F5)

0x06 总结

0x07 调试时查看程序当前信息

0x08 查看断点

0x09 监视

0x0A 自动窗口

0x0B 查看局部变量

0x0C 查看内存信息

0x0D 查看调用堆栈

0x0E 查看汇编信息

0x0F 查看寄存器信息

0x10 条件断点

0x11 调试的感悟

四、一些调试的实例

0x00 实例一

0x01 实例二

五、如何写出易于调试的代码(模拟实现strcpy)

0x00 优秀的代码

0x01 常见的coding技巧

0x02 strcpy函数介绍

0x03 模拟实现strcpy

0x04 优化 - 提高代码简洁性

0x05 优化 - 修改while

0x06 优化 - 防止传入空指针

0x07 关于const的使用

 0x08 优化 - 提高代码健壮性(加入const)

0x09 最终优化 - 使其支持链式访问

0x0A 库函数写法

六、模拟实现strlen函数

0x00 计数器实现

0x01 指针减指针实现

0x02 库函数写法

七、编程常见的错误

0x00 编译型错误

0x01 链接型错误

0x02 运行时错误

0x03 建议


一、调试(Debug)

0x00 何为调试

一名优秀的程序员是一名出色的侦探,每一次调试都是尝试破案的过程……

📚定义:调试,又称除错,是发现和减少计算机程序电子仪器设备中程序错误的一个过程;

0x01调试的基本步骤

📚基本步骤:

 ①发现程序错误的存在;

  ✅能够发现错误的人:

   ⑴程序员,自己发现;

   ⑵软件测试人员,测试软件;

   ⑶用户,代价严重;

📜 箴言:要善于承认自己的错误,不能掩盖错误

②以隔离、消除等方式对错误进行定位;

 ✅能知道大概在什么位置,再确定错误产生的原因是什么;

③提出纠正错误的解决方案;

④对程序错误订正,重新调试;

二、Debug和Release的介绍

0x00对比

📚Debug 通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序;

📚Release 称为发布版本,他往往是进行了各种优化,使得程序在代码大小和运行速度上是最优的,以便用户更好的使用;

📌注意事项:Release 版本是不能调试的;

💬用 Debug 和 Release 分别运行:

int main()
{
    char* p = "hello,world!";
    printf("%s\n", p);
 
    return 0;
}

🚩Debug 环境下运行结果如下:

🚩  Release 环境下运行结果如下:

💡我们可以发现:Release进行了优化,使得程序在运行速度和代码大小上是最优的;

💬 Debug和Release反汇编展示对比:

0x01Release的优化

❓使用Release版本调试时,编辑器进行了那些优化呢?

💬请看下列代码:

 int main()
{
    int arr[10] = {0};
    int i = 0;
    for(i=0; i<=12; i++) {
        arr[i] = 0;
        printf("hehe\n");
    }
 
    return 0;
}

🚩如果是 debug 模式去编译,程序结果是 死循环:

🚩如果是 release 模式去编译,程序没有死循环:

💡因为 release 的优化,避免了死循环的发生;

三、Windows环境调试介绍

0x00调试环境准备

📚在环境中选择 debug 选项,才能使代码正常调试;

📌注意事项:本章使用 VS2019 演示;

0x01开始调试(F5)

✅快捷键:F5

📚作用:启动调试,经常用来直接调到下一个断点处;

📌注意事项:

  ① 如果直接按 F5 ,如果没有阻挡的话程序一口气就干完了;

  ② 使用 F5 之前要先使用 F9 ,设置断点;

💬 按 F5 开始调试下列代码:

 int main()
{
    int arr[10] = { 0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
 
    int i = 0;
    for (i = 0; i < sz; i++) {
        arr[i] = i + 1;
    }
    for (i = 0; i < sz; i++) {
        printf("%d\n", arr[i]);
    }
 
    return 0;
}

🚩 运行后结果如下:

0x02 断点(F9)

✅快捷键:F9

📚作用:创建断点和取消断点,断电的重要作用可以在程序的任意位置设置断点;这样就可以使得程序在想要的位置随意停止执行,继而可以一步步执行下去;

💬按 F9 设置断点

🚩 这时按下 F5 就会直接跳到断点部分:

0x03 逐过程(F10)

✅ 快捷键:F10

📚 作用:通常用来处理一个过程,一个过程可以是一次函数的调用,或者是一条语句;

💬 逐过程:

💡 按一次 F10 代码就往下走一步;

0x04 逐语句(F11)

✅ 快捷键:F11(这是最常用的)

📚 作用:每次都执行一条语句,观察的细腻度比 F10 还要高,可以进入到函数内部;

📌 注意事项:F10F11 大部分情况是一样的,区别在于 F11 遇到函数时可以进到函数内部去,函数的内部也可以一步步观察,而 F10 遇到函数调用完之后就跳出去了;

💬 观察函数内部:

💡 如果想观察函数内部,就要使用 F11 (逐语句);

0x05 开始执行不调试(Ctrl + F5)

✅ 快捷键: Ctrl + F5

📚 作用:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用;

0x06 总结

F5 - 启动调试

F9 - 设置/取消断点

F10 - 逐过程

F11 - 逐语句 - 更加细腻

Ctrl + F5 - 运行

📌 注意事项:如果你按上面的快捷键不起作用时,可能是因为辅助功能键(Fn)导致的,此时按下 Fn 再按上面的快捷键即可;

❓ 想知道更多快捷键?

    VS中常用的快捷键  👈 戳我!

0x07 调试时查看程序当前信息

💬 查看方法:调试(D) → 窗口(W) → 选择相应的选项;

📌 注意事项:只有调试之后才会显示调试窗口里的选项;

0x08 查看断点

📚 作用:调试多个文件时,可以很好地管理多个文件的断点;

0x09 监视

📚 作用:在调试开始之后,便于观察变量的值;

📌 注意事项:要填入合法的表达式;

💬 监视操作(手动添加):

0x0A 自动窗口

📚 作用:编辑器自行监视,随着代码自动给出值;

📌 注意事项:

      ① 自动窗口和监视是一样的效果,但是自动窗口里的表达式会自动发生变化;

      ② 自由度低,自动窗口是编辑器自己监视,你管不了;

0x0B 查看局部变量

📚 作用:查看程序进行到当前位置时上下文的局部变量,编辑器自主放到窗口中进行相应的解释,只有局部变量和数组;

💬 查看局部变量:

0x0C 查看内存信息

📚 作用:在调试开始之后,用于观察内存信息;

💬 查看内存信息:

0x0D 查看调用堆栈

📚 作用:通过调用堆栈,可以清晰地反应函数的调用关系和所处的位置;

💬 查看调用堆栈:

0x0E 查看汇编信息

📚 在调试开始后,有两种方式转到汇编:

      ① 第一种方式:右击鼠标,选择 " 转到反汇编 "

      ② 第二种方式:调试 → 窗口 → 反汇编

💬 查看反汇编:

0x0F 查看寄存器信息

📚 作用:可以查看当前运行环境的寄存器的实用信息;

💬 查看寄存器:

0x10 条件断点

❓ 假设某个循环要循环1000次,我怀疑第500次循环时程序会出问题,那么我要打上断点然后再按500次 F10 吗?这样一来手指头不得按断了?

💡 方法:使用条件断点;

💬 在断点设置好之后右键鼠标,选中条件:

🐞 按下 F5 后,i 会直接变为 5 :

0x11 调试的感悟

📜 箴言:

  ① 多多动手,尝试调试,才能有进步;

  ② 一定要熟练掌握调试的技巧;

  ③ 初学者可能80%的时间在写代码,20%的时间在调试。

     但是一个程序员可能20%的时间在写程序,但是80%的时间在调试;

  ④ 我们所讲的都是一些简单的调试。

      以后可能会出现很复杂的调试场景:多线程程序的调试等;

  ⑤ 多多使用快捷键,提升效率;

四、一些调试的实例

0x00 实例一

💬 实现代码:求 1!+ 2! + 3! ··· + n!(不考虑溢出)

 int main()
{
    int n = 0;
    scanf("%d", &n); // 3
    // 1!+ 2!+ 3!
    // 1    2    6  =  9
    int i = 0;
    int ret = 1;
    int sum = 0;
    int j = 0;
    for (j = 1; j <= n; j++) {
        for (i = 1; i <= j; i++) {
            ret *= i;
        }
        sum += ret;
    }
    printf("%d\n", sum);
 
    return 0;
}
 

🚩 运行结果如下:

❓ 结果应该是9才对,但是输出结果为15,代码出错了;代码又没有语法错误,代码能够运行,属于运行时错误,而调试解决的就是运行时错误;

🐞 此时我们试着调试:

💡此时我们发现了问题:每一次求阶乘时,应该从1开始乘,所以每一次进入时 ret 要置为1;

 int main()
{
    int n = 0;
    scanf_s("%d", &n); // 3
    // 1!+ 2!+ 3!
    // 1    2    6  =  9
    int i = 0;
    int ret = 1;
    int sum = 0;
    int j = 0;
    for (j = 1; j <= n; j++) {
        ret = 1; // 每次进入,置为1,重新开始乘
        for (i = 1; i <= j; i++) {
            ret *= i;
        }
        sum += ret;
    }
    printf("%d\n", sum);
 
    return 0;
}

🚩 运行结果如下:

🔺 解决问题:

      ① 要知道程序应该是什么结果:预期

      ② 调试的时候发现不符合预期,就找到问题了;

0x01 实例二

💬 下列代码运行的结果是什么?

 int main()
{
    int i = 0;
    int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//                       👇 越界访问了
    for (i = 0; i <= 12; i++) {
        printf("hehe\n");
        arr[i] = 0;
    }
 
    return 0;
}

🚩 运行结果如下:

❓ 研究导致死循环的原因:

💡 解析:

🔺 本题正确答案:死循环,因为 i 和 arr 是里昂个局部变量,先创建 i,再创建 arr,又因为局部变量是放在栈区上的,栈区的使用习惯是先使用高地址再使用低地址,所以内存的布局是这样子的(如图),又因为数组随着下标的增长地址是由低到高变化的,所以数组用下标访问时只要适当的越界,就有可能覆盖到 i,而 i 如果被覆盖的话,就会导致程序的死循环;

五、如何写出易于调试的代码(模拟实现strcpy)

0x00 优秀的代码

0x01 常见的coding技巧

0x02 strcpy函数介绍

 /* strcpy: 字符串拷贝 */
#include <stdio.h>
#include <string.h>
 
int main()
{
    char arr1[20] = "xxxxxxxxxx";
    char arr2[] = "hello";
    strcpy(arr1, arr2);   // 字符串拷贝(目标字符串,源字符串)
    printf("%s\n", arr1); // hello
 
    return 0;
}

0x03 模拟实现strcpy

💬 示例 - 模拟实现 strcpy

 #include <stdio.h>
 
char* my_strcpy (
    char* dest, // 目标字符串
    char* src   // 源字符串
    )
{
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = *src; // 拷贝'\0'
}
 
int main()
{
    char arr1[20] = "xxxxxxxxxx";
    char arr2[] = "hello";
    my_strcpy(arr1, arr2);
    printf("%s\n", arr1); // hello
 
    return 0;
}

0x04 优化 - 提高代码简洁性

💬 函数部分的代码,++ 部分其实可以整合到一起:

 #include <stdio.h>
 
char* my_strcpy (char* dest, char* src)
{
    while (*src != '\0') {
        *dest++ = *src++;
    }
    *dest = *src;
}

0x05 优化 - 修改while

2. 甚至可以把这些代码都放到 while 内:

( 利用 while 最后的判断 ,*dest++ = *src++ 正好拷走斜杠0 )

 char* my_strcpy (char* dest, char* src)
{
    while (*dest++ = *src++) // 既拷贝了斜杠0,又使得循环停止
        ;
}

0x06 优化 - 防止传入空指针

❓ 如果传入空指针NULL,会产生BUG

💡 解决方案:使用断言

断言是语言中常用的防御式编程方式,减少编程错误;
如果计算表达式expression值为假(0),那么向stderr打印一条错误信息,然后通过调用abort来终止程序运行;
断言被定义为宏的形式(assert(expression)),而不是函数;

 #include <stdio.h>
#include <assert.h>
 
char* my_strcpy(char* dest, char* src)
{
    assert(dest != NULL); // 断言   "dest不能等于NULL"
    assert(src != NULL);  // 断言   "src 不能等于NULL"
 
    while (*dest++ = *src++)
        ;
}
 
int main()
{
    char arr1[20] = "xxxxxxxxxx";
    char arr2[] = "hello";
    my_strcpy(arr1, NULL); // 👈 实验:传入一个NULL
    printf("%s\n", arr1);
 
    return 0;
}

🚩 运行结果如下:

0x07 关于const的使用

💬 将 num的值修改为20:

 int main()
{
    int num = 10;
    int* p = &num;
    *p = 20;
    printf("%d\n", num);
 
    return 0;
}

🚩  20

💬 此时在 int num 前放上 const :

const 修饰变量,这个变量就被称为常变量,不能被修改,但本质上还是变量;

但是!但是呢!!

 int main()
{
    const int num = 10;
    int* p = &num;
    *p = 20;
 
    printf("%d\n", num);
 
    return 0;
}

🚩 运行结果如下

❗ 我们希望 num 不被修改,结果还是改了,这不出乱子了吗?!

num 居然把自己的地址交给了 p,然后 *p = 20,通过 p 来修改 num 的值,不讲武德!

💡 解决方案:只需要在 int* p 前面加上一个 const,此时 *p 就没用了

 int main()
{
    const int num = 10;
 
    const int* p = &num; 
// 如果放在 * 左边,修饰的是 *p,表示指针指向的内容,是不能通过指针来改变的
    *p = 20; // ❌ 不可修改
 
    printf("%d\n", num);
 
    return 0;
}

🚩 运行结果如下:

🔑 解释:const 修饰指针变量的时候,const 如果放在 * 左边,修饰的是 *p,表示指针指向的内容是不能通过指针来改变的;

❓ 如果我们再加一个变量 n = 100, 我们不&num,我们&n,可不可以?

 int main()
{
    const int num = 10;
    int n = 100;
 
    const int* p = &num;
    *p = 20 // ❌ 不能修改
    p = &n; // ✅ 但是指针变量的本身是可以修改的
 
    printf("%d\n", num);
 
    return 0;
}

🔑 可以,p 虽然不能改变 num,但是 p 可以改变指向,修改 p 变量的值

❓ 那把 const 放在 * 右边呢?

 int main()
{
    const int num = 10;
 
    int n = 100;
    int* const  p = &num;
// 如果放在 * 右边,修饰的是指针变量p,表示的指针变量不能被改变
// 但是指针指向的内容,可以被改变
    
    p = 20; // ✅ 可以修改
    p = &n; // ❌ 不能修改
 
    printf("%d\n", num);
 
    return 0;
}

🔑 此时指针指向的内容可以修改,但是指针变量

❓ 如果两边都放 const :

 int main()
{
    const int num = 10;
    const int* const p = &num;
    int n = 100;
 
    *p = 20; // ❌ 不能修改
    p = &n;  // ❌ 不能修改
    
    printf("%d\n", num);
 
    return 0;
}

 0x08 优化 - 提高代码健壮性(加入const)

💡 为了防止两个变量前后顺序写反,我们可以利用 const 常量,给自己“设定规矩”,这样一来,当我们写反的时候, 因为是常量的原因,不可以被解引用修改,从而报错,容易发现问题之所在!

 char* my_strcpy (char* dest, const char* src)
{
    assert(dest != NULL);
    assert(src != NULL);
 
    // while(*src++ = *dest) 👈 防止写反,加一个const
    while (*dest++ = *src++)
        ;
}

可以无形的防止你写出 while(*src++ = *dest) ,即使你写错了,编译器也会报错(语法错误);

📌 注意事项:按照逻辑加 const,不要随便加const(比如在dest前也加个const);

0x09 最终优化 - 使其支持链式访问

💬 实现返回目标空间的起始位置

 #include <stdio.h>
#include <assert.h>
 
char* my_strcpy (char* dest,const char* src)
{
    char* ret = dest; // 在刚开始的时候记录一下dest
    assert(dest != NULL);
    assert(src != NULL);
 
    while (*dest++ = *src++)
        ;
 
    return ret; // 最后返回dest
}
 
int main()
{
    char arr1[20] = "xxxxxxxxxx";
    char arr2[] = "hello";
    
    printf("%s\n", my_strcpy(arr1, arr2)); // 链式访问
 
    return 0;
}

0x0A 库函数写法

 /***
*char *strcpy(dst, src) - copy one string over another
*
*Purpose:
*       Copies the string src into the spot specified by
*       dest; assumes enough room.
*
*Entry:
*       char * dst - string over which "src" is to be copied
*       const char * src - string to be copied over "dst"
*
*Exit:
*       The address of "dst"
*
*Exceptions:
*******************************************************************************/
 
char * strcpy(char * dst, const char * src)
{
        char * cp = dst;
        assert(dst && src);
 
        while( *cp++ = *src++ )
               ;     /* Copy src over dst */
        return( dst );
}

六、模拟实现strlen函数

0x00 计数器实现

 #include <stdio.h>
#include <assert.h>
 
int my_strlen(const char* str)
{
    assert(str);
    int count = 0;
    while (*str) {
        count++;
        str++;
    }
    return count;
}
 
int main()
{
    char arr[] = "abcdef";
        int len = my_strlen(arr);
    printf("%d\n", len);
 
    return 0;
}

0x01 指针减指针实现

 #include <stdio.h>
#include <assert.h>
 
size_t my_strlen(const char* str)
{
    assert(str);
    const char* eos = str;
    while (*eos++);
    return(eos - str - 1);
}
 
int main()
{
    char arr[] = "abcdef";
    printf("%d\n", my_strlen(arr));
 
    return 0;
}

0x02 库函数写法

 /***
*strlen.c - contains strlen() routine
*
*       Copyright (c) Microsoft Corporation. All rights reserved.
*
*Purpose:
*       strlen returns the length of a null-terminated string,
*       not including the null byte itself.
*
*******************************************************************************/
 
#include <cruntime.h>
#include <string.h>
 
#pragma function(strlen)
 
/***
*strlen - return the length of a null-terminated string
*
*Purpose:
*       Finds the length in bytes of the given string, not including
*       the final null character.
*
*Entry:
*       const char * str - string whose length is to be computed
*
*Exit:
*       length of the string "str", exclusive of the final null byte
*
*Exceptions:
*
*******************************************************************************/
 
size_t __cdecl strlen (
        const char * str
        )
{
        const char *eos = str;
 
        while( *eos++ ) ;
 
        return( eos - str - 1 );
}

size_t :无符号整型(unsigned int)

__cdecl :函数调用约定

七、编程常见的错误

0x00 编译型错误

📚 直接看错误提示信息(双击),解决问题;

或者凭借经验就可以搞定,相对来说简单;

0x01 链接型错误

📚 看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。

一般是 标识符名不存在 或者 拼写错误 ;

0x02 运行时错误

📚 代码明明跑起来了,但是结果是错的;

🔑 借助调试,逐步定位问题,利用本章说的实用调试技巧解决;

0x03 建议

📜 做一个有心人,每一次遇到错误都进行自我总结,积累错误经验!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值