C语言VS2017 - 实用调试技巧 下

简介: C语言VS2017 - 实用调试技巧

六、调试实例

注:以下代码均为问题代码

1、实例一

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

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
  int n = 0;
  scanf("%d", &n);
  int i = 0;
  int j = 0;
  int ret = 1;
  int sum = 0;
  for(j = 1; j <=n; j++)
  {
    for(i = 1; i <= j; i++)
    {
      ret *= i;
    } 
    sum += ret;
  }
  printf("%d\n", sum);
  return 0;
}

现象,当求3的阶乘时,输出的是15,答案与预期不符(这段代码相对简单这里就自己调试解决)这种错误被称为运行时错误,也是未来比较常见和比较难发现的一种错误,能通过调试解决的就是运行时错误

2、实例二(出自《C陷阱和缺陷》曾经 nice2016的校招笔试题)

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

现象:死循环

经调试发现造成死循环的直接原因是:

为什么改了arr[12],而i也改了 ?其实不难想象它们同在一块空间

我们不妨大胆的猜测一下

这里面是有原因的,当然也有一定程度的巧合

1、i 和arr 是局部变量,而局部变量是放在栈区上的(注意不要跟数据结构的栈混淆了)

2、栈区内存的使用习惯:先使用高地址空间,再使用低地址空间

3、数组随着下标的增长,地址是由低到高变化的

这里如何避免死循环呢?

1、只要先定义arr数组再定义 i 即可

2、控制循环次数,<=11即可

经测试不同的编译器下 i 和 arr 在内存中的布局:中间相距的空间也不同,以上面代码为例:

1、VC6.0  ->  相差0个整型,<=10即死循环

2、gcc    ->  相差1个整型,<=11即死循环

3、VS2017  ->  相差2个整型,<=12即死循环

所以数组只要向上越界的合适就会造成死循环


Release相比于Debug的还有一点就是Release会对代码进行优化(使之不会死循环)

Release是怎么优化的?

这里Release在发现问题后,会对局部变量 i 和 arr 在栈区上的顺序进行适应的调整

七、如何写出好(易于调试)的代码

1、优秀的代码:

  • 代码运行正常
  • bug很少
  • 效率高
  • 可读性高
  • 可维护性高
  • 注释清晰
  • 文档齐全

2、常见的coding技巧

  • 使用assert
  • 尽量使用const
  • 养成良好的编码风格
  • 添加必要的注释
  • 避免编码的陷阱

3、实例示范

1、模拟实现strcpy

简单介绍strcpy函数,所在头string,它可以进行字符串拷贝(包括\0)

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

调试发现strcpy在拷贝字符串的时,也包含 \0:


使用my_strcpy函数来模拟strlen

#include<stdio.h>
void my_strcpy(char* dest, char* src)
{
  while(*src != '\0')
  {
    //赋值
    *dest = *src;
    //调整
    dest++;
    src++;
  }
  *dest = *src;
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  my_strcpy(arr1, arr2);
  printf("%s\n", arr1);//hello
  return 0;
}

1. 优化1(简洁)

#include<stdio.h>
void my_strcpy(char* dest, char* src)
{
  while(*src != '\0')
  {
    //赋值+调整   
    *dest++ = *src++;//hello的拷贝 
  }
  *dest = *src;//\0的拷贝
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  my_strcpy(arr1, arr2);
  printf("%s\n", arr1);//hello
  return 0;
}

2. 再优化2(简洁)

#include<stdio.h>
void my_strcpy(char* dest, char* src)
{
  while(*dest++ = *src++)//既拷贝了字符串(包括\0),又可以利用表达式让循环停下来
  {
    ;
  }
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  my_strcpy(arr1, arr2);
  printf("%s\n", arr1);//hello
  return 0;
}

3. 再优化3(从指针安全的角度考虑)

#include<stdio.h>
#include<assert.h>
void my_strcpy(char* dest, char* src)
{
  //如果my_strcpy传过来的参数是空指针时,此时再去解引用、++等一系列操作时,这是非法的
  //这里有一个函数assert:断言,所在头assert。如果表达式里为真,则什么都不执行,否则将会停留在断言为假的那一行,不再执行下面代码,并且会详细输出错误信息(当然不仅限于指针)
  //在以后编码中,如果要对指针进行一些操作时,断言可以讯速的帮我们找到问题所在
  assert(dest != NULL);
  assert(src);//同assert(src != NULL);
  while(*dest++ = *src++)
  {
    ;
  }
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  my_strcpy(arr1, arr2);
  printf("%s\n", arr1);//hello
  return 0;
}

