19 程序设计

19.1 模块

说明:模块是一组功能(服务)的集合,其中一些功能可以被程序的其它部分(客户)使用。每个模块都有一个接口来描述所提供的功能。模块的细节,包括这些功能自身的源代码,都包含在模块的实现中。

文件角色 说明
接口 模块名.h(头文件)
实现 对应头文件模块名.c文件
客户 通过#include引入模块的文件

优点:将程序分割成模块有一系列好处

  • 抽象:不必了解功能的实现细节,只需对模块的接口达成一致
  • 可复用:每个模块都可以在另一个程序中复用
  • 可维护性(最重要):当程序出现错误或需要升级时,通常只会影响一个模块

扩展:Fundamental of SoftWare Engineer

19.1.1 内聚性与耦合性

说明:一个好的模块接口并不是随意的一组声明,应具有下面两个性质

  • 高内聚:模块中的元素应该紧密相关
  • 低耦合:模块之间应该尽可能相互独立

19.1.2 模块的类型

类型 说明 举例 备注
数据池 一些相关的变量或常量的集合 <float.h>(23.1)、 <limits.h>(23.2) 通常不建议将变量放在头文件
一组相关函数的集合 <string.h>
抽象对象 对隐藏的数据结构进行操作的一组函数的集合 如果数据是隐藏起来的,那么这个对象那个就是“抽象的”
抽象数据类型 将具体数据实现方式隐藏起来的数据类型称为抽象数据类型 抽象数据类型在当今的程序设计中起着非常重要的作用

19.2 信息隐藏

说明:一个设计良好的模块经常会对它的客户隐藏一些信息。谨慎的对客户隐藏信息的方法称为信息隐藏。
优点:

  • 安全性:客户只能通过模块自身的函数进行参数
  • 灵活性:修改模块通常不必修改接口,对客户没有影响

原理:使用static

修饰 链接情况 说明
函数 内部链接 只能在只能被同一文件中被调用
变量(文件作用域) 内部链接 只能被同一文件中的其它函数访问

技巧:使用宏定义“公有”和“私有”可以使程序含义更加清晰(尤其是static,因为它在c语言中有许多用法)

