C语言学习记录——이십삼 调试

此篇笔记也会插入一些代码。调试需要实际操作去调试,观察效果。

目录

一、Debug、Release

二、vs里面的一些快捷键

三、调试的时候查看程序当前信息

查看临时变量的值

调用堆栈:

四、常见的敲打技巧

五、模拟实现strcpy这个函数

关于const

练习--strlen

六、编程常见错误

编译型错误

连接性错误

运行时错误


一、Debug、Release

Debug版本为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。

Release版本为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。


二、vs里面的一些快捷键

F5启动调试,与F9配合。F9为断点,程序执行时在这个断点处停止 ,并且表示可能程序的错误出现这部分代码

F5:经常用来直接调到下一个断点处。

F9:创建断点和取消断点。断点的重要作用,可以在程序的任意位置设置断点,这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。

F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句

F11:逐语句,都是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(经常用)

Ctrl + F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。

三、调试的时候查看程序当前信息

查看临时变量的值

自动窗口:程序进行过程中,打开自动窗口,就会自动打开程序进行到这的要观察的一些变量列出来观察。

局部变量:和自动窗口一样的外观。但是会把程序中所有局部变量都加起来观察,当经过一块程序后,其中的局部变量就不能看了。不能像自动窗口,随心所欲监视谁。

监视:手动添加监视对象

内存:可以观察内存存储的东西

反汇编:观察每一句c语言对应的汇编代码

寄存器:观察寄存器

调用堆栈:

void test2()
{
    printf("hehe\n");
}

void test1()
{
    test2();
}

void test()
{
    test1();
}

int main()
{
    test();
    return 0;
}

调用堆栈后,先出main,然后test,依次出现,从下往上出现。而到达test2函数后,实现test2函数后,test2就消失了,然后test1也消失,就像是栈区传入参数一样,栈顶传入,传出时依次向下消失。

看一个小题

int main()
{
    int i = 0;
    int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    for (i = 0; i <= 12; i++)
    {
        printf("hehe\n");
        arr[i] = 0;
    }
    system("pause");
    return 0;
}

结果是死循环。调用调试后,当i = 10时,程序会继续运行,此时其实也就是越界访问了,出现arr[10], arr[11],arr[12], 之后开始继续循环,arr[12]为0,i也变为0。打印i和arr[12]的地址,是一样的。这其中的原因还需要写一写

内存中的栈区被分成许多个小内存块,这些内存块的排列顺序是地址高低之分。高地址的地址号大。栈区默认先使用高地址空间,再使用低地址空间。而数组在内存中连续存放,随着下标增长,地址由低到高。  原本分配给arr的0-9的空间,循环到10后,进入一个不属于arr的空间,随机数,之后11,12,如果i的空间和循环到12的那个空间正好一样,那就开启了死循环。

所以要合适地写程序,要不不知道会发生什么。不同编辑器不一样的位置会开始死循环。

但是这个代码在release幻境下就不会死循环,因为进行了优化。

    printf("%p\n", arr);
    printf("%p\n", &i);

   观察地址,Debug下i的地址高于arr,但是Release下arr高于i。这就是进行了优化。

四、常见的敲打技巧

使用assert (断言)

尽量使用const

养成良好的编码风格

添加必要的注释

避免编码的陷阱

五、模拟实现strcpy这个函数

int main()
{
    char arr1[] = "*************";
    char arr2[] = "zyd";
    strcpy(arr1, arr2);
    printf("%s\n", arr2);
    return 0;
}

strcpy(目的地,源头),把arr2的内容放到arr1里头去。现在改成自定义一个函数实现此功能。

void my_strcpy(char* dest, char* src)
{
    while (*src != '\0')
    {
        *dest = *src;
        src++;
        dest++;
    }
    *dest = *src;
}

int main()
{
    char arr1[] = "*************";
    char arr2[] = "zyd";
    my_strcpy(arr1, arr2);
    printf("%s\n", arr2);
    return 0;
}

这个代码,其实一般般,需要优化。

void my_strcpy(char* dest, char* src)
{
    while (*dest++ = *src++)
    {
        ;
    }
}

当运行到*src = \0后,while的判断为0,所以停止执行。这样有所优化,但还不满足。

void my_strcpy(char* dest, char* src)
{
    if (dest != NULL && src != NULL)
    {
        while (*dest++ = *src++)
        {
            ;
        }
    }
}

