C语言调试大作战:与VS编译器共舞,上演一场“捉虫记”的艺术与科学

简介: C语言调试大作战:与VS编译器共舞,上演一场“捉虫记”的艺术与科学

少年们好,我是博主那一脸阳光,我们接下来介绍C语言的调试和bug的分享。

引言:

“如果你曾经在深夜与一串神秘莫测的C代码狭路相逢,彼此瞪大眼睛,犹如牛仔对决般紧张刺激;或者你曾试图驯服一段狂野不羁的循环,却发现自己陷入了一个深不见底的逻辑黑洞,那么恭喜你,你已经正式加入了C语言调试者联盟!在这个奇妙的编程世界里,我们可不是简单的码农,更像是手持放大镜和捕虫网的侦探,以寻找并消灭那些狡猾又隐秘的’小bug’为己任。今天,让我们一同展开这场充满笑声与泪水、挫折与突破的C语言调试奇幻之旅,看看那些让程序员们既爱又恨的’小家伙们’是如何诞生,又是如何被我们智勇双全地一一拿下!”

关于BUG的由来和故事

"Bug"一词在计算机领域的起源,与硬件故障和软件缺陷有着紧密联系。关于这个词的由来,有两种广为流传的故事:

硬件故障导致的“虫子”事件:

其中一个故事追溯到早期计算机的时代,具体时间大约在20世纪40年代末至50年代初。这个故事描述的是美国哈佛大学的马克-II(Mark II)电子管计算机发生的一次故障。1947年9月9日,计算机科学家格蕾丝·赫柏(Grace Hopper)在检查这台机器时发现了一个由于飞蛾误入继电器触点而引发的故障。工作人员把这只飞蛾从设备中取出,并把它粘贴在了日志本上,旁边标注着“First actual case of bug being found”(首次发现实际的bug)。从此,“bug”开始被广泛用来指代计算机系统中的错误或故障。

工程术语的沿用:

另一个版本的历史背景更为久远,甚至早于计算机的发明。在电气工程和其他工程领域,早在19世纪,“bug”就已经作为行话使用,用来描述机械设备中的小故障或设计问题。托马斯·爱迪生(Thomas Edison)在他的个人通信中就曾使用过“bug”来形容技术问题。当计算机出现后,这个术语自然而然地过渡到了新兴的计算机科学领域,继续用于表示程序或硬件的错误。

无论是哪种起源,最终“bug”成为了软件开发和信息技术行业内的通用词汇,用来指代程序代码中导致程序行为异常的问题。相应的,“debugging”即调试,指的是寻找并修复这些程序错误的过程。

什么是调试(debug)

当我们发现程序中存在的问题的时候,那下⼀步就是找到问题,并修复问题。

这个找问题的过程叫称为调试,英⽂叫debug(消灭bug)的意思。

调试⼀个程序,⾸先是承认出现了问题,然后通过各种⼿段去定位问题的位置,可能是逐过程的调

试,也可能是隔离和屏蔽代码的⽅式,找到问题所的位置,然后确定错误产⽣的原因,再修复代码,

重新测试。

Debug和Release

在VS上编写代码的时候,就能看到有 debug 和 release 两个选项,分别是什么意思呢

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

程序员在写代码的时候,需要经常性的调试代码,就将这⾥设置为 debug ,这样编译产⽣的是

debug 版本的可执⾏程序,其中包含调试信息,是可以直接调试的。

Release 称为发布版本,它往往是进⾏了各种优化,使得程序在代码⼤⼩和运⾏速度上都是最优的,

以便⽤⼾很好地使⽤。当程序员写完代码,测试再对程序进⾏测试,直到程序的质量符合交付给⽤⼾

使⽤的标准,这个时候就会设置为 release ,编译产⽣的就是 release 版本的可执⾏程序,这个

版本是⽤⼾使⽤的,⽆需包含调试信息等。

对⽐可以看到从同⼀段代码,编译⽣成的可执⾏⽂件的⼤⼩,release版本明显要⼩,⽽debug版本明

显⼤。

VS牛仔侦探福尔摩斯的快捷键西部抓“BUG”大戏

欢迎来到编程世界的狂野西部,这里的治安官不叫警长,而是一位身怀绝技的“VS牛仔侦探福尔摩斯”。他以Visual Studio为马鞍,代码行作为疆界,手握一连串神奇的快捷键法器,誓要在这片充满神秘bug的边陲之地,上演一场又一场惊心动魄、笑料百出的抓“小偷”(BUG)好戏。

