C语言VS2017 - 实用调试技巧

Posted 跳动的bit

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言VS2017 - 实用调试技巧相关的知识,希望对你有一定的参考价值。

一、什么是bug

1947年,哈佛大学的计算机哈佛二代(Harvard Mark II)突然停止了运行。原来,哈佛二代当时还没有二极管和晶体管,它是一部继电器计算机,无数个喀哒作响的电磁开关在其中运作。当开关断开的时候会有电弧发出闪光,于是这只妖蛾子奋不顾身地飞了上去,用节肢动物的鲜血开辟了脊索动物的Debug史,从此名垂千古,永远地保存在了华盛顿的美国国家历史博物馆中,后来,Bug这个名词就沿用下来,表示电脑系统或程序中隐藏的错误、缺陷或问题。
Bug可以翻译为幺蛾子 —— 所以可以说你这代码有个幺蛾子
在这里插入图片描述

二、调试

1、什么是调试

调试(Debugging / Debug),又称排错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程
所有发生的事情一定都有迹可循,如果问心无愧,就不需要掩盖了,如果问心有愧,就必然需要掩盖,就一定会有破绽,破绽越多就越容易顺藤而上,这就是推理的途径。
一名优秀的程序猿一定是一名出色的侦探 —— 每一次调试都是一次破案的过程
拒绝迷信调试,学会科学调试:
在这里插入图片描述

2、调试的基本步骤

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

三、Debug和Release

1、Debug和Release是什么

这里主要说明的就是这2个版本:
在这里插入图片描述

Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。

Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户更好的使用

2、Debug和Release的区别

1、所占空间

在此之前需要使用Debug和Release分别来运行代码,才能生成对应文件在这里插入图片描述

从生成文件的大小来看:Debug的文件比Release的文件大
在这里插入图片描述
在这里插入图片描述

2、功能

Debug可以用于调试,而Release不能调试

3、优化

Debug不可以优化,而Release可以对代码进行优化

四、windowns环境调试技巧

1、调试环境准备

这里所调试的环境是:Visual Studio && Windows
将版本改为Debug调试版本。注:Linux开发环境调试工具是gdb

2、学会快捷键

学会快捷键会使我们的编码效率大大提高
在这里插入图片描述
以下这个窗口需要在调试后才能打开
在这里插入图片描述


当然这些还是要配合一些场景作一些了解:
1、当有一段有问题的代码,你已经排锁定了bug代码的区域,这时代码量又过长,想直接跳过非bug代码的区域时:断点+调试即可(F9+F5)
在这里插入图片描述


2、如果想进入到一个函数的内部可以逐语句执行(F11),或者不想进入函数的内部逐过程执行(F10)。
在这里插入图片描述


3、当我们的写的代码量大时且一个工程中有多个文件时。调试的时候难免会用到断点,且可能不止一个断点,这里可能就会造成紊乱。所以在调试,窗口里有个断点可以管理断点(CTRL + ALT + B)
在这里插入图片描述


4、在调试的过程中可以通过下面的局部变量窗口(CTRL + ALT + V + L)或者自动窗口(CTRL + ALT + V + A)来观察代码的步骤。
在这里插入图片描述
在这里插入图片描述

以上2个窗口都有一定的局限性,自由度不高。如果想自己设置要观察的数据时:在调试,窗口里打开监视窗口即可(CTRL + ALT + W , 1/2/3/4)
在这里插入图片描述
在这里插入图片描述
5、如果想要观察数据的内存的话,在调试,窗口里打开内存窗口即可(CTRL + ALT + M , 1/2/3/4)
在这里插入图片描述


6、如果想要看函数调用的逻辑,在调试,窗口,打开调用堆栈(这里的堆栈就是栈)(CTRL + ALT + C)
在这里插入图片描述
发现调用堆栈会模拟栈的执行逻辑
在这里插入图片描述


7、一段for循环代码,可能觉得在第50次循环有问题,这时按步就班的话就很low。可以使用断点,然后右击断点点击条件设置即可
在这里插入图片描述
在这里插入图片描述

六、调试实例

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

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、运行时错误

这种错误没有错误信息提示,相对较难找。一般是输出结果与预想或与正确答案不符
解决方法:借助调试,逐步定位问题
可以把每天因为调试所解决的运行时错误代码写一个代码日志

以上是关于C语言VS2017 - 实用调试技巧的主要内容,如果未能解决你的问题,请参考以下文章

VS2017实用调试技巧

C语言学习笔记实用的调试技巧

C语言进阶学习笔记七程序执行+调试技巧(实用技巧篇)

实用调试的技巧,VS编译器常用调试详解

C语言有这个就够了七.实用调试技巧

VS编译器实用调试技巧