bug--->臭虫、虫子。
为什么含义是臭虫、虫子呢?
答案是:第一次被发现的导致计算机错误的是一只飞蛾,也是第一个计算机程序的错误。
前言:
所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧,就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。
顺着这条途径顺流而下就是犯罪,逆流而上就是真相。
一名优秀的程序员都是一名出色的侦探。
每一次调试都是尝试破案的过程。
调试(英语:Debugging/Debug),又称除错,是发现和减少计算机或电子仪器设备中程序错误的一个过程。
①发现程序错误的存在
②以隔离、消除等方式对错误进行定位
③确定错误产生的原因
④提出纠正错误的解决办法
⑤对程序错误予以改正,重新测试
Debug通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序。
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
代码:
#include
//自定义函数——实现对整数数组的冒泡排序
void bubble_sort(int* str, int sz)
{
//趟数
int i = 0;
for (i = 0; i < sz - 1; i++)
{
//一趟冒泡排序的过程
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
//升序
if (str[j] > str[j + 1])
{
int tmp = str[j];
str[j] = str[j + 1];
str[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 10,9,8,7,5,6,4,1,2,3 };//定义整形数组,并初始化
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组的大小
//调用函数,实现升序
bubble_sort(arr, sz);
//输出升序后的数组
int i = 0;//循环变量
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
观察分别在Debeg和Release环境下生成的可执行程序的大小:
代码:
#include
int main()
{
int i = 0;
//数组下标界限0~9
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (i = 0; i <= 12; i++)
{
//数组下标为10~12时数组越界
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
在Debeg和环境下该代码死循环,解释在调试的实例二。
在Release环境下该代码打印13次hehe,不死循环,因为编译器优化把i的地址放在数组arr地址的下面了。
在环境中选择debug选项,才能使代码正常调试。
F5
启动调试,经常用来直接跳到下一个断点处。
F9
创建断点和取消断点。
断点的重要作用,可以在程序的任意位置设置断点。这样就可以使得程序在想要的位置停止执行,继而一步步执行下来。
F5和F9常配合使用,F5一般不会单独使用的。断点再多文件、多代码中常用。
F10
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。
ctrl + F5
开始执行不调试。如果你想让程序直接运行起来而不调试就可以直接使用。
注意:只有先F10开始调试,才能看到程序当前信息。
在调试开始之后,用于观察变量的值。
在F10调试开始之后,用于观察内存信息。
在F10调试开始之后,有两种方式转到汇编。
(1)第一种方式:右击鼠标,选择[转到汇编]:
在F10调试起来之后,有两种方式观察寄存器信息。
(1)第一种方式:
在F10调试之后,可以观察调用堆栈。
通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。
①一定要熟练掌握调试技巧;
②初学者可能80%的时间在写代码,20%的时间在调试。但是一个程序员可能20%的时间在写代码,但是80%的时间在调试。
③我们现在所讲的都是一些简单的调试,以后可能会出现很复杂的调试场景:多线程程序的调试等。
④多多使用快捷键,提升效率。
代码1:实现阶乘
#include
int main()
{
//输入求几的阶乘
int n = 0;
scanf("%d", &n);
//实现求n! n!=n*(n-1)
int ret = 0;
int i = 0;
for (i = 1; i <= n; i++)
{
ret *= i;
}
//输出结果
printf("%d\n", ret);
return 0;
}
如果我们输入3,想输出6,但实际输出的是0.
why?
这里我们就得找我们的问题:
①首先通过经验推测问题出现的原因,初步确定问题可能的原因最好。
②实际上手调试很有必要。
③调试的时候我们要心里有数。
通过初步推测ret变量有问题,我们在在for循环打断点调试观察变量ret具体有什么问题。
代码2: 求 1!+2!+3! ...+ n!
#include
int main()
{
//输入有n个阶乘
int n = 0;
scanf("%d", &n);
//循环 求 1!+2!+3! ...+ n!
int ret = 1;
int i = 0;
int sum = 0;//存放阶乘的累加和
for (i = 1; i <= n; i++)
{
int j = 0;
//实现求i的阶乘
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
//输出结果
printf("%d\n", sum);
return 0;
}
如果我们输入3,想输出9,但实际输出15。
why?
分析:推测循环出错了,第一次调试在第二个循环处打断点,一步步调试监视变量的变化。
但是没有发现是哪里错了,第二次调试在断点处右击设置断点条件快速调试到错误处,符合断点条件就停止,再F10观察具体原因。
#include
int main()
{
int i = 0;
//数组下标界限0~9
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (i = 0; i <= 12; i++)
{
//数组下标为10~12时数组越界
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
数组越界应该是程序错误,不执行但是我们运行后发现程序死循环了。
why?
我们F10调试起来观察变量。
在调试的时候,我们发现每一次i的值都和arr[12]的值一样,当arr[12]=0时,i也变成0了,所以死循环。
那arr[12]和i是不是地址一样?我们调试观察之后确实是一样的。
图解:
在i和arr数组中间恰好就是2个整形吗?
答:不一定,该代码只是在VS2019 X86环境下实验的结果。
如果是VC6.0——i和arr之间没有多余的空间,gcc——i和arr之间有一个整形空间。
所以说平时我们写代码要注意不数组越界了。
①代码运行正常
②bug很少
③效率高
④可读性高(如良好的代码风格,函数名、变量名见名知意等)
⑤可维护性高
⑥注释清晰
⑦文档齐全
常见的coding技巧:
①使用assert
②尽量使用const
③养成良好的编码风格
④添加必要的注释
⑤避免编码的陷阱
模拟实现库函数strcpy:
strcpy:
1.函数原型:
2.函数功能:
4.函数的返回类型:
代码1:模拟实现strcpy
分析:
#include
#include
//自定义strcpy
//代码1
void my_strcpy(char* dest, const char* src)
{
//拷贝'\0'之前的字符
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
//拷贝'\0'
*dest = *src;
}
int main()
{
//将arr2中的字符串拷贝在arr1
char arr1[20] = "#############";
char arr2[] = "hello";
//调用库函数实现
//strcpy(arr1, arr2);
//调用自定义函数实现
my_strcpy(arr1, arr2);
//打印拷贝后的arr1
printf("%s\n", arr1);
return 0;
}
代码2:优化函数体
#include
#include
//自定义strcpy
//代码2
void my_strcpy(char* dest, const char* src)
{
//优化1:使用指针之前一定要检查是否有效,如果无效就报错
//assert--断言
// assert中可以放一个表达式,表达式结果为假就报错,为真就啥事都不发生,正常运行
//assert的头文件是assert.h
//assert其实在release版本中被优化调了
assert(dest && src);//断言指针的有效性
//优化2:使代码简化
//*dest++ = *src++;
//等价于
//*dest = *src;
//dest++;src++;
while (*dest++ = *src++)//'\0'的ASCII码值就是0,所以拷贝到'\0'停止
{
;
}
}
int main()
{
//将p中的字符串拷贝在arr1
char arr1[20] = "#############";
char* p = NULL;
//调用自定义函数实现
my_strcpy(arr1, p);
//打印拷贝后的arr1
printf("%s\n", arr1);
return 0;
}
程序结果:
代码3:优化函数的形参
如下代码我们程序不报错,但是没有成功完成我们想要的拷贝:
#include
#include
void my_strcpy(char* dest, char* src)
{
assert(dest && src);//断言指针的有效性
while(*src++ = *dest++)//程序员喝酒,写反了这样我们没有实现拷贝的目的
{
;
}//将src所指向内容拷贝到dest所指向数组
}
int main()
{
char arr[20] = "#############";
char arr1[20] = "hello";
my_strcpy(arr, arr1);
printf("%s\n", arr);
return 0;
}
该怎么避免出现这种错误呢?
我们先来学习const的作用:
#include
void test()
{
//代码1
//定义两个整型变量
int n = 10;
int m = 20;
//没有const修饰
int* p = &n;
//可以通过指针变量p将指针所指向的内容n的值改成20?
*p = 20;//ok
//可以修改指针变量本身?
p = &m; //ok
}
void test1()
{
//代码2
const int num = 10;
//num = 20;//err,因为num被const修饰,所以不能修改
//但是通过指针变量p,num能被修改了(p就像卖票的黄牛一样)
int* p = #
*p = 20;
}
void test2()
{
//代码3
int n = 10;
int m = 20;
//const放在*的左边
const int* p = &n;//也可写成:int const* p = &n;
//*p = 20;//err,因为const修饰的指针p指向的内容,所以不能通过指针来修改
p = &m; //ok,因为const只修饰的是指针p指向的内容,所以指针变量本身可以修改
}
void test3()
{
//代码4
int n = 10;
int m = 20;
//const放在*的右边
int* const p = &n;
*p = 20; //ok,因为const只修饰的是指针变量本身,所以指针指向的内容可以通过指针改变
//p = &m; //err,因为const修饰的是指针变量本身,所以指针变量本身不能被修改
}
int main()
{
//测试无cosnt的
test();
//测试const修饰变量
test1();
//测试const放在*的左边
test2();
//测试const放在*的右边
test3();
return 0;
}
结论:
const修饰指针变量的时候:
1.const如果放在*的左边,const修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变;但是指针变量本身可以修改。
2.const如果放在*的右边,const修饰的是指针变量本身,保证指针变量本身的内容不能被修改;但是指针指向的内容,可以通过指针来改变。
3.const就像法律,不能被修改。
学习了const的作用,我们来修改刚在代码的问题,可以运行但是没有完成拷贝,是因为*dest++和*src++写反了,因为src所指向的内容不变。所以我们可以在把第二个形参改成const int* src,用const修饰指针指向的内容,这样的话如果不小心将*dest++和*src++写反直接就编译错误,不会运行成功,很快就发现代码的错误了。
#include
#include
void my_strcpy(char* dest,const char* src)
{
assert(dest && src);//断言指针的有效性
while (*src++ = *dest++)//程序员喝酒,写反了这样我们没有实现拷贝的目的
{
;
}//将src所指向内容拷贝到dest所指向数组
}
int main()
{
char arr[20] = "#############";
char arr1[20] = "hello";
my_strcpy(arr, arr1);
printf("%s\n", arr);
return 0;
}
如下图:
代码4:优化函数的返回类型(最终的优化版本)
#include
#include
//库函数strcpy的返回值是目的地的起始地址
char* my_strcpy(char* dest,const char* src)
{
assert(dest && src);//断言指针的有效性
char* ret = dest;//存放目的地的起始地址
while (*dest++ = *src++)
{
;
}//将src所指向内容拷贝到dest所指向数组
return ret;
}
int main()
{
char arr[20] = "#############";
char arr1[20] = "hello";
//优点:链式访问(有返回值才可以)
printf("%s\n", my_strcpy(arr, arr1));
return 0;
}
运行结果:
练习:模拟strlen
#include
#include
//size_t是unsigned int的别名,因为长度没有负数
size_t my_strlen(const char* str)
{
assert(str != NULL);//断言指针的有效性
size_t count = 0;//计数
while (*str++)
{
count++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
printf("%d\n", my_strlen(arr));
return 0;
}
直接看错误提示信息(双击锁定),解决问题。或者凭借经验就可以搞定,相对来说简单。
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
类型1:库函数不包含头文件
类型2:拼写错误
借助调试,逐步定位问题,最难搞。
最后温馨提示:
做一个有心人,积累排错经验!