当夜幕降临,小镇沉睡,罪恶悄然滋生。别慌,瞧!我们的“VS牛仔侦探福尔摩斯”已经整装待发:

F9:设置断点,犹如布下天罗地网 —— 就像牛仔在小偷必经之路埋伏设陷阱,等待他们自投罗网;

F10:逐过程追踪,玩转逻辑迷宫 —— 福尔摩斯抽着烟斗,步步为营,每按一下F10就如同推开一扇门,揭示下一个可能藏匿犯罪线索的房间;

F11:步入函数内部,深挖地下暗道 —— 牛仔侦探奋不顾身跳入深深的洞穴,钻进函数的核心地带,挖掘那些潜藏最深的小bug;

F5:启动调试列车,全速追捕逃犯 —— 随着一声汽笛长鸣,搭载着智慧与勇气的F5列车疾驰而出,直奔目标,将那些试图逃脱的小bug逮个正着!

还有那神秘的Ctrl+5,这可是牛仔侦探的秘密武器,它如同瞬间移动到任意场景的魔法帽,让侦探能够迅速切换至第五个监视窗口,随时洞察案发现场的每个角落,确保任何蛛丝马迹都无处遁形。

准备好你的键盘和鼠标,跟随这位集西部牛仔的勇猛与福尔摩斯般智谋于一身的“VS牛仔侦探福尔摩斯”,踏上这场风趣幽默、紧张刺激的编程西部冒险,看他是如何运用这些神奇快捷键,在虚拟世界中惩治邪恶,维护程序正义的吧!

总结:

那程序员怎么调试代码呢?

F9:创建断点和取消断点

断点的作⽤是可以在程序的任意位置设置断点,打上断点就可以使得程序执⾏到想要的位置暂定执

⾏,接下来我们就可以使⽤F10,F11这些快捷键,观察代码的执⾏细节。

条件断点:满⾜这个条件,才触发断点

F5:启动调试,经常⽤来直接跳到下⼀个断点处,⼀般是 和F9配合使⽤。

F10:逐过程,通常⽤来处理⼀个过程,⼀个过程可以是⼀次函数调⽤,或者是⼀条语句。

F11:逐语句,就是每次都执⾏⼀条语句,但是这个快捷键可以使我们的执⾏逻辑进⼊函数内部。在函数调⽤的地⽅,想进⼊函数观察细节,必须使⽤F11,如果使⽤F10,直接完成函数调⽤。

CTRL + F5:开始执⾏不调试,如果你想让程序直接运⾏起来⽽不调试就可以直接使⽤。

实际的使用

我们发现加了断点以后,调试的时候,居然跳过了断点前的代码,这样通过断点减少了程序员遇到的问题,想象一下有一万行代码前五千行代码都没得问题,如果一行一行调试即使你摁F10速度再快依旧需要很长时间,但是我们有了断点以后发现了节省了很多时间,让程序员对修改代码,查找的bug方式给方便和简洁。

F10的使用和F11的使用

F10:是过程意思是说,它会一条一条往下执行到了,test哪里它不会进入函数的内部而是直接算出结果,

F11:F11就是可以进入函数的内部一条一条的进行调试以及使用。 ``F11比F10更注重细节。

不调试代码直接执行用CTRL+F5即可

监视以及断点的实际应用

C语言通过监视窗口可以看到i是如何递增的,在正常情况下这段代码i会打印到9以后,因为不满足接下来的循环条件而结束循环,但是如果我们想看第八次循环呢?那么是不是摁F10来实现呢?这里就需要了F9断点加上限制条件以及使用。

设计条件即可,这时候我们摁F10开始调试以后,我们就可以看到i从八开始进行打印以及循环的使用。

编程界的福尔摩斯

欢迎来到编程界的伦敦贝克街221B,只不过这里的侦探并非那位叼着烟斗、手持放大镜的福尔摩斯先生,而是我们英勇无畏的“VS福尔摩斯”。在这个由代码构筑的城市里,小偷不再偷金银珠宝,他们专偷程序的稳定性和效率——这些捣蛋鬼就是我们俗称的"BUG"。

我们的主人公“VS福尔摩斯”,凭借其无所不能的Visual Studio监视器和内存检查工具两大法宝,在错综复杂的函数丛林中游刃有余。他用监视器如鹰眼般精准定位每个变量的一举一动,通过内存观察则像显微镜一样洞察那些隐匿在数据结构深处的小bug们。

每当夜幕降临,城市陷入宁静,程序员们的噩梦却悄然开始。然而,不用担心,因为“VS福尔摩斯”已整装待发,他以调试窗口为笔记本,以断点为陷阱,挥舞着单步执行的大侦探手杖,誓要将每一个企图破坏代码和谐的小bug“窃贼”捉拿归案。

