C语言--调试技巧(下)

简介: C语言--调试技巧(下)

6.小技巧(监视观察多个值):


void test(int arr[])
{
}
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  test(arr);
  return 0;
}


fb05d2544c1b490f899386644e6f37a3.png

疑惑:

因为arr指针中存放的是第一个元素的地址 所以只能看到一个元素

解决方法: 在arr后面加入,和数字(必须小于数组长度)

5292bec10d134150a7a4c14b81feaa48.png

总结:

多多动手,尝试调试,才能有进步。

一定要熟练掌握调试技巧。

初学者可能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;
}


前提说明:

06b87952354e43379eb077b79ecec049.png

首先按下F10,开始调试,打开监视窗口输入以下的变量

e70f811875484d28b6af7571c43b67a7.png

问题出现:

解题顺序:1

可以看出当n=3时,sum已经求完了1!+2!,所以前两个阶乘的之和的值是为3的,接着当代码走完当n=3,i=1时此语句中的条件语句时发现此时ret的值为2,根据前面知识可知i的值是为了求某次阶乘的具体值

所以i=1应该为ret=1!=1*1,答案为啥是2呢,接着往下看

c5d95d7bd2c74fefa724b75e57d598fe.png

2

i=2时,ret=4,此时应该为ret=2!=1*2=2,答案也不相符

168b1afb73a54a7bb76c65d26eb45e51.png

3

到i等于3时竟发现ret=12,但是ret=3!=1*2*3=6,怎么可以等于12呢,推测应该是第一步的时候就错了

24dd41bd71f847b49334493a089a5290.png

4

可以看到,当n=2时,i=2时,此时ret=2,所以导致了n=3,i=1时,ret=1*2=2,说明了此时ret还是保留了那个2的值,没有经历过重置

3ea141c055b342e2bcdb0472ff6b2cbc.png

所以正确的代码是:

//实现代码:求 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;
}


代码运行:

39222e08afb54e538a2b4b084462ea93.png

研究程序死循环的原因。

图解:

72c0627e1e74473197a28bf6167b6500.png

原理:

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

类比为:对于同一个房间,灯是开着的,我进去把灯关了,那你进去的时候,房间就是黑的

274c2dccc334469d8f6bab37cbb15883.png


8.库函数:strcpy() -- 字符串拷贝


1a2d109ab0fe4a1987685f38820c0f16.png

#include<string.h>
int main()
{
  char arr1[] = "hello world";
  char arr2[20] = { 0 };
  strcpy(arr2, arr1);
  printf("%s\n", arr2);
}

代码运行:

3b92b0e93914464582c94dc836cf8015.png


模拟实现库函数:

方式一

#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);
}


原理:

5046fa15de1a4ba98ae533ea7dc5fb7e.png

这种写法在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);
}


代码运行:

4cb735e671ff45e8a93251e00951a814.png

假设不用断言,就不知道错在哪里

用以下这个代码

#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);
}


执行:

48ce96cc54694b6e98571cc6c6877a61.png


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来改变

图解:

282f9b869fda4f928114b0008e6b35d2.png

3.这张图还包括了const两边都有*,那么以上两种赋值方式都是错误的


结论:

const修饰指针变量的时候:

1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。

2. const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指 针指向的内容,可以通过指针改变


11.编程常见的错误


编译型错误

直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。


链接型错误

看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不 存在或者拼写错误。


运行时错误

借助调试,逐步定位问题。最难搞。


最后:

做一个有心人,积累排错经验。

相关文章
|
7月前
|
存储 编译器 C语言
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存2
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存
|
7月前
|
程序员 C语言 C++
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存1
【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存
|
7月前
|
C语言
C语言使用宏定义实现等级调试输出PRINT_LEVEL
C语言使用宏定义实现等级调试输出PRINT_LEVEL
143 0
|
26天前
|
NoSQL 编译器 C语言
C语言调试是开发中的重要技能,涵盖基本技巧如打印输出、断点调试和单步执行,以及使用GCC、GDB、Visual Studio和Eclipse CDT等工具。
C语言调试是开发中的重要技能,涵盖基本技巧如打印输出、断点调试和单步执行,以及使用GCC、GDB、Visual Studio和Eclipse CDT等工具。高级技巧包括内存检查、性能分析和符号调试。通过实践案例学习如何有效定位和解决问题,同时注意保持耐心、合理利用工具、记录过程并避免过度调试,以提高编程能力和开发效率。
42 1
|
27天前
|
存储 算法 C语言
用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容
本文探讨了用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容,旨在为开发者提供全面的指导和灵感。
45 2
|
2月前
|
C语言
C语言调试
C语言调试
24 0
|
4月前
|
C语言 索引
C语言编译环境中的 调试功能及常见错误提示
这篇文章介绍了C语言编译环境中的调试功能,包括快捷键操作、块操作、查找替换等,并详细分析了编译中常见的错误类型及其解决方法,同时提供了常见错误信息的索引供参考。
|
6月前
|
安全 编译器 程序员
【C语言】:VS实用调试技巧和举例详解
【C语言】:VS实用调试技巧和举例详解
57 1
|
6月前
|
存储 编译器 C语言
C语言学习记录——调试技巧(VS2019环境下)
C语言学习记录——调试技巧(VS2019环境下)
66 2
|
7月前
|
程序员 C语言 C++
C语言实用的调试技巧
C语言实用的调试技巧
56 3