1947年,哈佛大学的计算机哈佛二代(Harvard Mark II)突然停止了运行。原来,哈佛二代当时还没有二极管和晶体管,它是一部继电器计算机,无数个喀哒作响的电磁开关在其中运作。当开关断开的时候会有电弧发出闪光,于是这只妖蛾子奋不顾身地飞了上去,用节肢动物的鲜血开辟了脊索动物的Debug史,从此名垂千古,永远地保存在了华盛顿的美国国家历史博物馆中,后来,Bug这个名词就沿用下来,表示电脑系统或程序中隐藏的错误、缺陷或问题。
Bug可以翻译为幺蛾子 —— 所以可以说你这代码有个幺蛾子
调试(Debugging / Debug),又称排错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程
所有发生的事情一定都有迹可循,如果问心无愧,就不需要掩盖了,如果问心有愧,就必然需要掩盖,就一定会有破绽,破绽越多就越容易顺藤而上,这就是推理的途径。
一名优秀的程序猿一定是一名出色的侦探 —— 每一次调试都是一次破案的过程
拒绝迷信调试,学会科学调试:
Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户更好的使用
在此之前需要使用Debug和Release分别来运行代码,才能生成对应文件
从生成文件的大小来看:Debug的文件比Release的文件大
Debug可以用于调试,而Release不能调试
Debug不可以优化,而Release可以对代码进行优化
这里所调试的环境是:Visual Studio && Windows
将版本改为Debug调试版本。注:Linux开发环境调试工具是gdb
学会快捷键会使我们的编码效率大大提高
以下这个窗口需要在调试后才能打开
当然这些还是要配合一些场景作一些了解:
1、当有一段有问题的代码,你已经排锁定了bug代码的区域,这时代码量又过长,想直接跳过非bug代码的区域时:断点+调试即可(F9+F5)
2、如果想进入到一个函数的内部可以逐语句执行(F11),或者不想进入函数的内部逐过程执行(F10)。
3、当我们的写的代码量大时且一个工程中有多个文件时。调试的时候难免会用到断点,且可能不止一个断点,这里可能就会造成紊乱。所以在调试,窗口里有个断点可以管理断点(CTRL + ALT + B)
4、在调试的过程中可以通过下面的局部变量窗口(CTRL + ALT + V + L)或者自动窗口(CTRL + ALT + V + A)来观察代码的步骤。
以上2个窗口都有一定的局限性,自由度不高。如果想自己设置要观察的数据时:在调试,窗口里打开监视窗口即可(CTRL + ALT + W , 1/2/3/4)
5、如果想要观察数据的内存的话,在调试,窗口里打开内存窗口即可(CTRL + ALT + M , 1/2/3/4)
6、如果想要看函数调用的逻辑,在调试,窗口,打开调用堆栈(这里的堆栈就是栈)(CTRL + ALT + C)
发现调用堆栈会模拟栈的执行逻辑
7、一段for循环代码,可能觉得在第50次循环有问题,这时按步就班的话就很low。可以使用断点,然后右击断点点击条件设置即可
注:以下代码均为问题代码
实现代码:求1! + 2! + 3! … + n! ; 不考虑溢出
#define _CRT_SECURE_NO_WARNINGS
#include
int main()
{
int n = 0;
scanf("%d", &n);
int i = 0;
int j = 0;
int ret = 1;
int sum = 0;
for(j = 1; j <=n; j++)
{
for(i = 1; i <= j; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
现象,当求3的阶乘时,输出的是15,答案与预期不符(这段代码相对简单这里就自己调试解决)这种错误被称为运行时错误,也是未来比较常见和比较难发现的一种错误,能通过调试解决的就是运行时错误
#include
int main()
{
int i = 0;
int arr[10] = {
1,2,3,4,5,6,7,8,9,10};
for(i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
现象:死循环
经调试发现造成死循环的直接原因是:
为什么改了arr[12],而i也改了 ?其实不难想象它们同在一块空间
我们不妨大胆的猜测一下
这里面是有原因的,当然也有一定程度的巧合
1、i 和arr 是局部变量,而局部变量是放在栈区上的(注意不要跟数据结构的栈混淆了)
2、栈区内存的使用习惯:先使用高地址空间,再使用低地址空间
3、数组随着下标的增长,地址是由低到高变化的
这里如何避免死循环呢?
1、只要先定义arr数组再定义 i 即可
2、控制循环次数,<=11即可
经测试不同的编译器下 i 和 arr 在内存中的布局:中间相距的空间也不同,以上面代码为例:
1、VC6.0 -> 相差0个整型,<=10即死循环
2、gcc -> 相差1个整型,<=11即死循环
3、VS2017 -> 相差2个整型,<=12即死循环
所以数组只要向上越界的合适就会造成死循环
Release相比于Debug的还有一点就是Release会对代码进行优化(使之不会死循环)
Release是怎么优化的?
这里Release在发现问题后,会对局部变量 i 和 arr 在栈区上的顺序进行适应的调整
简单介绍strcpy函数,所在头string,它可以进行字符串拷贝(包括\0)
#include
#include
int main()
{
char arr1[20] = "xxxxxxxxxx";
char arr2[] = "hello";
strcpy(arr1, arr2);
printf("%s\n", arr1);//hello
return 0;
}
使用my_strcpy函数来模拟strlen
#include
void my_strcpy(char* dest, char* src)
{
while(*src != '\0')
{
//赋值
*dest = *src;
//调整
dest++;
src++;
}
*dest = *src;
}
int main()
{
char arr1[20] = "xxxxxxxxxx";
char arr2[] = "hello";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);//hello
return 0;
}
1. 优化1(简洁)
#include
void my_strcpy(char* dest, char* src)
{
while(*src != '\0')
{
//赋值+调整
*dest++ = *src++;//hello的拷贝
}
*dest = *src;//\0的拷贝
}
int main()
{
char arr1[20] = "xxxxxxxxxx";
char arr2[] = "hello";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);//hello
return 0;
}
2. 再优化2(简洁)
#include
void my_strcpy(char* dest, char* src)
{
while(*dest++ = *src++)//既拷贝了字符串(包括\0),又可以利用表达式让循环停下来
{
;
}
}
int main()
{
char arr1[20] = "xxxxxxxxxx";
char arr2[] = "hello";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);//hello
return 0;
}
3. 再优化3(从指针安全的角度考虑)
#include
#include
void my_strcpy(char* dest, char* src)
{
//如果my_strcpy传过来的参数是空指针时,此时再去解引用、++等一系列操作时,这是非法的
//这里有一个函数assert:断言,所在头assert。如果表达式里为真,则什么都不执行,否则将会停留在断言为假的那一行,不再执行下面代码,并且会详细输出错误信息(当然不仅限于指针)
//在以后编码中,如果要对指针进行一些操作时,断言可以讯速的帮我们找到问题所在
assert(dest != NULL);
assert(src);//同assert(src != NULL);
while(*dest++ = *src++)
{
;
}
}
int main()
{
char arr1[20] = "xxxxxxxxxx";
char arr2[] = "hello";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);//hello
return 0;
}
对比上面我们模拟的my_strcpy来说,库里的strcpy在原字符串上加了const来修饰。先来看一个场景:
赋值写反了:所造成的arr2数组越界
这里分析arr2的这块空间是不需要被改变的,所以加上const限定更安全,如果对const限定的字符串操作,编译器会主动报错
优化后
#include
#include
void my_strcpy(char* dest, const char* src)
{
assert(dest != NULL);
assert(src);
while(*dest++ = *src++)
{
;
}
}
int main()
{
char arr1[20] = "xxxxxxxxxx";
char arr2[] = "hello";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);//hello
return 0;
}
在之前的文章中有提到const,被const修饰的变量不能被修改
#include
int main01()
{
const int num = 0;
num = 20;//err
printf("%d\n", num);
return 0;
}
这里把num的地址交给p指针管理,然后发现被const限定的num能通过指针p改变num的值。当然这不是我们想要的
#include
int main()
{
const int num = 0;
int* p = #
*p = 20;
printf("%d\n", num);
return 0;
}
const和指针
#include
//const如果放在*的左边,修饰的是*p,表示指针指向的内容,是不能通过指针来改变的。但是指针变量本身(p->地址)是可以修改的
int main01()
{
const int num = 0;
int n = 20;
const int* p = #
//*p = 20;//err
p = &n;//ok
printf("%d\n", *p);//此时此刻p指针不再指向num,而是指向n
return 0;
}
//const如果放在*的右边,修饰的是p(地址),表示指针的地址,是不能改变指针变量的地址的,但是指针指向的内容是可以改变的
int main02()
{
const int num = 0;
int n = 20;
int* const p = #
//p = &n;//err
*p = 20;//ok
printf("%d\n", num);
return 0;
}
//const如果放在*的左边和右边,则指针指向的内容不可以被改变和指针变量也不能被改变
int main03()
{
const int num = 0;
int n = 20;
const int* const p = #
//p = &n;//err
//*p = 20;//err
printf("%d\n", num);
return 0;
}
6. 优化后(函数的返回值) -> 最终版
库里的strcpy的返回值是char*,而我们模拟的是my_strcpy是void
strcpy返回的是目标空间的起始地址,相比来说有返回值的strcpy可以使用链式访问
#include
#include
char* my_strcpy(char* dest, const char* src)
{
assert(dest != NULL);
assert(src);
char* ret = dest;//备份一份首地址
while(*dest++ = *src++)
{
;
}
return ret;//返回目标空间的首地址
}
int main()
{
char arr1[20] = "xxxxxxxxxx";
char arr2[] = "hello";
printf("%s\n", my_strcpy(arr1, arr2));//hello
return 0;
}
#include
#include
size_t my_strlen(const char* str)//size_t是无符号整型
{
assert(str);
size_t count = 0;
while(*str++)
count++;
return count;
}
int main()
{
char arr[] = "hello bit";
printf("%d\n", my_strlen(arr));
return 0;
}
这种类型属于语法错误,相对简单。
解决方法:直接看错误提示信息,(双击就可定位到有问题的代码上)
LNK(链接型错误)这种错误只要了解它为什么会产生,也不难找
主要产生的原因
1、这个函数压根就未定义
2、调用函数名时与定义的函数名不一
解决方法:错误信息上不可以定位到有问题的代码上,但是可以作为一些依据
这种错误没有错误信息提示,相对较难找。一般是输出结果与预想或与正确答案不符
解决方法:借助调试,逐步定位问题
可以把每天因为调试所解决的运行时错误代码写一个代码日志