8.调试技巧

什么是bug

第一次被发现的导致计算机错误的飞蛾
物理bug

调试

一名优秀的程序员是一名出色的侦探
每一次调试都是尝试破案的过程
调试Debug/Debugging即除错

调试基本步骤

  1. 发现程序错误的存在
  2. 以隔离、消除等方式对错误进行定位
  3. 确定错误产生的原因(越界,死循环…)
  4. 提出纠正错误的解决办法
  5. 对程序错误予以改正,重新测试

程序员 -> 可以发现错误
测试(开发)工程师 -> 在用户的角度发现错误
…程序发布
用户 -> 也可以发现bug
用户发现可能就会有风险了
如果是付费软件,给用户带来损失

Debug与Release

Debug调试版本,包含调试信息,便于开发人员调试
Release发布版本,进行各种优化,以便用户很好使用
测试人员测试的也是Release版本

区别:
Debug较Release更大
Release会有自主优化,使程序在代码和速度上最优
Release发布版本不能进行调试

image-20220112185959943

快捷键

F9切换断点/新建断点
断点:程序执行到断点处会主动停下来
F5与F9搭配使用
F5直接跳到断点处开始执行,F5启动调试
断点可以设置条件
假设bug出现在1000行以后,利用好断点
F5会向后执行代码,到下一个逻辑上的断点,而不是物理上的断点
如果没有断点,F5就会把程序执行完毕
在调试过程中也可以设置断点

F10逐过程,处理一个过程,会跳过函数
F11逐语句,可以进入函数内部
shift + F11跳出进入的某个函数
CTRL + F5开始执行不调试

Ctrl + K,Ctrl + D = 正确对齐所有代码
F12 = 转到定义
CTRL + G 跳到指定行

Ctrl+K+C 注释
Ctrl+K+U取消注释

调试时查看程序当前信息

自动窗口
局部变量 但只会监视上下文存在的变量
监视窗口 4个窗口,需要自己添加,不会消失
内存窗口 取变量地址查看内存变化信息
反汇编 能看到c语言对应的汇编代码
寄存器 可以观察寄存器变化
调用堆栈

#include<stdio.h>
void test2()
{
	printf("test2\n");
}
void test1()
{
	printf("test1\n");
	test2();
}
void test()
{
	printf("test\n");
	test1();
}

int main()
{
	test();
	return 0;
}

函数调用堆栈反馈函数的调用逻辑
栈=堆栈
看不懂代码时可以尝试调试看看代码执行逻辑

练习

1.求阶乘之和

int main()
{
	int i = 0;
	int sum = 0;
	int ret = 1;
	int n = 0;
	scanf("%d", &n);
	int j = 0;
	for (j = 1; j <= n; j++)
	{
		ret = 1;	//清空ret很有必要
		for (i = 1; i <= j; i++)
		{
			ret *= i;
		}
		sum = sum + ret;
	}
    printf("%d\n", sum);
	return 0;
}

调试实操,找到ret没有清空的问题

2.为什么死循环

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;
}
//为什么会死循环打印hehe呢

arr[12]和i用的是同一片空间,把arr[12]=0,i也变成了0
就死循环了
image-20220112191747804

  1. i和arr是局部变量,局部变量是放在栈区上的
    栈区内存的使用习惯:先使用高地址空间,再使用低地址空间
    i被放到高地址上了
  2. 数组随着下标增长地址由低到高增长
    这里的栈是栈区 与数据结果的栈没关系
  3. 如果i和arr之间的空间适合的话,就有可能使arr数组越界访问后访问到了i,造成循环变量i的改变

image-20220112191956380

中间空了2格完全是巧合!
由编译器决定(出自<<c陷阱和缺陷>>)

VC6.0h环境-0个整型

gcc - 1个整型

VS2013-2019- 2个整型

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

如果把i定义在arr下面,那么就不会再次碰到i,
但是会数组越界报错

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

但改成i<=11时,也会数组越界报错
越界但没到死循环
image-20220112192945984
如果一直死循环反而不会报错,程序还没有停止,无法进行报错

代码要想写正确,那就是不能越界

如果改成release版本,就不会有死循环了
Release对代码进行了优化

debug下 i的地址确实比arr[9]大
release版本下 i的地址被放在arr下面 自动优化

image-20220112193529576

niec公司的笔试题:

image-20220112193244527

写出好代码

优秀的代码

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

coding技巧

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

const作用

1.const修饰变量

const修饰变量,这个变量就是常变量,不能被修改,
但本质还是变量

