调试和多文件编程
随着我们学习的东西越来越多,编写的程序也会越来越复杂,尤其是函数使用之后,很多时候光靠我们自己去发现bug闲的很是吃力,或者自己的想法有出入,导致代码本身就有缺陷,这种时候我们就需要调试这个工具来帮助我们测试这个代码的运行状态,帮助我们判断代码的运行情况。
调试就是一点一点的执行程序,而不是像以前一样一下跑完,它中间会停住,我们可以查看一些变量的信息,看一下程序的状态是否正确,或者是否有逻辑的漏洞。
下面介绍一下Dev c++和VS2019的调试方法。
- Dev c++:
首先,我们要打开调试的选项:
在上面的工具里,打开编译选项
点击代码生成,再点连接器,更改设置如下
OK了,我们可以开始调试了。
首先,为了阻止程序一溜烟跑完了,我们还啥也没看呢,我们要先设置断点,断点就是程序执行到这里就会中断,会停止的意思。我这里使用前面函数的例子的第一版代码,这个是有bug的,正好来学习调试。
当然,调试前提是程序语法没有问题,是逻辑或者程序的漏洞,如果编译器都报错了,还是要赶紧改。
代码如下:
#include<stdio.h>
#include<math.h>
double Str2Num(char str[], int len);
int main()
{
char a[] = "123456";
int num = sizeof(a) / sizeof(a[0]) - 1;
double res = Str2Num(a, num);
printf("%lf", res);
return 0;
}
double Str2Num(char str[], int len)
{
int i;
double result = 0;
for (i = 0; i < len; i++)
{
result += str[i] * pow(10, len - i);
}
return result;
}
编译是过了的:
断点怎么设置:
把鼠标移到这里:
就是行号和代码中间的灰长条区域,移动到你想暂停的地方的所在行上,点一下,这一行就变绿了(当然颜色可能不一样,设置里可以改),同时前面出现一个红点,就是断点,再点一下,恢复正常,断点没了。
断点一般设置在可能出问题的代码前面一点,这样程序停下来的时候就能清楚执行这行代码前是什么状态,是怎么执行的。
比如我想看函数是怎么执行的,我就把断点加在函数的使用的前面,比如加在int num = sizeof(a) / sizeof(a[0]) - 1;这行上。
设置好断点后,我们点击调试按钮(第一章讲的啦。)
OK,我们已经进入调试状态了
蓝色代表程序正要执行的行(还没执行呢)可以看到它现在正好停在了断点处。
下面原本显示编译信息的地方编程了显示调试信息的地方了
调试就是我们想让程序怎么跑,程序就得怎么跑
具体怎么做呢,我们来介绍一下这些按键的操作:
从第一行开始说起,首先是调试,这个不用了,我们已经在调试了。
添加查看:我们可以查看想看的变量,比如我们想看num这个变量的值,我们点击,输入num
OK,现在我们能在代码左侧那一大块空白区域看到num的当前值了
当然,更方便的方法:鼠标放在哪个地方,就能显示哪个地方的值(这里不好截图)
OK了,鼠标晃一晃,左边几乎就都有了。
大家会看到,这里i说没有找到,为什么,其实这就是生存周期的体现,i只在函数中有效,我们程序现在在main函数中,自然没有i。
下一步:我们点击一下,程序就向下执行一行。
我们点一下,会发现num的值变为6了,说明刚刚停留的行被执行了,程序停在了下一行。
下一行我们调用了函数,但我们点击下一步就走到printf那一行了,没有进入函数。
如果我们想看程序是怎么跳转的,我们需要点单步进入,单步进入就是程序怎么执行的,它就怎么走,不会说明明执行了函数不给你看的情况。
跳过函数:当我们单步进入函数中后,我们不想看这个函数的具体执行过程,就可以点击跳过函数,程序会自动执行函数内容后返回主程序。
跳过:程序一直执行直到碰到断点,比如我们设置两个断点:
调试,程序会在前面的断点停下来,这时我们如果点击跳过,程序会像往常那样自己就跑了,知道它在函数中碰到了第二个断点才停下来:
这个东西还挺有用的,对于快速跳出多次循环,我们可以在循环结束后放一个断点,直接跳,程序就把循环跑完了,也能停下来。
停止执行:中止调试。
OK,常用的说完了,剩下的几个除非很奇怪的bug或者程序优化什么的时候会看,其他的时候根本不会用的,但我还是要说φ(゜▽゜*)♪
查看cpu窗口:我们点一下,会出来这个窗口:
我们前面说过,编译器就是翻译官,这些就是它翻译出来的代码,是更为底层的汇编代码,是用汇编语言写的,=>表示当前所在的汇编代码中的行,这个翻译不是一句变一句,有时候是一句变多句,看代码功能而区分。我也知识粗略的学过一点汇编,看这种代码也是看的半生不熟的,不过还是认得一些,知道它们是干什么的。大家看这个:
是不是很眼熟,这不是我们的函数吗,这里的callq是一个过程调用,汇编里的过程和我们的函数其实差不多,前面一堆0x什么的,是一个地址,这个地址是函数的入口地址,大家看这里:
你会发现这个函数名后面跟着的和汇编调用的是一个地方,其实就是函数的指针(指针又出来了,我已经不知道第几次提它了,只能说指针真是C语言的灵魂,别的语言都没有如此强调指针这个概念,但当你理解了指针是什么,你对整个程序是怎么跑的,汇编语言是怎么调用的,IP是怎么跳的,清楚的一批。(ps:IP里放的就是和指针差不多的))
大家还会发现,前面还有一个main函数的call指令,说明main函数也是函数,那么是谁调用的呢,答案是操作系统,如果在命令行窗口中调用我们这个程序,你就会清楚的知道这个程序是怎么被调用的了(可是我不会命令行……)。
说多了,下一个。
下一条语句:就是汇编语句的下一条,同样的,call指令也不会跳走,而是直接执行完了,永远执行相邻地址的下一条。
进入语句:汇编语句会跳的那种。
行了,调试的基本的东西都已经讲完了。
- VS2019调试:
从接触VS,大家就已经是专业的工具人了,可以写自己的项目了。
现在很多程序都是用VS这个IDE,我们最后也还是要用这个的,但是Dev c++比较友好,适合新手,所以开始的时候都是用那个(现在开始讲VS说明大家已经不是新手啦!)。
打开VS(怎么下的就靠各位大神了)
我们创建新项目:
选择C++空项目(C++和C差不多),创建,选择名称和存放路径就OK了。
现在直接写C的代码还是会有点问题,毕竟是专门写C++的代码的
点击项目-最下面的那个属性(快捷键p)
找到这里:
把SDL改为否,应用确定,ok了。
当然现在你还是什么都写不了,因为你只是新建了一个工程,这个工程是空的,要添加东西进去(这就是VS的复杂了,操作多)
点击解决方案资源管理器,在源文件的文件夹右键添加新建项(原本应该是空的,我这里有之前添加好的文件一个)
把名字改为.c的后缀,.cpp是c++的后缀
添加好了之后,就能敲代码了。
看起来也更好看了,不是吗?
断点还是一样的,点一下有,点两下无
加好之后直接运行(原本正常执行程序也是点这个)
程序会在断点处停下来
大家不要看这么多窗口,什么进程内存都出来,不要慌,很多都用不上。
首先,鼠标移到变量上看当前值的操作还是有的。
我们想看值的话,可以在监视1的窗口里显示。
添加一个num,回车就OK了
这里因为在函数里,所以num不在生存周期,就没了,这个i为什么是这么个奇怪的值,因为我们只是定义了i,没有给初值,所以i的初值不好确定,它现在就是这么个值了,当然,箭头指向的是还没执行要指向的行,这个for循环的执行过程是什么?(复习了复习了!)——先执行第一个部分【for(一;二;三)】,这里就给i赋值了。
然后看这里
调试主要是这三个:从左到右介绍
逐语句:相当于单步进入,碰到函数会进函数。
逐过程:相当于下一步,不进函数。
跳出:跳出函数
那个红色的方框按钮就是中止调试。
没了,VS调试主要就这几个,剩下的咱也不会用。
多文件编程
现在,我们向大工程文件又近了一步。
我们首先了解预处理。
预处理就是在编译器编译源程序之前做的准备工作,比如翻看头文件。
预处理的指令都是以#开头的,并且它们不是语句,结尾没有;
预处理的指令主要有下面几种:
#include | 包含头文件 |
---|---|
#define | 宏定义(#define PI 3.14159)讲PI这个字符串与3.14159挂钩,编译器见到PI,就用3.14159替换,注意是直接替换,有时可能会产生运算顺序的错误 |
#undef | 取消宏定义 |
#if | 如果给定条件为真,则编译下面代码 |
#ifdef | 如果已经有宏定义,则编译下面代码 |
#ifndef | 如果没有宏定义,则编译下面代码 |
#elif | 相当于elseif,如果#if条件为假,就看这个判断 |
#endif | #if……#else条件编译块的结束 |
#error | 停止编译,显示错误信息 |
对于头文件的包含,我们可以有这两种形式:
#include<stdio.h>
#include”stdio.h”
两种都是可以的,差别在编译器找头文件所在地的时候的顺序不同,一般的,我们包含原本提供的头文件的时候使用<>,包含自己的头文件的时候使用“”。
我们使用VS来做例子,因为以后的大工程文件大多都用VS来写的。
打开上次的项目,多文件编程肯定是要多个文件的,所以我们再创建一个.c的文件在源文件下面。
我们将在这个新建的文件夹下面编写我们自己的函数库。
首先,我们把上次写好的字符串转数字的函数搬过来。
再把我们之前写的任意整数次幂封装成函数整过来。
#include<math.h>
double Str2Num(char str[], int len)
{
int i, j = len;
char isNegative;
double result = 0;
for (i = 0; i < len; i++)
{
if (str[i] == '.')
{
if (j != len)
{
printf("ERROR\a\n");
return 0;
}
j = i;
}
if ((str[i] < 48 || str[i]>57) && str[i] != '.' &&str[0] != '-' &&str[0] != '+')
{
printf("NO\a\n");
return 0;
}
}
if (str[0] == '-')
{
i = 1;
isNegative = 1;
}
else if (str[0] == '+')
{
i = 1;
isNegative = 0;
}
else {
i = 0;
isNegative = 0;
}
for (; i < len; i++)
{
if (i < j)
{
result += (str[i] - 0x30) * pow(10, j - i - 1);
}
if (i > j)
{
result += (str[i] - 0x30) * pow(0.1, i - j);
}
}
if (isNegative)
{
result = -result;
}
return result;
}
int JieCheng(int n)
{
int a;
int result = 1;
for (a = n; a > 0; a--)
{
result *= a;
}
return result;
}
double Power(double di,int zhi)
{
double ans = 1;
int i;
for (i = 0; i < zhi; i++)
{
ans *= di;
}
printf("%lf", ans);
return ans;
}
好了,我们现在有3个自己的函数了。
对于自己的函数,光有定义不行,我们还需要一个头文件把我们写的函数的声明整合起来,这样到时候只要包含头文件就能使用我们的函数了。
在头文件夹下面新建一个和你放函数的.c文件一样文件名的.h文件(头文件)
建好之后是这样的
#pragma once是防止头文件被重复包含的,我们保留就行了。
我们把刚写的函数的声明写进去
#pragma once
double Str2Num(char str[], int len);
int JieCheng(int n);
double Power(double di, int zhi);
保存,好了,我们自己的头文件创建OK
接下来我们在第一个.c文件中包含我们的.h头文件,并调用里面的函数:
#include<stdio.h>
#include"second.h"
int main()
{
char a[] = "+123456.06659";
int num = sizeof(a) / sizeof(a[0]) - 1;
double res = Str2Num(a, num);
printf("%s to %lf\n",a, res);
printf("%d!=%d\n", 5, JieCheng(5));
printf("%lf^%d = %lf" ,2.5, 3, Power(2.5, 3));
return 0;
}
可以成功执行:
好了,最简单的分文件编程就OK了,后面会有更复杂的,更多种类的自定义的函数的头文件的使用,更多类型的变量的跨文件使用。
我们下一章再见!