4. 再优化4(使用const来限定不需要操作的字符串)

对比上面我们模拟的my_strcpy来说,库里的strcpy在原字符串上加了const来修饰。先来看一个场景:

赋值写反了:所造成的arr2数组越界

这里分析arr2的这块空间是不需要被改变的,所以加上const限定更安全,如果对const限定的字符串操作,编译器会主动报错

优化后

#include<stdio.h>
#include<assert.h>
void my_strcpy(char* dest, const char* src)
{
  assert(dest != NULL);
  assert(src);
  while(*dest++ = *src++)
  {
    ;
  }
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  my_strcpy(arr1, arr2);
  printf("%s\n", arr1);//hello
  return 0;
}

1、延伸const

在之前的文章中有提到const,被const修饰的变量不能被修改

#include<stdio.h>
int main01()
{
  const int num = 0;
  num = 20;//err
  printf("%d\n", num);
  return 0;
}

这里把num的地址交给p指针管理,然后发现被const限定的num能通过指针p改变num的值。当然这不是我们想要的

#include<stdio.h>
int main()
{
  const int num = 0;
  int* p = &num;
  *p = 20;
  printf("%d\n", num);
  return 0;
}

const和指针

#include<stdio.h>
//const如果放在*的左边,修饰的是*p,表示指针指向的内容,是不能通过指针来改变的。但是指针变量本身(p->地址)是可以修改的
int main01()
{
  const int num = 0;
  int n = 20;
  const int* p = &num;
  //*p = 20;//err
  p = &n;//ok
  printf("%d\n", *p);//此时此刻p指针不再指向num,而是指向n
  return 0;
}
//const如果放在*的右边,修饰的是p(地址),表示指针的地址,是不能改变指针变量的地址的,但是指针指向的内容是可以改变的
int main02()
{
  const int num = 0;
  int n = 20;
  int* const p = &num;
  //p = &n;//err
  *p = 20;//ok
  printf("%d\n", num);
  return 0;
}
//const如果放在*的左边和右边,则指针指向的内容不可以被改变和指针变量也不能被改变
int main03()
{
  const int num = 0;
  int n = 20;
  const int* const p = &num;
  //p = &n;//err
  //*p = 20;//err
  printf("%d\n", num);
  return 0;
}

6. 优化后(函数的返回值) -> 最终版

库里的strcpy的返回值是char*,而我们模拟的是my_strcpy是void
strcpy返回的是目标空间的起始地址,相比来说有返回值的strcpy可以使用链式访问

#include<stdio.h>
#include<assert.h>
char* my_strcpy(char* dest, const char* src)
{
  assert(dest != NULL);
  assert(src);
  char* ret = dest;//备份一份首地址
  while(*dest++ = *src++)
  {
    ;
  }
  return ret;//返回目标空间的首地址
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  printf("%s\n", my_strcpy(arr1, arr2));//hello
  return 0;
}

2、模拟实现strlen

#include<stdio.h>
#include<assert.h>
size_t my_strlen(const char* str)//size_t是无符号整型
{
  assert(str);
  size_t count = 0;
  while(*str++)
    count++;
  return count;
}
int main()
{
  char arr[] = "hello bit";
  printf("%d\n", my_strlen(arr));
  return 0;
}

八、补充

如果想要去了解一下源码是怎么实现的,建议大家去翻下VS的根目录

VS2017参考路径:

C:\Program Files (x86)\Windows Kits\10\Source\10.0.17763.0\ucrt

这里有个快速查找的工具推荐给大家

Everthing

九、编程常见错误

1、编译型错误

这种类型属于语法错误,相对简单。

解决方法:直接看错误提示信息,(双击就可定位到有问题的代码上)

2、链接型错误

LNK(链接型错误)这种错误只要了解它为什么会产生,也不难找

主要产生的原因

1、这个函数压根就未定义

2、调用函数名时与定义的函数名不一

解决方法:错误信息上不可以定位到有问题的代码上,但是可以作为一些依据

3、运行时错误

这种错误没有错误信息提示,相对较难找。一般是输出结果与预想或与正确答案不符

解决方法:借助调试,逐步定位问题

可以把每天因为调试所解决的运行时错误代码写一个代码日志

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