第一节:调试是什么?
1. 调试
2. 调试的基本步骤
- 发现程序错误的存在
- 以隔离、消除等方式对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新测试
3. Debug和Release的介绍
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
第二节:调试介绍
1. 调试环境准备
2. 常用快捷键
int main()
{
int i = 0;
int arr[10] = { 0 };
//赋值
//for (i = 0; i < 10; i++)
//{
// arr[i] = i;
//}
//输入
for (i = 0; i < 10; i++)
{
scanf("%d", &arr[i]);
}
//假设经过分析,上方for循环没有问题,那么要用F9设置断点直接调试下方for循环
//断点:程序遇到断点就会停下来不会在往后执行,这样就可以一步一步调试
//打印
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
3. 查看临时变量的值
4. 查看内存信息
5. 查看调用堆栈
6. 查看反汇编
F10打开调试有,在编辑窗口点击鼠标右键,选择反汇编
7. 查看寄存器
第三节:调试实例
实例一
通过上图可以看到,在j=3求3的阶乘时,第一次i * ret应该是1*1,但是第一次进入内层循环的时候ret就已经等于2了。这是因为上一次求完2的阶乘留下的。
通过调试可以清晰的看到循环中每个变量的变化,由此找到了问题。所以应该在内层循环开始前,在增加一条语句ret=1,这样每次重置ret的结果,防止上一次计算的阶乘结果被带到下一次阶乘的计算中。
实例二
从上图可以看到,i和arr[12]总是同时变化,到修改arr[12]时,i也被修改了,所以造成了死循环
第四节:如何写出好代码
1. 模拟实现库函数:strcpy
首先可以在MSDN中看到strcpy的用法char* strcpy(char* strDestination, const char* strSource);
int main()
{
char arr1[20] = "XXXXXXXXXXXX";//验证是否拷贝了\0,如果拷贝了\0,那么倒数第三个X会被覆盖
char arr2[] = "hello bit";
strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
通过上图可以看到strcpy库函数在拷贝时,会拷贝\0。
版本一
void my_strcpy(char* dest, char* src)
{
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
//上方代码没有将\0拷贝,但*src已经指向了\0
*dest = *src;//最后一步将\0也拷贝
}
优化版本一
//优化版本1
void my_strcpy(char* dest, char* src)
{
while (*src != '\0')
{
*dest++ = *src++;//优化1:后置++。因为先解引用才++
}
*dest = *src;
}
优化版本二
//优化版本2
#include <assert.h>
//为了防止(下方代码)有人把源字符串和destination写反,在源字符串指针前const,把源字符串的指针修改为常对象,让它不能修改
void my_strcpy(char* dest, const char* src)
{
//防止传了空指针
//断言
assert(src != NULL);//这个表达式判断为假(即src为空指针)时,assert就报错
assert(dest != NULL);
//这样,如果像下面写反,编译器会报错
//while (*src++ = *dest++)
//1. 首先*src指向字符串的h,并放到dest第一个X位置上
//2. 这是一个赋值表达式,h赋值过去的是时候,这个表达是的结果就是h的ASCII码值
//3. 这个ASCII码值不为0,不是假,所以进入循环
//4. 然后因为后置++,src和dest往后移动一位,继续重复上述123步骤
//因为拷贝的每一个字符的ASCII码值都不为0,所以都进入循环,
//遇到\0,*src依然赋值给了*dest。此时表达式结果0,循环停止
while (*dest++ = *src++)
{
;
}
//*dest = *src;//上面也拷贝,这里也拷贝可以优化
}
优化版本三
为什么库函数strcpy返回char*,是为了实现链式调用,返回类型写成char*,那么这个函数的返回值就可以作为其他函数的参数。
char* my_strcpy(char* dest, const char* src)
{
char* ret = dest;//一开始保存目标地址,方便最后返回
assert(src != NULL);
assert(dest != NULL);
while (*dest++ = *src++)
{
;
}
//return dest;//err,原因strcpy返回的是目标空间的起始地址,但是目标地址在循环过程中一直在++
return ret;
}
int main()
{
char arr1[20] = "XXXXXXXXXXXX";
char arr2[] = "hello bit";
//模拟实现strcpy
my_strcpy(arr1, arr2);
//因为my_strcpy返回的是目标地址,所以可以直接打印
//printf("%s\n", my_strcpy(arr1, arr2));
printf("%s\n", arr1);//类似上方,arr1也是字符串的起始地址
return 0;
}
2. const的作用
1. const修饰变量
如果const修饰一个整形变量,那么无法重新赋值修改它。但是获取该变量的地址,通过指针可以修改该变量。
int main()
{
const int num = 10;
num = 20;//不能改
int* p = #//可以改
*p = 20;//可以修改
return 0;
}
2. const修饰指针变量
int main()
{
//为了防止修改,在指针变量前加const
//const修饰指针变量
//1. const放在*的左边(下方两种写法都一样)
//const int* p;//一般使用这种,const修饰的是*p
//int const* p;
//意思是:p指向的对象不能通过p来改变,但是p变量本身的值可以改变(也就是指针p里面存的地址可以改变,即可以让它指向另外一个对象)
const int num = 10;
const int* p = #
int n = 100;
*p = 20;//err
p = &n;//ok
//2. const放在*的右边
//意思是:p指向的对象可以通过p来改变,但是不能修改p变量本身的值(即指针变量p里面存放的地址不能改变,即不能让它指向其他对象)
const int num = 10;
int* const p = #
*p = 0;//ok
int n = 100;
p = &n;//err
//3. 如果*两边都加const,那么p指向的对象不能修改,p本身也不能修改
printf("%d\n", num);
return 0;
}
第五节:编程常见错误
1. 编译型错误
直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。
2. 链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
3. 运行时错误
借助调试,逐步定位问题。