实用调试技巧【下篇】

本文章是在 Visual Studio 2022(VS2022)编译环境下进行操作讲解

文章目录

    • 3.2.调试的时候查看程序当前信息
      • 3.2.1.查看临时变量的值
      • 3.2.2.查看内存信息
      • 3.2.3.查看调用堆栈
      • 3.2.4.查看汇编信息
  • 4.调试实例
  • 5.如何写出(易于调试)的代码
    • 5.1.模拟实现库函数: strcpy
    • 5.2. const 修饰指针
  • 6.编程常见的错误

实用调试技巧【下篇】_第1张图片

3.2.调试的时候查看程序当前信息

3.2.1.查看临时变量的值

自动窗口
调试–>窗口–>自动窗口
实用调试技巧【下篇】_第2张图片
实用调试技巧【下篇】_第3张图片
它会出现一个自动窗口,它会自动捕获变量的值,让你监视,方便的我们不用手动添加变量,但是如果它不想让你看的时候过了一会就会自动取消了,我们就监视不到了
局部变量
在自动窗口的下面
实用调试技巧【下篇】_第4张图片
与自动窗口比较相似,但不同的是放的是局部变量,监视的是程序执行中的局部变量
监视
调试–>窗口–>监视
实用调试技巧【下篇】_第5张图片
这才是我们用到最多的调试功能
实用调试技巧【下篇】_第6张图片
我们想监视谁,就输入谁,它不会自己取消,会一直显示,来为我们提供对变量的监视
在这里插入图片描述
在这里插入图片描述
数组也可以观察,变量的地址也都可以观察到
如果有需要,可以同时打开4个监视窗口,每个窗口都可以拖动,放到你想放的位置

3.2.2.查看内存信息

内存
调试–>窗口–>内存
实用调试技巧【下篇】_第7张图片
实用调试技巧【下篇】_第8张图片
☝️内存中本放的是二进制的数据,但是为了展示方便,所以是以十六进制来显示的☝️

3.2.3.查看调用堆栈

调用堆栈
调试–>窗口–>调用堆栈
实用调试技巧【下篇】_第9张图片
简单知道一下 什么是栈
实用调试技巧【下篇】_第10张图片

看这段代码
实用调试技巧【下篇】_第11张图片
调用堆栈
实用调试技巧【下篇】_第12张图片
实用调试技巧【下篇】_第13张图片
实用调试技巧【下篇】_第14张图片
实用调试技巧【下篇】_第15张图片
实用调试技巧【下篇】_第16张图片
实用调试技巧【下篇】_第17张图片
实用调试技巧【下篇】_第18张图片
☝️反映的是函数的调用逻辑☝️

3.2.4.查看汇编信息

反汇编
在内存的下面
实用调试技巧【下篇】_第19张图片
看到的是c语言代码翻译出来的汇编代码
实用调试技巧【下篇】_第20张图片

4.调试实例

看下面这段代码

#include

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

运行起来是什么样呢?
实用调试技巧【下篇】_第21张图片
数组已经越界了,为什么没有崩溃而是死循环了呢?
进入调试
实用调试技巧【下篇】_第22张图片
i 等于9的时候一切都正常,我们继续
实用调试技巧【下篇】_第23张图片
这个代码真的是在越界访问!胆大包天啊,如果我们继续下一步呢
实用调试技巧【下篇】_第24张图片
继续一直执行
实用调试技巧【下篇】_第25张图片
再改
实用调试技巧【下篇】_第26张图片
i 到12之后并没有++变成13, i 再一直跟着 arr[12] 改变,陷入了死循环,这是为什么呢?有没有可能它们是在同一个空间呢?我们看一下
在这里插入图片描述
地址居然一摸一样,它俩在一个空间
那么底层原理什么呢?为什么会这样呢?

原理:
1. i 和 arr 是局部变量,局部变量是放在栈区上的
2. 栈区内存的使用习惯是 使用高地址的空间,再使用低地址处的空间
3. 数组随着下标的增长,地址是由低到高变化的

实用调试技巧【下篇】_第27张图片

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

优秀的代码:

1. 代码运行正常
2. bug很少
3. 效率高
4. 可读性高
5. 可维护性高
6. 注释清晰
7. 文档齐全

常见的coding技巧:

1. 使用assert
2.尽量使用const
3. 养成良好的编码风格
4. 添加必要的注释
5. 避免编码的陷阱

5.1.模拟实现库函数: strcpy

实用调试技巧【下篇】_第28张图片
拷贝一个字符串
示例

#include
#include

int main()
{
	char arr1[] = "hello world";
	char arr2[20] = { 0 };
	strcpy(arr2, arr1);
	printf("%s\n", arr2);

	return 0;
}

在这里插入图片描述

模拟实现 strcpy

#include
#include
void my_strcpy(char* dest, char* src)
{
	while (*src != 0)
	{
		*dest = *src;
		dest++;
		src++;
	}
	*dest = *src; // \0的拷贝
}
int main()
{
	char arr1[] = "hello world";
	char arr2[20] = { 0 };
	my_strcpy(arr2, arr1);
	printf("%s\n", arr2);

	return 0;
}

