目录
一、 调试是什么
二、 Debug与Release的介绍
三、Windows环境调试
四、 调试的时候查看程序当前信息
查看内存信息
查看临时变量的值(常用)
查看调用堆栈
五、如何写出好(易于调试)的代码
1. 优秀代码的特点
2. assert
3. const
六、编程常见错误
编译型错误
链接型错误
运行时错误
调试(Debug)又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
在日常写代码出现非语法问题的情况时,我们就会借助调试解决问题,当我们写的代码越来越多,犯的语法错误就越来越少,非语法错误需要调试解决,因此调试极其重要
Debug 称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。 Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是 最优的,以便用户很好地使用。在Release版本中无法进行调试操作
所以调试也就是在Debug版本环境中,找出代码中潜伏问题的过程,
1. 首先要将环境调为Debug版本,这样才能正常调试
2. 调试中常用的快捷键
Ctrl + F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句
按了F10之后,在main函数的第一行会出现如图所示的箭头,说明调试开始了,然后再按一下F10就会跳转到下一条语句,当箭头到第14行时,你会发现箭头并没有跳转到函数中,而是直接跳到第15行,这就是我们刚刚说的F10是逐过程,而一个过程可以是一次函数调用。
那如果要观察函数内部的情况呢?我们介绍下一个快捷键
F11:逐语句,就是每次都执行一条语句,而这个快捷键可以使我们的箭头进入到函数内部
F9: 创建断点和取消断点,我们可以在程序的任意位置设置断点。 这样就可以使得程序在想要的 位置(断点位置)随意停止执行,继而一步步执行下去
F5:启动调试,经常用来直接跳到下一个断点处;F5和F9经常配合使用
下图中红色的点就是断点
再按F5之后,箭头就会直接跳转到断点位置处,之后再按F10或F11进行调试
下列代码中,假设我们知道在i = 5时出现了问题(实际上下面的代码没有问题)那我们要通过调试一次一次按F10或F11直到i = 5吗,这样效率太低
我们可以鼠标右键断点,点击条件
出现下述界面,按照示例输入即可(这里可以输入i == 5)
在调试过程中,通过监视窗口可以看到临时变量的值,输入count,就可以跟随观察count值的变化
通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置
1. 代码运行正常
2. bug很少
3. 效率高
4. 可读性高
5. 可维护性高
6. 注释清晰
7. 文档齐全
常见的coding技巧:
1. 使用assert
2. 尽量使用const
3. 养成良好的编码风格
4. 添加必要的注释
下面模拟实现库函数 strcpy
strcpy的原型定义为:char * strcpy ( char * destination, const char * source );
其返回的是目标空间的起始地址也就是destination的起始地址
我们以下列代码为例,对其进行逐步优化:
void mystrcpy(char* des, char* src)
{
while (*src != '\0')
{
*des = *src;
des++;
src++;
}
*des = *src;
}
int main()
{
char arr1[] = "abcdef";
char arr2[20];
mystrcpy(arr2, arr1);
printf("%s", arr2);
return 0;
}
在上述代码中,是将字符串和 \0 分开拷贝的,那能不能想个办法通过一个操作将它们一起拷贝?
看下面的代码:
void mystrcpy(char* des, char* src)
{
while (*des++ = *src++)
;
}
当*src = '\0'时,将其赋值给*des,整个表达式为0,终止循环。可以看出该代码十分巧妙地完成了我们的问题,但是还有个问题就是:如果向指针des和src传过去的是空指针呢?
我们知道空指针是不能被解引用的,因为它不指向任何地址,如果尝试对空指针进行解引用操作,会导致程序崩溃,那么也许有人就会说在while循环前面加上个if语句就行了,如:
void mystrcpy(char* des, char* src)
{
if (des == NULL || src == NULL)
{
return;
}
while (*des++ = *src++)
;
}
这样写的确可以解决问题,但是当我们通过使用if语句知道自己传过去的是空指针,将其改掉之后,if语句还是要判断其是否是空指针,也就是说无论如何if语句都会执行,这样会降低效率,
所以我们可以使用assert
void mystrcpy(char* des, char* src)
{
assert(des != NULL);
assert(src != NULL);
while (*des++ = *src++)
;
}
assert是一个宏,不是函数,它的原型定义为void assert(int expression);其功能主要是程序诊断,当表达式expression为假时程序就会报错并显示如下:
它会告诉你是哪个地方报错了,而你就可以根据assert的报错改正相应的bug,assert还有一个优点,当你在Debug环境中将bug修改后,再改为Release环境时,Release环境会将assert优化掉,也就不会执行它(因为你已经再Debug环境中根据assert的报错将bug修改了,此时代码中就没有bug) 这样就提高了效率,Release环境不会优化if语句。
我们一开始的时候说过:strcpy返回的是目标空间的起始地址,那我们模拟实现的strcpy函数肯定也要这样,那么下面这样对吗:
char* mystrcpy(char* des, char* src)
{
assert(des != NULL);
assert(src != NULL);
while (*des++ = *src++)
;
return des;
}
错误!因为通过自身的++,des已经不是起始地址了,所以应在函数一开始用一个指针保存它的起始地址并返回该指针变量:
char* mystrcpy(char* des, char* src)
{
char* ret = des;
assert(des != NULL);
assert(src != NULL);
while (*des++ = *src++)
;
return ret;
}
现在只剩最后一个问题了,那就是char * strcpy ( char * destination, const char * source ); 中const是什么?
const在修饰变量时,是在语法层面上限制了变量的修改,但在本质上num还是变量,是不能被直接修改的变量。那么如果用指针呢,如:
可以看到使用指针竟然可以修改被const修饰的变量,我们接下来讲解一下const修饰指针的情况
const在修饰指针时,我们先简单回顾一下指针:
*p是p指向的对象
p是一个变量,用来存放地址
当const放在*的左边时:限制的指针指向的内容,也就是说,不能通过指针来修改指针指向的内容,但是指针变量是可以修改的,也就是说指针是可以指向其它变量的
当const放在*右边时:限制的是指针变量本身,指针变量不能再指向其他对象,但是可以通过指针变量来修改指向的内容
我们再次回到刚刚的代码:
char* mystrcpy(char* des, char* src)
{
char* ret = des;
assert(des != NULL);
assert(src != NULL);
while (*des++ = *src++)
;
return ret;
}
在strcpy函数是将src拷贝到des中,我们不希望src被改变,所以在其*前面加一个const
char* mystrcpy(char* des, const char* src)
{
char* ret = des;
assert(des != NULL);
assert(src != NULL);
while (*src++ = *des++)
;
return ret;
}
在上述代码中不小心将while的循环条件写反了,也就是将des拷贝到src中,这样src就发生了改变,而我们已经用const修饰了指针,那么src就不能改变,会报错
直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。
比如:
错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不 存在或者拼写错误。
运行的结果与自己预期的结果不同,借助调试,逐步定位问题。最难搞