VS环境C语言调试
Suzhou 56 0

调试的基本步骤

  • 发现程序错误的存在
  • 以隔离、消除等方式对错误进行定位
  • 确定错误产生的原因
  • 提出纠正错误的解决办法
  • 对程序错误予以改正,重新测试
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界面的调试选项卡的窗口-调用堆栈,
    内存空间的使用
  • 电脑内存空间分为栈区、堆区、静态区。局部变量存在于栈区中。*
  • 栈区中大的地址编号称为高地址,小的地址编号称为低地址。*
  • 栈区默认先使用高地址处的空间,再使用低地址处的空间。*
  • 数组元素随着下标增长,地址由低到高变化。*
    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;
    }
    上述代码有一定概率产生死循环,如下图VS环境C语言调试 main函数内部创建了局部变量i和arr,存储在内存的栈区,栈区默认先使用高地址处的空间,i存储在较高的内存空间,arr存储在较低的内存空间,数组元素的地址随下标增长由低到高变大,数组越界后继续增大,有可能造成arr[?]的地址和i的地址重合,越界改变数组元素的值时就有可能改变i的值。上述代码中一旦将i的值置为0,使得i<=12的条件满足,就会陷入死循环。

经过试验,VS2022版的内存布局如下: VS环境C语言调试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的问题。

易于调试的代码

优秀的代码:

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

常见的coding技巧∶

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

代码优化

下面是手写的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;
}

VS环境C语言调试 如上图,如果传入一个空指针(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,可以在运行时报错提示,如下:VS环境C语言调试


代码优化:

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*VS环境C语言调试 VS环境C语言调试 文档的返回值标明:每一个函数返回的都是目标字符串。 在函数中返回值应该是目标的地址,经过多次的后置++,返回目标的起始地址更合适,需要在目标地址改变之前设立一个变量暂存,在最后将这个地址返回。 :::

::: 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放在*号左边和右边两种写法。 VS环境C语言调试 const放在星号的左边时,修饰的是 *p,效果是不能修改 *p(a)的值。 const放在星号的右边时,修饰的是p,效果是不能修改p的值。 VS环境C语言调试

优化后的模拟实现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 VS环境C语言调试
  • 运行时错误:需要通过调试逐步定位问题,排错复杂。
评论区

索引目录