调试的基本步骤
- 发现程序错误的存在
- 以隔离、消除等方式对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新测试
debug和release
debug称为调试版本,包含调试信息,且不做任何优化,便于程序员调试程序。 release称为发布版本,进行了各种优化,便于用户使用。
保存debug文件时:
#include <stdlib.h>
int main()
{
int a = 0;
for (a = 0; a < 100; a++)
{
printf("%d", a);
}
system("pause");
return 0;
}
debug文件保存目录中的exe文件打开后会一闪而过,在代码中添加#include <stdlib.h>头文件和system("pause");可以使得程序暂停。 release文件保存目录中的exe文件,运行后可以把代码运行结果通过命令行窗口打印出来。
常用快捷键
- F5:
- 启动调试。可以使程序快速执行到断点处停止。F*5是跳到程序执行逻辑上的下一个断点,而不是物理的断点(如果断点设置在循环内,F5表示跳到下一次循环过程中的该断点处。)
- F9:
- 创建断点和取消断点。* 可以在程序的任意位置设置断点。使得程序在想要的位置停止执行。
- F10:
- 逐过程。* 用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。逐过程的调试不会进到语句或函数内部。
- F11:
- 逐语句。每次都执行一条语句,可以使执行逻辑进入函数内部。*
- Shift+F11:
- 调试进入函数内部时,可以使用Shift+F11直接跳出当前函数。*
- CTRL+F5:
- 开始执行而不调试。*
调试时查看程序当前信息
查看临时变量的值:
- VS界面的调试选项卡的窗口-自动窗口,可以实时显示出调试时的变量值的变化。
- VS界面的调试选项卡的窗口-自动变量,可以显示出调试位置上下局部范围内(比如大括号内)的局部变量的相关信息。
- VS界面的调试选项卡的窗口-监视,手动添加需要观察的变量等信息进行全程监视。(较为常用)
- VS界面的调试选项卡的窗口-内存,观察当前程序执行过程中的内存信息。(较为常用)
- VS界面的调试选项卡的窗口-调用堆栈,
内存空间的使用
- 电脑内存空间分为栈区、堆区、静态区。局部变量存在于栈区中。*
- 栈区中大的地址编号称为高地址,小的地址编号称为低地址。*
- 栈区默认先使用高地址处的空间,再使用低地址处的空间。*
- 数组元素随着下标增长,地址由低到高变化。*
上述代码有一定概率产生死循环,如下图 main函数内部创建了局部变量i和arr,存储在内存的栈区,栈区默认先使用高地址处的空间,i存储在较高的内存空间,arr存储在较低的内存空间,数组元素的地址随下标增长由低到高变大,数组越界后继续增大,有可能造成arr[?]的地址和i的地址重合,越界改变数组元素的值时就有可能改变i的值。上述代码中一旦将i的值置为0,使得i<=12的条件满足,就会陷入死循环。int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int i = 0; for (i = 0; i <= 12; i++) { printf("hehe\n"); arr[i] = 0; } return 0; }
经过试验,VS2022版的内存布局如下: Rrelease版本后就可以正常打印结果,编译器会对上述代码进行优化。**如下: Debug版本:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
printf("%p\n",arr);
printf("%p\n", &i);
return 0;
}
0000006DA652FC78
0000006DA652FCB4
上述代码在变量i创建在数组arr之后的情况下,i在内存中的地址仍高于arr(可能是内存中变量的地址会高于数组???),即存在arr越界后指向i。 Release版本:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
printf("%p\n",arr);
printf("%p\n", &i);
return 0;
}
0000003BA999FC18
0000003BA999FC10
在Release版本中,编译器将i的地址放在了arr的下面,避免了arr越界指向i的问题。
易于调试的代码
优秀的代码:
- 代码运行正常
- bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
常见的coding技巧∶
- 使用assert
- 尽量使用const
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱
代码优化
下面是手写的strcpy函数:
void my_strcpy(char* dest, char* src)
{
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
*dest = *src; //将\0传给数组arr1
}
int main()
{
char arr1[] = "################";
char arr2[] = "HELLO WORLD";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
代码优化:
void my_strcpy(char* dest, char* src)
{
while (*dest++ = *src++)
{
;
}
}
int main()
{
char arr1[] = "################";
char arr2[] = "HELLO WORLD";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
::: tip 上述代码中,将自定义函数中的while循环中的循环体写成了循环条件。后置++的执行顺序和先执行赋值后执行自增是一样的。判断条件开始时每次的赋值结果都不是0,进入循环,直到 *src把\0赋值给 *dest,在c语言中,\0的ASCII码是0,即while判断条件为0,离开循环。 :::
代码优化(增强健壮性):
#include <assert.h>
void my_strcpy(char* dest, char* src)
{
assert(dest != NULL); //断言
assert(src != NULL);
while (*dest++ = *src++)
{
;
}
}
int main()
{
char arr1[] = "################";
char arr2[] = "HELLO WORLD";
my_strcpy(arr1, NULL);
printf("%s\n", arr1);
return 0;
}
如上图,如果传入一个空指针(0),加上断言的语句,打印窗口时可以显示出错误的位置。(VS2022不加assert也可以显示出报错位置)
代码优化:
#include <assert.h>
void my_strcpy(char* dest, const char* src)
{
assert(dest != NULL); //断言
assert(src != NULL);
while (*src++ = *dest++)
{
;
}
}
int main()
{
char arr1[] = "################";
char arr2[] = "HELLO WORLD";
my_strcpy(arr1, NULL);
printf("%s\n", arr1);
return 0;
}
如上述代码,如果代码第6行写反了源和目的,为了保护源不会被修改,在第2行的函数定义中为源加上一个const,可以在运行时报错提示,如下:
代码优化:
char* my_strcpy(char* dest, const char* src)
{
char* ret = dest;
assert(dest != NULL); //断言
assert(src != NULL);
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char arr1[] = "################";
char arr2[] = "HELLO WORLD";
printf("%s\n", my_strcpy(arr1, arr2));
return 0;
}
上述代码中第17行将函数的返回值作为另一个函数的参数,称为链式访问。 ::: tip 如下图,strcpy的函数是将源数据拷贝到目的位置,后续使用目的位置中存放的数据,指针可以访问到目的位置中存放的值,即定义的返回值类型是char*: 文档的返回值标明:每一个函数返回的都是目标字符串。 在函数中返回值应该是目标的地址,经过多次的后置++,返回目标的起始地址更合适,需要在目标地址改变之前设立一个变量暂存,在最后将这个地址返回。 :::
::: warning 总结: 上述代码共优化了四处。修改自定义函数的循环条件和循环体、加入assert、加入const、修改自定义函数的返回类型。 :::
const的使用
一种非法的写法:
int main()
{
const int a = 10; //变量a被const修饰,不允许修改。
int* p = &a;
*p = 20;
printf("%d",a);
return 0;
}
const语法限制不允许修改变量a的值,但是通过指针,依然能将变量a的值修改。 可以认为是指针p改变了主观意愿不允许修改的变量的值。 需要优化的点是,变量a的地址可以交给指针p,但指针p不能改动变量a的值。可以给指针p也加上const修饰。 给指针p加const修饰时有const放在*号左边和右边两种写法。 const放在星号的左边时,修饰的是 *p,效果是不能修改 *p(a)的值。 const放在星号的右边时,修饰的是p,效果是不能修改p的值。
优化后的模拟实现strlen
int my_strlen(const char* str)
{
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdefg";
int len = my_strlen(arr);
printf("%d", len);
return 0;
}
编程常见错误
- 编译型错误:语法错误等,可通过错误提示信息定位解决(直接双击)
- 链接型错误:因标识符名不存在或标识符名拼写错误等而出现“无法解析的外部符号”的错误,需要从其后的名字入手排查如Add
- 运行时错误:需要通过调试逐步定位问题,排错复杂。