这段充满欢笑与智慧的抓“贼”故事即将上演,让我们跟随“VS福尔摩斯”的脚步,一同踏上这场妙趣横生又充满挑战的编程冒险之旅,看他是如何利用强大的VS工具,如同现实世界中的福尔摩斯那样,破解一个个看似无解的编程谜团,将那些令人头疼不已的小bug“小偷”一网打尽!

比如:`

#include <stdio.h>`
int main()
{
  int arr[10] = { 0 };
  int num = 100;
  char c = 'w';
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    arr[i] = i;
  }
  return 0;
}
我们来分析一下上面的代码
```c
#include <stdio.h>

这一行是预处理器指令,它告诉编译器在编译之前先包含stdio.h头文件。stdio.h(Standard Input Output)包含了标准输入输出函数的声明,例如 printf() 和 scanf() 等。虽然在这个示例中并未使用这些函数,但在实际编程中,我们通常会包含这个头文件以备后续可能的输入输出操作。

C
int main()
{

main() 函数是 C 语言程序执行的起点,它是程序的入口点。main() 返回一个整数值,用来表示程序的退出状态,0 表示正常结束。

C
    int arr[10] = { 0 };

这里声明了一个具有10个元素的一维整数数组 arr,并使用初始化列表 { 0 } 初始化所有元素为0。实际上,只给一个初始值时,C语言会将剩余的元素也默认初始化为该初始值,所以数组 arr 的所有元素都被初始化为0。

C
    int num = 100;
    char c = 'w';

接着声明了两个变量:一个整型变量 num 赋值为100,另一个字符型变量 c 赋值为字符 ‘w’。不过,在这段代码片段中,这两个变量没有被进一步使用,它们可能是为了展示如何声明和初始化不同类型的变量。

C
    int i = 0;

声明并初始化一个整数变量 i 作为循环计数器,它的初始值设为0。

C
    for (i = 0; i < 10; i++)
    {
        arr[i] = i;
    }

这是一个经典的for循环结构。循环从 i 为0开始,每次循环迭代都会递增 i 的值,直到 i 不再小于10为止。在循环体内,数组 arr 的每个元素通过下标 i 被赋值为当前 i 的值。因此,当循环结束后,数组 arr 的内容将是 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]。

C
    return 0;
}

最后,main() 函数返回0,表明程序已成功执行完毕且无错误。

综上所述,此段C语言代码的主要功能是对一个大小为10的整数数组 arr 进行初始化,使得其存储从0到9的连续整数。同时展示了如何声明和初始化其他类型(如整型和字符型)的变量,尽管在这段特定代码中它们未被实际使用。

好,我们接下来来观察一下内存。

很多少年在这里看不懂了,这是什么啊,这其实就是内存的一个分配,这设计到一个东西叫做函数栈帧后期会给大家分享,剩下就是我们如何理解这个内存呢?我们在地址处输入arr,arr是一个数组,数组本身就是一个地址,正常要用&操作符的,但是arr是一个数组所以不需要。

调试举例

求 1!+2!+3!+4!+…10! 的和,我们知道阶乘的公式是要求1!+2!+3!+4!+…+10!的和,首先我们明确阶乘(Factorial)的定义:对于任意正整数n,n!表示的是所有小于等于n且大于0的整数的乘积。

公式如下: n! = 1 * 2 * 3 * … * (n-1) * n

所以,要计算1!到10!的和,我们可以逐个计算每个数的阶乘然后相加。下面是具体计算过程:

Markdown
1! = 1
2! = 2 * 1 = 2
3! = 3 * 2 * 1 = 6
4! = 4 * 3 * 2 * 1 = 24
5! = 5 * 4 * 3 * 2 * 1 = 120
6! = 6 * 5 * 4 * 3 * 2 * 1 = 720
7! = 7 * 6 * 5 * 4 * 3 * 2 * 1 = 5040
8! = 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 = 40320
9! = 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 = 362880
10! = 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 = 3628800
求和:
1! + 2! + 3! + 4! + ... + 9! + 10!
= 1 + 2 + 6 + 24 + 120 + 720 + 5040 + 40320 + 362880 + 3628800

将上述阶乘值相加得到最终结果。由于这是一个手动计算的过程,我这里直接给出计算器的结果:

1! + 2! + 3! + 4! + ... + 9! + 10! = 5,006,550

请看下⾯的代码:

#include <stdio.h>
//写⼀个代码求n的阶乘
int main()
{
 int n = 0;
 scanf("%d", &n);
 int i = 1;
 int ret = 1;
 for(i=1; i<=n; i++)
 {
 ret=ret*i;
 }
return 0;
}

特例代码和内存

  1. 栈区内存的使⽤习惯是从⾼地址向 低地址使⽤的,所以变量i的地址是 较⼤的。arr数组的地址整体是⼩ 于i的地址。
  2. 数组在内存中的存放是:随着下标 的增⻓,地址是由低到⾼变化的。 所以根据代码,就能理解为什么是左 边的代码布局了。 如果是左边的内存布局,那随着数组 下标的增⻓,往后越界就有可能覆盖 到i,这样就可能造成死循环的。为什么i和arr
    数组之间恰好空出来2个整型的空间 呢?这⾥确实是巧合,在不同的编译 器下可能中间的空出的空间⼤⼩是不 ⼀样的,代码中这些变量内存的分配
    和地址分配是编译器指定的,所以的 不同的编译器之间就有差异了。所以 这个题⽬是和环境相关的。

从这个理解我们能够体会到调试的重要性,只有调试才能观察到程序内部执⾏的细节,就像医⽣给病 ⼈做B超,CT⼀样。在这片充满狂野西部风情的C语言编程疆域,我们摇身一变成为Visual Studio(VS)编译舞台上的牛仔程序员,骑着代码骏马,在栈区、堆区和静态存储区这三大内存荒原上演绎了一场别具一格、笑料百出的捉虫大戏。这些如同草原上狡猾狐狸的小bug们,总是在不经意间躲进内存区域的沟壑之间,却不知我们的牛仔眼明手快,正握紧VS编译器这把熠熠生辉的六发左轮,准备在内存的广袤天地中来一场酣畅淋漓的追逐赛。

我们在栈区狭长的峡谷里与递归bug斗智斗勇,犹如牛仔在曲折蜿蜒的山道上追踪狡猾的羚羊;在堆区那片辽阔无垠的草原上,面对动态分配带来的迷雾重重,我们化身成精准定位的猎人,一次次击中那些藏匿其中的bug群落;而在静态区永恒不变的石漠之中,我们探寻全局变量的神秘踪迹,展现出了牛仔般的坚韧与耐心。

未来的编程拓荒之路上,让我们戴上牛仔帽,腰挎VS编译器这把威力无比的“Bug终结者”,在栈、堆、静态区的三重奏中翩翩起舞,演绎更多令人捧腹又不失智慧的“牛仔捉虫记”。不论是遭遇坎坷还是收获荣耀,每个在VS编译舞台上挥洒汗水的“内存牛仔”都将化身为英勇且诙谐的英雄,用他们的奇思妙想和过硬技能,为这片编程世界的内存荒原带来欢笑与和谐!

相关文章
|
7月前
|
存储 编译器 C语言
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存2
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存
|
7月前
|
程序员 C语言 C++
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存1
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存
|
7月前
|
编译器 C语言 C++
【C语言】手把手教你配置VS的常见函数如何不报错!
【C语言】手把手教你配置VS的常见函数如何不报错!
|
2月前
|
编译器 C语言
C语言编译器为什么能够用C语言编写?
C语言编译器为什么能够用C语言编写?
43 9
|
22天前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
41 7
|
7月前
|
C语言
C语言使用宏定义实现等级调试输出PRINT_LEVEL
C语言使用宏定义实现等级调试输出PRINT_LEVEL
143 0
|
24天前
|
NoSQL 编译器 C语言
C语言调试是开发中的重要技能,涵盖基本技巧如打印输出、断点调试和单步执行,以及使用GCC、GDB、Visual Studio和Eclipse CDT等工具。
C语言调试是开发中的重要技能,涵盖基本技巧如打印输出、断点调试和单步执行,以及使用GCC、GDB、Visual Studio和Eclipse CDT等工具。高级技巧包括内存检查、性能分析和符号调试。通过实践案例学习如何有效定位和解决问题,同时注意保持耐心、合理利用工具、记录过程并避免过度调试,以提高编程能力和开发效率。
41 1
|
26天前
|
存储 算法 C语言
用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容
本文探讨了用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容,旨在为开发者提供全面的指导和灵感。
45 2
|
2月前
|
C语言
C语言调试
C语言调试
24 0
|
4月前
|
C语言 索引
C语言编译环境中的 调试功能及常见错误提示
这篇文章介绍了C语言编译环境中的调试功能,包括快捷键操作、块操作、查找替换等,并详细分析了编译中常见的错误类型及其解决方法,同时提供了常见错误信息的索引供参考。