【C语言学习】实用调试技巧【初阶详解篇12】

实用调试技巧

什么是bug?

第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。

调试是什么?

调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。

调试的基本步骤

1.发现程序错误的存在
2.以隔离、消除等方式对错误进行定位
3.确定错误产生的原因
4.提出纠正错误的解决办法
5.对程序错误予以改正,重新测试

Debug和Release的介绍
  • Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
  • debug版本可以调试
  • Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优
    的,以便用户很好地使用。
  • release版本不能调试

调试演示的代码如下:

#include<stdio.h>
int main()
{
 int  arr[10] = { 0 };
 int sz = sizeof(arr) / sizeof(arr[0]);
 int i = 0;
 for (i = 0; i < sz; i++)
 {
  arr[i] = i + 1;
 }
 for (i = 0; i < sz; i++)
 {
  printf("%d\n", arr[i]);
 }
 return 0;
}

在这里插入图片描述
在这里插入图片描述

  • 调试的前提条件:将环境设置成debug版本
  • F9设置断点,与F5启动调试配合使用,先按F9再按F5
  • 断点:程序执行到某一处会断开
  • f9和f5应用场景:当一段代码的上一部分已经确定正确之后,但下面的代码不确定时,就在下面的代码处设置断点,点击f9后点击f5这样就不会再调试前面正确的代码,而只需调试后面的代码就行了,有利于提高调试的效率。
常用快捷键
  • F5:启动调试,经常用来直接调到下一个断点处。
  • F9:创建断点和取消断点 断点的重要作用,可以在程序的任意位置设置断点。这样就可以使得程序在
    想要的位置随意停止执行,继而一步步执行下去。
  • F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。但F10无法进到函数内部,它一遇到函数就会直接跳过。
  • F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是
    最常用的),它的细粒度更高。
  • CTRL + F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。

在这里插入图片描述
点击:查看更多快捷键

调试的时候查看程序当前信息
查看临时变量的值

在这里插入图片描述

查看内存信息

在这里插入图片描述
提示:内存中一个16进制表示4个二进制位,2个16 进制表示8个二进制位,8个二进制位=1个字节

查看调用堆栈

1.函数的调用堆栈反映的是函数的调用逻辑
2.通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。

在这里插入图片描述

查看汇编信息

在调试开始之后,有两种方式转到汇编:
(1)第一种方式:右击鼠标,选择【转到反汇编】:
在这里插入图片描述

(2)第二种方式:按照步骤可以切换到汇编代码。

在这里插入图片描述

查看寄存器信息

通过步骤:可以查看当前运行环境的寄存器的使用信息。
在这里插入图片描述

模拟实现strcpy

实现目标:将arr2的内容拷贝放到arr1里面去

#include<stdio.h>
#include<string.h>
int main()
{
 char arr1[20] = "xxxxxxxxxxx";
 char arr2[] = "hello";
 strcpy(arr1, arr2);//arr1:目标空间的起始地址;arr2:源空间的起始地址
 printf("%s\n", arr1);//打印结果:hello
  return 0;
}

疑问:为什么只打印hello后面的xxxx怎么没打印?
答:strcpy将arr2的字符串连同\0一起拷贝到arr1里了,arr1前五个x被hello覆盖,第六个x被\0覆盖,因为\0是字符串结束标志,所以碰到\0就结束了,因此后面的xxxx没有被打印。

下面我们来通过自定义函数进行调用,来模拟实现strcpy

#include<stdio.h>
#include<string.h>
void my_strcpy(char* dest,char* src)//dest:目的地,src:源头
{
 while (*src != '\0')
 {
  *dest = *src;
  dest++;//接着下一个字符
  src++;
  //不断赋值,++,赋值...可见它是一个循环,因此可以使用while循环,那什么时候停下来呢?
  //答:根据循环条件,*src!=\0时就循环,等于\0的话就停止循环跳出,
  //但是此时\0并没有拷进arr1中,所以需要在循环停止后,将 *dest = *src;
  }
 *dest = *src;//把\0放到第6个x中去
   //此时这个*src就是\0,并将其放到*dest中
}
int main()
{
 char arr1[20] = "xxxxxxxxxxx";
 char arr2[] = "hello";
 my_strcpy(arr1, arr2);//arr1:目标空间的起始地址;arr2:源空间的起始地址
 printf("%s\n", arr1);//打印结果:hello
 return 0;
}

代码分析:
在这里插入图片描述
监视结果:
在这里插入图片描述
在这里插入图片描述
其实这个代码不够好,下面我们来将它进行优化

优化1

#include<stdio.h>
#include<string.h>
void my_strcpy(char* dest, char* src)
{
 while (*src != '\0')
 {
  *dest++ = *src++;//hello的拷贝
 }
 *dest = *src;//\0的拷贝
}

更进一步优化2

void my_strcpy(char* dest, char* src)
{
 while (*dest++ = *src++)//这样写会将包括\0的字符也传给dest,而\0的assic码值为0,0为假,因此,循环结束。这样既完成了拷贝,又完成了停下来的动作,代码简练,完美!!
 {
  ;
 }
}
int main()
{
 char arr1[20] = "xxxxxxxxxxx";
 char arr2[] = "hello";
 my_strcpy(arr1, arr2);
 printf("%s\n", arr1);
 return 0;
}

还可以再优化吗?可以!! 加断言assert!!!有利于帮助我们定位问题
比如如果arr2传过来的是一个空指针,而空指针又不能解引用操作该怎么办呢?

