实用调试技巧
- 什么是bug
- 调试是什么,调试有何重要
- 调试是什么?
- 调试的基本步骤
- Debug和Release的介绍。
- Windows环境调试介绍
- 调试快捷键
- 调试的时候查看程序当前信息
- 查看临时变量的值
- 查看内存信息
- 查看调用堆栈
- 查看汇编信息
- 查看寄存器信息
- 小tips:
- 如何写出好(易于调试)的代码
-
- 编程常见的错误
-
什么是bug
Bug一词的原意是“臭虫”或“虫子”。但是现在,在电脑系统或程序中,如果隐藏着的一些未被发现的缺陷或问题,人们也叫它“Bug”,这是怎么回事呢?
原来,第一代的计算机是由许多庞大且昂贵的真空管组成,并利用大量的电力来使真空管发光。可能正是由于计算机运行产生的光和热,引得一只小虫子Bug 钻进了一支真空管内,导致整个计算机无法工作。研究人员费了半天时间,总算发现原因所在,把这只小虫子从真空管中取出后,计算机又恢复正常。后来,Bug这个名词就沿用下来,表示电脑系统或程序中隐藏的错误、缺陷或问题。
与Bug相对应,人们将发现Bug并加以纠正的过程叫做“Debug”,意即“捉虫子”或“杀虫子”。遗憾的是,在中文里面,至今仍没有与“Bug”准确对应的词汇,于是只能直接引用“Bug”一词。虽然也有人使用“臭虫”一词替代“Bug”,但容易产生歧义,所以推广不开。
所谓“(Bug)”,是指电脑系统的硬件、系统软件(如操作系统)或应用软件(如文字处理软件)出错。硬件的出错有两个原因,一是设计错误,一是硬件部件老化失效等。软件的错误全是厂家设计错误。那种说用户执行了非法操作的提示,是软件厂商不负责的胡说八道。用户可能会执行不正确的操作,比如本来是做加法但按了减法键。这样用户会得到一个不正确的结果,但不会引起bug发作。软件厂商在设计产品时的一个基本要求,就是不允许用户做非法的操作。只要允许用户做的,都是合法的。用户根本就没有办法知道厂家心里是怎么想的,哪些操作序列是非法的。
从电脑诞生之日起,就有了电脑BUG。第一个有记载的bug是美国海军的编程员,编译器的发明者格蕾斯·哈珀(GraceHopper)发现的。哈珀后来成了美国海军的一个将军,领导了著名计算机语言Cobol的开发。
1945年9月9日,下午三点。哈珀中尉正领着她的小组构造一个称为“马克二型”的计算机。这还不是一个完全的电子计算机,它使用了大量的继电器,一种电子机械装置。第二次世界大战还没有结束。哈珀的小组日以继夜地工作。机房是一间第一次世界大战时建造的老建筑。那是一个炎热的夏天,房间没有空调,所有窗户都敞开散热。
突然,马克二型死机了。技术人员试了很多办法,最后定位到第70号继电器出错。哈珀观察这个出错的继电器,发现一只飞蛾躺在中间,已经被继电器打死。她小心地用摄子将蛾子夹出来,用透明胶布帖到“事件记录本”中,并注明“第一个发现虫子的实例。”
从此以后,人们将计算机错误戏称为虫子(bug),而把找寻错误的工作称为(debug)。
程序中隐藏的功能缺陷或错误。由于现在的软件复杂程度早已超出了一般人能控制的范围,如Win95、Win98这样的较成熟的操作系统也会不定期地公布其中的Bug。如何减少以至消灭程序中的Bug,一直是程序员所极为重视的课题。
调试是什么,调试有何重要
所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧,就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。
顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。
一名优秀的程序员是一名出色的侦探。每一次调试都是尝试破案的过程。
有的人在平时是怎么写代码呢
那么遇到bug是如何进行代码错误的排查呢?
上面说的这种写代码排查过程属于迷信式排查,我们应拒绝迷信式排查
调试是什么?
调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
调试的基本步骤
- 发现程序错误的存在
- 以隔离、消除等方式对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新测试
在日后的工作中程序员写代码自己找到错误还好,让测试人员找到你写的bug对你自己的评价会有影响,但是如果在用户使用的过程中出现了bug,就会给公司带来损失。所以程序员应该避免写bug,写出bug来了自己也能找到bug,并且解决它。不要总是以来网络答疑来解决,日后工作在封闭环境中断网你靠网络答疑能解决吗?所以掌握调试技巧非常重要
Debug和Release的介绍。
Debug: 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release: 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
上图经过了两次编译生成的两个可执行程序.exe文件,Relase环境下比Debug环境下生成的文件所占内存空间明显更小,印证了Relase环境下是进行了各种优化时期空间更少。
我们说调试就是在Debug版本的环境中,找代码中潜伏的问题的一个过程。
release 模式会对代码进行优化避免一些bug的出现
例子:
#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;
}
上面的这段代码在vs2019编译器
- 运行如果是 debug 模式去编译,程序的结果是死循环。
- 如果是 release 模式去编译,程序没有死循环。
产生上面这种情况的原因也是代码优化的结果
Windows环境调试介绍
注:linux开发环境调试工具是gdb。
这里就先介绍vs编译器下的调试
调试快捷键
F5
启动调试,经常用来直接跳到下一个断点处。
F9
创建断点和取消断点
断点的重要作用,可以在程序的任意位置设置断点。
这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
F10
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最
长用的)。
CTRL + F5
开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
其他更多调试快捷键
调试的时候查看程序当前信息
查看临时变量的值
查看临时变量的值有三个窗口,监视,局部变量,自动窗口。其中自动窗口和局部变量都是查看所在函数内部变量的信息有些信息自己不需要但也会展示,局部窗口和自动窗口的区别是一个观察整个函数一个观察执行步骤周围的变量,但都不能够观察你函数外的信息。监视窗口可以让使用者根据自己的需求监视变量,监视窗口有4个,如果电脑屏幕足够大可以由四个窗口分工监视变量。
监视可以查看的更符合程序员的需求但需要自己输入,大部分还是用的监视。局部变量和自动窗口用的更省心,在很简单的程序中用起来方便。
在vs编译器找到三个窗口,(注:只能在调试的状况下才能找到)
小tips:
查看内存信息
在调试开始之后,用于观察内存信息
查看调用堆栈
通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。
查看汇编信息
在调试开始之后,有两种方式转到汇编
第一种方式:右击鼠标,选择【转到反汇编】:
第二种方式:
不同的编译器对应着不同的汇编代码,所以汇编代码不用特意去学,何时用何时学。
查看寄存器信息
小tips:
- 一定要熟练掌握调试技巧。
- 初学者可能80%的时间在写代码,20%的时间在调试。但是一个程序员可能>20%的时间在写
- 程序,但是80%的时间在调试。
- 我们所讲的都是一些简单的调试。
- 以后可能会出现很复杂调试场景:多线程程序的调试等。
- 多多使用快捷键,提升效率。
如何写出好(易于调试)的代码
何为优秀的代码
- 代码运行正常
- bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
常见的coding技巧:
- 使用assert
- 尽量使用const
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱。
优秀代码示范
模拟实现库函数:strcpy
以往的思路
#include<stdio.h>
void my_strcpy(char* dest, char* soure)
{
while (*soure != '\0')
{
*dest = *soure;
++dest;
++soure;
}
*dest = *soure;
}
int main()
{
char arr1[20] = { 0 };
char arr2[] = "hello world";
my_strcpy(arr1, arr2);
printf("%s", arr1);
return 0;
}
在官方库函数解释中,这个库函数有返回值,可以直接用函数的返回值打印复制字符串。形参二有const修饰使形参二中的数据不会被改变只起到复制粘贴板的作用。
改进版
#include<stdio.h>
char* my_strcpy(char* dest, const char* src)
{
char* start = dest;
while (*soure != '\0')
{
*dest = *soure;
++dest;
++soure;
}
*dest = *soure;
return start;
}
int main()
{
char arr1[20] = { 0 };
char arr2[] = "hello world";
printf("%s", my_strcpy(arr1, arr2));
return 0;
}
但是上述模拟函数还是有一些问题,如果两个参数传进去的地址为NULL的话,不会警告错误,我们就无法准确找到是否传参问题。这样我们就用到了assert函数,该函数是断言函数,函数内输入判断表达式,为真继续运行,为假程序运行终止于此,头文件assert.h。
在复制过程中循环语句内部可以改变一下会更简洁,
再次改进版:
#include<stdio.h>
#include<assert.h>
char* my_strcpy(char* dest, const char* src)
{
assert(dest != NULL);
assert(src != NULL);
char* start = dest;
while ((*dest++=*src++) != '\0')
{
;
}
return start;
}
int main()
{
char arr1[20] = { 0 };
char arr2[] = "hello world";
printf("%s", my_strcpy(arr1, arr2));
return 0;
}
const的作用
#include<stdio.h>
int main()
{
int a = 20;
int b = 50;
const int* pa1 = &a;
pa1 = &b;
int* const pa2 = &a;
*pa2 = 30;
const int* const pa3 = &a;
return 0;
}
const修饰指针变量的时候:
- const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改
变。但是指针变量本身的内容可变。 - const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
模拟实现一个strlen函数
#include<stdio.h>
#include<assert.h>
int my_strlen1(const char* str)
{
assert(str != NULL);
if (*str == '\0')
{
return 0;
}
return 1 + my_strlen1(str + 1);
}
int my_strlen2(const char* str)
{
assert(str != '\0');
int num = 0;
while (*str != '\0')
{
++num;
++str;
}
return num;
}
int my_strlen3(const char* str)
{
assert(str != '\0');
char* start = str;
while (*str != '\0')
{
++str;
}
return (str - start);
}
int main()
{
printf("%d\n", my_strlen1("abcdef"));
printf("%d\n", my_strlen1("abcdef"));
printf("%d\n", my_strlen1("abcdef"));
return 0;
}
编程常见的错误
译型错误
编译性错误指语法错误,比如少个;,或者;写成中文。像这种直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。
链接型错误
这种错误实在链接过程中出现的,标识符未定义,比如使用未定义的函数,或者函数名命名错误,这种不存在语法错误。排查错误时看错误提示信息,主要在代码中按键ctrl+f搜索找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
运行时错误
这种错误是最常见的错误有两种可能会实现,一是程序设计时数组越界或者栈溢出,二是程序设计思路导致程序异常不能得到预期结果。这种最难搞,需要借助调试,逐步定位问题。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)