C语言调试技巧

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

文章目录

  • 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 的值, okp = &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.mzph.cn/bicheng/42316.shtml

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

相关文章

RedHat运维-LinuxSELinux基础3-用布尔值调整SELinux政策

1. 获取所有的布尔值SELinux政策的命令是_____________________________&#xff1b; 2. 获取所有的布尔值SELinux政策的命令是_____________________________&#xff1b; 3. 获取所有的布尔值SELinux政策的命令是_____________________________&#xff1b; 4. 获取httpd_ena…

嵌入式c语言5——位运算符

<<与>>是c语言中两个移位运算符&#xff0c;分别有乘以2与除以2的意义 位运算符还包括&#xff0c;与&&#xff0c;或|&#xff0c;均进行按位操作 同时&#xff0c;还可以进行取反以及异或操作

synchronized和ReentrantLock

ReentrantLock ReentrantLock fairLock new ReentrantLock(true);// 这里是演示创建公平锁&#xff0c;一般情况不需要。 fairLock.lock(); try {// do something } finally {fairLock.unlock(); }

商家店铺电商小程序模板源码

橙色通用的商家入驻&#xff0c;商户商家&#xff0c;商家店铺&#xff0c;购物商城&#xff0c;商家购物平台app小程序网页模板。包含&#xff1a;商家主页、优先商家、商品详情、购物车、结算订单、个人中心、优惠券、会员卡、地址管理等功能页面。 商家店铺电商小程序模板源…

Spark SQL----内置函数Csv Functions Misc Functions

Spark SQL----内置函数Csv Functions& Misc Functions Csv Functions 例子&#xff1a; -- from_csv SELECT from_csv(1, 0.8, a INT, b DOUBLE); ---------------- |from_csv(1, 0.8)| ---------------- | {1, 0.8}| ----------------SELECT from_csv(26/08/2015,…

《妃梦千年》第二十六章:风暴前夕

第二十六章&#xff1a;风暴前夕 经过一系列的斗争&#xff0c;宫中的局势暂时得到了稳定。然而&#xff0c;林清婉知道&#xff0c;这一切只是表面的平静&#xff0c;敌人不会轻易放弃&#xff0c;他们必定会再次发动进攻。为了迎接即将到来的挑战&#xff0c;林清婉决定进一…

基于CentOS Stream 9平台搭建RabbitMQ3.13.4以及开机自启

1. erlang与RabbitMQ对应版本参考&#xff1a;https://www.rabbitmq.com/which-erlang.html 2. 安装erlang 官网&#xff1a;https://www.erlang.org/downloads GitHub: https://github.com/rabbitmq/erlang-rpm/releases 2.1 安装依赖&#xff1a; yum -y install gcc glib…

Nginx实战:nginx性能压测(ab)

在nginx的生产实践中,不管是服务上线,还是性能优化,都会遇到需要对nginx的性能压测,本文介绍一个简单的压测工具:ab命令 ab(Apache Bench)是一个常用的HTTP压力测试工具,可以用来测试Nginx的性能和压力。ab命令可以指定并发请求数、请求数、请求类型等参数,并输出测试…

显示近期用户登录情况的命令last

显示近期用户登录情况的命令last There is no nutrition in the blog content. After reading it, you will not only suffer from malnutrition, but also impotence. The blog content is all parallel goods. Those who are worried about being cheated should leave quick…

Spring Boot中集成DJL运行Python PyTorch模型:MNIST实战

Java Spring Boot 使用DJL 部署python训练的PyTorch模型&#xff08;MNIST&#xff09; Python 训练Pytorch模型Java Spring Boot使用DJL调用模型 pom.xml TranslatorCriteriaNDArray 创建NDArray(Tensor) 变更数据类型运算切片赋值翻转 PredictorMNIST实战 环境 Java 使用 …

算法力扣刷题总结篇 ——【四】

前言 栈和队列篇题目学习结束。 总结&#xff1a; 一、结构基础 stack类 &#xff08;1&#xff09;使用栈&#xff08;后入先出&#xff09;的结构时&#xff0c;用它。 &#xff08;2&#xff09;容器适配器&#xff08;container adaptor&#xff09;。本身不算容器&…

550kg级大载重长航时无人机直升机技术详解

550kg级大载重长航时无人机直升机&#xff0c;作为一种高性能的无人机系统&#xff0c;具备了多项先进的技术特点&#xff0c;以满足高海拔、高寒等复杂环境下的应用需求。这些无人机直升机通常具备高载重、长航时、强适应性、高可靠性和良好的任务拓展性。 设备由无人直升机平…

【面试八股文】java基础知识

引言 本文是java面试时的一些常见知识点总结归纳和一些拓展&#xff0c;笔者在学习这些内容时&#xff0c;特地整理记录下来&#xff0c;以供大家学习共勉。 一、数据类型 1.1 为什么要设计封装类&#xff0c;Integer和int区别是什么&#xff1f; 使用封装类的目的 对象化:…

数据结构——单链表专题

前言 本篇我们继续来介绍数据结构的知识——链表&#xff0c;这与顺序表是一个类别的知识&#xff0c;同样也非常重要&#xff0c;所以大家需要掌握好链表的内容&#xff0c;这对我们后面数据结构的学习也很重要&#xff0c;如果你对本篇的内容感兴趣&#xff0c;希望大佬可以…

使用RabbitMQ实现可靠的消息传递机制

使用RabbitMQ实现可靠的消息传递机制 大家好&#xff0c;我是微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 1. RabbitMQ简介 RabbitMQ是一个开源的消息代理软件&#xff0c;实现了高级消息队列协议&#xff08;AMQP&…

四大常见的排序算法JAVA

1. 冒泡排序 相邻的元素两两比较&#xff0c;大的放右边&#xff0c;小的放左边 第一轮比较完毕之后&#xff0c;最大值就已经确定&#xff0c;第二轮可以少循环一次&#xff0c;后面以此类推 如果数组中有n个数据&#xff0c;总共我们只要执行n-1轮的代码就可以 package Bu…

mq虚拟主机实现数据隔离

如果一个mq系统需要被多个实际业务系统共用&#xff0c;那么我们需要对于不同的业务系统分设不同的虚拟机进行数据隔离 本文将主要演示如何使用mq的web端进行创建不同虚拟机完成数据隔离 首先我们需要针对新的业务系统创建一个专属的用户&#xff0c;并赋予admin权限&#xf…

【matlab】状态空间模型与传递函数模型的建立与转换

目录 SISO系统 MIMO系统 状态空间模型 状态空间模型到传递函数模型的转换 传递函数模型到状态空间模型的转换 (1) 转换函数ss() (2) 规范形转换函数canon() (3) 常微分方程(传递函数)转换为状态空间模型函数dif2ss() 状态空间模型的变换 特征值、特征向量与广义特征向量的计算…

everything高级搜索-cnblog

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

力扣刷题--908. 最小差值 I【简单】

题目描述 给你一个整数数组 nums&#xff0c;和一个整数 k 。 在一个操作中&#xff0c;您可以选择 0 < i < nums.length 的任何索引 i 。将 nums[i] 改为 nums[i] x &#xff0c;其中 x 是一个范围为 [-k, k] 的整数。对于每个索引 i &#xff0c;最多 只能 应用 一次…