目录
实例一:实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出。
1什么是bug?
bug:本意是臭虫、缺陷、损坏、窃听器、小虫等意思。
第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。
多用来形容电脑程序或者软件出现问题,内部隐藏着不容易被发现的缺陷,会引起软件无法正常使用。
2.调试
所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧, 就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。 顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。
一名优秀的程序员是一名出色的侦探。 每一次调试都是尝试破案的过程。
2.1调试是什么?
调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序 错误(bug)的一个过程。(多练才能掌握技巧)
2.2 调试的基本步骤
2.3 Debug和Release
程序员的版本:
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
测试人员的版本:
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优 的,以便用户很好地使用。
release版本调试:
按下F10开始执行调试
打开监视窗口,输入变量名字
先按F10走一步,接着按下F11
会发现进入以下程序,无法观察值的变化
但如果改成debug,结果就不一样了
debug版本调试:
可以看到调试箭头进入函数内部,以及进入了循环体内,可以观察值的变化
3.如何调试
方法:
F5 - 开始调试 ,运行结果直接出现
ctrl + F5 开始执行(不调试)
F9 设置断点/取消断点
F10 逐过程 ,仅仅是一条语句一条语句的执行,会跨越函数执行F11 逐语句,会进入函数内部
F5和F9是配合使用的,若按下了F9设置断点,再按下F5,就会直接跳到第一个断点处,这两个功能键一起使用,才能发挥用处
特殊按键:
Fn 是辅助功能键(可以关闭)
Fn键是组合键,一般位于键盘的左下角,它是需要配合其它按键一起来使用的,使用fn配合其它按键可以实现调节笔记本屏幕亮度、声音大小、关闭屏幕显示、禁用触控板、开启/关闭无线网络等功能。
3.1开始调试
按下F10,调试开始,若遇到循环条件数值大的, 或者代码量大的程序,将会异常麻烦,不可能都按F10或者F11让程序一步一步走,
若此时认为前面的程序语句已经没有问题了,这时可以直接在你认为有毛病的或者你想停下的代码程序下按下F9,接着直接按下F5
结果:
此时可以发现整个程序就直接从断点处开始,前面的语句已经全部执行完了
3.2特殊情况(按F11的次数庞大):
若认为程序在循环语句下第500次出现了问题,像这种程序一步一步的按下去是很麻烦的,
方法1
可以对着断点按鼠标右键,打开条件, 断点符号会发生变化(x输入错误,应该是i)
接着按下F5, 可以发现直接变成了500,但是此语句还未执行(前面的条件都执行了)
方法2
可在想检查的循环次数中加入条件语句,下面是程序
#include<stdio.h>
void test()
{
printf("hehe\n");
printf("haha\n");
printf("enen\n");
}
int main()
{
int i = 0;
char ch[] = "abcdef";
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
test();
for (i = 0; i < 100; i++)
{
if (i == 50)
{
printf("aaa\n");
}
printf("%d ", i);
// printf("%d ", i);
}
/*for (i = 0; i < 10; i++)
{
printf("wakawaka\n");
}*/
return 0;
}
加入的条件语句:
if (i == 50)
{
printf("aaa\n");
}
3.3自动窗口和局部变量:
自动窗口:自动添加一些信息和捕获一些信息让你看到,程序执行的过程中上下文的变量自动加到这个窗口里面,优点是不用手动添加变量,自动就会添加,缺点是打印过程中有些变量会自动消失(自动删除)
自动窗口过程:
1
2
3
局部变量和自动窗口很类似,只是不监视全局变量(两者都是自动生成不用手动添加)
监视窗口可以监视变量的值和地址,也可以同时打开多个窗口
4.关于内存
每一个变量都有它对应的地址,这里是一行显示1列
这里是一行显示4列
5.调用堆栈
void test2()
{
printf("hehe\n");
}
void test1()
{
test2();
}
void test()
{
test1();
}
int main()
{
test();
return 0;
}
原理:
5.1栈的动态变化:
起初:
过程:
最终结果:
6.小技巧(监视观察多个值):
void test(int arr[])
{
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
test(arr);
return 0;
}
疑惑:
因为arr指针中存放的是第一个元素的地址 所以只能看到一个元素
解决方法: 在arr后面加入,和数字(必须小于数组长度)
总结:
多多动手,尝试调试,才能有进步。
一定要熟练掌握调试技巧。
初学者可能80%的时间在写代码,20%的时间在调试。
但是一个程序员可能20%的时间在写 程序,但是80%的时间在调试。
我们所讲的都是一些简单的调试。 以后可能会出现很复杂调试场景:多线程程序的调试等。
多多使用快捷键,提升效率。
7一些调试的实例
实例一:实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出。
错误代码:
//实现代码:求 1!+ 2!+3!+...+10!不考虑溢出
int main()
{
int n = 0;
int ret = 1;
int i = 0;
int sum = 0;
for (n = 1; n <= 3; n++)
{
for (i = 1; i <= n; i++)
{
ret *= i;
}
sum = sum + ret;
}
printf("%d\n", sum);
return 0;
}
前提说明:
首先按下F10,开始调试,打开监视窗口输入以下的变量
问题出现:
解题顺序:1
可以看出当n=3时,sum已经求完了1!+2!,所以前两个阶乘的之和的值是为3的,接着当代码走完当n=3,i=1时此语句中的条件语句时发现此时ret的值为2,根据前面知识可知i的值是为了求某次阶乘的具体值
所以i=1应该为ret=1!=1*1,答案为啥是2呢,接着往下看
2
i=2时,ret=4,此时应该为ret=2!=1*2=2,答案也不相符
3
到i等于3时竟发现ret=12,但是ret=3!=1*2*3=6,怎么可以等于12呢,推测应该是第一步的时候就错了
4
可以看到,当n=2时,i=2时,此时ret=2,所以导致了n=3,i=1时,ret=1*2=2,说明了此时ret还是保留了那个2的值,没有经历过重置
所以正确的代码是:
//实现代码:求 1!+ 2!+3!+...+10!不考虑溢出
int main()
{
int n = 0;
int ret = 1;
int i = 0;
int sum = 0;
for (n = 1; n <= 3; n++)
{
ret=1;
for (i = 1; i <= n; i++)
{
ret *= i;
}
sum = sum + ret;
}
printf("%d\n", sum);
return 0;
}
ret要在每次求完一个阶乘的时候重置为1,才能正确算出值
实例二 死循环与数组越界该值
#include<stdio.h>
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;
}
代码运行:
研究程序死循环的原因。
图解:
原理:
1.i和arr是局部变量,局部变量是放在栈区上的
2.栈区内存的使用习惯是:先使用高地址处的空间,再使用低地址处的空间
解释一下第2条:这条原理的作用体现在栈上,代码先定义的i,那么i就会在高地址处,后定义的是数组arr,数组在低地址处,所以上图的arr是位于i的下方处的
3.数组随着下标的增长, 地址是由低到高变化的
关于arr和i关系这是我的个人的理解:
假设i的地址是0x11223344,arr这个数组经过循环越界到了i的那块空间里面去,直到数组arr的下标为i时,即为arr[i]时,arr[i]和i是属于同一块空间,所以对于arr[12]=0,进行赋值的时候,相当于将i的值也改成了0
类比为:对于同一个房间,灯是开着的,我进去把灯关了,那你进去的时候,房间就是黑的
8.库函数:strcpy() -- 字符串拷贝
#include<string.h>
int main()
{
char arr1[] = "hello world";
char arr2[20] = { 0 };
strcpy(arr2, arr1);
printf("%s\n", arr2);
}
代码运行:
模拟实现库函数:
方式一
#include<string.h>
void my_stcpy(char* dest, char* src)
{
while (*src != '\0')
{
*dest = *src++;
}
*dest = *src;//\0的拷贝
}
int main()
{
char arr1[] = "hello world";
char arr2[20] = { 0 };
strcpy(arr2, arr1);
printf("%s\n", arr2);
}
原理:
这种写法在while循环内不会拷贝'\0',所以要单独写一条 *dest = *src;
方式二
#include<stdio.h>
void my_stcpy(char* dest, char* src)
{
while (*dest++ = *src++)
{
;
}
}
int main()
{
char arr1[] = "hello world";
char arr2[20] = { 0 };
my_strcpy(arr2, NULL);//若传参传错了就会抛出问题
printf("%s\n", arr2);
}
遇到'\0',其ascll码值是0,0为假,跳出循环,这样写即把字符串拷贝过去,也把'\0'拷贝过去,但是到了'\0'的位置,dest和src还是会++
9.断言
作用:发现问题,就会把问题抛出来
假设传参传错了,错误参数:NULL
my_strcpy(arr2, NULL);
使用断言
#include<stdio.h>
#include<string.h>
#include<assert.h>
void my_strcpy(char* dest, char* src)
{
//断言
assert(dest&&src != NULL);//条件为真的话就不会报错
while (*dest++ = *src++)
{
;
}
}
int main()
{
char arr1[] = "hello world";
char arr2[20] = { 0 };
my_strcpy(arr2, NULL);//若传参传错了就会抛出问题
printf("%s\n", arr2);
}
代码运行:
假设不用断言,就不知道错在哪里
用以下这个代码
#include<stdio.h>
void my_strcpy(char* dest, char* src)
{
if (dest == NULL || src == NULL)//遇到问题不解决问题,逃避问题
{
return;
}
while (*dest++ = *src++)
{
;
}
}
#include<string.h>
int main()
{
char arr1[] = "hello world";
char arr2[20] = { 0 };
my_strcpy(arr2, NULL);
printf("%s\n", arr2);
}
执行:
10.const的作用
const在*左边
int main()
{
int n = 10;
const int m = 0;//m=20;//err,错误
//const修饰指针
//1.const 放在*的左边,*p不能改了,也就是p指向的内容,不能通过p来改变了。但是p是可以改变的,p可以指向其他的变量
const int* p = &m;
//*p = 20;err
p = &n;//ok
return 0;
}
1.const 放在*的左边,*p不能改了,也就是p指向的内容,不能通过p来改变了。但是p是可以改变的,p可以指向其他的变量
const在*右边
#include<stdio.h>
int main(){
int n = 10;
const int m = 0;//m=20;err,错误
//2.const 放在*的右边,限制的是p,p不能改变,但是p指向的内容*p,是可以通过p来改变
int* const p = &m;
*p = 20;
//p = &n;
printf("%d\n", m);
return 0;
}
2.const 放在*的右边,限制的是p,p不能改变,但是p指向的内容*p,是可以通过p来改变
图解:
3.这张图还包括了const两边都有*,那么以上两种赋值方式都是错误的
结论:
const修饰指针变量的时候:
1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。
2. const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指 针指向的内容,可以通过指针改变
11.编程常见的错误
编译型错误
直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。
链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不 存在或者拼写错误。
运行时错误
借助调试,逐步定位问题。最难搞。
最后:
做一个有心人,积累排错经验。