第一章主要分两个环节讲述。第一节涵盖大部分C语言需要复习回顾的内容,该PPT的1-27页。引入了部分面向对象思想进行深入阐述。重难点是多文件工程。
同学们在学习课件之余,还应做好编译器开发环境的配置工作。推荐使用VC6短小精悍。支持更好的工作区和多文件工程开发的IDE工具,还有codeBlocks。将编译和链接两阶段分开的,还有dev C++,但他不支持多文件工程。
本课程不加说明的话,都将使用Gcc编译器和Gdb调试器,IDE环境使用CodeBlocks。教师机演示环境多为VC6.0。
下面是一个VC6的Step by Step小视频,便于大家快速了解最简单的C、C++编译开发过程。
下面是VC6的hello工程小视频,与上面的文档对应。
最后复习多阅读下面的每章的要点笔记(本单元涵盖前三页)。
一、功能分解
也被称作“水平功能分解”。对于小的程序来说,可以理解成一个程序的功能应该拆解为几个不为来实现,或者被调用执行。比如
(1) 输入。。。
(2)数据处理。。。
(3)输出。。。
这就是一个简单的功能分解,一分为三。即便我们的程序就完成一个功能,至少也应该拆解为三个部分。
对于更大的模块,甚至软件系统来说,更可以认为一个功能点就是一个模块、甚至一个函数实现的。因此,用软件工程来方法来描述,我们可以说,功能分解,就是解释说明“是什么”的过程,也就是系统需求分析的过程。
教材的p1和p2页给出了“学生管理系统”和"QQ"的功能分解的例子。
二、分层求精
也被称作“垂直逐步分层求精”。对于小的程序单位来说,可以理解成一个程序的功能应逐步实现。先水平分解,然后对某一个功能点,采用逐步求精。
下面给出一个微视频。
一、代码装载执行
对于简单的C程序,我们“总是”认为从main函数的第一句开始执行,其实这是一种误解。
例如:
extern int a; int main() { a++; } int a = 0; |
又或者我们用一分为三的方法,对于下面的程序:
//main.c #include “a.h" int main() { a++; } | //a.h extern int a; | //a.cpp int a = 0; |
显然,这个程序的两种形式是等价的,当然,我们推荐第二种写法:因为只有第二种写法,才体现了“测试驱动”的用户视角去开发。
需要指出的是,首先执行的,都是黄色的语句,而并非红色语句。
程序代码最终链接成为二进制的exe文件,在代码载入内存的时候就会自动执行的代码,被称为静态/全局代码。
所以,静态的main函数驱动执行的第一步之前,前面还要加上一个限定:
代码首先载入执行“静态和全局代码”,随后从main函数开始执行。
二、函数驱动执行的栈过程
教材P4和P5页给出了两张表,有小的纰漏错误。
表1-1 执行到funB内第一句时的系统栈状态:

同理,表1-2也有这个小纰漏,大家自行调整一下。
(1)当程序需要跳转到别的函数时,比如执行我们这个例子程序,shell的cmd启动控制台,开始执行main所在的.exe,此时代码载入执行完后,进入main之前,要先保存当前OS的状态,需要保存cmd的命令行状态,这对应最下面两行push压栈。通俗来说的话,就是“先保存小状态,再保存大状态”。这样从栈顶弹出的时候,我们能够首先拿到返回到cmd程序的地址,然后再返回当前cmd程序的小状态。
这个过程可以打个比方:
比如当前的电脑全屏活动窗口是word,上面line1行被选中,然后过了几分钟,屏幕保护程序启动了,那么再启动屏保程序之前,系统需要先记录当前屏幕的line选中状态,然后再记录要返回的当前窗口是word;然后启动屏保程序。
那么当屏保关闭时(比如我们移动鼠标或按键唤醒),系统首先要回到word。怎么回呢,是因为从系统栈pop弹出栈顶,得到“大状态”(word程序地址),进程返回;接着再pop,拿到当前窗口(word程序恢复运行)的“小状态”(line1行选中)。
函数驱动的过程,就是每当发生函数调用跳转前的系统栈压栈保存,和函数调用返回时的系统栈弹出返回的过程。
在“功能分解与逐步求精”中,已经给出了示例程序。
这里进一步阐释多文件工程的软件工程意义:
(1)需要建立人员分工,不同角色负责不同任务。
对于一个C程序来说,用户角色(裁判程序、测试数据提供方)负责编写main所在的cpp。他只需要#include 授权声明.h文件,就可以调用获得服务,不需要实现的c代码。
对于开发人员来说,可以再次分工。接口经理负责获取用户的需求,转化为.h约束的接口文件,固定下来,要求实现人员按照用户的约束和调用习惯来实现功能。接口经理相当于系统分析员或架构设计师的角色。
第三个角色是功能函数的开发人员,他接受接口经理的下发约束文件,负责功能实现。
如果运用MVC(model-view-control)模式,那么一个C/C++程序就应该分解成三层,view层即.h文件,表明系统对外提供的功能接口;C表示调用分配,类似于switch case结构的控制中枢,这里就是main.c文件;具体的功能实现fun.c属于模型Model层。
(2)有利于错误隔离,将问题的规模缩小。
两个.c文件的分别编译,部分IDE工具还支持对.h的语法减少(.h文件只是文本约束,放在最前面供编译器决策优化用,所以成为“头文件”),多文件分别编译减少了错误的集中爆发。
(3)测试驱动,用户规范必须首先遵循
显然,调用结构的编译能够强迫检查开发者是否遵循了规范,以免造成开发人员闭门造车,最后交付func才发现参数规范不对。
重复包含问题比较复杂。
先插入一个视频
这个视频中,罗列了单个cpp文件内,函数声明是可重复的,但是类型定义声明是不可重复的。使用#ifndef...#endif宏控制可以避免重复定义的编译错误。
特别要强调指出的是,对于不可重复的唯一资源,必须使用唯一的信号量宏进行控制。
除此之外,变量定义也是不可重复的,但在编译阶段的正确,不代表链接阶段的正确。我们以全局变量的定义放入.h声明为例,验证了编译成功而链接失败。这从侧面证明了,#ifndef的宏控制只在编译期有效,对链接无效。
结论:不能将变量或函数定义放入声明文件中。
完整版的微课视频如下:
资源被使用,需要首先在使用端顶部给出资源的使用列表,可以称之为授权访问声明,或使用规格约束。
很多时候,需要在多个不同处访问资源,为此,一再重复出现的声明形式显得容易不一致。一旦用户的使用规格变化,开发人员还得每处去修改声明。因此,编写唯一的声明文件,多处#include就能更为便利的多处访问该资源。
声明文件(.h文件)生就是为了多处访问授权而存在。对于每个要访问资源的调用体来说,都需要#include该资源的声明文件在头部,故声明文件也被称之为.h(header)文件。
资源的被使用,被称为调用或使用。
资源的实体,被称之为资源的定义。
声明、调用、定义,合称为三要素。
对于变量来说,首次出现一般是隐藏式的声明,我们将出现在访问/使用之前的定义称之为“隐式声明”(implicit declaration)。由于变量通常具有局部作用域,只在本地调用(不推荐使用全局变量),故只有全局变量才具有显式声明的意义——需要跨文件在多处被访问。