C 语言程序设计:信息隐藏

本文内容

目录汇总:C 语言零基础入门教程

设计良好的模块经常会对它的客户隐藏一些信息。例如,我们的栈模块的客户就不需要知道栈是用数组、链表还是其他形式存储的。这种故意对客户隐藏信息的方法叫作信息隐藏。信息隐藏有以下两大优点。

  • 安全性。如果客户不知道栈是如何存储的,就不可能通过栈的内部机制擅自修改栈的数据。它们必须通过模块自身提供的函数来操作栈,而这些函数都是我们编写并测试过的。
  • 灵活性。无论对模块的内部机制进行多大的改动,都不会很复杂。例如,我们可以首先将栈用数组实现,以后再改用链表或其他方式实现。我们当然需要重写这个模块的实现,但只要模块是按正确的方式设计的,就不需要改变模块的接口。

在 C 语言中,强制信息隐藏的主要工具是 static 存储类型(C 语言存储类型简介)。将具有文件作用域的变量声明为 static 可以使其具有内部链接,从而避免它被其他文件(包括模块的客户)访问。(将函数声明为 static 也是有用的——函数只能被同一文件中的其他函数直接调用。)

栈模块

为了清楚地看到信息隐藏所带来的好处,下面来看看栈模块的两种实现。一种使用数组,另一种使用链表。假设模块的头文件如下所示:

stack.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#ifndef STACK_H
#define STACK_H

#include <stdbool.h>        /* C99 only */

void make_empty(void);
bool is_empty(void);
bool is_full(void);
void push(int i);
int pop(void);

#endif

这里包含了从 C99 开始才有的 <stdbool.h>,从而使得 is_emptyis_full 函数可以返回 bool 结果而非 int 值。

首先,用数组实现这个栈:

stack1.c

 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
#include "stack.h"

#define STACK_SIZE 100

static int contents[STACK_SIZE];
static int top = 0;

static void terminate(const char *message)
{
  printf("%s\n", message);
  exit(EXIT_FAILURE);
}

void make_empty(void)
{
  top = 0;
}

bool is_empty(void)
{
  return top == 0;
}

bool is_full(void)
{
  return top == STACK_SIZE;
}

void push(int i)
{
  if (is_full())
    terminate("Error in push: stack is full.");
  contents[top++] = i;
}

int pop(void)
{
  if (is_empty())
    terminate("Error in pop: stack is empty.");
  return contents[--top];
}

组成栈的变量(contentstop)都被声明为 static 了,因为没有理由让程序的其他部分直接访问它们。terminate 函数也声明为 static。这个函数不属于模块的接口;相反,它只能在模块的实现内使用。

出于风格的考虑,一些程序员使用宏来指明哪些函数和变量是“公有”的(可以在程序的任何地方访问),哪些是“私有”的(只能在一个文件内访问):

1
2
#define PUBLIC  /* empty */
#define PRIVATE static

static 写成 PRIVATE 是因为 static 在 C 语言中有很多用法,使用 PRIVATE 可以更清晰地指明这里它是被用来强制信息隐藏的。下面是使用 PUBLICPRIVATE 后栈实现的样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
PRIVATE int contents[STACK_SIZE];
PRIVATE int top = 0;

PRIVATE void terminate(const char *message)  { ... }

PUBLIC void make_empty(void)  { ... }

PUBLIC bool is_empty(void)  { ... }

PUBLIC bool is_full(void)  { ... }

PUBLIC void push(int i)  { ... }

PUBLIC int pop(void)  { ... }

现在换成使用链表实现:

stack2.c

 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
#include <stdlib.h>
#include "stack.h"

struct node {
  int data;
  struct node *next;
};

static struct node *top = NULL;

static void terminate(const char *message)
{
  printf("%s\n", message);
  exit(EXIT_FAILURE);
}

void make_empty(void)
{
  while (!is_empty())
    pop();
}

bool is _empty(void)
{
  return top == NULL;
}

bool is_full(void)
{
  return false;
}

void push(int i)
{
  struct node *new_node = malloc(sizeof(struct node));
  if (new_node == NULL)
    terminate("Error in push: stack is full.");

  new_node->data = i;
  new_node->next = top;
  top = new_node;
}

int pop(void)
{
  struct node *old_top;
  int i;

  if (is_empty())
    terminate("Error in pop: stack is empty.");

  old_top = top;
  i = top->data;
  top = top->next;
  free(old_top);
  return i;
}

注意,is_full 函数每次被调用时都返回 false。链表对大小没有限制,所以栈永远不会满。程序运行时仍然可能(不过可能性不大)出现内存不够的问题,从而导致 push 函数失败,但事先很难测试这种情况。

我们的栈示例清晰地展示了信息隐藏带来的好处:使用 stack1.c 还是使用 stack2.c 来实现栈模块无关紧要。这两个版本都能匹配模块的接口定义,因此相互替换时不需要修改程序的其他部分。

请参阅

(完)

comments powered by Disqus

本文内容