如果传参时传入了NULL,那么对程序是不好的。这时候应该加入判断。但是这样操作后,不利于程序员发现问题,程序发现是NULL后,就不输出东西了,也一样执行。

#include <assert.h>

void my_strcpy(char* dest, char* src)
{
    assert(dest != NULL);
    assert(src != NULL);
    while (*dest++ = *src++)
    {
        ;
    }
}

assert,错则报错,对则通过。 再继续优化

void my_strcpy(char* dest, const char* src)
{
    assert(dest != NULL);
    assert(src != NULL);
    while (*dest++ = *src++)
    {
        ;
    }
}

加个const,*src就不能被修改,防止while的括号里写反顺序。

关于const

int main()
{
    const int num = 10;
    int* p = &num;
    *p = 20;
    printf("%d\n", num);
    return 0;
}

虽然加上了const,但是num的值还是被改成20,这是一种违反规则的写法。如果要把num地址给p,编译器会报出警告不让这样做。

int main()
{
    const int num = 10;
    const int* p = &num;
    *p = 20;
    printf("%d\n", num);
    return 0;
}

这样p也不能改了。如果是int num = 10,const int* p,此时不能通过p来改变指向的内容的数值,也就是说不能这样写*p = 20,但是可以用其它不是const修饰的指针来更改;而int* const p,说明p所指向的内容不能再更改,p只能指向这一个地址,而const int* p则可以更改指向的地址,但int* const p可以修改指向的内容的数值。现在是const int num,也就是num不能被改变,那么int* const p也就无法改变num,而const int* p,本来就不能修改*p。

回到代码中,src是源头,src不要被改变,所以const char* src,这样下面while的括号里写反后就会及时报错。

void my_strcpy(char* dest, const char* src)
{
    assert(dest != NULL);
    assert(src != NULL);
    while (*dest++ = *src++)
    {
        ;
    }
}

int main()
{
    char arr1[] = "*************";
    char arr2[] = "zyd";
    my_strcpy(arr1, arr2);
    printf("%s\n", arr2);
    return 0;
}

这样的代码之后,还可以再作改进。对于strcpy,这个函数是有返回值,将数据拷贝到目的地后,要返回这个目的地。所以要去掉void,加上返回值。不能直接写return dest。因为代码块运行到那里,dest已经不是之前的dest了,所以要事先保存。所以main函数里面也可以做修改。

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[] = "zyd";
    //my_strcpy(arr1, arr2);
    printf("%s\n", my_strcpy(arr1, arr2));
    return 0;
}

将一个函数的返回值作为另一个函数的参数,这也就是链式访问: printf("%s\n", my_strcpy(arr1, arr2))。

所以整个优化过程就是while括号改变(节省代码量),加上assert(保证传参成功,防止野指针),加上const(保证代码正常循环),改成char*类型(模拟strcpy加上返回值,做出链式访问)。

​​​​​​这里面也需要写注释

char* my_strcpy(char* dest, const char* src)
{
    char* ret = dest;
    assert(dest != NULL);
    assert(src != NULL);
    //把src指向的字符串拷贝到dest指向的空间,包含'\0'字符
    while (*dest++ = *src++)
    {
        ;
    }
    return ret;
}

int main()
{
    char arr1[] = "*************";
    char arr2[] = "zyd";
    //my_strcpy(arr1, arr2);
    printf("%s\n", my_strcpy(arr1, arr2));
    return 0;
}

这样代码就完善了。

练习--strlen

int my_strlen(const char* str)防止字符串被改变
{
    int count = 0;
    assert(str != NULL);//保证指针有效性
    while (*str++)
    {
        count++;
    }
    return count;
}

int main()
{
    char arr[] = "abcdef";
    int len = my_strlen(arr);
    printf("%d\n", len);
    return 0;
}

六、编程常见错误

编译型错误

语法错误。直接看错误提示信息,解决问题。或者凭借经验修改。

连接性错误

比如头文件,传数据,使用未被定义的函数等等。无连接属性。看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在,一般是标识符名不存在或者拼写错误。

运行时错误

借助调试,逐步定位问题。难。 

调试需要自主动手做,才会有大进步,有成就感,促进知识积累。此篇尽量展现了调试过程,在vs code调试,只能尽量去理解,毕竟没有图,还是比较难受的。

结束。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值