C语言调试技巧

news2024/9/20 7:57:57

叠甲:以下文章主要是依靠我的实际编码学习中总结出来的经验之谈,求逻辑自洽,不能百分百保证正确,有错误、未定义、不合适的内容请尽情指出!

文章目录

  • 1.bug 是什么
  • 2.debug 是什么
  • 3.两种模式的区别
  • 4.IDE 调试介绍
    • 4.1.模式调整
    • 4.2.基础调试
    • 4.3.进阶调试
      • 4.3.1.监视窗口
      • 4.3.2.内存窗口
      • 4.3.3.寄存器窗口
      • 4.3.4.调用堆栈窗口
      • 4.3.5.汇编窗口
  • 5.编程常见错误
  • 6.编写易于调试代码
    • 6.1.优秀代码的基本要求
    • 6.2.常见的代码编写技巧
      • 6.2.1.assert() 宏函数
      • 6.2.2.const 关键字
    • 6.3.尝试编写出优秀代码

概要:…

资料:…


1.bug 是什么

关于 Bug 的来源,这个故事很多人都知道:“1949 年 9 月 9 日,我们晚上调试机器的时候,开着的窗户没有纱窗,机器闪烁的亮光几乎吸引来了世界上所有的虫子。果然机器故障了,我们发现了一只被继电器拍死的飞蛾,翅膀大约 4 英寸。”

第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误,而 bug 这个名词也被延用至今。

2.debug 是什么

调试又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程,调试的基本步骤如下:

  1. 编码开发过程中、项目运行过程中发现有错误的存在
  2. 以隔离、消除等方式对错误进行定位,比对上一次没有 bug 的版本查看差异
  3. 分析代码,确定错误产生的原因
  4. 提出纠正错误的解决办法
  5. 对程序错误予以改正,重新进行测试
  6. 总结错误的原因,避免下次再犯

补充:拒绝迷信调试,最好每次出现 BUG 都解析到底,不要忽略每一处的细节…

3.两种模式的区别

VS2022 中,其实有两种编译代码的形式:

  • Debug 称为“调试版本”,它包含调试信息,并且不作任何优化,便于程序员调试程序,但体积会因为携带调试信息而变得较大。
  • Release 称为“发布版本”,它往往对代码进行了各种优化,使得最后生成的可执行程序在代码大小和运行速度上都是较优的,以便用户很好地使用。

而两种模式具体的区分如下:

  • 文件区别debugrelease 模式下,会在 VS 项目文件里面各自生成一个 debugRelease 文件
  • 反汇编区别: 他们两个的反汇编代码有着明显差别,一般来说 DebugRelease 多,因为没有经过
  • 内存区别: 由于 debug 模式下的可执行文件中包含了调试信息,所以大小会比 release 模式下的可执行文件要大
  • 应用区别: debug 主要用于程序员调试,release 主要交予测试员和用户使用
  • 运行区别: release 会在一定程度上对代码进行优化,极少数情况会出现与 debug 模式不符和的结果

VS2022 x86 环境中,下述代码的运行在 DebugRelease 两种模式下运行结果可能不同(如果您没有重现这个错误,可以尝试使用版本更老的 VS,当然如果嫌麻烦,简单看一下就行…)

// 一份故意越界的代码
#include <stdio.h>
int main()
{
    int i = 0;
    int arr [10] = {0};
    for(i = 0; i <= 12; i++)
    {
        arr [i] = 0;
        printf("hehe\n");
    }
    return 0;
}

4.IDE 调试介绍

Linux 中的调试工具是 gdb,而 Windows 下的 VS 以强大的调试界面闻名,并且该 IDE 也在不断增强现代化(2024-6-6)。

4.1.模式调整

必须要在 Debug 模式中进行一次编译,才能使用 VS 的调式功能开始调试代码,无法通过编译的代码是无法进行调试的。

