常用调试方法

调试


调试:指的是发现和减少程序错误的过程

调试的步骤:发现程序错误->通过消除、隔离等方式定位错误->确定错误产生原因->提出解决方法->纠正错误,重新测试
在这里插入图片描述

Debug:调试版本,包含调试信息,且不作优化,方便对程序进行调试

在debug模式下,通过快捷键F10可以进行逐过程调试,并通过“监视”窗口观察变量的变化

在这里插入图片描述

Release:发布版本,一般是对Debug版本进行优化后给用户使用的版本,在代码大小和运行速度上优于Debug版本

release版本无法逐过程调试,且因不包含调试信息,相较于debug版本,release版本的文件大小较小

基础调试操作

调试快捷键

按键功能
F5启动调试,直接跳到下一断点
F9创建和取消断点
F10逐过程(一个过程可以是一次函数调用或是一条语句)
F11逐语句,每次执行一条语句(可以进入函数内部)
CTRL+F5开始执行(不调试),即直接运行程序
CTRL+F9启用/禁用断点
SHIFT+F5停止调试

在这里插入图片描述

注:

  1. 当没有断点等对代码运行进行暂停时,直接按下F5会使得程序直接运行完成,因此F5一般搭配F9使用

  2. 断点:程序只要经过断点,就会暂停执行

scanf等输入函数无法跳过,当断点在此类函数之后时,需要进行输入后才能继续执行


断点的跳转:断点的跳转实际上指的是逻辑上的下一个断点。eg:当断点在循环中,且循环执行未完成时,跳到下一个断点即跳到下一个循环中该断点的位置,若要跳到循环外的断点,可以把循环中的断点取消或者鼠标右键(ctrl + F9)断点禁用

断点可以设在函数内部

  1. 断点的设置(条件断点):对断点的触发条件进行设置

    在这里插入图片描述

    此时,当i == 5 时才会触发断点

  2. 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之后创建,则不会出现这种情况

这个问题通过直接运行程序是无法发现的,必须通过调试中对变量的观察才能发现问题

在这里插入图片描述

常用代码技巧

  1. 使用断言(assert)对函数中的变量进行确认
  2. 使用const防止函数变量被篡改
  3. 养成良好的代码习惯
  4. 添加必要注释

const的使用

const修饰变量的时候,可以通过指针来修改(如果要防止,给指针再加个const)

#include <stdio.h>

int main()
{
    const int num = 10;
    int num2 = 50;
    num = 20;
    //这样改会报错

    int* p = &num;
    *p = 20;
    //这样改可以

    //1、const 在 * 左边
    int const* p = &num;
    *p = 20;
    //这样也会报错
    //这里int const* p(const在*左边)的意思是:p指向的对象不能通过p来改变了,但是p自身可以改变
    p = &num2;
    //没有问题

     
    //2、const 在 * 右边
    int* const p = &num;
    //意思是p指向的对象可以通过p改变,但是不能修改p本身
    *p = 0;
    //此时num被修改
    p = &num2;
    //p不能被修改

    //*的左右可以各加一个const
    const int* const p = &num;
 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了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值