int main()
{
	const int num = 10;
	int* p = &num;
	*p = 20;
	printf("%d\n", num);//20
    //n变了
	return 0;
}

2.const修饰指针

const int* p;
int* const p;
const int* const p;

本来不能直接修改num但通过地址间接修改了num的值
违背了const的原意,给int* p 也用const修饰

int main()
{
	const int num = 10;
	const int* p = &num;
	//*p = 20;  会报错
	printf("%d\n", num);
	return 0;
}

const修饰指针变量的时候
如果放在* 的左边
修饰的是* p
表示指针指向的内容,不能通过指针来改变

*p不能改,但是p本身可以改
int* const p=&num;

当const放在p的左边,修饰的是p
此时p就不能修改了
但*p可以修改

int const * const p=&a;
const int * const p=&a;
//这二者等价
//但一般const放最前面,认为int*是一种类型
const修饰二级指针
#include<stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	//const修饰二级指针
	const int* const * const ppa = &pa;
	return 0;
}
//中间的const修饰*ppa
//最左边的const修饰int**

练习

1.模拟实现strcpy

image-20220112193836423
把源头字符串拷贝到目的地

\0也被拷贝过去了

image-20220112194106040

#include <stdio.h>
#include<string.h>
int main()
{
	char arr1[20] = { "xxxxxxxxxxxxx"};
	char arr2[] = { "hello" };
	strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}
#include <stdio.h>
void my_strcpy(char* dest, char* src)
{
	while (*src != '\0')
	{
		*dest = *src;
		dest++;
		src++;
	}
	*dest = *src; //拷贝斜杠0 
}
int main()
{
	char arr1[20] = { "xxxxxxxxxxxxx"};
	char arr2[] = { "hello" };
	my_strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}

缺点:
传过去的指针如果是空指针怎么办?
直接崩溃

假设传过去NULL利用好断言
当传过去空指针时assert会把错误信息报出来
加上assert更容易发现问题,提高代码健壮性

改进:
#include <stdio.h>
#include<assert.h>
void my_strcpy(char* dest, char* src)
{
	assert(dest != NULL);//断言
	assert(src != NULL);
    //assert(dest && src);这样写也行
    //空指针本质也就是0,0也为假,能触发错误信息
	while (*src != '\0')
	{
		*dest = *src;
		dest++;
		src++;
	}
	*dest = *src; //拷贝斜杠0 
}
int main()
{
	char arr1[20] = { "xxxxxxxxxxxxx" };
	char arr2[] = { "hello" };
	my_strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}

简化代码,继续改进

while (*src != '\0')
	{
		*dest++ = *src++;//先拷贝后++
	}
	*dest = *src; //拷贝斜杠0 
继续简化:
while (*dest++ = *src++)
	{
		;//先拷贝后++
		//既拷贝了内容和\0,又让斜杠0停止了
    //\0的ASCII值就是0
	}

\0 与数字0数值上是等价的

继续改进:
strcpy库函数返回的是目标空间的首地址
故用char*类型接收

返回值为目标空间起始地址,实现链式访问
避免修改源头的内容

const char* src
这样如果dest src赋值赋反了,就根本无法编译
写成const char* src
就能维持dest指向的内容被修改,src指向的内容不该被修改的原意

库函数中的strcpy返回值是目标空间起始地址

image-20220112195133518

最终版:
#include <stdio.h>
#include<assert.h>
char* my_strcpy(char* dest, const char* src)
    //*src不能被修改
{
	assert(dest && src);
	char* ret = dest;
	while (*dest++ = *src++)
	{
		;//先拷贝后++
		//既拷贝了,又让斜杠0停止了
	}
	return ret;
}
int main()
{
	char arr1[20] = { "xxxxxxxxxxxxx" };
	char arr2[] = { "hello" };
	printf("%s\n", my_strcpy(arr1, arr2));//链式访问
	return 0;
}

2.模拟实现strlen

#include<stdio.h>
#include<assert.h>
int my_strlen(const char* str)//不希望arr内容被修改
{
	assert(str != NULL);//断言
	//char* end = str;//把安全的str交给了安全的end,会报警告,end也要加const
	const char* end = str;
	while (*end != '\0')
	{
		end++;
	}
	return end - str;
}
int main()
{
	char arr[] = "abcdef";
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

编程常见错误

编译型错误

编译错误一般都是语法错误引起
看错误信息一般能看出来

链接型错误

无法解析的外部符号…
要么函数没定义
要么定义时名字写错了(main写成mian…)

运行时错误

只能一步步去调试找到错误

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值