程序员核心------详解调试(2)

在这里插入图片描述
所爱隔山海,山海皆可平,所念皆星河,星河不可及。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

上课!

接着上节课讲的调试(1),本节课进一步讲解调试(2).

校招笔试题

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

3.编程常见的错误

实例二:

//代码改错
int main()
{
	int i = 0;
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	for (i = 0; i <=12; i++)
	{
		printf("hehe\n");
		arr[i] = 0;
	}
	return 0;
}

请复制这段代码到本地编译器运行,看看为什么会出现问题。

在这里插入图片描述

  • 运行起来后会发现,死循环打印hehe,为什么呢? 我们调试起来

在这里插入图片描述

  • 会发现一个非常奇怪的现象:i的值和arr[12]的值居然是同步改变的,这到底是为什么呢?
  • 规定:
    1.局部变量的存储是存储于栈区上的
    2.栈区的使用习惯是:
    先使用高地址的空间
    再使用低地址的空间

在内存中大致有下面这三块空间分布,吧栈区放大后,假设顶部是高地址,则底部就是低地址,如图:

在这里插入图片描述

  • 先创建局部变量i,i就使用高地址处的一块空间,

  • 再创建数组arr,arr[0]开始创建低地址处的一块空间。

  • 然后再创建到arr[9]时,i和arr[9]之间恰好有两块整型大小的空间,也就是绿色的空间

  • 随后,arr[12]的空间创建自然而然地就和 i 在同一块空间了

  • 你一定会问,为什么在 i 和arr[9]之间恰好有两块整型大小的空间?不能是三块空间吗?不能是一块空间吗?

  • 声明:以上现象只针对于vs2019 X86 Debug调试环境下产生的现象
    在 VC6.0环境下 i 和arr 之间没有多余的空间
    在gcc环境下 i 和arr之间只有一个整型空间

说那么多东西,只为了说明一件事: 调试的重要性!!调试可以帮助你找到自己的错误!学会调试,你的能力将达到一个质的飞跃。
上节课讲过,Debug和Relese版本的不同
在这个例子中,在不同版本下所出现的情况也不同:
在这里插入图片描述
在Release版本底下,i和 arr[12]所在的地址也不一样了。

  • 到这里,有同学会问到:既然 局部变量i 先创建内存空间,arr后创建
  • 那为什么不先创建arr的内存空间,后创建局部变量i的空间呢?
  • 这样i的内存空间不就在arr下面了吗,这样不就不会死循环了吗?
  • 来演示一下结果:
    在这里插入图片描述
    现在把 i 放在arr之后,也就是 i 的创建是在arr之后的,出现上面的结果
    在这里插入图片描述
    意思就是,数组越界了
    那为什么第一次死循环的时候 , 又没有报这个错误呢?
    因为程序在忙着打印hehe,没空给你报错
    就是这个道理
    程序在忙着做自己的事情打印hehe
    根本没空搭理你,给你报错

    下面来一道题目

- 公司校招笔试题:

  • 在Linux x86_64 gcc环境下,下面的程序会出现什么问题?运行的结果是什么?
int main(int argc,char*argv[])
{
	long i;
	long a[16];
	for (i = 0; i <= 17; i++)
	{
		a[i] = 0;
		printf("%d", i);
	}
	return 0;
}
  • 其实这道题的本质是与上面的例题相同的
  • 只要你写出上面的分析过程,就能拿下这道题
  • 该题运行的现象仍然是会出现死循环

2.如何写出好的代码?

2.1

代码运行正常

bug很少

效率高

可读性高

可维护性高

注释清晰

文档齐全

常见的coding技巧

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

下面以一个例子说明上面所有的要点:

模拟实现my_strcpy的功能

strcpy函数,简单来说就是把一个字符串拷贝到另一个字符串里面

//先观察strcpy的功能
#include<string.h>
int main()
{
	char arr[20] = "xxxxxxxxxxxxxxx";
	char str[] = "hello world";
	strcpy(arr, str);
	printf("%s\n", arr);
}

运行结果如下:
在这里插入图片描述

c库中,可以搜索到strcpy函数的相关信息:
在这里插入图片描述

  • 现在来模拟my_strcpy函数
void mystrcpy(char* dst, char* src)
{
	while(*dst++=*src++)
	//等价于
	//*dst = *src;
	//dst++,src++;
	{
		;
	}
}

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

  • 我们一般是这么写的
  • 但是这样写还有什么隐患呢?
  • 1.假如传过去的指针是空指针,就会出现问题,
  • 所以我们需要在while前面判断是否为空指针
  if(*dst ==NULL || *stc == NULL)
	 {
 		return ;
	 }