4.2.基础调试

  • [F9] 创建/取消断点,断点可以使程序执行到在想要的停止的位置,继而一步步执行下去(这在循环语句中尤其好用,无需进入大量循环)
  • [ctrl+F9] 停用断点
  • [ctrl+shift+F9] 删除全部断点
  • [F5] 启动调试,经常用来直接跳转到代码的第一个断电/下一个断点之处
  • [shift+F5] 取消调试,想退出调试的时候可以用这个功能停止调试
  • [ctrl+F5] 直接运行程序,程序直接跑起来而不进行调试,这个我们以前经常使用
  • [ctrl+shift+F5] 重新启动调试,就是上面两个快捷键的结合版
  • [F10] 逐过程调试,通常用来执行一个过程,一个过程可以是一次函数调用或者一条完整语句,无需进入过程细节
  • [F11] 逐语句调试,就是每次都执行语句,这个快捷键可以使得我们的执行逻辑进入函数内部,执行调试比较细一点
  • [ctrl+F10] 调试跳转到光标处,也是一种断点调试,但是是根据光标位置进行跳转

补充:还有一些比较有用的快捷键,但是和调试没什么关系,这里可以给您提及一下…

  • [ctrl+u/ctrl+shift+u] 把选中的字符串进行小写/大写
  • [ctrl+shift+v] 唤出内部剪贴板
  • [ctrl+g] 跳转到某个代码行
  • [ctrl+<-/->] 跳过一个单词
  • [shift+alt] 矩形文本选框
  • [tab+tab] 快速编写代码

4.3.进阶调试

4.3.1.监视窗口

1!+2!+3! ...+ n! 的值,不考虑溢出。

// 待调试代码
#include <stdio.h>

// 计算 1! + 2! + 3! + ... + n! 的值
int main() {
    // 输入 n 的大小
    int n = 0;
    scanf("%d", &n);

    int sum = 0; // 保存计算的最终结果
    int ret = 1; // 保存 n 的阶乘

    for (int i = 1; i <= n; i++) { // 这个循环把 1! 和 2! 和 3! ... j! ... n! 加起来
        int j = 0; // 问题在这里的上一步没有把 ret 重新置为 1, 但是假设您遗忘了
        for (j = 1; j <= i; j++) { // 这个循环把 j! 计算出来
            ret *= j;
        }
        sum += ret;
    }
    
    printf("%d\n", sum);
    return 0;
}

先使用 printf() 打印以下读取到的 n 值是否正常,并且打上断点。使用快捷键 [F5] 打开 debug 模式,会一直跳转到 printf() 代码的执行(但是由于 scanf() 需要读取一个数才能一次性走到 printf())。

在这里插入图片描述

在这里插入图片描述

此时就停到执行 printf() 之前的地方,接下来按 [F10] 就可以执行 printf() 打印之前 n 此时的值。

在这里插入图片描述

因此 n 的读取没有出错,还有一种检查方法就是打开监视窗口,点击 添加要监视的项 添加要监视的变量。

在这里插入图片描述

再考虑在循环处打一个断点再执行 [F5] 走到 for 之前的代码处。代码的主要逻辑就在循环这里,因此很可能在这里出错。然后把之前检查的 n 项去除(选中然后使用 [delete] 即可去除),并且把之前测试 n 用的 printf("%d", n) 也删除掉,重新添加 i, j, ret, sum 四个变量的检查项。

在这里插入图片描述

然后不断点击 [F10] 观察四个变量在循环的过程中的变化,可以观察到每次内循环执行结束后,由于 ret 是一个较大域的变量,因此会保持上一次内循环执行的结果,然后参与下一次内循环的运行结果。

因此需要在每次执行内循环之前,将 ret 重新置为 1,改正代码如下。

// 更改后的代码
#include <stdio.h>

