前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >了解bug以及如何解决bug------调试(使用技巧)

了解bug以及如何解决bug------调试(使用技巧)

作者头像
摘星
发布2023-04-28 10:02:34
6960
发布2023-04-28 10:02:34
举报
文章被收录于专栏:C/C++学习

前言

人非圣贤孰能无过,我们在编写程序代码的时候,或多或少都会有一些计算机程序错误(bug)出现。

可能是编译型错误:一般是语法错误,看错误提示信息就能解决;

也可能是链接型错误:一般是标识符名不存在(未声明)或者标识名符名的拼写错误

但最让人头疼的还是运行时的错误:看不懂的英文版错误提示,甚至有时候都没有错误提示,这时候要找到出现问题的位置就很困难了,为了解决这类bug,我们本次文章将引入一个新的名词------调试。

如果你也和我一样,常常因为找不到程序中的bug而苦恼,每天迷信式修改bug,修改成功了不知道为什么成功,修改失败了,也不知道为什么失败,那么请仔细阅读这篇文章,相信你会收获颇多。

一、bug

1.谁会发现bug?

  1. 程序员自己
  2. 测试人员
  3. 用户

2.如何发现并解决bug?(步骤)

  1. 通过隔离、删除等方式对bug进行定位
  2. 确定bug产生的原因
  3. 提出纠正bug的办法
  4. 对程序错误予以改正,并且重新测试

二、调试

1.调试是什么?为什么要进行调试?

调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中 程序 错误的一个过程。

我们为什么要进行调试呢?

每次程序运行,我们只能看到程序运行的最终结果,而不知道程序运行的过程中到底发生了什么。举个例子:当实际输出值和预期输出值不同,我们不能只通过表面上的几行代码来确定到底是哪一步运行错误了。而调试可以带我们走进程序运行的过程,帮我们确定到底是运行的哪一步出现错误,所以可以通过调试找出错误。

2.调试的环境

(作者本人在学习C语言的过程中使用的是Windows环境下的VS2013,所以本次讲解的调试技巧以及范例测试都是在VS2013上进行的,其他环境下的调试方法也都相类似,本文仅供参考)

要设置调试的环境,我们首先要了解和调试有关的概念------版本:

  • Debug:调试版本,包含调试信息(我们进行调试时就要将程序调整到这个版本下)
  • release:发布版本,相较于调试版本,他进行了更多的优化,使程序在内存大小和运行速度上优于调试版本,以便用户得到更好的对用体验。(release版本不能进行调试)

具体位置如图所示:

e23e114bd9e4425cadce676aaa5c1a55.png
e23e114bd9e4425cadce676aaa5c1a55.png

 ​​我创建的项目名叫Debugging,首先分别在程序中运行debug版本和release版本,再打开程序所在的文件夹,里面会产生debug和release两个文件夹。由下面两张图片可以对比看出release版本在内存上比debug版本小了很多。

5aede5c74b7249d0ab67b90d64c9e33f.png
5aede5c74b7249d0ab67b90d64c9e33f.png

①debug文件打开后的内容:

1aa67c4cf6524de3babd825b7fbcca02.png
1aa67c4cf6524de3babd825b7fbcca02.png

 ②release文件打开后的内容:

7cc414be452a472da59f2e2c0c299d85.png
7cc414be452a472da59f2e2c0c299d85.png

3.调试的快捷键

(只列举了几个常用的,如果有需要之后会专门整理一次)

gif.latex?F_%7B5%7D
gif.latex?F_%7B5%7D

//启动调试,运行到下一个断点处;

gif.latex?F_%7B9%7D
gif.latex?F_%7B9%7D

  //(一般和

gif.latex?F_%7B5%7D
gif.latex?F_%7B5%7D

搭配使用)创建断点和取消断点;

断点: ①可以在程序的任意位置设置断点,从而使程序在想要的地方停止再一步一步运行下去; ②可以通过设置断点,跳过之前的正常代码直接运行到断点处; ③可以通过设置断点范围,将程序停止在某一次的循环或者递归。

gif.latex?F_%7B10%7D
gif.latex?F_%7B10%7D

//逐语句运行代码;

gif.latex?F_%7B11%7D
gif.latex?F_%7B11%7D

//逐句运行代码,与

gif.latex?F_%7B10%7D
gif.latex?F_%7B10%7D

的区别:使用

gif.latex?F_%7B11%7D
gif.latex?F_%7B11%7D

可以使执行逻辑进入所调用的函数内部(常用)

  • Ctrl+
gif.latex?F_%7B5%7D
gif.latex?F_%7B5%7D

  //直接运行程序,不进行调试

如果直接使用

gif.latex?F_%7B10%7D
gif.latex?F_%7B10%7D

