常用:
调试->窗口->
断点
监视
自动窗口
局部变量
调用堆栈
内存
反汇编(也可以右键,转到反汇编)
寄存器
快捷键:
F5:启用调试,经常用来跳到下一个断点处
F9创建断点和取消断点。断点的重要作用:可以在程序任意位置设置断点。这样就可以使程序在想要的位置随意停止,继而一步一步进行下去。
F10逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11逐语句,就是每次都执行一条语句,但这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)
Crtl+F5:开始执行不调试
一些调试的实例:
实例1:
实现代码:求1!+2!+3!+……+n!
所以正确的代码就应该是
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int i = 0;
int n = 0;
scanf("%d", &n);
int sum = 0;
int ret = 1;
for (i = 1;i <= n;i++)
{
ret = 1;
int j = 0;
for (j = 1;j <= i;j++)//计算i!
{
ret *= j;//经过调试我们可以发现ret的值会在程序进行中改变,而我们希望每次计算n!的时候ret初始化为1,所以应该在函数内部定义ret
}
sum += ret;//i从1到n的阶乘相加
}
printf("%d\n", sum);
return 0;
}
实例2:
研究程序死循环的原因
我们观察程序,分析为什么会死循环打印hello world
(以上结果演示是在vs编译器的x86环境下,不同编译器,预留空间不同)
在调试过程中我们可以发现arr[12]的值始终是和i的值相等的,那么这是为什么呢,可能是他们占了同一块内存空间,我们将他们的地址取出观察发现确实占的是同一块内存空间,等i自增到12是,将arr[12]这块空间赋值为10,在重复,i永远不可能大于12,所以死循环打印hello world。
原因:局部数据在栈区中存放,而栈区内存的使用习惯是先使用高地址处的空间,在使用低地址处的空间。但是数组地址随着下标的增长是由低到高变化的。所以如果i和arr之间有适当的空间,那么利用数组的越界操作就有可能会覆盖到i,导致死循环出现。
(这类题目,在《C陷阱和缺陷》书籍中提到)
如果是release版本的话,就会对上面的代码,做出适当的优化,打印结果在这题中会打印出12个hello world。
- DeBug通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员运行调试程序。
- Release版本称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好的使用(也是测试人员测试的版本)
如何写出好的代码:
- 代码运行正常
- bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
常见技巧
- 尽量使用assert
- 尽量使用conse
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱
例:模拟实现strcpy函数
我们可以初步写一个这样的代码,但是这个代码还有许多地方可以优化
void my_strcpy(char*dest, char*src)//destination目标空间,source源数据
{
while (*src != '\0')
{
*dest = *src;
src++;
dest++;
}
*dest = *src;//将\0,也拷贝过来
}
int main()
{
char arr1[20];
char arr2[] = "hello world";
//strcpy再拷贝字符串的时候会把\0也拷进去,这边便于识别字符长度
my_strcpy(arr1, arr2);//用一个strcpy函数实现将arr2里面的数据拷贝到arr1中
return 0;
}
比如函数部分可以写为
void my_strcpy(char* dest, char* src)//destination目标空间,source源数据
{
while (*dest++ = *src++)
{
;
}
}
\0的ASCII码值是数字0,所以我们可以指将将其放在判断里面,后置++,先赋值后++,当判断是\0的时候再自增一次,就是想当于将\0的值也赋进去。
我们也可以将代码进一步优化,使用assert和const
#include<assert.h>
void my_strcpy(char* dest, const char* src)//destination目标空间,source源数据//加const src的值不能被改变,避免出现拷反的情况
{
//断言(如果assert为假就报错,需要包含assert.h的头文件)
assert(src != NULL);// 空指针不能直接使用,传参是不能对空指针进行解引用
assert(dest != NULL);
while (*dest++ = *src++)
{
;
}
}
assert函数,表示断言如果assert为假,编译器就会报错,并能定位到错的位置,使用时需要包含assert.h的头文件。
const表示常变量,这个值不能被直接修改,但是我们可以通过他的地址找到这个数对其进行修改,如:
const修饰指针变量分为两种情况
- const放在*左边(如const int* p或者int const *p)意思是:p指向的对象不能通过p来改变了,但是p变量本身的值是可以改变的。
- const放在*右边(int* const p)意思是:p指向的对象是可以通过p来改变的,但是不能修改p变量本身的值
- 若两边都放(const int* const p)意思是:p指向的对象和p变量本身都不可通过p来改变。
但是我们发现在库函数里,strcpy的返回值类型是char*,这是为什么呢?
是为了实现链式访问,strcpy函数返回的是目标空间的起始地址。
我们可以将代码进一步完善:
#include<stdio.h>
#include<assert.h>
//模拟实现strcpy函数
char* my_strcpy(char* dest, const char* src)//destination目标空间,source源数据//加const src的值不能被改变,避免出现拷反的情况
{
//断言(如果assert为假就报错,需要包含assert.h的头文件)
char* ret = dest;//在程序运行过程中dest的地址已经发生了改变,所以我们可以向将他的起始地址存起来。之后我们使用这个数据的时候就可以通过起始地址找到它
assert(src != NULL);// 空指针不能直接使用,传参是不能对空指针进行解引用
assert(dest != NULL);
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char arr1[20];
char arr2[] = "hello world";
//strcpy再拷贝字符串的时候会把\0也拷进去,这边便于识别字符长度
printf("%s\n",my_strcpy(arr1, arr2));//用一个strcpy函数实现将arr2里面的数据拷贝到arr1中
return 0;
}
(《高质量C/C++编程》一书中最后章节试卷有关strlcpy模拟实现的项目 )
注意:
- 分析参数的设计(命名,类型),返回值类型的设计
- 野指针,空指针的危害
- assert的使用
- const的使用
- 注释的添加
模拟一个strlen函数:
#include<stdio.h>
#include<assert.h>
//模拟实现strlen函数
int my_strlen(const char* str)
{
int count = 0;
assert(str != NULL);//这里也可以直接写str
while (*str != '\0')//这里也可以直接写*str
{
str++;
count++;
}
return count;
}
int main()
{
char arr[] = "hello world";
printf("%d\n", my_strlen(arr));
return 0;
}
变成常见错误:
- 编译型错误
(看错误提示信息,双击)
- 链接型错误
(一般是标示符名不存在,或者拼写错误,可以用 ctrl+F查看文本)
- 运行时错误
借助调试,一步步分析。