// 计算 1! + 2! + 3! + ... + n! 的值
int main() {
    // 输入 n 的大小
    int n = 0;
    scanf("%d", &n);

    int sum = 0; // 保存计算的最终结果
    int ret = 1; // 保存 n 的阶乘

    for (int i = 1; i <= n; i++) { // 这个循环把 1! 和 2! 和 3! ... j! ... n! 加起来
        ret = 1;
        int j = 0; // 问题在这里的上一步没有把 ret 重新置为 1, 但是假设您遗忘了
        for (j = 1; j <= i; j++) { // 这个循环把 j! 计算出来
            ret *= j;
        }
        sum += ret;
    }

    printf("%d\n", sum);
    return 0;
}

补充:一个小技巧,如果是监视指针,将格式写为 指针, 数字 就可查看该指针后面指针的值。

补充:这里在补充一个研究程序死循环/异常终止的代码,您可以尝试一下。

// 供您测试的代码
#include <stdio.h>

int main() {
  int i = 0;
  int arr [10] = {0};

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

  return 0;
}

这段代码会在数组 arr 上进行越界访问,因为其索引 i 会达到 12(希望您可以通过 debug 引发这个问题),而数组的有效索引范围仅从 09。在 debug 模式下,许多编译器会检查数组边界并可能导致程序崩溃或触发异常。

release 模式下,由于优化,程序的行为可能不那么可预测,有时可能不会立即显示出错误。但无论如何,数组越界都是不安全的,应该避免。

补充:自动窗口和局部变量也可以达到监视窗口的目的,不过比较自动(自动显示需要显示的变量和自动显示当前代码块中的局部变量),灵活性有些不够。

在这里插入图片描述

4.3.2.内存窗口

基于上述的基本使用其实就可以解决很多问题了,但是有时需要观察变量在内存中的具体情况,因此就会使用内存窗口,这个窗口我在后续代码中讲解原理时会频繁用到,到那个时候再来详细使用。

在这里插入图片描述

4.3.3.寄存器窗口

由于 C/Cpp 这种编程语言和硬件距离较为接近,因此属于中层偏下的语言,在一些硬件编程(例如单片机编程)中,就有可能需要观察 CPU 寄存器的情况,本系列中有机会再使用…

在这里插入图片描述

4.3.4.调用堆栈窗口

通过调用堆栈可以清晰反应函数的调用关系以及当前调用所处的位置,这个涉及到数据结构的栈,本系列中用到的次数也很少,本系列中有机会再使用…

在这里插入图片描述

4.3.5.汇编窗口

调试模式开始之后右键代码编辑窗口,选择“转到反汇编”,或者再调试状态下打开反汇编窗口,查看汇编代码通常也是在和硬件打交道较多的场景中使用,本系列中有机会再使用…

在这里插入图片描述

在这里插入图片描述

5.编程常见错误

  • 编译型错误:直接看 VS2022 的错误提示信息或者凭借经验就可以搞定就可以解决的问题,相对来说简单
  • 链接型错误:看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误,会出现字眼“无法解析的外部命令”的报错
  • 运行时错误:在程序的运行过程中出现的错误,只能借助调试,逐步定位问题,最难解决

6.编写易于调试代码

6.1.优秀代码的基本要求

  • 代码运行正常:没有导致程序崩溃或异常终止的错误
  • bug 很少:代码经过充分测试,减少了潜在的错误
  • 效率高:代码执行速度快,资源消耗低
  • 可读性高:代码结构清晰,易于理解
  • 可维护性高:代码易于修改和扩展
  • 注释清晰:关键部分有适当的注释,说明代码意图
  • 文档齐全:提供完整的文档,包括 API 说明、使用指南等

6.2.常见的代码编写技巧

  • 使用 assert:断言用于检查程序运行时的条件是否满足预期
  • 尽量使用 const:定义常量,防止变量被意外修改
  • 养成良好的编码风格:遵循一定的命名规范和格式,使代码整洁易读
  • 添加必要的注释:为关键代码段添加注释,说明其功能和逻辑
  • 避免编码的陷阱:了解并避免常见的编程错误和陷阱

