C语言不完整类型与封装

了解c语言不完整类型与封装的概念。

使程序设计更加高内聚、低耦合

减少对结构体成员的直接访问,防止用户随意破坏模块内部的抽象数据类型。

本文档适用于C语言开发的人员。

封装(Encapsulation)是一个在现代程序设计里无处不在的手段。对过程的封装,我们称其为函数(Function),对某个对象的属性及行为的封装我们称其为类(Class)。很多高级程序设计语言都提供了足够的特性来支持封装。

封装的目的是信息隐藏(Information Hiding),这也是操作系统及计算机体系结构领域经常提到的最小特权原则(Least Privilege Principle)的一种特殊体现,它规定了程序的目标用户只应该知道他所需要的信息,对于他不该知道的信息或没有必要知道的信息,多一点儿也不让他知道。许多面向对象的程序设计语言都提供了较好的信息隐藏的手段,例如C++,JAVA。他们通过在类(Class)中,对访问权限进行限制,以公有(Public),私有(Private)等标签规定了一套访问规则(当然,还有protected),从而实现了信息隐藏。

在实践中,我们也把上面的技术称为接口(Interface)与实现(Implementation)的分离,用户看到的(或者说,能访问的,能调用的……)是接口,而用户看不见的,在接口之下默默运行着的是实现。

利用这样一套机制,我们可以极大地丰富程序设计语言的语义,其方法就是编写许多完备的用户自定义类型,或者说抽象数据类型。而这些抽象数据类型有着这样的特点:它们的含义远比内置类型(比如int,float,char……),而在使用的时候却又应该与内置类型一样方便。它们有着自己的属性与行为,比如一个栈(Stack),可以有栈顶(Top),长度(Length)等属性,可以有压栈(Push),出栈(Pop)等操作

 

C语言将类型分为三类(C99 6.2.5):

(1)对象类型(object types):对象的大小(size)、内存布局(layout)和对齐方式(alignment requirement)都很清楚的对象。

(2)不完整类型(incomplete types):与对象类型相反,包括那些类型信息不完整的对象类型(incompletely-defined object type)以及空类型(void)。

(3)函数类型(function types):这个很好理解,描述函数的类型 -- 描述函数的返回值和参数情况。

这里我们详细了解下不完整类型。先看哪些情况下一个类型是不完整类型:

       (1)具体的成员还没定义的结构体(共用体)

       (2)没有指定维度的数组(不能是局部数组)

       (3)void类型(it is an incomplete type that cannot be completed)

 

不完整类型也就是不知道变量的所有的类型信息。

比如可以声明一个数组,但是不给出该数组的长度;声明一个指针,但是不给出该指针的类型;声明一个结构体类型,但是不给出完整的结构体定义,只说它是一个结构体。

但是最终你必须得给出完整的类型信息。要不然编译会报错的。编译器在编译某个单元时,如果遇到一个不完整类型的定义的类型或变量(假设它叫p),它会把这当作正常现象,然后继续编译该单元,如果在本单元内找不到p完整的类型信息,它就去其它编译单元找。如果把整个编译过程分为编译、链接两个过程。在编译阶段遇到不完全类型是正常的,但是在链接过程中,所有的不完整类型必须存在对应的完整类型信息,否则报错。

举个例子,下面的代码先声明了一个不完全类型的变量字符数组str,没有给出它的长度信息。然后再定义了一次str数组,这次给出的长度信息。

char str[];//不完全类型定义

char str[10];//终于遇到了str数组的完整类型信息,编译器松了一口气

注意:不完全类型定义不适合局部变量,如果把上面两行代码放在一个函数体中,会出现符号重定义错误。

再举一个结构体的例子。下面的代码先声明了一个不完全类型的结构体s。然后又定义了该结构体。

struct s;

struct s{

      int a;

      int b;

};

C语言提供的唯一封装工具就是不完整类型。

 

将具体数据实现方式隐藏起来的数据类型称为抽象数据类型(Abstract Data Type,ADT)。

客户模块可以使用该类型来声明变量,但不会知道这些变量的具体数据结构。如果客户模块需要对这种变量进行操作,则必须调用抽象数据类型模块所提供的函数。C语言中的抽象数据类型可以简单的理解为C++、JAVA中的类(Class)。

比如有一个Stack.h如下:

#define STACK_SIZE 100

typedef struct {

int contents[STACK_SIZE];

int top;

} Stack;

void make_empty(Stack *s);

bool is_empty(const Stack *s);

bool is_full(const Stack *s);

void push(Stack *s, int i);

int pop(Stack *s);

 

在客户模块中就可以使用这个Stack类型了。

Stack s1, s2;

make_empty(&s1);

make_empty(&s2);

push(&s1, 1);

push(&s2, 2);

if (!is_empty(&s1)) {

       printf("%d\n", pop(&s1));

}

