【调试技巧】一名优秀的程序员是一名出色的侦探

在这里插入图片描述

个人主页:Weraphael
✍作者简介:目前正在回炉重造C语言(2023暑假)
✈️专栏:【C语言航路】
希望大家多多支持,咱一起进步!
如果文章对你有帮助的话
欢迎 评论 点赞 收藏 加关注


目录

  • 一、调试是什么?
  • 二、Debug和Release的介绍
  • 三、Windows环境调试介绍
      • 3.1 调试环境准备
      • 3.2 快捷键(重点)
      • 3.3 调试时查看程序的当前信息(介绍常用的)
        • 3.3.1 查看临时变量的值
        • 3.3.1 查看内存信息
        • 3.3.3 查看调用堆栈
        • 3.3.4 查看反汇编
  • 四、调试的实例
  • 五、如何写出易于调试的代码
      • 5.1 优秀的代码
      • 5.2 如何写出好的代码
  • 六、 实例:模拟实现strcpy(讲解assert、const的使用)
  • 六、补充:const的作用

一、调试是什么?

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

二、Debug和Release的介绍

【调试技巧】一名优秀的程序员是一名出色的侦探_第1张图片

  • Debug通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序。
  • Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户更好地使用。

例如如下代码,在DebugRelease环境下分别做了哪些优化呢?

#include 
int main()
{
	int i = 0;
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (i = 0; i <= 12; i++)
	{
		printf("hello world!\n");
		arr[i] = 0;
	}
	return 0;
}

【Debug环境】

【调试技巧】一名优秀的程序员是一名出色的侦探_第2张图片

如上图所示,在Debug环境下,程序的结果是死循环。而为什么会死循环呢?在后面调试的时候会讲解到。

【Release环境】

【调试技巧】一名优秀的程序员是一名出色的侦探_第3张图片

如上图,在Release环境下,程序并没有发生死循环。这就是因为优化导致的。

三、Windows环境调试介绍

注:本篇文章以VS2019开发工具为例

3.1 调试环境准备

在这里插入图片描述

环境一定要选择Debug选项,才能使代码正常调试,Release环境下是不能调试的。

3.2 快捷键(重点)

最常用的几个快捷键:

  1. F5:启动调试,经常用来直接跳到下一个断点处
  2. F9:选中一行创建断点和取消断点。断点的作用:当代码量大的时候,可以在程序的任意位置设置断点,这样就可以使得程序在想要的位置随意停止执行。F9一般是配合F5来使用的。
  3. F10:通常用来处理一个过程。意思就是当遇到函数时按此快捷键是看不到自定义函数内部的细节,或者还可以处理一条语句。
  4. F11:每次都只执行一条语句。此快捷键看似和F10好像差不多,但它最重要的是可以使我们的执行逻辑进入到函数内部(这才是最常用的)
  5. Ctrl + F5:开始执行不调试,就是可以直接运行程序查看结果

当然还有很多的快捷键:点我跳转

3.3 调试时查看程序的当前信息(介绍常用的)

注意:在使用以下功能之前要先按下F10

3.3.1 查看临时变量的值

首先先按下F10,在根据以下步骤操作

【调试技巧】一名优秀的程序员是一名出色的侦探_第4张图片

然后就可以一步一步按F10逐语句来观察值的变换:

【调试技巧】一名优秀的程序员是一名出色的侦探_第5张图片

这里在提一嘴,当数组传参时,由于传参传的是首元素地址,因此我们在监视只能观察到数组的第一个元素:

【调试技巧】一名优秀的程序员是一名出色的侦探_第6张图片

那如何能观察到数组的所有元素呢?数组名,数组元素个数

【调试技巧】一名优秀的程序员是一名出色的侦探_第7张图片

3.3.1 查看内存信息

首先先按下F10,在根据以下步骤操作:

【调试技巧】一名优秀的程序员是一名出色的侦探_第8张图片

可以观察变量在内存中是如何存储的:

【调试技巧】一名优秀的程序员是一名出色的侦探_第9张图片

3.3.3 查看调用堆栈

首先先按下F10,在根据以下步骤操作:

【调试技巧】一名优秀的程序员是一名出色的侦探_第10张图片

通过调用堆栈,可以清晰的反映函数的调用关系以及当前调用所处的位置:

【调试技巧】一名优秀的程序员是一名出色的侦探_第11张图片

通过上图我们发现:其实在调用main函数之前还调用了其他的函数。

再举个例子:

【调试技巧】一名优秀的程序员是一名出色的侦探_第12张图片

通过上图可以发现:arr_test函数是被main函数调用的。

3.3.4 查看反汇编

有两种方式可以查看反汇编:

  1. 先按F10,然后右击鼠标,选择反汇编

【调试技巧】一名优秀的程序员是一名出色的侦探_第13张图片

  1. 先按F10,剩下步骤如下图所示:

【调试技巧】一名优秀的程序员是一名出色的侦探_第14张图片