这里重点来看看 assert() 宏函数和 const 关键字的使用,来看看两者是如何提高代码健壮性的。

6.2.1.assert() 宏函数

assert() 是断言宏函数,它用于程序调试阶段,确保程序在开发过程中满足某些条件。如果断言的条件不满足,程序将输出错误信息并终止执行。这有助于开发者发现和修复代码中的错误。

// 尝试使用 assert() 宏函数
#include <stdio.h>
#include <assert.h>

int main() {
    int user_input;

    // 提示用户输入一个正整数
    printf("Please enter a positive integer: ");
    scanf("%d", &user_input);

    // 使用 assert() 来验证输入是否为正整数
    assert(user_input > 0);

    // 如果 assert() 通过, 继续执行下面的代码
    printf("You entered the positive integer: %d\n", user_input);

    // 程序的其他部分...

    return 0;
}

上述代码中,如果用户输入的是非正整数,就会导致程序提前退出。

6.2.2.const 关键字

const 关键字用于定义常变量,即一旦初始化后其值不能被修改的变量。它也可以用于指针和函数参数,以保证变量的值在特定上下文中不被改变。

// 尝试使用 const 关键字
#include <stdio.h>

int main() {
    const int m = 100;
    const int *p = &m;
    const int **pp = &p;
    int ***ppp = &pp;

    // 下面的代码将导致编译错误, 因为 m 是 const 变量, 不能被赋新值
    // ***ppp = 200;

    printf("%d\n", m);
    return 0;
}

再尝试更多对 const 关键字的测试。

// 更多 const 的测试
#include <stdio.h>

void test1() {
    int n = 10;
    int m = 20;
    int *p = &n;
    *p = 20; // 修改 n 的值, ok
    p = &m;  // p指向新的地址, ok
}

void test2() {
    int n = 10;
    int m = 20;
    const int* p = &n;
    // *p = 20; // 错误, p 指向 const 变量, 不能修改其值
    p = &m; // ok, p可以指向另一个变量
}

void test3() {
    int n = 10;
    int m = 20;
    int *const p = &n;
    *p = 20; // ok, 修改p指向的变量
    // p = &m; // 错误, p 是指向 const 地址的指针, 不能改变其指向
}

int main() {
    test1();
    test2();
    test3();
    return 0;
}

6.3.尝试编写出优秀代码

再来尝试把 assert()const 加入到实际的代码中,下面我将带您模拟实现 strlen()strcpy()

// 模拟实现 strlen() 并加入 assert() 和 const 提高代码健壮性
#include <stdio.h>
#include <assert.h>

int my_strlen(const char *str) {
    int count = 0;
    assert(str);
    while (*str) {
        count++;
        str++;
    }
    return count;
}

int main() {
    int len = my_strlen("abcdef");
    printf("%d\n", len);
    return 0;
}
// 模拟实现 strcpy() 并加入 assert() 和 const 提高代码健壮性
#include <stdio.h>
#include <assert.h>

void my_strcpy(char *dest, char *src) {
    while (*dest++ = *src++) {
        // Copy src over dst
    }
    *dest = '\0'; // Ensure null-termination
}

/* 或者换一种写法
char *my_strcpy(char *dest, char *src) {
    assert(dest && src);
    char *ret = dest;
    while (*dest++ = *src++);
    return ret;
}
*/

补充:VS2022 编译器库内对于 strcpy() 的实现如下。

// 编译器库内对于 strcpy() 的实现之一
char *strcpy(char *dst, const char *src) {
    char *cp = dst;
    assert(dst && src);
    while (*cp++ = *src++);
    return (dst);
}

结语:…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1906041.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

满足信创环境运行的国产FTP服务器是什么样的?

2018 年以来&#xff0c;受“华为、中兴事件”影响&#xff0c;我国科技尤其是上游核心技术受制于人的现状对我 国经济发展提出了严峻考验。在全球产业从工业经济向数字经济升级的关键时期&#xff0c;中国明确 “数字中国”建设战略&#xff0c; 抢占数字经济产业链制高点。 在…

