C调试技巧 下篇

116 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

一、调试实例

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

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

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