如果写成这样的代码,这算一个好的代码吗?当然不算一个好的代码,我们可以优化一下

void my_strcpy(char* dest, char* src)
{
	while (*src != 0)
	{
		*dest++ = *src++;
	}
	*dest = *src; // \0的拷贝
}

我们还可以再融合一下

void my_strcpy(char* dest, char* src)
{
	while (*dest++ = *src++)
	{
		;
	}
}

可以直接一次搞定
这样改进完好像已经足够好了,但是我们还能再改进

void my_strcpy(char* dest, char* src)
{
	if (dest == NULL || src == NULL)
	{
		return;
	}
	while (*dest++ = *src++)
	{
		;
	}
}

防止遇到空指针,但是这样处理遇到问题只是回避掉了,并不做处理

#include
#include
#include
void my_strcpy(char* dest, char* src)
{
	//断言
	assert(dest != NULL);
	assert(src != NULL);

	while (*dest++ = *src++)
	{
		;
	}
}
int main()
{
	char arr1[] = "hello world";
	char arr2[20] = { 0 };
	my_strcpy(arr2, NULL);
	printf("%s\n", arr2);

	return 0;
} 

实用调试技巧【下篇】_第29张图片
使用断言处理之后,如果有问题它就会报错,把问题抛出来并且告诉你出错在哪里,如果不使用断言执行程序的话代码就崩掉了,不会告诉你哪里有问题
实用调试技巧【下篇】_第30张图片
断言是对程序员非常友好的东西,我们使用断言是个很好的编程习惯
使用断言别忘记引用头文件

void my_strcpy(char* dest, char* src)
{
	//断言
	assert(dest && src);

	while (*dest++ = *src++)
	{
		;
	}
}

简便一点可以直接写成这样☝️

5.2. const 修饰指针

实用调试技巧【下篇】_第31张图片
const修饰变量m之后,m的值就更改不了了
但是可以使用一些小聪明把m改掉

int main()
{
	const int m = 10;
	//m = 20;//error
	int* p = &m;
	*p = 20;
	printf("%d\n", *p);

	return 0;
}

在这里插入图片描述
这个代码非常奇葩,因为 const 只是在语法层面限制了m不能改,但是通过地址是可以更改的
我们可以在语法层面把指针p 也进行限制
实用调试技巧【下篇】_第32张图片

int main()
 {
	int n = 100;
	const int m = 10;
	//m = 20;//error
	//const 修饰指针
	//1. const放在*的左边,*p不能改了,也就是p指向的内容,不能通过p来改变了。但是p是可以改变的,p可以指向其他变量
	const int* p = &m;
	p = &n;

	printf("%d\n", *p);

	return 0;
 } 

实用调试技巧【下篇】_第33张图片

int main()
 {
	int n = 100;
	const int m = 10;
	//m = 20;//error
	//const 修饰指针
	//2. const放在*的右边,限制的是p,p不能改变,但是p指向的内容*p,是可以通过p来改变的
	int* const p = &m;
	*p = n;

	printf("%d\n", *p);

	return 0;
 } 

实用调试技巧【下篇】_第34张图片

1. const放在的左边,p不能改了,也就是p指向的内容,不能通过p来改变了。但是p是可以改变的,p可以指向其他变量
2. const放在的右边,限制的是p,p不能改变,但是p指向的内容p,是可以通过p来改变的

void my_strcpy(char* dest,const char* src)
{
	//断言
	assert(dest && src);

	while (*dest++ = *src++)
	{
		;
	}
}

所以上面模拟实现strcpy的代码这样写就更严谨了,加上了 const 来修饰char* src
提高了代码的健壮性(鲁棒性)
因为如果下面的 *dest++ = *src++ 位置要是不小心写反了程序就会报错,所以这样写才是有意义的!

我们再来进行最后一次优化

#include
#include
#include

//strcpy函数返回的是目标空间的起始位置
char* my_strcpy(char* dest,const char* src)
{
	//断言-- 保证指针的有效性
	assert(dest && src);
	char* ret = dest;
	//把src指向的字符串拷贝到dest指向的数组空间,包括\0字符
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}
int main()
{
	char arr1[] = "hello world";
	char arr2[20] = { 0 };
	//链式访问
	printf("%s\n", my_strcpy(arr2, arr1));

	return 0;
} 

这些都是一些技巧,希望大家可以理解

6.编程常见的错误

优秀的代码:

1. 编译型错误
直接看错误提示信息 解决问题,或者凭借经验就可以搞定,相对简单
2. 链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误(ctrl + F 可以进行搜索)
3. 运行时错误
借助调试,逐步定位问题,最难搞

做个用心的人!积累排错经验!

总结
本文章是在 Visual Studio 2022(VS2022)编译环境下进行操作讲解
以上就是调试技巧下篇内容啦
希望我们可以做一个用心的人
小的会继续学习,继续努力带来更好的作品
创作写文不易,还多请各位大佬uu们多多支持哦

实用调试技巧【下篇】_第35张图片

你可能感兴趣的:(C语言知识篇,c语言,算法,c++,数据结构,学习方法)