空指针不能进行解引用操作的演示
在这里插入图片描述

assert的使用

加断言assert的代码如下:

#include<assert.h>
void my_strcpy(char* dest, const char* src)
{
 assert(src != NULL);//断言,使用assert需要引用头文件assert.h
 assert(dest!= NULL);
 //当src为有效内容时,即为真时,assert不做任何反应,当src为空指针时,即为假时,assert会提示报错
 while (*dest++ = *src++)
  //优化2加const
 //while (*src++ = *dest++)//如果没发现条件写反了怎么办?此时编译也能编译过去,但结果错误,如截图(无const编译),但是如果我们在形参里写个const,编译时编译不过去,还会提示错误信息
 {
  ;
 }
}
int main()
{
 char arr1[20] = "xxxxxxxxxxx";
 char arr2[] = "hello";
// my_strcpy(arr1, NULL);//arr2传过来的是一个空指针
 my_strcpy(arr1, arr2);//断言报错,改正
 printf("%s\n", arr1);
 return 0;
}

断言失败的崩溃提示如图:
在这里插入图片描述
无const编译:
在这里插入图片描述
提示:断言不一定只能断言指针哦!

const的使用
#include<stdio.h>
int main()
{
 //const修饰变量,这个变量就被称为常变量,不能被修改,但是本质上还是变量
 const int num = 10;//加const,不让num的值被修改
 num = 20;//此时num不能被修改
 int *p = &num;//既然直接改不行那就用指针的方法改
 *p = 20;//此时num被改了,但是违背了不让改的const设置
 //有没有一种方法,即使将num的地址交给了p,解引用之后也修改不了num的值
 //有!!!
 //只需要在int *p = &num;前面加上const就可以了
 const int * p = &num;//const放在*左边
 int n = 100;
 *p = 20;//此时就编译不过去了,也就不能改了error
 p = &n;//这个p是指针变量,可以被修改  ok
  int *const p = &num; //const放在*的右边
 int n = 100;
 *p = 20;//ok
 p = &n; //error
 printf("%d\n", num);
 return 0;
}

代码讲解:

  • const int * p = & num;
    const修饰变量的时候,const如果放在 * 的左边,修饰的是*p,表示指针指向的内容,是不能通过指针来改变的,但是指针变量本身可以修改。
  • int *const p = #
    const修饰变量的时候,const如果放在 * 的右边,修饰的是指针变量p,表示指针变量不能被改变但是指针指向的内容,可以被改变
    const修饰指针的作用:场景描述案例:将上述代码和此场景类比结合加深理解
    在这里插入图片描述

下面我们再返回这个代码看看,来理解const放在char* src的前面具有的特殊意义!!

#include<assert.h>
#include<stdio.h>
#include<string.h>
void my_strcpy(char* dest, const char* src)
{
 assert(src != NULL);//断言,使用assert需要引用头文件assert.h
 assert(dest != NULL);
 while (*dest++ = *src++)
 {
  ;
 }
}
int main()
{
 char arr1[20] = "xxxxxxxxxxx";
 char arr2[] = "hello";
 my_strcpy(arr1, arr2);
 printf("%s\n", arr1);
 return 0;
}
  • 这个代码是 把src指向的内容拷贝放进dest指向的空间中,从本质上讲希望dest指向的内容被修改,src指向的内容不能被修改,所以在char* src前面加上const,以防止我们在while()里的条件被写反,一旦写反就会报错,容易被发现并及时修改!
    但是不能在char* dest前加const,因为我们的目的是要修改dest的,所以加不加const,加在哪里,也是需要慎重考虑的!!

进一步优化:下面这个代码才是标准的进行strcpy库函数的模拟实现

strcpy这个库函数,其实返回的是目标空间的起始地址

char*  my_strcpy(char* dest, const char* src)
{
 assert(src != NULL);//断言,使用assert需要引用头文件assert.h
 assert(dest != NULL);
 char* ret = dest;//将目标空间的起始地址放到ret里面去
 while (*dest++ = *src++)
 {
  ;
 }
 return ret;//返回目标空间的起始地址,此时返回类型也要改成char*
}
int main()
{
 char arr1[20] = "xxxxxxxxxxx";
 char arr2[] = "hello";
 printf("%s\n", my_strcpy(arr1,arr2));//my_strcpy可以直接返回目标空间的起始地址,
 //这里是用my_strcpy的返回值作为printf函数的参数,这就是链式访问(将函数串起来)
 return 0;
}

上述代码模拟实现需要注意的四个方面
在这里插入图片描述

下面再写一个库函数strlen的模拟实现

返回类型也可设成size_t,size_t相当于unsigned int(无符号整型),防止产生负数
int  my_strlen(const char* arr1)//不希望arr1数组的内容被改变,加上const,
{
 assert(arr1 != NULL);//断言断言的是指针变量本身,保证指针有效性
 int count = 0;//使用计数器方法计算长度
 //size_t count = 0;
 while (*arr1 != '\0')
 {
  count++;
  arr1++;
 }
 return count;//count是整型返回值类型也要设为整型
}
int main()
{
 char arr1[] = "hello world";
 int len =my_strlen(arr1);
 printf("%d\n", len);
}
编译常见错误分类
  • 编译型错误:直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。
  • 链接型错误:看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
  • 运行时错误:借助调试,逐步定位问题。最难搞。
补充:库函数源码的查找方式

以我电脑上vs2013版本为例演示步骤:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
搜索结果
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值