RK3588编译rkmpp,拉取海康威视网络摄像头264码流并运行yolo

硬件&#xff1a;EVB评估版 SOC&#xff1a;Rockchip RK3588 背景&#xff1a; 由于项目需要&#xff0c;需要拉取264码流&#xff0c;并通过将yolov5s.pt将模型转化为rknn模型&#xff0c;获取模型分析结果。取流可以通过软件解码或者硬件解码&#xff0c;硬件解码速度更快&…

tesseract-ocr 字库训练(提高识别率进阶版)

github字库地址&#xff1a;https://github.com/tesseract-ocr/tessdata 一、tesseract-ocr字库训练 1、配置 jdk 环境变量 步骤&#xff08;略&#xff09; 2、安装 tesseract-ocr 并配置环境变量 下载地址&#xff1a;https://digi.bib.uni-mannheim.de/tesseract/ 配置环境…

无人机群辅助边缘计算系统的任务卸载和资源分配联合优化

源自&#xff1a;系统工程与电子技术 作者&#xff1a;刘世豪 黄仰超 胡航 司江勃 韩蕙竹 安琪 注&#xff1a;若出现无法显示完全的情况&#xff0c;可 V 搜索“人工智能技术与咨询”查看完整文章 摘 要 为提升无人机群辅助边缘计算系统在负载不均衡场景下的性能, 构…

红黑树模拟实现

概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或Black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c;红黑树确保没有一条路径会比其他路径长出俩倍&#xff0c;因而是接近平衡…

使用GZip对npm run build打包的vendor.js文件进行压缩

vue-cli项目 安装npm i compression-webpack-plugin -D npm i compression-webpack-plugin -D使用&#xff1a;在vue.config.js文件中 const CompressionPlugin require(compression-webpack-plugin) module.exports {configureWebpack: {plugins: [new CompressionPlugin…

html+css+js贪吃蛇游戏

贪吃蛇游戏&#x1f579;四个按钮控制方向&#x1f3ae; 源代码在图片后面 点赞❤️关注&#x1f64f;收藏⭐️ 互粉必回&#x1f64f;&#x1f64f;&#x1f60d;&#x1f60d;&#x1f60d; 源代码&#x1f4df; <!DOCTYPE html> <html lang"en"&…

【2024最新】Arduino通过Python进行串口通信控制电机

1. 背景 最近想研究一下用 Python 控制 Arduino 的技术&#xff0c;通过上网查询&#xff0c;发现可以用 Python 中的 serial 库来实现和 Arduino 主板的串口通信&#xff0c;从而控制 Arduino。 特此记录一下这个小项目的过程及出现的问题。 2. 基础准备 主板&#xff1a;…

中仕公考:“三支一扶”岗位分别做什么工作?

“三支一扶”计划旨在招募应届毕业生或近两年内毕业的毕业生&#xff0c;部分省份还考虑技工院校高级工班、预备技师班毕业生。在湖北省&#xff0c;报考支医岗位不限制毕业年限&#xff0c;安徽和云南等省对支医类岗位取消了开考比例要求。为解决招人留人难题&#xff0c;艰苦…

毛绒玩具音乐芯片:OTP语音芯片WTN6040方案解析

随着科技的不断发展&#xff0c;智能化和互动性已经成为玩具设计中的关键因素。在毛绒玩具市场中&#xff0c;集成音乐播放功能的毛绒玩具因其趣味性和互动性而备受欢迎。本文将详细介绍OTP&#xff08;One Time Programmable&#xff09;语音芯片WTN6040在毛绒玩具音乐芯片中的…

网页UI:被客户说不大气!大气能当饭吃?真能,最起码保住你饭碗