栈模块(实现部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define PUBLIC  // 定义为空
#define PRIVATE static

PRIVATE int contents[STACK_SIZE] { ... }

PRIVATE int top = 0;

PUBLIC int is_empty (void) { ... }

PRIVATE int is_full () { ... }

PUBLIC void push (void) { ... }

PUBLIC int pop (void) { ... }

19.3 抽象数据类型

说明:c语言没有设计专门用于封装类型的特性(class),即无法定义真正的抽象数据类型。
案例缺陷:之前定义的栈模块提供的栈不基于一种抽象数据类型,而是仅仅提供了一个相当于“栈的实例”的数据结构。当需要多个栈实例时就无能为力了。

stack.h

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 下面定义的Stack不是抽象数据类型,因为stack.h暴露了Stack的具体实现方式(结构体实例的成员是暴露的)
*/

#define STACK_SIZE 100
typedef struct {
int contents[STACK_SIZE];
int top;
} Stack;
void make_empyt (Stack *s);
int is_empty (const Stack *s);
void push (Stack *s, int i);
int pop (Stack *s);

客户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include "stack.h"

int main () {
// 理想的使用方式
Stack s1, s2;
make_empty(&s1);
make_empty(&s2);
push(&s1, 1);
push(&s2, 2);
if (!is_empty(&s1)) {
printf("%d\n", pop(&s1)); // 1
}

// 不理想的方式
Stack s3;
s3.top = 0; //直接访问了暴露出来的成员
s3.contents[top++] = 1;
}

19.4 C++语言

说明:C++语言是由AT&T贝尔实验室的Bjqrne Stroustrup在20世纪80年代开发出来的C语言的扩展版。
新特性(相对C):

  • 面向对象:允许从已经存在的类“派生”(继承)出新的类
  • 运算符重载:为传统的C语言的运算符赋予新的含义
  • 模版:可以使我们写出通用的、高度可复用的类和函数
  • 异常处理:一种同一的方式用来检测并响应错误

兼容C:C++语言包含了标准C的全部特性,然而不是所有C语言都可以在C++的环境下编译,因为C++语言增加了更多强制限制,比C语言更加安全。

19.4.1 C语言与C++语言之间的差异

19.4.1.1 注释

新特性:C++语言支持单行注释

1
// This is a Comment

19.4.1.2 标记与类型名

新特性:标记(结构、联合或枚举的名字)会自动被认为是类型名

1
2
3
4
5
6
7
8
9
struct Complex {
double re, im;
};
// 相当于下列形式的c语言
/*
typedef struct Complex {
double re, im;
} Complex;
*/

19.4.1.3 不带参数的函数

新特性:在声明或定义一个不带参数的C++函数时,可以不使用void

1
2
void draw (void); // 不带参数
void draw (); // 和void相同

19.4.1.4 默认实际参数

新特性:C++语言允许函数的实际参数有默认值

1
2
3
4
5
6
7
8
9
// 显示任意数量的换行符,如果没有提供参数,默认为1
void new_line (int n = 1) {
while (n-- > 0) {
putchar('\n');
}
}

new_line(3); // 三个换行
new_line(); //一个换行

19.4.1.5 引用参数

新特性:允许实际参数被声明为引用,而不是指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// c语言的方式
/*
void swap (int *a, int *b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
swap(&i, &j);
*/


// c++的方式
void swap (int& a, int& b) {
int temp;
temp = a;
a = b;
b = temp;
}
swap(i, j); // 不需要再参数前加&运算符

19.4.1.6 动态存储分配

新特性:使用运算符new(分配空间)、 delete(释放空间)
语法:

  • 分配内存:new 类型说明符
  • 释放内存: delete 指针
1
2
3
4
5
6
7
8
9
10
// 声明变量
int *int_ptr, *int_array;

// 分配空间
int_ptr = new int; // 为整数分配存储空间
int_array = new int[10]; // 为数组分配内存

// 释放空间
delete int_ptr; // 为整数空间释放内存
delete [] int_array; // 为数组释放空间

19.4.2 类

类:一个类从根本上说就是一个抽象数据类型:一组数据以及操作这些数据的函数
说明:这个新数据类型的功能可以同基本数据类型同样强大。
类的不足:类的设计和实现比较复杂,这是易用性必须付出的代价,而这也是计算机领域近几年内的妥协。

19.4.3 类定义

类标记(class tag):可以直接作为类型名使用,不要求类名以大写开始,但许多C++程序员尊循首字母大写的规范。
数据成员(data memeber):类似结构的成员(但是默认是隐藏的,即“私有的”)
类的实例(instance):任何类的实例就是对象(object)
访问成员:使用运算符.->来访问公有的成员

1
2
3
4
5
6
7
8
9
10
11
class Fraction {
int id; // 默认是私有的

// 公有成员
public:
int numeratitor;

// 私有成员
private:
int denominator;
}

19.4.4 成员函数

成员函数(member function):属于类的函数称为成员函数,特别的,那些需要访问类的私有数据成员的函数必须声明在类里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Fraction {
public:
// 将函数的定义放在类的定义之中
void create(int num, int denom) {
numertor = num;
denominator = denom;
};
void print();
// 只声明不定义(定义部分放在类外部)
/**
* 乘法函数
* @param {Fraction f}
*/

Fraction mul(Fraction f);
};

/**
* 乘法函数的定义
* Fraction::前缀是必需的,否则C++编译器会将mul作为一个普通函数
* @param {Fraction f} 分数
*/

Fraction Fraction::mul(Fraction f) {
Fraction result;
result.numerator = numerator * f.numerator;
result.denominator = denominator * f.denominator;
result.reduce();
return result;
}
1
2
3
Fraction f1;
f1.create(1, 2);
Fraction f2 = f1.mul(f1);

19.4.5 构造函数

构造函数(controctor):

  • 通常时自动调用的(编译器安排在合适的时机自动调用)
  • 定义在public成员部分,不需要指定返回值类型
  • 命名和类名相同
1
2
3
4
5
6
7
8
9
10
11
class Fraction {
public:
Fraction(int num = 0, int denom = 1) {
numerator = num;
denominator = denom;
};
};

Fraction f1(3, 4); // 通过构造函数声明并初始化实例
Fraction f2(3); // 等同于Fraction f2(3, 1);
Fraction f3; // 等同于 f3(0, 1);

19.4.6 构造函数和动态存储分配

说明:构造函数和析构函数为那些内存分配和回收函数提供了比较合适的时机。
举例:创建自己的String类型

比较 C++的String(自定义) C语言实现方式(char数组)
字符串长度 任意长度 受限于数组的长度
获取字符串长度 O(1) O(n)
扩展性 需要时可给String类添加操作 无法修改(就算引入,但无法扩展)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class String{
public:
// 声明构造函数
String(const char *s);
private:
char *text;
int len;
};

// 定义构造函数
String::String(const char *s) {
// 计算s所指向的字符串的长度
len = strlen(s);
// 分配足够大的空间
text = new Char[len + 1];
// 将字符串复制到刚刚分配的内存中
strcpy(text, s);
}

19.4.7 析构函数

析构函数(destructor):

  • 名字:~类名
  • 返回值:没有返回值
  • 参数:没有

用途:自动存储期限的类的实例,当其生存期结束后,普通成员的内存会被释放,但在构造函数中分配内存的成员指向的内存不会被释放(内存泄漏)。所以需要析构函数在对象释放时自动调用,清理构造函数动态分配的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class String{
public:
// 声明并定义构造函数
String(const char *s) {
// 计算s所指向的字符串的长度
len = strlen(s);
// 分配足够大的空间
text = new Char[len + 1];
// 将字符串复制到刚刚分配的内存中
strcpy(text, s);
}
// 声明并定义析构函数
~String() {
delete [] text;
}
private:
char *text;
int len;
};

19.4.8 重载

函数重载

说明:在同一作用域下存在两个或以上同名但参数不同的函数(包括构造函数)叫做函数的重载。
用途:需要记住更少的函数名,编译器会根据实际参数的情况自动判断调用哪一个函数。
默认构造函数:不带时机参数的构造函数,会在声明对象而没有制定初始值时被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Sring {
public:
// 构造函数
String(const char *s);

/*
* 重载构造函数(默认)
* @overload
*/

String() {
text = 0;
len = 0;
};
// 析构函数
~String() {
delete [] text;
};
...
private:
char *text;
int len;
};
1
String s; // 不带实际参数,会调用默认构造函数

运算符重载

说明:重载运算符后,根据操作数类型的不同,同样的运算符号可以代表不同的操作。
用途:更易读,更自然(不需要定义一些难以记住的函数名)

1
2
3
4
5
6
7
8
9
10
11
12
13
class Fraction {
public:
...
// 声明重载操作符:*
Fraction operator*(Fraction f);
private:
...
};

// 定义重载操作符:*
Fraction Fraction::operation*(Fraction f) {
//代码
};
1
2
...
f3 = f1 * f2; // 定义的*是一个二元运算符(相当于f3 = f1.operator*(f2);)

C++语言的输入/输出(