如何看反汇编,建议大家可以看看我的往期博客:函数栈帧的创建和销毁

四、调试的实例

刚刚讲过以下代码在Debug环境下会死循环,现在我们通过调试来分析为什么会造成死循环

#include 
int main()
{
	int i = 0;
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (i = 0; i <= 12; i++)
	{
		printf("hello world!\n");
		arr[i] = 0;
	}
	return 0;
}

【调试结果】

【调试技巧】一名优秀的程序员是一名出色的侦探_第15张图片

当在调试的过程中,我发现i的值的变化也会影响arr[12]值的变化,于是我观察它们的地址,发现它们竟然用的是用一块空间,导致当arr[12]被修改成0,i随之也被修改成0继续循环,最终导致了死循环

接下来我用图来为大家解释为什么iarr[12]可能会使用同一块空间地址(注:本环境是在vs2019 x86,不同的环境可能会造成不同的结果)

【调试技巧】一名优秀的程序员是一名出色的侦探_第16张图片

五、如何写出易于调试的代码

5.1 优秀的代码

  1. 代码能够正常运行(最基本的)
  2. Bug尽可能少
  3. 效率高
  4. 可读性高(要让别人看得懂)
  5. 可维护性高
  6. 注释清晰(难理解的代码加上注释后更容易理解)
  7. 文档齐全

5.2 如何写出好的代码

  1. 使用assert(后面会介绍)
  2. 尽量使用const(后面会介绍)
  3. 养成良好的编码风格(比如取变量名要有意义)
  4. 调价必要的注释
  5. 避免编码陷阱(如:数组越界)

六、 实例:模拟实现strcpy(讲解assert、const的使用)

【strcpy文档】

文档地址:点击跳转

【调试技巧】一名优秀的程序员是一名出色的侦探_第17张图片

【代码实现】

#include 
#include 
// 函数原型:
//char * strcpy (char * destination, const char * source)

// 返回值:目标空间的起始地址要被返回char*
char* my_strcpy(char* dest, const char* sour)
{
	// 记录目标空间的起始地址
	char* res = dest;

	// 判断指针的有效性
	assert(dest != NULL && sour != NULL);

	// 赋值拷贝
	while (*sour != '\0')
	{
		*dest = *sour;
		dest++;
		sour++;
	}
	// 来到此处'\0'还未被拷贝
	*dest = *sour;
	
	// 返回值
	return res;
}
int main()
{
	// 将arr1的内容拷贝到arr2中
	char arr1[] = "hello world!";
	char arr2[20] = { 0 };
	//       目的地  源头
	my_strcpy(arr2, arr1);

	printf("%s\n", arr2);
	return 0;
}
  1. 什么是assert是及好处

assert是断言,是一个暴力的检查方法。括号内可以放表达式,如果表达式的结果为假,就会报错;如果表达式的结果为真,则什么事都不发生。它的好处:可以准确的告诉我们哪里发生了错误。 注意:assert需要包含头文件#include
【调试技巧】一名优秀的程序员是一名出色的侦探_第18张图片

  1. 为什么形参需要用const修饰

const修饰的变量不能被修改。假设有一个“糊涂”的程序员不小心将*dest = *sour写成*sour = *dest,导致不需要修改的空间被修改了。加上了const更安全。

六、补充:const的作用

首先先来看看一下代码:

#include 
int main()
{
	int n = 985;
	int m = 211;
	printf("n的地址:%p\n", &n);
	printf("n的地址:%p\n", &m);
	int* p = &n;
	printf("修改前p的地址:%p\n", p);
	*p = 20;
	printf("n = %d\n", n);
	p = &n;
	printf("修改后p的地址:%p\n", p);

	return 0;
}

【程序结果】

【调试技巧】一名优秀的程序员是一名出色的侦探_第19张图片

通过以上代码我们发现:可以通过指针来间接修改变量的值,同时指针变量也能被修改

假设const*的前面,结果会是如何呢?

int main()
{
	int n = 985;
	int m = 211;
	const int* p = &n;
	*p = 20;
	p = &n;

	return 0;
}

【程序结果】

【调试技巧】一名优秀的程序员是一名出色的侦探_第20张图片

我们发现,*p不能被修改。这是因为const放在*的左边,修饰的是指针指向的内容,也就是说,指针指向的内容不能被修改。

那假设const*的后面结果又会是如何呢?

int main()
{
	int n = 985;
	int m = 211;
	int* const p = &n;
	*p = 20;
	p = &n;

	return 0;
}

【程序结果】

【调试技巧】一名优秀的程序员是一名出色的侦探_第21张图片

我们发现变量p不能被修改。这是因为此时的const修饰的是指针变量本身,要保证指针变量的内容不能被修改。

总结

  1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可修改。
  2. const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变

你可能感兴趣的:(C语言航路,c语言,笔记,学习,visualstudio,开发语言)