程序组织与软件开发方法

I.库与接口

1.库与程序文件

  • 程序文件:源文件(.cpp), 头文件(.h、*.hpp、 *), 实际上对于头文件有没有后缀名无关紧要,但是对于编译器来讲,以.h结尾, 有利于编译器更好的管理程序
  • 库: (不需 main 函数) - 源文件: 包括了具体的实现代码 - 头文件: 提供了库的接口

    2.接口

  • 通过接口使用库: 包括制定库的头文件与源文件
  • 优势: 不需了解库的实现细节,只需了解库的使用方法,就是说只知道 .h 头文件就可以了,不需要了解 .cpp

3.标准库

C标准库

标准输入输出库、工具与辅助函数库、字符串库

C++标准库(与C标准库不一样)

输入输出流库、字符串库、标准模版库 写程序的时候,既可以选择 C 的标准库,也可以选择 C++ 的标准库

4.数学库

数学库

  • 头文件: math.h / cmath
  • 库文件:libm
  • 链接方式: g++ -lm main.cpp, 链接数学库到 main.cpp里, 因为windows下,cmath 是c C++ 标准库的一部分,但 linux 下,数学库是单列的,缺省的时候是不链接数学库的,所以如果链接 那你就用-l,链接特定库,后面跟着的就是库的名字

数学函数

  • 三角函数与反三角函数系列
  • 幂函数与对数函数系列
  • 其他数学函数

5.标准辅助函数库

工具与辅助函数

  • 头文件:stdlib.h/cstdlib
  • 常用函数: - void exit(int status); :退出程序的执行 - void free(void * p); : C 下进行动态内存分配的函数 - void * malloc(size_t size); : C 下进行动态内存分配的函数 - int rand(); :用于生成随机数 - void srand(unsigned int seed); : 用于生成随机数

II.随机数库

库的设计就是定义好库的接口,给出库的实现,最后测试库

1.随机数的生成

编写程序,调用 rand 函数生成五个随机数

  • 第一版
#include <iostream>
#include <cstdlib>
using namespace std;

int main()
{
    int i;
    cout << "On this computer, the RAND_MAX is " << RAND_MAX << ".\n";
    cout << "Five numbers the rand function generates as follows:\n";
    for (int i = 0; i < 5; i++)
    {
        cout << rand() << ";";
        // 生成的随机数范围在 0 - RAND_MAX
    }
    cout << endl;
    return 0;
}

但有个问题,当编译完,执行两次,发现执行结果是一样的,问题在哪里呢?是因为rand()这个函数生成的随机数,是标准库给提供的一个随机数发生器,它的基本工作原理,就是使用一个特殊的值,作为随机数发生器的初始值,通过一系列的复杂的运算,生成下一个数, 然后使用下一个数,作为一个新的初始值,再生成下下一个数,它是按照这样一个方式去滚动生成下一个随机数的,因为它内部的计算做的很巧妙,看上去生成的数是完全无关的一样,但事实上,只要给一个固定的初始值,生成的随机数列肯定是一样的。但怎么处理,需要做一个额外的处理,就是调用 srand

  • 第二版
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

int main()

{
    cout << "On this computer, the RAND_MAX is " << RAND_MAX << ".\n";
    cout << "Five numbers the rand function generates as follows: \n";
    // time(0) 应该使用 <ctime> 头文件
    srand((int)time(0));
    for(int i = 0; i < 5; i++)
    {
        cout << rand() << ";";
    }
    cout << endl;
    return 0;

}
// srand() : s --> seed,即种子,srand()将会设定随机发生器的种子
// 从而决定随机数发生器的初始值是几
// 如果程序运行的时候,每次初始值 x0 都不一样
// 那么随机数发生器就会根据不同的 x0 来生成随机数序列
// 所以为了使每次运行的时候,初始值不能,所以srand的参数就设定为时间
// 在linux里,我们可以用 time()来获得时间, 返回值是一个整数
// 所以这里的参数就使用了 time(0) 当前时间作为种子
// 注意,srand在函数中,只能调用一次,必须是在生成地一个随机数之前

2.库的设计原则

接口设计原则

  • 用途一致: 接口里要提供一系列的函数,这些函数都应该属于同一类问题
  • 操作简单: 函数调用方便,要最大限度的去隐藏细节,
  • 功能充足: 要提供足够的功能
  • 性能稳定: 要经过严格测试,不存在显然的bug

3随机数库接口

随机数库应该能生成指定范围内的随机整数, 和指定范围内的随机实数(两个最基本功能),还要进行随机化,即设定随机数发生器的种子。

设计随机数接口原型

  • void Randomize();: 随机化
  • int GenerateRandomNumber(int low, int high);: 生成随机范围内整数
  • double GenerateRandomReal(double low, double high): 生成随机范围内随机数

