调试
1.调试是什么?
调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。当我程序出现了逻辑错误时就需要调试了。
2.调试的步骤
- 发现程序错误的存在
- 以隔离、消除等方式对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新测试
3. Release 和 Debug的介绍
- Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
- Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优 的,以便用户很好地使用。
例如:在VS2022中:
实例一:
#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;
}
这段代码在debug
模式去编译,程序的结果是死循环。
如果是 release
模式去编译,则没有死循环。
他们之间有什么区别呢?
【Debug
】下:
通过调试操作,发现数据访问的第12个元素刚好就是 i
变量所在的空间,通过修改 arr[12]
将 i
改成了0。就这样 i
变量一旦增加到了12就会被改成0,程序发生了死循环。
原因:程序在栈上开辟空间时,优先会利用高地址的空间,因为此处
i
变量先创建,所以变量i
的地址高于数组的地址。随后创建数组,数组的起始内存空间肯定是在i
变量的下面,当数组越界访问时,访问的地址越来越高,由于i
与arr
数组的最后一个元素的地址空间只差了8个字节,刚好数组向上多访问两个元素,就访问到了i
变量,并且数组还做了将i
变量置为0的操作,此时,arr[i]
就又开始访问数组的第一个元素了,等到访问到arr[i](i==12)
时,又将i
置为0,一直循环往复,形成了死循环。
【release
】下:
release
版本之下,程序不会出现死循环。因为编译器做了一些优化处理。我们可以适当观察一下:
可以发现:变量在内存中开辟的顺序发生了变化,影响到了程序的执行结果。
4. Visual studio的调试
4.1环境准备
首先要在编译环境中选择 Debug
选项,代码才能正常调试。
4.2快捷键的使用
常用的快捷键需要记住,后期会帮我们节省很多时间。例如:shift+F11
用于跳出该函数。
F5
:启动调试,经常用来直接跳到下一个断点处
F9
;创建断点和取消断点。断点可以在程序的任意位置设置。
F10
:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11
:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。
CTRL + F5
:开始执行但是不调试。
4.3 调试的时候查看程序信息
调试启动之后。
查看内存信息:
总之,调试的功能应有尽有,要根据不同的实例恰当的选取。
5. 如何写出好的(易于调试的代码)
常见技巧:
- 尽量使用
assert
- 尽量使用
const
- 养成良好的编码习惯
- 添加必要的注释
实例二
【strcpy库函数的实现】
/***
*char *strcpy(dst, src) - copy one string over another
*
*Purpose:
* Copies the string src into the spot specified by
* dest; assumes enough room.
*
*Entry:
* char * dst - string over which "src" is to be copied
* const char * src - string to be copied over "dst"
*
*Exit:
* The address of "dst"
*
*Exceptions:
*******************************************************************************/
char * strcpy(char * dst, const char * src)
{
char * cp = dst;
assert(dst && src);
while( *cp++ = *src++ )
; /* Copy src over dst */
return( dst );
}
仔细观察,这里库函数的第二个参数是被const
修饰的。那么这里的const
有什么作用呢?
const 的作用
#include <stdio.h>
//代码1
void test1()
{
int n = 10;
int m = 20;
int *p = &n;
*p = 20;//编译通过
p = &m; //编译通过
}
void test2()
{
//代码2
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;//编译失败
p = &m; //编译通过
}
void test3()
{
int n = 10;
int m = 20;
int *const p = &n;
*p = 20; //编译通过
p = &m; //编译失败
}
int main()
{
//测试无cosnt的
test1();
//测试const放在*的左边
test2();
//测试const放在*的右边
test3();
return 0;
}
const
修饰指针时:
const
如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。const
如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
assert 的作用
assert的作用是:当程序运行时,若assert的括号中的条件不满足时,程序会强制提醒程序员。例如:
但是
assert
需要包含头文件:#include<assert.h>
。
#include<stdio.h>
#include<assert.h>
int div1(int a, int b)
{
assert(0);
int ret = a / b;
return ret;
}
int main()
{
int a = 4;
int b = 1;
//scanf("%d %d", &a, &b);
int ret = div1(a, b);
printf("%d", ret);
return 0;
}
只要程序不满足assert括号中的语句,程序就会强制提醒用户出错的地方。
小练习:模拟实现strlen函数
#include<stdio.h>
#include<assert.h>
//模拟实现strlen函数
int MyStrlen(const char* arr)
{
assert(arr != NULL);
int count = 0;
while (*arr != '\0')
{
count++;
arr++;
}
return count;
}
int main()
{
char arr[1000];
gets(arr);//读取字符串
int len = MyStrlen(arr);
printf("%d \n", len);
return 0;
}
这里要注意
assert
的使用和const
的使用。
6. 编程常见错误
编译型错误:
直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单.
链接型错误:
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
运行时错误:
借助调试,逐步定位问题。最难搞。
所以,还是要靠我们做一个有心人,积累排错经验。
7.完结
本章的内容就到这里啦,若有不足,欢迎评论区指正,最后,希望大佬们多多三连吧,下期见!