但是这样写,还是会有问题,为什么呢?
1.每次进入my_strcpy函数内部,都要执行if语句,不管它是不是空指针。
2.它不会暴露错误出来,就算是空指针,也会悄悄规避掉,程序员无法知道自己穿的是空指针。
所以,引用 “断言”-----assert
assert内部可以放一个表达式,表达式如果为假,就报错,为真,啥事没有。

仍然用上面的代码,来举例:

void my_strcpy(char* dst,  char* src)
{
	assert(dst !=NULL && src != NULL);//断言
	while (*dst++ = *src++)
	{
		;
	}
}

int main()
{
	char arr1[20] = { 0 };
	char* p = NULL;//p指向的常量字符串无法更改
	my_strcpy(arr1, p);
	//如果是反着来,是无法更改的

	printf("%s\n", arr1);
}

运行结果如下:
在这里插入图片描述

  • 它不仅能报错,还指出是哪个文件目录下的错误
  • 这就是(断言) assert 的好处

假设有一个程序员,将

void my_strcpy(char* dst, char* src)
{
	assert(dst && src);
	while(*dst++ = *src++ )
	{
		;
	}
}

int main()
{
	char arr1[20] = { 0 };
	char* p = "hello";//p指向的常量字符串无法更改
	my_strcpy(arr1, p);
	printf("%s\n", arr1);
}

写成了

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

也就是在while循环里面,将两个指针位置互换了

这样写一定会有问题,那么会是什么问题?
在这里插入图片描述

可以知道,arr里面只有\0,把\0 复制到src的第一个位置后
循环马上停下了,输出的也就是arr1里面的内容,即\0

如何避免呢?-----const
先来看一个例子,
在这里插入图片描述

  • 可以通过指针修改 n 内部的值。
  • 但是当我在指针变量前面加上了 const后 , 结果如下:

在这里插入图片描述
它说,左值是必须可修改的,这就意味着,加上了const 后,指针指向的内容不可更改

再来看一个例子:
在这里插入图片描述
看看有什么不同?
在这个例子中,我把const 放在了 * 的后面,这时候 p不能更改了。

总结:
const修饰指针变量的时候
1.const放在的左边,修饰的是指针指向的内容
表示指针指向的内容,不能通过指针来改变
*

2.也可以放在的右边,const修饰的是指针变量本身
表示指针变量本身的内容不能被修改,但是指针指向的内容,
可以通过指针来改变

回到上面的例子,所以当我写成
const char* src的时候,就算写反了位置,也没关系
因为这样写的时候

void my_strcpy(char* dst, const char* src)
{
	assert(dst && src);
while(*src++ = *dst++ )
{
	;
}
}

int main()
{
	char arr1[20] = { 0 };
	char* p = hello;
	my_strcpy(arr1, p);
	printf("%s\n", arr1);
}
  • 编译器都报错了,更别说运行起来了

在这里插入图片描述

看到这里,细心的朋友还会发现,
在这里插入图片描述

这里是char *类型的返回值呀,为什么上面写的都是void类型呢?
别着急,这就马上讲

char* my_strcpy(char* dst, const char* src)
{
	assert(dst && src);
	char* ret = dst;//存储dst的起始位置
	while(*src++ = *dst++ )
	{
		;
	}
	return ret ;
	//这里不能返回dst,因为dst指向的空间不再是起始位置的地址了
}

int main()
{
	char arr1[20] = { 0 };
	char* p = hello;
	my_strcpy(arr1, p);
	printf("%s\n", arr1);
}

大功告成!
总结:

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

3.编程常见的错误

3.1编译型错误

  • 直接看错误信息提示(双击错误行),或者根据经验直接判断,相对比较简单

下面这个例子就是把英文的小括号写成了中文的小括号 在这里插入图片描述

3.2链接型错误
下面是一个链接型错误,链接型错误中,双击错误行是没有什么反应的,看行数也没什么效果,解决办法就是 “搜索”
在这里插入图片描述
Ctrl +F5 打开搜索框进行搜索
在这里插入图片描述
下面是最后一个,也是最难的一个
3.3运行时错误

  • 运行时错误,也是最难解决的一个错误,

  • 就需要用到调试,上面讲的第一个例子

  • 就是运行时错误产生的,

  • 调试可以说是专门为了解决这个错误而产生的。

  • 今天的内容就到这里。

    总结:调试是重中之重,万事开头难,当你勇敢地迈出第一步,就成功了一半。
    下课在这里插入图片描述



————— END —————




  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邓富民

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值