遗憾的是,上面的Stack不是抽象数据类型,因为Stack.h暴露了Stack类型的具体实现方式,因此无法阻止客户讲Stack变量作为结构直接使用。

Stack s1;

s1.top = 0;

s1.contents[top++] = 1;

因此抽象数据类型需要利用不完整数据类型进行封装。

 

 

继续用Stack类型来举例。

首先我们需要一个定义抽象数据类型的头文件

//****stackADT.h****//

#ifndef STACKADT_H

#define STACKADT_H

#include <stdbool.h>

typedef struct stack_type *Stack;

Stack create(void);

void destroy(Stack s);

void make_empty(Stack s);

bool is_empty(Stack s);

bool is_full(Stack s);

void push(Stack s, int i);

int pop(Stack s);



#endif

 

这样包含头文件stackADT.h的客户可以声明Stack类型的变量,这些变量都可以指向stack_type结构,就可以调用在stackADT.h中声明的函数来对栈变量进行操作。但是客户不能访问stack_type结构的成员,因为该结构的定义在另一个文件中。

//****stackclient.c****//

#include <stdio.h>

#include "stackADT.h"



int main(void)

{

       Stack s1, s2;

       int n;

       s1 = create();

       s2 = create();

       push(s1, 1);

       push(s1, 2);

       n = pop(s1);

       printf("pop %d from s1\n",  n);

       push(s2, n);

       n = pop(s1);

       printf("pop %d from s1\n",  n);

       push(s2, n);

       destroy(s1);

       destroy(s2);

}

//下面就实现抽象数据类型

//****stackADT.c****//

#include <stdio.h>

#include <stdlib.h>

#include "stackADT.h"



#define STACK_SIZE 100

struct stack_type {

       int contents[STACK_SIZE];

       int top;

};
static void terminate (const char *message) {

       printf("%s\n", message);

       exit(EXIT_FAILURE);

}

Stack create (void) {

       Stack s = malloc(sizeof(struct stack_type));

       if (s == NULL) {

              terminate ("error");

       }

       s->top = 0;

       retrun s;

}



void destroy (Stack s) {

       free(s);

}



void make_empty (Stack s) {

       s->top = 0;

}



bool is_empty (Stack s) {

       return s->top == 0;

}



bool is_full (Stack s) {

       return s->top == STACK_SIZE;

}



void push (Stack s, int i) {

       if (is_full(s)) {

              terminate ("error");

       }

}



int pop(Stack s) {

       if (is_empty(s))

       {

              terminate ("error");

       }

       return s->contents[--s->top];

}

 

现在已经有抽象数据类型的一个版本了,当之后需要对Stack类型进行改进时,不需要对stackADT.h和stackclient.c进行修改,仅仅需要修改stackADT.c就可以了,比如将Stack类型中使用的数组改成链表。

//****stackADT.c****//

#include <stdio.h>

#include <stdlib.h>

#include "stackADT.h"

struct node {

         int data;

         struct node *next;

};

struct stack_type {

         struct node *top;

};

static void terminate (const char *message) {

         printf("%s\n", message);

         exit(EXIT_FAILURE);

}

Stack create (void) {

         Stack s = malloc(sizeof(struct stack_type));

         if (s == NULL) {

                   terminate ("error");

         }

         s->top = 0;

         retrun s;

}



void destroy (Stack s) {

         make_empty(s);    //释放链表中结点所占的内存

         free(s);           //释放stack_type结构所占的内存

}



void make_empty (Stack s) {

         while (!is_empty(s))

                   pop(s);

}



bool is_empty (Stack s) {

         return s->top == NULL;

}



bool is_full (Stack s) {

         return false;

}



void push (Stack s, int i) {

         struct node *new_node = malloc(sizeof(struce node));

         if(new_node == NULL)

                   terminate("error");



         new_node->data = i;

         new_node->next = s->top;

         s->top = new_node;

}



int pop(Stack s) {

         struct node *old_top;

         int i;

         if (is_empty(s))

                   terminate("error");



         old_top = s->top;

         i = old_top->data;

         s->top = old_top->next;

         free(old_top);

         return i;

}
  • 总结

最后总结一下封装的优点:

1.       隐藏了内部实现细节,强制用户按接口规则访问。减少沟通成本。

2.       便于修改。

上面两点都是实现模块化编程所必须的。而且个人认为,站在客户的角度,知道的细节越少越好,知道的越多,要记忆和思考的东西也越多。就像电视剧中的经典台词:“有时候知道的太多并不是好事”。角色被人干掉的理由也是“你知道的太多了”,或者“你知道了你不该知道的事情”。我只按照既定的规则来访问别人提供的代码,有什么问题直接问别人,而不是看实现代码。按照既定规则来访问也便于职责划分。如果非要总结延迟定义带来的第3个好处,个人认为是:便于职责划分,清晰职责边界。

猜你喜欢

转载自blog.csdn.net/zhangyufeikk/article/details/92805956