实用调试技巧 下

简介: 实用调试技巧 下

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

①一定要熟练掌握调试技巧;

②初学者可能80%的时间在写代码,20%的时间在调试。但是一个程序员可能20%的时间在写代码,但是80%的时间在调试。

③我们现在所讲的都是一些简单的调试,以后可能会出现很复杂的调试场景:多线程程序的调试等。

④多多使用快捷键,提升效率。


5.一些调试的实例

实例一:

实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出。

代码1:实现阶乘

#include<stdio.h>
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<stdio.h>
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 <stdio.h>
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之间有一个整形空间。

所以说平时我们写代码要注意不数组越界了

6.如何写出好(易于调试)的代码

6.1优秀的代码:

①代码运行正常

②bug很少

③效率高

④可读性高(如良好的代码风格,函数名、变量名见名知意等)

⑤可维护性高

⑥注释清晰

⑦文档齐全

常见的coding技巧:

①使用assert

②尽量使用const

③养成良好的编码风格

④添加必要的注释

⑤避免编码的陷阱

6.2示范

模拟实现库函数strcpy:

strcpy:

1.函数原型

2.函数功能:

3.函数参数:

4.函数的返回类型:

代码1:模拟实现strcpy

分析:

#include<stdio.h>
#include<string.h>
//自定义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<stdio.h>
#include<assert.h>
//自定义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<stdio.h>
#include<assert.h>
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 <stdio.h>
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 = &num;
  *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<stdio.h>
#include<assert.h>
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<stdio.h>
#include<assert.h>
//库函数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<stdio.h>
#include<assert.h>
//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;
}

7.编程常见的错误

7.1编译型错误(语法错误)

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


7.2链接型错误

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

类型1:库函数不包含头文件


类型2:拼写错误

我们怎么找到错误位置?


7.3运行时错误(编译、链接都没错,但是运行结果有问题)

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

最后温馨提示:

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

相关文章
|
10月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
970 4
|
存储 安全 API
阿里云oss收费标准新版介绍
阿里云对象存储服务(Object Storage Service,简称OSS)是阿里云提供的一种海量、安全、低成本、高可靠的云存储服务。它适用于各种场景,如网站、移动应用、大数据分析、备份与归档等。OSS提供标准的RESTful API接口,支持多种语言SDK,方便用户进行文件上传、下载、管理和授权等操作。
|
API Serverless 监控
函数组合的N种方式
随着以函数即服务(Function as a Service)为代表的无服务器计算(Serverless)的广泛使用,很多用户遇到了涉及多个函数的场景,需要组合多个函数来共同完成一个业务目标,这正是微服务“分而治之,合而用之”的精髓所在。
2470 0
|
数据采集 中间件 Python
Scrapy爬虫:利用代理服务器爬取热门网站数据
Scrapy爬虫:利用代理服务器爬取热门网站数据
|
11月前
|
传感器 机器学习/深度学习 人工智能
光子集成电路:光子学与电子学的结合
【10月更文挑战第18天】光子集成电路(PIC)结合了光子学与电子学的优势,利用光子作为信息传输和处理的载体,具备高速传输、大带宽、低功耗和高集成度等特点。本文介绍其基本原理、技术优势及在高速光通信、光计算、传感器和激光雷达等领域的应用前景,展望未来发展趋势与挑战。
|
6月前
|
Rust JavaScript 前端开发
[oeasy]python075_什么是_动态类型_静态类型_强类型_弱类型_编译_运行
本文探讨了编程语言中的动态类型与静态类型、强类型与弱类型的概念。通过实例分析,如Python允许变量类型动态变化(如`age`从整型变为字符串),而C语言一旦声明变量类型则不可更改,体现了动态与静态类型的差异。此外,文章还对比了强类型(如Python,不允许隐式类型转换)和弱类型(如JavaScript,支持自动类型转换)的特点。最后总结指出,Python属于动态类型、强类型语言,对初学者友好但需注意类型混淆,并预告下期内容及提供学习资源链接。
160 22
|
JavaScript API 数据安全/隐私保护
【Azure Developer】Azure AD 注册应用的 OAuth 2.0 v2 终结点获取的 Token 解析出来依旧为v1, 这是什么情况!
【Azure Developer】Azure AD 注册应用的 OAuth 2.0 v2 终结点获取的 Token 解析出来依旧为v1, 这是什么情况!
159 0
【R语言实战】——带有新息为标准学生t分布的金融时序的GARCH模型拟合预测
【R语言实战】——带有新息为标准学生t分布的金融时序的GARCH模型拟合预测
|
10月前
|
SQL 监控 大数据
优化AnalyticDB性能:查询优化与资源管理
【10月更文挑战第25天】在大数据时代,实时分析和处理海量数据的能力成为了企业竞争力的重要组成部分。阿里云的AnalyticDB(ADB)是一款完全托管的实时数据仓库服务,支持PB级数据的秒级查询响应。作为一名已经有一定AnalyticDB使用经验的开发者,我发现通过合理的查询优化和资源管理可以显著提升ADB的性能。本文将从个人角度出发,分享我在实践中积累的经验,帮助读者更好地利用ADB的强大功能。
261 0
|
Java API Spring
Spring6(七):手写IoC
Spring6(七):手写IoC
126 0