总结一下这几天学习的内容,将调试章节的剩下部分全部学习完,学习内容主要分为以下五个部分:
**以下调试主要用VS2022做示例**
(1)调试的时候查看程序当前信息
按下F5(F5+Fn)进入调试(一些时候可能需要设置断点才能进入调试),在调试环境下,选择【调试】选项卡→【窗口】选项进行以下菜单的选择

1.查看临时变量的值:便于观察变量的值
查看临时变量的值可以通过的【自动窗口】、【局部变量】、【监视】(监视窗口可以有多个同时监视不同变量)。以下面代码为例:
cs
int main()
{
int arr[10] = { 0 };
int i;
for (i = 0; i < 10; i++)
{
arr[i] = i;
printf("%d\n", arr[i]);
}
return 0;
}
【自动窗口】

【局部变量】

【监视】

2.查看内存信息:可设置行的列数(一列一个字节,内存用16进制显示)
通过【内存】选项进行查看(内存窗口可以有多个,方便多个变量同时查看)

3.查看调用堆栈:清晰的反应函数调用关系和该函数所处位置
数据结构中的站:压栈、出栈。可以通过【并行堆栈】和【调用堆栈】查看,以以下代码为例:
cs
int add(int n, int m)
{
return n + m;
}
int main()
{
int a = 10;
int b = 20;
int c = add(a, b);
printf("%d\n", c);
return 0;
}
【并行堆栈】

【调用堆栈】

4.查看汇编信息:可切换到汇编代码
通过【反汇编】进行查看

5.查看寄存器信息:当前运行环境的寄存器的使用信息
【寄存器】

(2)多多动手。尝试调试,才能有进步
①要熟练掌握调试技巧
②对于大多数初学者是80%的时间写代码,20%的时间在调试
但是程序员实际上只有20%的时间写代码,80%的时间在调试。
③从简单的调试入手,慢慢尝试更为复杂的调试场景,例如多线程调试。
④多使用快捷键,提升效率。
(3)调试实例
1.n的阶乘的和
cs
int main()
{
int n = 0;
int sum = 0;
int ret = 1;
int i, j;
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
for (j = 1; j <= i; j++)
{
ret *= i;
}
}
return 0;
}
以上代码的ret为重置所以会导致代码最终结果会错误,可以通过调试找到错误并进行修改。
2.数组的越界访问(代码在X80环境下运行)
cs
int main()
{
int i = 0;
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
该代码越界访问,为什么不报错?且为什么结果为死循环?
原因:1.栈区内存的使用习惯是先使用高地址的空间,再使用低地址处的空间。
2.数组随着下标的增长地址是由低到高变化的。
3.如果i和arr之间由适当的arr空间,利用数组的越界操作就可能会覆盖到i,就可能会导致死循环出现。

* *这一知识点在《C语音的缺陷》中有讲过,在nice公司的笔试题也曾经出现过* *
(4)如何写出好的(易于调试)的代码
1.优秀的代码:
①代码运行正常
②bug很少
③效率高
④可读性高
⑤可维护性高
⑥注释清晰
⑦文档齐全
2.常见的coding(编程)技巧:
①使用assert(断言,使用时需包含assert.h头文件)
②尽量使用const(修饰常量)
③养成良好的编程风格
④添加必要的注释
⑤避免编程陷阱
3.示范
①模拟写出库函数strcpy函数
首先了解strcpy函数,strcpy 是一个标准字符串处理函数,用于将一个字符串复制到另一个字符数组中。使用该库函数时需要包含头文件string.h,使用模版 char* strcpy( char* strDestination , cnost char* strSource ),其中strDestinetion代表目标空间,strSource代表源数据。
使用示例代码:
cs
#include<string.h>
int main()
{
char arr1[] = { "XXXXXXXXXXXXXXXXXXXXX" };
char arr2[] = { "Holle Word!" };
strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
在这个示例中可以看出strcpy函数在拷贝字符的时候,会把源字符串中的'\0 '也拷贝
了解strcpy函数后模拟自己写一个strcpy函数:一共分为两个版本,一个是以初学者写出较为简单的代码,而另一个更符合以上所说的"优秀的代码"也是更接近库函数实际使用的代码。
cs
//版本一:
void my_strcpy(char* dest, char* str)
{
while (*str != '\0')
{
*dest++ = *str++;
}
*dest = *str;
}
//版本一不符合优秀代码的要求
//版本二:
#include<assert.h>
char* my_strcpy1(char* dest, const char* str)
//在第二个源数据参数前添加const,以防代码中两个参数的赋值的错误
{
char* ret = dest;
//使用断言函数assert控制使用的两个字符串的可操作性(不能对空指针进行操作)
assert(str != NULL);
assert(dest != NULL);
//将操作语句直接写到循环的判断语句中,使得代码更为简洁
while (*dest++ = *str++)
{
;
}
return ret;
}
//为什么返回值写char*呢?
//是为了实现链式访问。
//strcpy函数返回的是目标空间的起始地址
int main()
{
char arr1[] = { "XXXXXXXXXXXXXXXXXXXXX" };
char arr2[] = { "Holle World!" };
my_strcpy(arr1, arr2);
printf("%s\n", arr1);
printf("%s\n", my_strcpy1(arr1, arr2));
return 0;
}
在版本二中更好的解释了断言函数assert和const修饰变量的含义,以及使用char*的原因:
①使用断言函数assert控制使用的两个字符串的可操作性(不能对空指针进行操作)
②在第二个源数据参数前添加const,以防代码中两个参数的赋值的错误。
③为什么返回值写char*呢?
是为了实现链式访问。strcpy函数返回的是目标空间的起始地址。
const修饰变量指针:
①const放在*的左边
意思:p指向的对象不能通过p改变,但是p变量本身的值可以改变。
②const放在*的右边
意思:t指向的对象可以通过t改变,但是t变量本身的值不能改变。
cs
int main()
{
int num = 20;
int n = 10;
//1.const放在*的左边
const int* p = #
*p = 10; //false
p = &n;//true
//意思:p指向的对象不能通过p改变,但是p变量本身的值可以改变
//2.const放在*的右边
int* const t = #
*t = 10; //true
t = &n; //false
//意思:t指向的对象可以通过t改变,但是t变量本身的值不能改变
return 0;
}
练习:模拟实现一个strlen函数(求字符串长度)
cs
#include<assert.h>
int my_strlen(const char* str)//保证参数不能被修改
{
int count = 0;
assert(str);//保证指针不为空指针
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = { "Holle World!" };
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
(5)编程常见的错误
先了解一下编程的过程:test.c文件→编译→链接→可执行程序
1.编译型错误(也就是语法错误,比较简单)在编译步骤中出现。
直接看错误提示信息(双击),解决问题。或者凭借经验就可以解决。
cs
int mian()
{
int a=0
//常见忘记打分号
return 0;
}

2.链接型错误(标识符不存在或者拼写错误,出现在链接期间)
查看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。查找时可使用Ctrl+F进行查找,如果找不到可能的原因有两个:一是不存在;二是写错了。
cs
int main()
{
int a = 10;
int b = 20;
int c=add(a, b);
//add函数未定义(不存在)
printf("%d\n", c);
return 0;
}

3.运行时错误(逻辑错误,最复杂)
借助调试,逐步定位问题(以上文的调试实例n的阶乘的和为例)。