👦个人主页:Weraphael
✍🏻作者简介:目前正在回炉重造C语言(2023暑假)
✈️专栏:【C语言航路】
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
一、调试是什么?
调试(英语:
Debug
),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
二、Debug和Release的介绍
Debug
通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序。Release
称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户更好地使用。
例如如下代码,在Debug
和Release
环境下分别做了哪些优化呢?
#include <stdio.h>
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环境】
如上图所示,在
Debug
环境下,程序的结果是死循环。而为什么会死循环呢?在后面调试的时候会讲解到。
【Release环境】
如上图,在
Release
环境下,程序并没有发生死循环。这就是因为优化导致的。
三、Windows环境调试介绍
注:本篇文章以
VS2019
开发工具为例
3.1 调试环境准备
环境一定要选择
Debug
选项,才能使代码正常调试,Release
环境下是不能调试的。
3.2 快捷键(重点)
最常用的几个快捷键:
F5
:启动调试,经常用来直接跳到下一个断点处F9
:选中一行创建断点和取消断点。断点的作用:当代码量大的时候,可以在程序的任意位置设置断点,这样就可以使得程序在想要的位置随意停止执行。F9
一般是配合F5
来使用的。F10
:通常用来处理一个过程。意思就是当遇到函数时按此快捷键是看不到自定义函数内部的细节,或者还可以处理一条语句。F11
:每次都只执行一条语句。此快捷键看似和F10
好像差不多,但它最重要的是可以使我们的执行逻辑进入到函数内部(这才是最常用的)Ctrl + F5
:开始执行不调试,就是可以直接运行程序查看结果当然还有很多的快捷键:点我跳转
3.3 调试时查看程序的当前信息(介绍常用的)
注意:在使用以下功能之前要先按下
F10
3.3.1 查看临时变量的值
首先先按下F10
,在根据以下步骤操作
然后就可以一步一步按F10
逐语句来观察值的变换:
这里在提一嘴,当数组传参时,由于传参传的是首元素地址,因此我们在监视只能观察到数组的第一个元素:
那如何能观察到数组的所有元素呢?数组名,数组元素个数
3.3.1 查看内存信息
首先先按下F10
,在根据以下步骤操作:
可以观察变量在内存中是如何存储的:
3.3.3 查看调用堆栈
首先先按下F10
,在根据以下步骤操作:
通过调用堆栈,可以清晰的反映函数的调用关系以及当前调用所处的位置:
通过上图我们发现:其实在调用main
函数之前还调用了其他的函数。
再举个例子:
通过上图可以发现:arr_test
函数是被main
函数调用的。
3.3.4 查看反汇编
有两种方式可以查看反汇编:
- 先按
F10
,然后右击鼠标,选择反汇编
- 先按
F10
,剩下步骤如下图所示:
如何看反汇编,建议大家可以看看我的往期博客:函数栈帧的创建和销毁
四、调试的实例
刚刚讲过以下代码在Debug
环境下会死循环,现在我们通过调试来分析为什么会造成死循环
#include <stdio.h>
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;
}
【调试结果】
当在调试的过程中,我发现
i
的值的变化也会影响arr[12]
值的变化,于是我观察它们的地址,发现它们竟然用的是用一块空间,导致当arr[12]
被修改成0,i
随之也被修改成0继续循环,最终导致了死循环
接下来我用图来为大家解释为什么i
和arr[12]
可能会使用同一块空间地址(注:本环境是在vs2019 x86
,不同的环境可能会造成不同的结果)
五、如何写出易于调试的代码
5.1 优秀的代码
- 代码能够正常运行(最基本的)
Bug
尽可能少- 效率高
- 可读性高(要让别人看得懂)
- 可维护性高
- 注释清晰(难理解的代码加上注释后更容易理解)
- 文档齐全
5.2 如何写出好的代码
- 使用
assert
(后面会介绍)- 尽量使用
const
(后面会介绍)- 养成良好的编码风格(比如取变量名要有意义)
- 调价必要的注释
- 避免编码陷阱(如:数组越界)
六、 实例:模拟实现strcpy(讲解assert、const的使用)
【strcpy文档】
文档地址:点击跳转
【代码实现】
#include <stdio.h>
#include <assert.h>
// 函数原型:
//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;
}
- 什么是
assert
是及好处
assert
是断言,是一个暴力的检查方法。括号内可以放表达式,如果表达式的结果为假,就会报错;如果表达式的结果为真,则什么事都不发生。它的好处:可以准确的告诉我们哪里发生了错误。 注意:assert
需要包含头文件#include <assert.h>
- 为什么形参需要用
const
修饰
const
修饰的变量不能被修改。假设有一个“糊涂”的程序员不小心将*dest = *sour
写成*sour = *dest
,导致不需要修改的空间被修改了。加上了const
更安全。
六、补充:const的作用
首先先来看看一下代码:
#include <stdio.h>
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;
}
【程序结果】
通过以上代码我们发现:可以通过指针来间接修改变量的值,同时指针变量也能被修改
假设const
在*
的前面,结果会是如何呢?
int main()
{
int n = 985;
int m = 211;
const int* p = &n;
*p = 20;
p = &n;
return 0;
}
【程序结果】
我们发现,
*p
不能被修改。这是因为const
放在*
的左边,修饰的是指针指向的内容,也就是说,指针指向的内容不能被修改。
那假设const
在*
的后面结果又会是如何呢?
int main()
{
int n = 985;
int m = 211;
int* const p = &n;
*p = 20;
p = &n;
return 0;
}
【程序结果】
我们发现变量
p
不能被修改。这是因为此时的const
修饰的是指针变量本身,要保证指针变量的内容不能被修改。
总结
const
如果放在*
的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可修改。const
如果放在*
的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变