调试是什么?
调试:又称除错,是发现和减少计算机程序或电子仪器设备错误的一个过程。
调试的基本步骤
1:发现程序错误的存在
2:以隔离和消除等方式对错误进行定位
3:情定错误产生的原因
4:提出纠正错误的解决办法
5:对程序错误予以改正,重新测试
Debug和Release版本的
Debug通常称为调试版本:包含调试信息,所以占的内存比较大,并且不做任何优化,利于程序员进行调试
Release为发布版本,进行了各种优化,程序在代码和圆形速度上都是最优的,以便用户更好的使用
我们用代码讲一下dubug版本
int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d", i);
}
return 0;
}
在debug版本下,我们按F10或F11
我们可以通过F10或F11来控制代码运行的过程,我们打开监视也能看见变量的变化过程,假如我们切换为release版本,再按F10结果为
证明release版本下是不存在调试信息的,所以无法进行调试
接下来,我们介绍一下调试的快捷键
F5:启动调试,经常用来跳到下一个断点
F9:创建断点和取消断点
断点的重要作用:可以在程序的任意位置设置断点
这样就可以让程序在想要停下的位置停止,进而一步步执行下去
F10:逐过程,通常用来处理一个过程,这个过程可以是一个语句,也可以是一个函数
F11:逐语句,就是每次执行一个语句,但是这个快捷键可以让我们的执行逻辑进入函数内部
CTRL+F5:开始执行不调试,通常用于函数运行
F5调试
F5调试通常是与F9一起配合使用的
例如正常情况下
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d", i);
}
return 0;
}
这假如我们知道我们的第一个for循环是没有问题的,我们想要直接跳过第一个for循环来访问第二个for循环,如果用最普通的方法的话,就需要按10多次F11,这时候我们就可以使用F9和F5配套使用
我们在第二个for循环处设置断点,按下F5
可以发现,我们的箭头直接指向了第二个for循环,我们可以通过监视看
可以发现,我们数组的十个元素全部已经被初始化了,所以F5配合F9配套使用是直接跳过前面的所有代码(但会执行),直接跳到断点位置,继续执行。
这时候,有人会问,那怎么跳过scanf呢?scanf是需要输入值的,怎么跳过呢?
很简单,我们写个代码
我们设置一个断点,该段点是在scanf之后的,我们按下F5
这时候,弹出一个对话框,我们输入十个值,这十个代表我们要输入scanf的值
我们输入1 2 3 4 5 6 7 8 9 10
可以发现,我们已经跳过第一个for循环,并且我们的数组元素已经被初始化完毕
我们如果要设置两个端点的话
那我们能不能按两下F5,直接跳到858第二个for循环呢?
答案是不能
原因是我们跳过断点跳到的是逻辑上的下一个断点,逻辑上的下一个断点就表示i=1的第二次循环。
禁用断点
假设一个断点我们不想用了但不想删掉,这时候我们禁用断点
这时候保存了第一个断点,但是这个断点并不起作用
假设我们的断点设置处是在一个循环中,并且我们知道这个循环的前五次是没有问题的,这时候我们如何设置呢
我们右键断点
点击条件
输入条件,i==5
我们按下F5,这时候我们需要输入5个值所为scanf的需要
可以发现,我们直接跳过了前五次循环 。
F10和F11的区别
区别在函数调用上
int add(int a, int b)
{
return a + b;
}
int main()
{
int a = 10;
int b = 20;
int c = add(a, b);
printf("%d", c);
return 0;
}
我们首先按F10进行调试,调试到函数部分
再按一下F10
可以发现,直接跳过了函数的内部部分,没有对函数进行访问
假设我们按F11
可以发现,进入了函数内部,由此可得,F11和F10的区别是F11可以进入函数内部,而F10不能
自动窗口:
int add(int a, int b)
{
return a + b;
}
int main()
{
int a = 10;
int b = 20;
int c = add(a, b);
printf("%d", c);
return 0;
}
我们打开调试,窗口里面的自动窗口int add(int a, int b)
可见其自动加入了参数a和b,随着代码的运行,参数又多了个c
接下来,当我们调试到函数内部时
可以发现,其自动添加了x,y,而删除了参数a,b,c
所以自动窗口会自动加载我们上下文的一些信息,但自动窗口也会自作主张,可能达不到我们的预期
局部变量
我们使用局部变量窗口进行调试
我们可以发现,参数的位置会发生跳动,观察起来也不够方便。
监视窗口
需要我们自动输入,但观察起来方便
而且就算我们进入函数内部,也可以对a,b进行查看
而且我们能根据我们的需求,随意进行参数的查看
而且我们甚至可以输入其他非参数值,例如
接下来,我们讲一下数组传参的一些知识
void test(int a[])
{
//
}
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
test(arr);
return 0;
}
我们进行调试,输入arr,可以看到数组的全部内容
但当我们输入a的时候呢,a也是数组
可以发现,a并不能显示所有数组的元素,原因是 a在这里表示的是数组首元素的地址,我们如何显示数组arr的全部元素呢?
我们把这里的a换成a,10就可以了
内存
我们打开调试窗口里的内存
最左侧的0x00表示数组的地址 中间的四行表示内存, 一行表示四个字节,四行中的内容用16进制表示。
我们进行调试的实例
求出n!+(n-1)!+(n-2)!----1!
我们首先学习求n的阶乘
int main()
{
int a = 0;
scanf("%d", &a);
int ret = 1;
int i = 0;
for (i = 1; i <= a; i++)
{
ret *= i;
}
printf("%d", ret);
return 0;
}
我们进行思考,如何求阶乘的和呢?
int main()
{
int a = 0;
scanf("%d", &a);
int ret = 1;
int i = 0;
int j = 0;
int sum = 0;
for (j = 1; j <= a; j++)
{
for (i = 1; i <= j; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d", ret);
return 0;
}
这个代码看上去没什么错误,我们运行一下,检验结果
正确的结果应该为6+2+1=9,但结果却为12,我们进行调试检查错误
可以发现,我们在还没有计算在求3!的时候,可以发现ret的值并不是1,3!的结果是6,但最后ret求出的结果却为12
所以我们要在前面进行初始化操作
int main()
{
int a = 0;
scanf("%d", &a);
int ret = 1;
int i = 0;
int j = 0;
int sum = 0;
for (j = 1; j <= a; j++)
{
ret = 1;
for (i = 1; i <= j; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d", sum);
return 0;
}
我们进行运行,输入3
可以发现,结果正确。
我们再写一个代码
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;
}
正常情况下,这组代码应该会造成越界访问,因为我们的数组元素只有10个,我们却访问了13个元素,我们进行运行
可以发现,无限循环打印hehe,这是为什么呢?我们进行调试
我们可以发现,arr[12]始终等于i,
可以发现,i的地址和arr[12]的地址也相同
得出,arr[12]=i
为什么呢?
这时候,我们要说一下栈
数组arr和变量i都是在栈区,
栈区的使用规则:先使用高地址的内存空间,再使用低地址的内存空间
2:数组随着下标的增长,地址开始由低到高
由此,我们可以写出原因:我们先创建高地址处的i变量,在创建相对低地址的数组arr,因为数组元素随下标是从低到高增长的,所以随着下标的增长,低地址的数组的元素可能会覆盖到高地址的变量i上,这时候,这两个数就相等了,因为i值始终等于arr[12],所以当arr[12]被初始化为0时,i也为0
为什么没有报错呢?
因为代码一直在执行死循环,来不及报错。
字符串拷贝
最简单的方法
#include<string.h>
int main()
{
char arr1[] = "hello bit";
char arr2[20] = { 0 };
strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
其中,字符串拷贝函数strcpy的头文件是#include<string.h>,格式是strcpy(目标数组,原数组)
但是这种方法没法知道是否把/0也拷贝过去了,所以我们换一个初始化方式
#include<string.h>
int main()
{
char arr1[] = "hello bit";
char arr2[20] = "xxxxxxxxxx";
strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
假如我们传递过去了/0,那么打印的结果应该就是hello bit 否则就是hello bitx,我们进行运行
所以得出结果,strcpy是会把/0也拷贝过去的
我们自己模拟实现一个字符串拷贝函数
void my_strcpy(char*dest, char*src)
{
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
*dest = *src;
}
int main()
{
char arr1[] = "hello bit";
char arr2[20] = "xxxxxxxxxx";
my_strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
*dest = *src;
对这一部分进行解释:src实际上是arr1数组首元素的地址,dest是数组arr2数组首元素的地址,我们把src对应的元素赋值给dest对应的元素,顺便让两个地址++,表示循环执行赋值,然后我们设置限定条件,当str不等于\0的时候执行循环,当他等于\0的时候中断循环,中断循环后我们的\0还没有赋值,这时候我们把\0赋值给*dest。
但这种方法略显繁琐,我们进行优化
void my_strcpy(char*dest, char*src)
{
while (*src != '\0')
{
*dest++ = *src++;
}
*dest = *src;
}
我们可以使用后置++来简化步骤
但是这两种方法有个共性,那就是把\0和其他元素分隔开进行了运算,有没有方法让他们合并起来运算呢?
void my_strcpy(char*dest, char*src)
{
while (*dest++ = *src++);
{
;
}
}
我们把我们要执行的while循环内部的语句直接设置成为while循环的判断部分,因为while循环的判断部分也需要执行,当运行到\0时,执行*dest++ = *src++,但是由于*src=*dest=\0 \0 的arcii码值是0,所以我们的循环终止,但我们的元素包括\0全部被拷贝过去了
void my_strcpy(char*dest, char*p)
{
while (*dest++ = *src++);
{
;
}
}
int main()
{
char arr1[] = "hello bit";
char arr2[20] = "xxxxxxxxxx";
char*p = NULL;
my_strcpy(arr2, NULL);
printf("%s\n", arr2);
return 0;
}
假设我们传递错误,传递过去了一个空指针,对空指针解引用就会产生错误,那怎么办呢?
这时候,我们引入函数assert,assert的头文件是#include<assert.h>,assert(i),assert函数的内部的i为真的情况下,程序继续运行,假设i为假的情况下,则报错
#include<assert.h>
void my_strcpy(char*dest, char*src)
{
assert(src != NULL);
while (*dest++ = *src++);
{
;
}
}
假设没有src!=NULL,则代码顺利进行,如果src==NULL,则代码报错
假如我们不下心把src和dest写反了呢?然后我们进行赋值的结果也犯了,也就产生错误了。如何进行纠正呢?
void my_strcpy(char*dest,const char*src)
我们只需在函数声明部分这样写,这样写的意思是固定*src的值,不允许通过解引用改变src的值,为什么要这样写呢?
假如我们在函数定义的内部把dest和src写反了,*src=*dest,这时候就会产生错误,程序无法运行
接下来,我们介绍一下const
int main()
{
const int num = 10;
num = 20;
return 0;
}
说明num这时候是不可修改的值,原因是被const修饰的对象,相当于num是一个常量,无法进行更改
那num就不能改变了吗?
答案是否定的
int main()
{
const int num = 10;
int *p = #
*p = 20;
printf("%d", num);
return 0;
}
我们绕一下,我们通过指针的方法改变num的值是可以实现的
假设我们的const修饰指针会怎么样
int main()
{
int num = 10;
const int *p = #
*p = 20;
printf("%d", num);
return 0;
}
可以发现,当const在*的左侧的时候,不能通过解引用的方法改变p值
但当我们打印*p的时候,也是可以打印的,得出结论,*修饰指针的时候,是可以访问的,但不可以更改。
但是这时候能不能改变num的值呢?同理,也是可以改变的
可以得出结论,当const修饰指针的时候,我们不能通过解引用的方法改变被const修饰的指针指向的元素值,但我们可以直接通过赋值的方法改变指针指向的元素值。
那么p可以改吗?
可以发现,p是可以更改的,所以const修饰指针,限制的只是指针指向的变量,而不是指针本身