调试
调试:指的是发现和减少程序错误的过程
调试的步骤:发现程序错误->通过消除、隔离等方式定位错误->确定错误产生原因->提出解决方法->纠正错误,重新测试
Debug:调试版本,包含调试信息,且不作优化,方便对程序进行调试
在debug模式下,通过快捷键F10可以进行逐过程调试,并通过“监视”窗口观察变量的变化
Release:发布版本,一般是对Debug版本进行优化后给用户使用的版本,在代码大小和运行速度上优于Debug版本
release版本无法逐过程调试,且因不包含调试信息,相较于debug版本,release版本的文件大小较小
基础调试操作
调试快捷键
按键 | 功能 |
---|---|
F5 | 启动调试,直接跳到下一断点 |
F9 | 创建和取消断点 |
F10 | 逐过程(一个过程可以是一次函数调用或是一条语句) |
F11 | 逐语句,每次执行一条语句(可以进入函数内部) |
CTRL+F5 | 开始执行(不调试),即直接运行程序 |
CTRL+F9 | 启用/禁用断点 |
SHIFT+F5 | 停止调试 |
注:
-
当没有断点等对代码运行进行暂停时,直接按下F5会使得程序直接运行完成,因此F5一般搭配F9使用
-
断点:程序只要经过断点,就会暂停执行
scanf等输入函数无法跳过,当断点在此类函数之后时,需要进行输入后才能继续执行
断点的跳转:断点的跳转实际上指的是逻辑上的下一个断点。eg:当断点在循环中,且循环执行未完成时,跳到下一个断点即跳到下一个循环中该断点的位置,若要跳到循环外的断点,可以把循环中的断点取消或者鼠标右键(ctrl + F9)断点禁用断点可以设在函数内部
-
断点的设置(条件断点):对断点的触发条件进行设置
此时,当i == 5 时才会触发断点
-
F10和F11的区别:F10在遇到函数时,按照普通语句处理,即直接执行函数,F11则会进入函数内部进行执行(注:库函数不一定支持调试,视编译器版本而定)
调试过程中查看程序信息
1.监视
监视窗口用于查看程序中的变量信息
调试->窗口->监视
只有调试开始后才能查看
自动窗口:自动将程序中的变量体现在窗口中
局部变量:将程序中的局部变量体现在窗口中
监视:自行输入变量名对相应变量进行观察
监视数组参数:在写数组名称的时候在后面加上逗号和要监视的变量个数
2.内存
调试->窗口->内存
在地址栏中直接输入arr即可查看arr对应的地址(其它变量同理)
这里显示的是内存数据,以十六进制显示,实际以二进制储存,每一部分是一个字节,可以自行选择一行显示的字节数
最右边显示的为编译器尝试对内存中的数据的解析(不一定准确,如文本可以解析)
3.调用堆栈
调试->窗口->调用堆栈
可以查看函数之间相互调用的关系
(通过鼠标右键->显示外部代码 可以查看main函数被调用之前有哪些程序启动了)
4.查看汇编信息
调试->窗口->反汇编
可以查看当前代码如何被翻译为汇编代码
5.寄存器
调试->窗口->寄存器
用于查看寄存器中的信息
一个经典BUG
//经典bug,VS在x86环境下运行
#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("wdnmd\n");
}
return 0;
}
//运行结果:死循环
//在release环境下不会出现死循环,而是打印13次
//到i = 10开始越界访问0,到arr[12]会把i重置
//只有在x86环境下才能复现
//原因:i和arr[12]在同一内存空间
//i和arr都是局部数据,放在内存的栈区
//栈区内存的使用习惯:先使用高地址处的空间,再使用低地址处的空间
//数组随着下标的增长,地址由低到高变化
//如果i和arr之间有适当的空间,利用数组的越界操作就有可能覆盖i,从而导致死循环
//i和arr之间的空间大小取决于编译器,如vs2022在x86环境下为8个字节
//如果i在arr之后创建,则不会出现这种情况
这个问题通过直接运行程序是无法发现的,必须通过调试中对变量的观察才能发现问题
常用代码技巧
- 使用断言(assert)对函数中的变量进行确认
- 使用const防止函数变量被篡改
- 养成良好的代码习惯
- 添加必要注释
const的使用
const修饰变量的时候,可以通过指针来修改(如果要防止,给指针再加个const)
#include <stdio.h>
int main()
{
const int num = 10;
int num2 = 50;
num = 20;
//这样改会报错
int* p = #
*p = 20;
//这样改可以
//1、const 在 * 左边
int const* p = #
*p = 20;
//这样也会报错
//这里int const* p(const在*左边)的意思是:p指向的对象不能通过p来改变了,但是p自身可以改变
p = &num2;
//没有问题
//2、const 在 * 右边
int* const p = #
//意思是p指向的对象可以通过p改变,但是不能修改p本身
*p = 0;
//此时num被修改
p = &num2;
//p不能被修改
//*的左右可以各加一个const
const int* const p = #
p = &num2;
*p = 20;
//都不行
printf("%d", num);
return 0;
}
运用:模拟实现strcpy
//模拟实现strcpy
#include<stdio.h>
void my_strcpy(char* dest, char* src)
//传入字符串地址
{
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
*dest = *src;
}
int main()
{
char arr2[20] = "xxxxxxxxxxxx";
char arr1[] = "hello world";
my_strcpy(arr2, arr1);
printf("%s", arr2);
}
//改进代码
#include <stdio.h>
#include <assert.h>
char* my_strcpy(char* dest, const char* src)//这里利用const保证src不会因为代码编写错误而修改
//返回char*是为了链式访问,使得该函数的返回值可以作为其它函数的参数,返回目标空间的起始地址
{
//断言,当src为NULL(空指针)时,会报错并指出错误(可以用于任意自己想判断的语句)
assert(src != NULL);
assert(dest != NULL);
//在release版本中assert会被优化
char* ret = dest;
while (*dest++ = *src++);
//一个赋值表达式,当拷贝字符时,不为0,当拷贝\0时,为假,停止循环
//同时利用后置++简化代码,while的大括号可去
return ret;
}
int main()
{
char arr2[20] = "xxxxxxxxxxxx";
char arr1[] = "hello world";
my_strcpy(arr2, arr1);
printf("%s", arr2);
}
摆烂了好久,差点不会用MD了