目录
初阶的东西我们基本上学习完了,但是我们还需要掌握一些调试技巧帮助我们刚好的写代码和成长,这节课我们就好好学习调试里面的东西
一.Bug
什么是bug呢
第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。
二.调试
1.调试是什么
调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧, 就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。 顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。 一名优秀的程序员是一名出色的侦探。 每一次调试都是尝试破案的过程
我们的调试也是一样的,我们怎么写代码
如何排查问题
2.调试的基本步骤
1.程序员->可以发现错误
2.测试(开发)工程师->也可以发现程序的错误 报bug
----------------程序发布----------------
3.用户->也可以发现(可能就有风险了!)
第二步:以隔离、消除等方式对错误进行定位
第三步:确定错误产生的原因
第四步:提出纠正错误的解决办法
第五步:对程序错误予以改正,重新测试
3.Debug和Release的介绍
Debug通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用
当程序选择debug或release时ctrl+F5进行编译,这时文件夹里就会有debug或release文件夹,文件夹里放的就是debug调试版本或release发布版本,我们对比他们的大小
可以看出debug调式版本里的.exe运行文件要比release发布版本里的.exe运行文件大很多(因为debug调式版本里包含调试信息
我们release里面的优化的放在后面来讲
三.windows环境调试介绍
1.快捷键的使用
我们要熟练的使用vs中的快捷键方便我们调试
(1)窗口快捷键
凡跟窗口挂上钩的快捷键必有一个W(Windows);
Ctrl+W,W: 浏览器窗口 (浏览橱窗用有道的翻译是window shopping)
Ctrl+W,S: 解决方案管理器 (Solution)
Ctrl+W,C: 类视图 (Class)
Ctrl+W,E: 错误列表 (Error)
Ctrl+W,O: 输出窗口(输出Output;输出程序的的编译信息 ;可在vs中“工具”—-“选项”—-“调试”—-“输出窗口”进行配置需要查看哪些信息)
Ctrl+W,P: 属性窗口 (属性 Property)
Ctrl+W,T: 任务列表 (任务Task)
Ctrl+W,X: 工具箱 (事实上工具应该是Tool 但t已被任务列表占用了 ;参照一些大神的记忆方法:“X”长得四通八达,而工具就有这样一个特点,所以属性的快捷键是“X”)
Ctrl+W,B: 书签窗口 (书签 Bookmark 书签非常好用,如果有几千行代码,在寻找代码的时候添加书签找起来要快很多)
Ctrl+W,U: 文档大纲 (OutLine;使用第二个字母U)
Ctrl+D,B: 断点窗口 (breakpoint)
Ctrl+D,I: 即时窗口 (immediately)
(2)项目功能快捷键**
ctrl是强制功能键
shift有给项目增加功能
Ctrl+ F6 /CTRL + TAB下一个文档窗口即活动窗体切换 (windows操作系统是alt+tab表示在任务之间切换
Ctrl + SHIFT + F6 /CTRL + SHIFT + TAB上一个文档窗口 (在windows系统操作中 相信大家都知道shift有相反的功能 哈哈 在这里体现了)
F7: 查看代码 (WebForm开发里面,是查看后台代码)
Shift+F7: 查看窗体设计器 (在后台cs文件的时候,这种方式很方便跳转到前台.aspx页面)
Ctrl+Shift+N: 新建项目 (N是New新建的意思 那如果需要强制在项目中新建项目的话 自然就是组合键Ctrl+Shift+N)
Ctrl+Shift+O: 打开项目 (Open)
Ctrl + Shift + C显示类视图窗口(C代表Class类的意思)
Ctrl + F4关闭文档窗口 (相信用过qq的大家都有使用alt+f4来关闭当前聊天窗口 想想用ctrl+tab在活动标签窗口切换就知道为什么关闭当前标签窗口是ctrl+f4)
Ctrl + Shift + E显示资源视图 (E代表Explorer资源管理器的意思)
Ctrl + Shift + B生成解决方案 (B代表Build生成的意思 其实用F6也可以实现)
Shift+F6表示生成当前项目 (上面已经说过使用f6可以生成整个解决方案 那如果是当前项目的生成那自然就是加上功能键shift了 )
F4 显示属性窗口 SHIFT + F4显示项目属性窗口
Ctrl+Shift+S: 全部保存 (S代表Save保存的意思 这里表示全部保存是因为如果只是单个保存Ctrl+S跟整个项目没有任何瓜葛 而全部保存的概念应该是说在整个项目中 所以组合键中自然会有shift了哦)
Ctrl+Shift+A: 新建项(A是Add的意思)
Shift+Alt+C: 新建类 (shift是跟项目有关的功能键;Alt用的非常多,空格(用的非常多)旁;C是Class;而且添加类用的非常多;所以自然就是:Shift+Alt+C)
(3)查找相关快捷键
Ctrl+F: 查找 (Find) Ctrl+Shift+F: 在文件中查找 (上面已经提过了shift是表示在项目中 所以如果需要在项目中的文件中查找的话 那自然就少不了Shift)
F3: 查找下一个 (相信使用过windows系统的人都知道f3是查找的快捷键)
Shift+F3: 查找上一个 (shift在此有反向的功能哦)
Ctrl+H: 替换
Ctrl+Shift+H: 在文件中替换
(4)代码快捷键
Ctrl+E,D(ctrl+k,d) —-格式化全部代码 ;让你的代码瞬间整洁起来。
Ctrl+E,F —-格式化选中的代码(如果你已经记住Ctrl+E+D是格式化全部代码的话 那你想想规律不就知道了吗 F不就在D的右边表示它是特定某一范围)
Ctrl+K,C: 注释选定内容 (Comment)
Ctrl+K,U: 取消选定注释内容 (UnComment)
Ctrl+J /Ctrl+K,L: 智能提示 列出成员 (kernel核心内容 list列表 如果我们想查看一个对象具有的成员具体信息的时候试下这个快捷键吧)
Ctrl+K,P: 参数信息 (kernel核心内容 Parameters参数 如果我们想查看一个方法的具体参数的时候这个组合键可是挺有用的哦)
Ctrl+K,I: 快速查看信息(Infomation)
Ctrl+K,S: 外侧代码(平时个人惯会时不时的用#region 用了region之后代码看起来就特别整洁 所以自然而然的就用**惯了这个)
CTRL + M, CTRL + M 折叠或展开当前方法
CTRL + M, CTRL + O 折叠所有方法
CTRL + M, CTRL + L展开所有方法
CTRL + M, CTRL + P展开所有方法
Ctrl+M,P: 停止大纲显示 (用了region将代码折叠起来之后试试用这组组合键吧 体验一下折叠和展开的**吧 看着舒服的代码我相信你记住这对快捷键肯定是值得的)
ctrl+shift+f10:自动添加using命名空间(在实例化对象的时候,使用的非常多)
(5)调试快捷键
F5: 启动调试
Ctrl+F5: 开始执行(不调试)
Shift+F5: 停止调试
Ctrl+Shift+F5: 重启调试
F9: 启用/关闭断点
Ctrl+F9: 停止断点
Ctrl+Shift+F9: 删除全部断点
F10: 逐过程
Ctrl+F10: 运行到光标处
F11: 逐语句
(6)编辑快捷键
Shift+Alt+Enter: 切换全屏编辑(如果想一心一意的只写代码 让整个vs铺满全屏 感觉还不错哦)
F12: 转到所调用过程或变量的定义
Alt+F12: 查找符号(列出所有查找结果)
shift+f12:查找所有引用(讲光标放在单词上, 然后按Shift + F12)
Ctrl+U: 全部变为小写 (的sql语句全部转换成大写以提高性能 )
Ctrl+Shift+U: 全部变为大写 (U表示Upper )
Ctrl+Shift+V: 剪贴板循环 (平时我们都只**惯用ctrl+c 和ctrl+v 大家可能还不知道事实上微软都已经帮我们把多次剪切的结果都保存了下来 记下这组快捷键吧 可以粘贴上几次剪切的结果 一用便知道它的强大厉害之处)
Ctrl+Shift+L: 删除当前行 (这个很有用哦 因为大家常常会要删除多余的空行 哈哈 这组快捷键会让你省力不少)
Ctrl+E,S: 查看空白(ctrl+r,w 和它一样可以查看空白或者说显示或隐藏tab标记)
Ctrl+E,W: 自动换行 (代码太长还有滚动条,特别是写append(sql.toString)语句的时候,太长,所以这时候就不得不拖动滚动条,这样以后就可以换行显示了)
Ctrl+G: 转到指定行 (通过情况下 我们想在跳转到具体某一行 用它太方便了)
Shift+Alt+箭头键: 选择矩形文本 Alt+鼠标左按钮: 选择矩形文本
Ctrl + Delete删除至词尾 Ctrl + Backspace删除至词头 Shift + Tab取消制表符
Ctrl+左右箭头键:一次可以移动一个单词
Ctrl+单击: 选中当前点击的整个单词
Shift + End选择至行尾
Shift + Home选择至行开始处
Ctrl + SHIFT + END选择至文档末尾 Ctrl + Shift + Home选择至文档末尾开始
Ctrl + SHIFT + PAGE UP选择至本页前面 Ctrl + Shift + Page Down选择至本页后面
Ctrl + PAGE DOWN光标定位到窗口上方 Ctrl + Page Up光标定位到窗口下方
Ctrl + END文档定位到最后 Ctrl + HOME文档定位到最前
按两下tab快速**代码段(写for, foreach循环,或者try, 还有绑定事件方法)
Ctrl+减号:回退到光标上一次的位置(这个真心挺有用的)
2.几个常用调试
F5
启动调试,经常用来直接跳到下一个断点处
F9
创建断点和取消断点(按一下设置断点,再按一下取消断点)
断点的重要作用,可以在程序中的任意位置设置断点
这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去
F9和F5是配合使用的:当我们想从某一行代码开始调试时(前面的代码自动运行完),在该行代码设置断点,然后F5进行调试,然后再按F10往下调试即可
F5是向后执行代码,到下一个逻辑上的断点,如下图,断点设在循环内,每按一次F5代码 ,循环都会执行一次并且执行到这个断点处停止,因此我们可以直观的看到按一次F5,右边监视窗口i的值增加1
F10
逐过程,通常用来处理一个过程可以是一次函数调用,或者是一条语句
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)
F10和F11在普通语句上是没有什么区别的,在函数调用上是有区别的
F11能够进入函数内部观察细节,而F10认为被调用的函数是一条语句,直接到下一条语句
CTRL+F5
开始执行不调试,如果想要程序直接运行而不调试就可以直接使用
3.调试查看信息
特别注意的是必须先开始调试,一些信息才能看见,先点F5,才能看见里面内存
自动窗口
vs编译器自动获取程序上下文环境中的某些变量,进行展示,自动展示某些变量自动隐藏展示的变量,不方便,一般不进行使用
局部变量
会把程序执行过程中上下文环境中的局部变量进行列举展示
监视
可以把想要观察的数据自己加进去
内存
输入地址可以观察到内存地址中存的数据值
反汇编
可以观察到程序在执行过程中,其汇编代码的变化(也可以在代码界面点击鼠标右键,点击反汇编)
寄存器
可以观察到各类寄存器的变化状态(在监视窗口输入想要观察的寄存器名字也可以观察到)
调用堆栈
可以观察到函数调用的逻辑
四.调试实例
1.求n个数的阶乘和
#include<stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int i = 0;
int ret = 1;
int j = 0;
int sum = 0;
for (j=1; j<=n; j++)
{
for (i = 1; i <= j; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
我们观察到当j=3时算3!应该是6,而代码算出来的是ret=12,此时代码已经走过去了,我们想退回去从j=3重新开始进行观察
我们从j=3重新开始进行观察,我们在i的for循环处设置断点,鼠标放在断点红圈处单机鼠标右键,选择条件,条件改成j==3,关闭后,断点变成了条件断点,此时按F5调试可以观察到是从j=3开始
此时我们可以看到当j=3算3的阶乘时,ret的初始值并不是1,而是2。此时我们便发现了问题
修改如下
int main()
{
int n = 0;
scanf("%d", &n);
int i = 0;
int ret = 1;
int j = 0;
int sum = 0;
for (j=1; j<=n; j++)
{
ret = 1;
for (i = 1; i <= j; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
2.研究死循环原因
int main()
{
int i = 0;
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
注:
1.i和arr是局部变量,局部变量是放在栈区上的,栈区上内存的使用习惯是:先使用高地址处的空间,再使用低地址处的空间
2.数组随着下标的增长,地址是由低到高变化的
3.如果i和arr之间的空间适合的话,就有可能使用的arr数组向后越界就访问到了i,造成循环变量i的改变,最终死循环
4.编译器不一样,数组后面空余的空间数也不一样,所以该代码是严格依赖环境的
vs编译器 空2个空间
vc6.0 不空空间
gcc 空1个空间
五.写出好代码
代码运行正常
bug很少
效率高
可读性高
可维护性高
注释清晰
文档齐全
常见的coding技巧:
使用assert
尽量使用const
养成良好的编码风格
添加必要的注释
避免编码的陷阱。
1.strcpy函数中
我们实现字符串的拷贝,我们可以直接用库函数来写,我们自己实现,先实现主函数
int main()
{
char arr1[20] = "xxxxxxxxxxx";
char arr2[] = "hello";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
我们一步一步进行优化
//就是交换两个字符串,我们可以使用库函数strcop,但是我们自己来写
void my_strcpy(char*dest, char* src)
{
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
*dest = *src;//拷贝\0
}
进行优化,把加加放到一步里,都是后置的加加,所以值先使用,再加加
void my_strcpy(char* dest, char* src)
{
while (*src != '\0')
{
*dest++ = *src++;
}
*dest = *src;//拷贝\0
}
进行优化,把内容和\0一起拷贝,同时加加也放到一起,\0的值是0,出循环
void my_strcpy(char* dest, char* src)
{
while (*dest++ = *src++)
{
;
}
}
我们来看看空指针,然后我们到函数里面会报错,所以我们要加一个判断是否为空指针
我们用的if每次进来的时候都会报错,所以我们用一个断言assert
void my_strcpy(char* dest, char* src)
{
if (src == NULL || dest == NULL)
{
return;
}
while (*dest++ = *src++)
{
;
}
}
我们用一个断言,后面可以放一个表达式,表达式为假的时候会报错,真的话啥事也没有
我们assert,再release版本里面已经被优化了,断言不仅仅是指针可以用,值也可以用
void my_strcpy(char* dest, char* src)
{
assert(dest != EOF);
assert(src != EOF);
assert(dest && src);//这个写法很简练,断言指针的有效性,是否为空指针
while (*dest++ = *src++)
{
;
}
}
假如一个程序员喝多了写反了代码,p跟arr1写反了,所以程序直接挂了
因为p指向的常量字符串不能改,说明常量字符串不能更改,我们反过来就是0就被拷贝过来了,直接就结束了
void my_strcpy(char* dest,char* src)
{
assert(dest && src);
while (*dest++ = *src++)
{
;
}
}
int main()
{
char arr1[20] = { 0 };
char* p = "hello";
my_strcpy(p, arr1);
printf("%s\n",arr1);
return 0;
}
假如一个程序员喝多了写反了代码,把函数里面的src跟dest写反了
程序也会崩溃的,常量字符串不能改,如何避免,我们加一个const,来看一下
void my_strcpy(char* dest, char* src)
{
assert(dest && src);
while (*src++ = *dest++)
{
;
}
}
int main()
{
char arr1[20] = { 0 };
char* p = "hello";
my_strcpy(arr1, p);
printf("%s\n", arr1);
return 0;
}
我们加上一个const,避免情况,如果再这样写反的时候,我们加上const,直接就不能编译过去,直接就是报错了
void my_strcpy(char* dest, const char* src)
{
assert(dest && src);
while (*src++ = *dest++)
{
;
}
}
这里我们开始讲const,我们看下面的代码,我们修改值,可以用指针去改,也可以直接去改
int main()
{
int num = 10;//通过指针p将num的值改为20
int* p = #
*p = 20;
num = 20;//这是直接改
printf("%d\n", num);
}
我们定义的时候加上const,假如我们直接去修改,不可以直接报错了
我们用地址去改的时候,可以去修改,就破坏了规则,只是num不能直接改,但是可以用指针去改
int main()
{
const int num = 10;
//num = 20;//因为num被const修饰,不能被修改
int* p = #
*p = 20;
printf("%d\n", num);
}
所以我们要制止的时候,我们给p带上const,这样*p就不能被修改了
int main()
{
const int num = 10;
const int* p = #
*p = 20;
printf("%d\n", num);
}
但是*p不能被修改了,p能不能被修改,p是可以被修改的,如果把const放到里面,这样p就不能被修改了,但是*p可以被修改
int main()
{
int n = 1000;
//第一种,const放在里面,这时候p不能被修改
int* const p = #
*p = 20;//ok
p = &n;//err
printf("%d\n", num);
//第二种,const放在外面,这时候*p不能被修改
const int* p = #
int const* p = #//这个跟上面写法一样道理
*p = 20;//err
p = &n;//ok
printf("%d\n", num);
return 0;
}
总结:const 修饰指针变量的时候
const int num = 10;//num不能被修改,所以我们const就是限制不被修改
int* p = #
int n = 1000;
1. const放在*的左边,const修饰的是指针指向的内容,表示指针指向的内容,不能通过指针来改变了;但是指针变量本身可以修改
const int* p = #
*p = 20;//err
p = &n;//ok
2. const放在*的右边,const修饰的指针变量本身,表示指针变量本身的内容不能被修改,但是指针指向的内容,可以通过指针来改变。
int* const p = #
*p = 20;//ok
p = &n;//err
库函数的strcpy返回的是目标空间的起始地址,我们再进行优化
char* my_strcpy(char* dest, const char* src)
{
assert(dest && src);//断言指针的有效性
char* ret = dest;//保证返回的时候是起始地址,所以用个ret保留一下
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char arr1[20] = { 0 };
char* p = "hello";+
//我们函数返回的就是起始的地址,所以我们打印的时候,直接可以把函数放到打印里面
printf("%s\n", my_strcpy(arr1, p));//链式访问
return 0;
}
2.strlen函数中
最开始加上我们的assert函数去写
int my_strlen(const char* str)
{
int count = 0;
assert(str != NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
printf("%d\n", my_strlen(arr));
return 0;
}
我们看strlen本身这个库函数的返回是无符号整型,所以我们加上unsigned
unsigned my_strlen(const char* str)
{
unsigned count = 0;
assert(str != NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
我们的unsigned等于size_t 所以替换一下size_t - unsigned int
size_t my_strlen(const char* str)
{
size_t count = 0;
assert(str != NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
六.编程常见的错误
1.编译型错误
直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。
下面这个就是错在中文的大括号
int main()
{
printf("hehe");
return 0;
}
2.链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不
存在或者拼写错误。
下面这个就是函数名前后不一样
void test()
{
printf("hehe\n");
}
int main()
{
Test();
return 0;
}
3.运行时错误
借助调试,逐步定位问题。最难搞。
温馨提示:
做一个有心人,积累排错经验。
讲解重点:
介绍每种错误怎么产生,出现之后如何解决
结束语
C语言初阶到这里就结束了,后续会进行新的C语言进阶的学习,也会出几篇C语言的刷题,加油