• VS实用调式技巧


    目录

    调式的实例

    实例一

    实例二

    实例小游戏

    如何写出好(易于调试)的代码

    优秀代码特征

    常见的coding技巧

    示例1:模拟实现strcpy函数

    strcpy的使用 

    strcpy模拟代码 

    优化tip1

    优化tips2

    assert断言

    const修饰

    const

    const修饰变量

    const修饰指针

    示例2:模拟实现strlen函数

    编程常见的错误

    编译型错误

    链接型错误

    运行时错误


    今天我们接着调式技巧介绍。

    调式的实例

    实例一

    实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出。
    这时候我们如果3,期待输出9,但实际输出的是15。

    why?这里我们就要用到调试技巧

    •  首先推测问题出现的原因。初步确定问题可能的原因最好。
    •  实际上手调试很有必要。
    •  调试的时候我们要思路清晰,看代码是否按照我们预期的在执行。
    1. #include
    2. int main()
    3. {
    4. int i = 0;
    5. int sum = 0;//保存最终结果
    6. int n = 0;
    7. int ret = 1;//保存n的阶乘
    8. scanf("%d", &n);
    9. for (i = 1; i <= n; i++)
    10. {
    11. int j = 0;
    12. for (j = 1; j <= i; j++)
    13. {
    14. ret *= j;
    15. }
    16. sum += ret;
    17. }
    18. printf("%d\n", sum);
    19. return 0;
    20. }

     经过调试我们发现原因是:ret没有每次进入for循环的时候初始化为1,在1的基础上去累乘。

    1. #include
    2. int main()
    3. {
    4. int i = 0;
    5. int sum = 0;//保存最终结果
    6. int n = 0;
    7. int ret = 1;//保存n的阶乘
    8. scanf("%d", &n);
    9. for (i = 1; i <= n; i++)
    10. {
    11. int j = 0;
    12. ret = 1;
    13. for (j = 1; j <= i; j++)
    14. {
    15. ret *= j;
    16. }
    17. sum += ret;
    18. }
    19. printf("%d\n", sum);
    20. return 0;
    21. }

    实例二

     实现代码:循环打印10次hehe。
     但是我们在VS上发现程序在死循环打印hehe。

     在其他编译器也出现了不同的情况打印了12次hehe 

    1. //注意这个代码的验证环境VS底下,X86环境
    2. #include
    3. int main()
    4. {
    5.   int i = 0;
    6.   int arr[10] = {0};
    7.   for(i=0; i<=12; i++)
    8.  {
    9.     arr[i] = 0;
    10.     printf("hehe\n");
    11.  }
    12.   return 0;
    13. }

     

    调试过后,我们发现当我们把arr[12]设置为0的时候,同时我们的i也变回到了0。这样就导致了死循环。我们不禁猜测:arr[12] i 使用的是同一块地址

    通过监视窗口,我们发现果然arr[12]和i使用的是同一块地址 

     我们画图来让大家更易于理解

    导致死循环的原因:

    1.  在栈区上内存使用的习惯时:从高地址向低地址处使用
    2.  数组随着下标的增长,地址是由低到高变化的

    巧合arr[12]和i使用就是同一块地址。代码中arr和i之间空2个整型,完全是巧合(取决于编译器)VC i和arr之间没有空隙,gcc i和arr之间空1个整型。所以说完全是巧合,取决于编译器。这个代码是严格依赖环境的。未来再次遇到这样的代码,我们就会向着方向去分析,数组越界和死循环。

     还有同学这样修改我们的代码不导致死循环。

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

    按照代码执行的逻辑顺序关系在栈区上创建的临时变量来说,这样是可以的消除死循环。但是不太符合我们正常思维。

    在上一篇博文我们提到Dug和Release版本。在这里有什么不用呢?

    Dug死循环

    Release优化

    我们清晰的发现了,release版本将我们的 临时变量 i 创建在栈区的时候,放在arr后面,这样越界访问也不会访问到i的地址了 ,真的非常聪明!

    实例小游戏

    我们在学习函数和数组的时候写了两个小游戏:

    C语言之三子棋游戏实现篇_唐唐思的博客-CSDN博客

    C语言之扫雷游戏实现篇-CSDN博客

    像这种较复杂的代码怎么去调试呢?

    • 理清楚代码逻辑,想清楚代码应该怎么走
    • F10进去函数内部调试
    • 看代码有没有按照自己的预期去执行
    • 一维数组和二维数组传参访问

    大家一定要自己动一动小手去调试代码,这是灰常灰常重要的!!!!

    一维数组和二维数组传参访问的tip

    数组名,下标

    这样就可以查看每一行里的元素,而不仅仅是只看得到每一行第一个元素。 

    如何写出好(易于调试)的代码

    优秀代码特征

    • 代码运行正常
    • Bug很少
    • 效率高
    • 可读性高
    • 可维护性高
    • 注释清晰
    • 文档齐全

    常见的coding技巧

    • 使用assert(断言)
    • 尽量使用const
    • 养成良好的编码风格(变量名的命名体现)
    • 添加必要的注释
    • 避免编码的陷阱

    示例1:模拟实现strcpy函数

    strcpy - C++ Reference (cplusplus.com)

    strcpy的使用 

    1. #include
    2. int main()
    3. {
    4. char arr1[] = "xxxxxxxxxxxxxx";
    5. char arr2[] = "abcdef";//注意'\0'也会拷贝过去
    6. strcpy(arr1, arr2);
    7. printf("%s", arr1);
    8. return 0;
    9. }

    strcpy模拟代码 

    关于模拟实现strcpy,在前面我们也非常详细的讲解了

    戳这里!C语言之字符函数&字符串函数篇(1)-CSDN博客 

    1. #include
    2. char* my_strcpy(char* des, char* src)//目的地址,源地址
    3. {
    4. char* ret = des;
    5. while (*src != '\0')
    6. {
    7. *des = *src;
    8. des++;
    9. src++;
    10. }//注意没有将'\0'拷贝过去
    11. *des = *src;
    12. return ret;
    13. }
    14. int main()
    15. {
    16. char arr1[] = "xxxxxxxxxxxxxx";
    17. char arr2[] = "abcdef";//注意'\0'也会拷贝过去
    18. char* ret =my_strcpy(arr1, arr2);//返回值是目的空间的起始地址
    19. printf("%s", ret);
    20. return 0;
    21. }

     

    接下来我们来一点点优化上面这个模拟strcpy的代码。

    优化tip1

    1. char* my_strcpy(char* des, char* src)//目的地址,源地址
    2. {
    3. char* ret = des;
    4. while (*src != '\0')
    5. {
    6. *des++ = *src++;
    7. }//注意没有将'\0'拷贝过去
    8. *des = *src;
    9. return ret;
    10. }
    11. int main()
    12. {
    13. char arr1[] = "xxxxxxxxxxxxxx";
    14. char arr2[] = "abcdef";//注意'\0'也会拷贝过去
    15. //返回值是目的空间的起始地址
    16. printf("%s", my_strcpy(arr1, arr2));//链式访问
    17. return 0;
    18. }

    后置++表现为两个效果,一个是原值与*结合之后,一个是地址往后移动一位。 

    优化tips2

    1. #include
    2. char* my_strcpy(char* des, char* src)//目的地址,源地址
    3. {
    4. char* ret = des;
    5. while (*des++ = *src++)
    6. {
    7. ;
    8. }
    9. return ret;
    10. }
    11. //这里des和src在跳出循环之前,往后走了一步,超出数组范围,
    12. //但是在这里不影响,后面不用des src,但是要注意
    13. int main()
    14. {
    15. char arr1[] = "xxxxxxxxxxxxxx";
    16. char arr2[] = "abcdef";//注意'\0'也会拷贝过去
    17. //返回值是目的空间的起始地址
    18. printf("%s", my_strcpy(arr1, arr2));//链式访问
    19. return 0;
    20. }

     

    表达式的值  

    !!!特别提醒:++ = = =  不要弄混。

    assert断言

    如果传给des或src是空指针?为了以防万一

    1. #include
    2. char* my_strcpy(char* des, char* src)//目的地址,源地址
    3. {
    4. if (des == NULL || src == NULL)
    5. {
    6. return;
    7. }
    8. char* ret = des;
    9. while (*des++ = *src++)
    10. {
    11. ;
    12. }
    13. return ret;
    14. }

    但是无论是在Dug还是Release版本底下,若传入的指针为空,if语句都会执行。那能不能不执行,直接报错呢?当然可以。

    1. #include
    2. #include//头文件
    3. char* my_strcpy(char* des, char* src)//目的地址,源地址
    4. {
    5. assert(des != NULL || src != NULL);
    6. assert(des || src );
    7. //表达式为假就会报错
    8. char* ret = des;
    9. while (*des++ = *src++)
    10. {
    11. ;
    12. }
    13. return ret;
    14. }
    •  包含头文件#include
    •  一旦括号里的表达式为假,那么会报错
    •  两种写法
    •  在Release版本底下会直接优化掉assert

    const修饰

    1. #include
    2. #include//头文件
    3. char* my_strcpy(char* des, const char* src)//目的地址,源地址
    4. {
    5. assert(des != NULL || src != NULL);
    6. //assert(des || src );
    7. //表达式为假就会报错
    8. char* ret = des;
    9. while (*des++ = *src++)
    10. {
    11. ;
    12. }
    13. return ret;
    14. }

    const

    const修饰变量

    const修饰变量的时候,是在语法层面限制了const修改

    但本质上,num还是变量,是一种不能被修改的变量

    1. #include
    2. int main()
    3. {
    4. int a = 10;
    5. int b = 0;
    6. printf("b=%d\n", b);
    7. b = a;
    8. printf("b=a=%d\n", b);
    9. return 0;
    10. }
    11. #include
    12. int main()
    13. {
    14. int a = 10;
    15. const int b = 0;
    16. printf("b=%d\n", b);
    17. b = a;
    18. printf("b=a=%d\n", b);
    19. return 0;
    20. }

    虽然b被const修饰了,不能被改变了,那b还是变量吗?

    1. #include
    2. int main()
    3. {
    4. int a = 10;
    5. const int b = 0;
    6. int arr[b] = { 0 };
    7. printf("b=%d\n", b);
    8. //b = a;
    9. //printf("b=a=%d\n", b);
    10. return 0;
    11. }

     

    那如果找到b的地址,修改根据地址找到所指向空间里的值, 可以吗?居然可以。

    const修饰指针

    1. #include
    2. int main()
    3. {
    4. int a = 10;
    5. const int b = 0;
    6. printf("b=%d\n", b);
    7. int* p = &b;
    8. *p = a;
    9. printf("b=a=%d\n", b);
    10. return 0;
    11. }

     

     但是这样就打破了语法平衡,就像本来锁上大门不想让人进入,但是你偏偏要破窗而入。

    所以我们需要制约,const修饰指针有两种形式:

    • const放在*的左边
    • const放在*的右边
    1. const放在*左边
    2. #include
    3. int main()
    4. {
    5. int a = 10;
    6. const int b = 0;
    7. printf("b=%d\n", b);
    8. const int* p = &b;
    9. *p = a;//err
    10. printf("b=a=%d\n", b);
    11. p = &a;//ok
    12. return 0;
    13. }
    1. const放在*右边
    2. #include
    3. int main()
    4. {
    5. int a = 10;
    6. const int b = 0;
    7. printf("b=%d\n", b);
    8. int* const p = &b;
    9. *p = a;//ok
    10. printf("b=a=%d\n", b);
    11. p = &a;//err
    12. return 0;
    13. }
    1. //都不能修改
    2. #include
    3. int main()
    4. {
    5. int a = 10;
    6. const int b = 0;
    7. printf("b=%d\n", b);
    8. const int* const p = &b;
    9. *p = a;//err
    10. printf("b=a=%d\n", b);
    11. p = &a;//err
    12. return 0;
    13. }

    综上四种情况:

     不限制,限制*p,限制p,限制*p和p

    • const 放在*的左边:限制的指针指向的内容。也就是说:不能通过指针来修改指针指向的内容,但是指针变量是可以修改的,也就是指针指向其他变量的
    • const 放在*的右边:限制的是指针变量本身,指针变量不能再指向其他对象。但是可以通过指针变量来修改指向的内容。
    1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。
    2. const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。 

    注:《高质量C/C++编程》 

    示例2:模拟实现strlen函数

    1. #include
    2. #include
    3. size_t my_strlen(const char * arr)
    4. {
    5. size_t ret = 0;
    6. assert(arr != NULL);
    7. //assert(arr);这样写也可
    8. while (*arr)
    9. {
    10. arr++;
    11. ret++;
    12. }
    13. return ret;
    14. }
    15. int main()
    16. {
    17. char arr[] = "abcdef";
    18. size_t count = my_strlen(arr);
    19. printf("%Zd\n", count);
    20. return 0;
    21. }

    编程常见的错误

     

    编译型错误

    直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。

    链接型错误

    看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。C语言大小写敏感

    1. int add(int a, int b)
    2. {
    3. return a + b;
    4. }
    5. int main()
    6. {
    7. int a = 10;
    8. int b = 20;
    9. int c = Add(a, b);
    10. printf("%d\n", c);
    11. return 0;
    12. }
    13. //Link 链接
    14. //Link.exe

     

    运行时错误

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

    特别提醒,做一个有心人,积累排错经验,写成文章博客。以及笔记等等。(介绍每种错误怎么产生,出现之后如何解决)

    ✔✔✔✔✔最后,感谢大家的阅读,若有错误和不足,欢迎指正!下篇博文我们总结各种函数的模拟实现。每个人都要为自己所做的事情负责,所以不要拖延,不要害怕,不要在意任何的人目光,勇敢去做就好了,失败也没关系的77🙂

    代码------→【gitee:唐棣棣 (TSQXG) - Gitee.com

    联系------→【邮箱:2784139418@qq.com】

  • 相关阅读:
    Android学习笔记 38. 网络加载框架Retrofit
    codeforce 158B Taxi
    PyCharm使用心得体会1
    适用于 Windows 10 和 Windows 11 设备的笔记本电脑管理软件
    mysql 高性能搭建3: 2022 mysql5.7.29(主主复制)+nginx的stream模块实现负载均衡
    海康Visionmaster-全局脚本:VM 加载方案后自动执行 的方法,如何让VM在方案加载后自动执行流程?
    低代码在物品领用领域数字化转型的案例分析
    XGBoost预测及调参过程(+变量重要性)--血友病计数数据
    注册的业务、登录业务、个人中心、nginx配置【VUE项目】
    【go-zero】go如何1秒优雅地生成 protobuf 中的CRUD (关于 go-zero RPC中如何快速生成protobuf)
  • 原文地址:https://blog.csdn.net/m0_74841364/article/details/133419823