大气的网页UI设计可以带来以下几个好处&#xff1a; 提升品牌形象&#xff1a;大气的设计能够给用户留下深刻的印象&#xff0c;增强品牌的认知度和形象。通过精心设计的元素、色彩和排版&#xff0c;可以传达出品牌的专业、高端和可信赖的形象。强化用户体验&#xff1a;大气…

SpringBoot开发实用篇(二)

目录 一&#xff1a;Redis 1&#xff1a;SpringBoot整合Redis 2&#xff1a;SpringBoot读写Redis的客户端 3&#xff1a;SpringBoot操作Redis实现技术切换&#xff08;jedis&#xff09; 二&#xff1a;Mongodb 1&#xff1a;Mongodb基础操作 2&#xff1a;SpringBoot整合…

【DataSophon】DataSophon1.2.1 ranger usersync整合

目录 一、简介 二、实现步骤 2.1 ranger-usersync包下载编译 2.2 构建压缩包 2.3 编辑元数据文件 2.4 修改源码 三、重新安装 一、简介 如下是DDP1.2.1默认有的rangerAdmin&#xff0c; 我们需要将rangerusersync整合进来 ,实现将Linux机器上的用户和组信息同步到Ranger…

『粽享端午』交互小程序 小游戏 案例赏析

在这片古老而又年轻的土地上&#xff0c;地域的差异孕育了丰富多彩的饮食文化。粽子&#xff0c;作为端午节的象征&#xff0c;承载着南咸北甜的口味之争&#xff0c;自古便在人们舌尖上演绎着不同的风味传奇。 然而&#xff0c;在快节奏的现代生活洪流中&#xff0c;我们渐渐失…

家谱管理系统

《家谱管理系统》 一个家谱关系由若干家谱记录构成&#xff0c;每个家谱记录由父亲、母亲和子女姓名构成&#xff0c;其中姓名是关 键字。设计并实现一个简单的家谱管理系统。定义一个主菜单&#xff0c;界面友好&#xff0c;演示程序以用户和计算机的对话方式进行&#xff0c…

开关电源——调制模式和工作模式

一、开关电源的调制模式 开关电源作为一种广泛应用于电子设备中&#xff0c;用于将一定电压和电流转换为另一种电压和电流的技术&#xff0c;以下是开关电源三种常见的调制模式&#xff1a; 脉冲宽度调制&#xff08;Pulse Width Modulation&#xff09; 脉冲频率调制&#xff…

触发器编程-创建(CREATE TRIGGER)、删除(DROP TRIGGER)

一、定义 1、触发器&#xff08;Trigger&#xff09;是用户对某一表中的数据做插入、更新和删除操作时被处罚执行的一段程序&#xff0c;通常我们使用触发器来检查用户对表的操作是否合乎整个应用系统的需求&#xff0c;是否合乎商业规则以维持表内数据的完整性和正确性 2、一…

从nginx返回404来看http1.0和http1.1的区别

序言 什么样的人可以称之为有智慧的人呢&#xff1f;如果下一个定义&#xff0c;你会如何来定义&#xff1f; 所谓智慧&#xff0c;就是能区分自己能改变的部分&#xff0c;自己无法改变的部分&#xff0c;努力去做自己能改变的&#xff0c;而不要天天想着那些无法改变的东西&a…

AI视频教程下载-使用ChatGPT成为全栈JavaScript开发者

学习使用Express JS和React JS进行全栈JavaScript开发 ChatGPT Express JS MongoDB React JS Tailwind 解锁全栈网页开发的世界&#xff0c;我们为初学者和中级学习者设计了全面的课程。在这段沉浸式的旅程中&#xff0c;你将深入前端和后端开发的基本概念&#xff0c;为自…

everything高级搜索-cnblog

everything高级搜索用法 基础4选项验证 总结搜索方式 高级搜索搜指定路径文件名: 文件名 路径不含文件名: &#xff01;文件名包含单词 路径包含指定内容: 路径 content:内容 大小写 区分大小写搜索搜指定路径文件名: case:文件名 路径全字匹配 全字搜指定路径文件名: wholewo…