gif.latex?F_%7B11%7D
gif.latex?F_%7B11%7D

等快捷键不起作用,可以尝试用

gif.latex?F_%7Bn%7D
gif.latex?F_%7Bn%7D

+

gif.latex?F_%7Bx%7D
gif.latex?F_%7Bx%7D

(

gif.latex?F_%7Bx%7D
gif.latex?F_%7Bx%7D

指代

gif.latex?F_%7B1%7D
gif.latex?F_%7B1%7D

gif.latex?F_%7B12%7D
gif.latex?F_%7B12%7D

)

三、调试时所查看的内容

1.临时变量的值

调试开始后可以直观看到变量中的值

bbb83c468de740e0890236fa138e5eb8.png
bbb83c468de740e0890236fa138e5eb8.png
8885f2ea35ae48b0be07897d2bf6d91c.png
8885f2ea35ae48b0be07897d2bf6d91c.png

(如果要删除所观察的某个变量,可以用鼠标选中这个变量然后用Delete键,即可删除)

2.内存信息

efaf1c49c19e45c18574a50840ac79d7.png
efaf1c49c19e45c18574a50840ac79d7.png

3.调用堆栈

8aa402a80fd74d7889de92a83dbb9b14.png
8aa402a80fd74d7889de92a83dbb9b14.png

4.汇编信息

这个在之前的函数栈帧的创建与销毁的文章中有提到,可以通过汇编信息查看程序运行的底层逻辑(有两种方法:①右击鼠标②调试项)

25c1b954e57548c6abdb2f675e8f48a0.png
25c1b954e57548c6abdb2f675e8f48a0.png
0307be35090f4f2985a5c010e7018c0d.png
0307be35090f4f2985a5c010e7018c0d.png

5.寄存器信息

寄存器的相关概念也在函数栈帧的创建与销毁中提到,想了解的伙伴可以去看看。

0928f3f52d0a4180b2a81de3fddb3ea4.png
0928f3f52d0a4180b2a81de3fddb3ea4.png

四、调试示例

(一个经典的笔试题)

代码如下:

代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i <= 12; i++)
	{
		arr[i] = 0;
		printf("haha\n");
	}
	return 0;
}

上面的代码,很直观的一个错误是数组发生了越界访问,这个错误会影响我们正常打印"haha"吗,如果打印会打印几个"haha"呢? ​

或许大家会认为打印12个"haha",但事实如此吗?

我们将这个代码运行一下:

97641742dfb448a1a2b1787c18a936b7.png
97641742dfb448a1a2b1787c18a936b7.png

 可以看到,这个程序是死循环的打印"haha"而非只打印12个"haha"。

为什么会出现这种情况呢?

我们对这个程序进行调试观察变量中的内容以及地址信息

9596ec8c883d41599c594f11556d0b9a.png
9596ec8c883d41599c594f11556d0b9a.png

调试过程中发现,数组越界访问到的arr[12]和变量i的值是一起变化的,而当数组越界访问到arr[12]并将arr[12]赋值为0时,i的值也变为了0.

c155c1efe97240fcb5b386b7bef009df.png
c155c1efe97240fcb5b386b7bef009df.png

观察arr[12]和变量i的内存地址我们发现他们的地址是相同的,即这个程序中数组的越界访问,恰好访问到了变量i的内存空间,改变arr[12]就是改变变量i。

因此循环的条件i<=12是永远都会满足,程序变成了死循环。 

 下面我来简单说明一下出现这种情况的原因:

8658486624e940be87a7ccf2d6d429b1.png
8658486624e940be87a7ccf2d6d429b1.png

①数组arr和变量i都是放在栈区的;

②栈区的使用习惯是先用高地址再使用低地址(由高向低),因此先创建的变量i的地址会比数组arr的地址高;

③数组随着下标的增长,地址是从低地址向高地址变化的 (由低向高);

因此数组arr越界访问到arr[12]时,正好访问了变量i的空间。

(这是在vs空间上的特殊情况,其他编译器中数组和变量之间的空间不一定是2:例如在VC6.0中,变量i和数组arr之间是没有空间的,而在gcc中变量i和数组arr之间空出一个int的空间。)

五、如何写出优秀(易于调试)的代码?

1.优秀的代码

1.代码运行正常

2.Bug少

3.效率高

4.可读性高

5.可维护性高

6.注释清晰

7.文档齐全

2.常见的coding技巧

1.使用assert

断言:编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式。

2.使用const

1.用const修饰变量时,该变量的值就不能再被赋值,除非使用存有该变量地址的指针直接通过地址访问该变量。

2.用const修饰指针变量时:

(1)const放在*左边(eg:const int *p;),修饰的是该指针指向的内容,用来确保该指针指向的内容不会通过该指针修改;

(2)const放在*右边(eg:int * const p;),修饰的是指针变量本身,保证了指针变量的内容不会被修改,而该指针变量指向的内容可以通过该指针来修改。

3.有一个良好的代码风格

变量的命名、代码的编写格式、代码的整齐度……

增强代码的可读性,方便自己和其他人读懂代码。

4.添加必要的注释

对必要的内容进行注释,例如所创建的函数的功能、变量的含义、程序的头文件中的内容……

增强代码的可读性,方便自己和其他人读懂代码。

5.避免编码的陷阱

空指针、野指针的错误解引用……

3.示例

用C语言编写代码实现库函数strcpy(下图是运行结果,对自己实现的my_strcpy和库函数的strcpy进行了比较,两者结果是相同的)

代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<assert.h>
#include<string.h>
//strcpy是一个库函数,strcpy把含有'\0'结束符的字符串复制到另一个地址空间,返回值的类型为char*。

//将源变量的内容拷贝放置进目标变量
//这个函数是将src的值拷贝到dst中,为了避免出现将dst的值拷贝到src这种错误,可以用const修饰src
//形参名具有一定意义,便于识别
char * my_strcpy(char * dst, const char * src) 
{
	char * cp = dst;
	assert(dst && src);//用assert判断函数传参传过来的是否是空指针,避免出现空指针的解引用

	while (*cp++ = *src++)
		;   
	return(dst);
}
int main()
{
	char arr1[] = "abcdef";
	char arr2[] = "ghi";
	my_strcpy(arr1, arr2);
	printf("%s\n",arr1);
	strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}
2d4d7416cc2d4484b7d6409e7353e6cd.png
2d4d7416cc2d4484b7d6409e7353e6cd.png

六、小彩蛋

       最初的计算机键盘上的

gif.latex?F_%7B1%7D
gif.latex?F_%7B1%7D

gif.latex?F_%7B12%7D
gif.latex?F_%7B12%7D

等按键都是自己本身的功能,但随着计算机的不断发展,企业、家庭、个人都能够使用计算机。为了方便用户对计算机的使用,生产方就给

gif.latex?F_%7B1%7D
gif.latex?F_%7B1%7D

gif.latex?F_%7B12%7D
gif.latex?F_%7B12%7D

赋予了新的功能,比如调节屏幕亮度、调节音量大小等等。

       那么如何使用他们本身的功能呢?这里给大家两种方法:

  1. 一般键盘上会有一个
gif.latex?F_%7Bn%7D
gif.latex?F_%7Bn%7D

按键,用

gif.latex?F_%7Bn%7D
gif.latex?F_%7Bn%7D

+

gif.latex?F_%7B5%7D
gif.latex?F_%7B5%7D

就可以使用

gif.latex?F_%7B5%7D
gif.latex?F_%7B5%7D

本身的功能,即运行程序到断点处。(其他按键的使用和它类似)

  1. 在计算机的设置中关闭
gif.latex?F_%7B1%7D
gif.latex?F_%7B1%7D

gif.latex?F_%7B12%7D
gif.latex?F_%7B12%7D

的功能(由于每个人电脑型号系统都不同,作者不能列举出每一种方法,所以具体操作方法可以在百度上自行搜索)。

总结

        以上就是今天要讲的内容,本文简单的介绍了bug和调试的概念,还进一步用实例演示了如何通过调试来找到bug并且解决它。

        本文的作者也只是一个正在学习C语言等编程知识的萌新,若这篇文章中有哪些不正确的内容,请在评论区向作者指出(也可以私信作者),欢迎大佬们指点,也欢迎其他正在学习C语言的萌新和作者进行交流。

        最后,如果本篇文章对你有所启发的话,也希望可以支持支持作者,后续作者也会定期更新学习记录。谢谢大家!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-03-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、bug
    • 1.谁会发现bug?
      • 2.如何发现并解决bug?(步骤)
      • 二、调试
        • 1.调试是什么?为什么要进行调试?
          • 2.调试的环境
            • 3.调试的快捷键
            • 三、调试时所查看的内容
              • 1.临时变量的值
                • 2.内存信息
                  • 3.调用堆栈
                    • 4.汇编信息
                      • 5.寄存器信息
                      • 四、调试示例
                      • 五、如何写出优秀(易于调试)的代码?
                        • 1.优秀的代码
                          • 2.常见的coding技巧
                            • 1.使用assert
                            • 2.使用const
                            • 3.有一个良好的代码风格
                            • 4.添加必要的注释
                            • 5.避免编码的陷阱
                          • 3.示例
                          • 六、小彩蛋
                          • 总结
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档