4随机数库实现

#include <iostream>
#include <cstdlib>
#include <ctime>
#include "random.h"
using namespace std;

void Randomize()
{
    // 随机化,调用 srand, 用 time 调用当前时间, NULL在这里就是跟time(0)一样
    srand((int)time(NULL);
}

int GenerateRandomNumber(int low, int high)
{
    double _d;
    if(low > high)
    {
        cout << "GenerateRandomNumber: Make sure low <= high.\n";
        exit(1);
    }
    _d = (double)rand() / ((double)RAND_MAX + 1.0);
    return (low + (int)(_d * (high - low + 1)));
}

double GenerateRandomNumber(double low, double high)
{
    double _d;
    if(low > high)
    {
        cout << "GenerateRandomReal: Make sure low <= high.\n";
        exit(2);
    }
    _d = (double)rand() / (double)RAND_MAX;
    return (low + _d * (high - low));
}

// 这个库,在生成随机数之前,一定要调用 Randomize()进行随机化
// 即库的使用者就需要知道这一条,但是每次使用都需要调用,所以可以优化

5.随机数库测试

单独测试库的所有函数

  • 合法参数时,返回结果是否正确
  • 非法参数时,返回结果是否正确,即容错功能是否正常

联合测试

  • 多次运行程序,查看生成的数据是否随机
  • 测试整数与浮点数随机数是否均能正确工作

注意,在源代码中包含自己写的头文件时, 应该用双引号:#include ”header.h", 编译的时候,g++ -Wall test.cpp header.cpp, 不需编译.h文件

III.作用域与生存期

1.量的作用域与可见性(空间概念)

  • 作用域: 标识符的有效范围
  • 可见性: 程序中某个位置是否可以使用某个标识符
  • 标识符只在其作用域可见
  • 位于作用域内的标识符不一定可见

2.局部数据对象

  • 定义于函数或复合语句块内部的数据对象(包括变量、常量与函数形式参数等), 即{}内,所以不同的函数中可以定义同名变量
  • 局部数据对象具有块作用域,仅在它的块内有效
  • 有效性从定义处开始直到该块结束
  • 多个函数定义同名的数据对象是允许的

局部数据对象的作用域

可以用一对大括号{}来构造一个局部的复合语句块, 从而允许在块中定义数据对象,作用域仅限于本块

int func(int x, int y)
{
    int t;
    t = x + y;
    // 单独出现的花括号对用于引入嵌套块
    {
        //允许在块中定义数据对象, 作用域仅限本块
        // n在这里是一个局部变量,只在这个大括号内起效果
        int n = 2;
        cout << "n = " << n << endl;
    }

    return t;
}

3.全局数据对象

  • 定义于函数或函数或复合语句块之外的数据对象
  • 全局数据对象具有文件(全局)作用域,有效性从定义处开始直到本文件结束, 其后函数都可以直接使用
  • 若包含全局数据对象定义的文件被其他文件包含,则其作用域扩展到宿主文件中,这可能会导致问题,所以, 不要在头文件中定义全局数据对象!!!(因为工程中不允许定义同名的全局对象)

4.函数原型作用域

  • 定义在函数原型中的参数,具有函数原型作用域,其有效性仅延续到此函数原型结束
  • 函数原型中参数名称可以与函数实现中的不同, 也可以省略

5.作用域与可见性示例

int i;            // 全局变量 i 作用域开始,可见
int func(int x);
int main()

{
    int n;        // 局部变量 n 作用域开始, 可见
    i = 10;        // 全局变量 i 有效且可见
    cout << "i = " << setw(2) << i << " ; n = " << n << endl;
    n = func(i);
    cout << "i = " << setw(2) << i << "; n = " << n << endl;
}        // 局部变量 n 作用域开始,不再可见

int n;    // 全局变量 n 作用域开始,可见

int func(int x)    // 形式参数 x 作用域开始,可见
{
    i = 0;        // 全局变量 i 有效且可见
    cout << "i = " << setw(2) << i << "; n = " << n << endl;
    n = 20;        // 全局变量 n 有效且可见
    {
        int i = n + x;        //局部变量 i 有效且可见;全局变量 n 有效且可见, 全局变量 i, 有效不可见
        cout << "i = " << setw(2) << i << ";n = " << n << endl;
    }        //局部变量 i 作用域结束, 全局变量 i 有效且可见

    return ++i;        // 局部变量 x 作用域结束, 不再可见

}        // 文件结束, 全局变量 i 、 n 作用域结束

如果 全局量没有被初始化,则全部自动初始化为0;

同名局部量和全局量 i , 局部量 i 会覆盖全局量 i,但可以用 两个冒号 ::来访问全局量的 i,称之为 全局解析操作符;::i访问全局量 i, 而 i访问局部量 i;

6.量的存储类与生存期(时间概念)

生存期

  • C / C++ 使用存储类表示生存期
  • 作用域表达量的空间特性,存储类表达量的时间特性

静态(全局)生存期

  • 全局数据对象一般都具有静态(全局)生存期
  • 生死仅和程序是否执行相关, 整个程序运行期间都是有效的

自动(局部)生存期

  • 局部数据对象具有自动(局部)生存期
  • 生死仅与程序流程是否位于该块中有关,进入语句块,局部量出生, 离开语句块,局部量就死了
  • 两次进入该块时,使用的不是同一个数据对象
  • 程序每次进入该块时,就为该对象分配内存,退出该块时,释放内存

7.static 关键字

  • 修饰局部变量:静态局部变量 - 使局部变量具有静态生存期(局部变量生命被拉长) - 程序退出该块时, 局部变量仍然存在, 且下次进入该块时,使用上一次的数据值,即使用原先的量参与运算 - 静态局部变量必须进行初始化 - 不改变量的作用域, 仍然具有块作用域, 即只能在该块中访问,其他代码段不可见
  • 修饰全局变量 - 全局量生命周期不变 - 使其作用域仅限定于本文件内部,其他文件不可见

8.静态局部变量示例

#include<iostream>
using namespace std;
int func(int x);

int main()
{
    int i;
    for(i = 1; i < 4; i++)
    {
        cout << "Invoke func" << i << "time(s): Return" << func(i) << ".\n";
        return 0;
    }

    int func(int x)
    {
        static int count = 0;    //定义静态局部变量 count, 函数结束后仍然存在
        cout << "x =" << x << ".\n";
        return ++count;
    }
}

9.函数的作用域与生存期

  • 所有函数都具有文件作用域与静态生存期 - 在程序每次执行时都存在,并且可以在函数原型或者函数定义之后的任意位置调用 - 存在范围尽可能大,时间尽可能长
  • 内部函数与外部函数 - 外部函数: 可以被其他文件中的函数所调用 - 内部函数: 不可以被其他文件中的函数所调用 - 函数在默认情况下, 均为外部函数, 具有文件作用域和生存期 - 内部函数定义: 使用 static 关键字 - 内部函数示例:
    • static int Transform(int x);
    • static int Transform(int x) {...}

10.声明与定义

定义会构造新的型来,而声明没有

  • 声明不是定义 - 定义是在程序中产生一个新实体 - 声明则仅仅是在程序中引入一个实体, 而不创造它,可能是别人给我们创造的,可能是我们在另外一个文件中创造的
  • 函数的定义与声明 - 声明是给出函数原型, 定义是给出函数实现代码
  • 类型的声明与定义 - 产生新类型就是定义 - 类型定义示例: typedef enum_BOOL {False, TURE} BOOL;, enum_BOOL就是新的枚举类型,有两个可能的取值, 又用typedef 定义BOOL,等价于enum_bool - 不产生新的类型就不是定义,而仅仅是声明 - 类型声明定义: enum_BOOL, 没有给出实现,不产生新的类型,则是声明

11.全局变量的作用域扩张

  • 不能在头文件中定义全局变量! 是因为在一个工程项目中,可能很多个源文件都有#include 头文件,如果在头文件中声明全局量,则每个包含头文件的源文件都会写一个全局量的定义,就是说每个包含头文件的源文件都会定义同名的全局变量,编译器是无法通过
  • 全局变量的定义不能出现在头文件中,只有其声明才可以出现在头文件中 - 声明格式: 使用extern关键字 - 这样可以使得全局变量能够被工程项目中其他的文件所访问
//库的头文件
//此处仅引入变量 a, 其定义位于对应源文件中
extern int a;
// 变量a 可以导出,其他文件可用
// 这条语句是变量int a 的声明,而不是变量int a 定义

凡是包含了这个头文件的源文件,就能把整型变量 a 导入到其空间里去,从而可以使用在这个库的源文件中定义的整型变量 a

//库的源文件
//在源文件中定义变量 a
int a;

IV.典型软件开发流程

1.软件工程概要

  • 问题的提出
  • 需求分析: 确定软件需要解决什么问题, 明确问题的输入、输出以及其他信息,不要轻视任何问题
  • 方案设计: 设计程序框架 - 概要设计:设计总体方案,形成高层模块划分 - 详细设计: 细化模块,获得各模块的输入、输出与算法
  • 编码实现:实际编程
  • 系统测试:测试程序的正确性与稳定性
  • 经验总结
  • 上述每个过程之间都需要反馈和修改

Share this on