不透明指针用来在C中实现数据封装。一种方法是在头文件中声明不包含任何实现细节的结构体,然后在实现文件中定义与数据结构的特定实现配合使用的函数。数据结构的用户可以看到声明和函数原型,但是实现会被隐藏(在.c/.obj文件中)。
只有使用数据结构所需的信息会对用户可见,如果太多的内部信息可见,用户可能会使用这些信息,从而产生依赖。一旦内部结构发生变化,用户的代码可能就会失效。
我们会开发一个链表来说明不透明指针的用法。用户会用一个函数来获取链表指针,然后用这个指针来向链表添加信息以及从链表删除信息。链表的内部结构细节和支持函数对用户不可见。这个结构的唯一可见部分通过头文件提供,如下所示:
//link.h
typedef void *Data;
typedef struct _linkedList LinkedList;
LinkedList* getLinkedListInstance();
void removeLinkedListInstance(LinkedList* list);
void addNode(LinkedList*, Data);
Data removeNode(LinkedList*);
Data
声明为void
指针,这样允许实现处理任何类型的数据。LinkedList
的类型定义用了名为_linkedList
的结构体,这个结构体的定义在实现文件中,对用户隐藏。
我们提供了四种方法来使用链表。用户一开始用getLinkedListInstance
函数来获取一个LinkedList
实例,一旦不再需要链表就应该调用removeLinkedListInstance
函数。通过传递链表指针可以让函数处理一个或多个链表。
要将数据添加到链表,需要用addNode
函数,我们给它传递链表和要添加到链表的数据指针。removeNode
方法会返回链表头部的数据。
链表的实现在名为link.c的独立文件中。实现的第一部分,如下所示,声明持有用户数据和下一个链表节点的结构体,接着是_linkedList
结构体的定义。在这个简单的链表中,我们只用到了一个头指针:
// link.c
#include <stdlib.h>
#include "link.h"
typedef struct _node {
Data* data;
struct _node* next;
} Node;
struct _linkedList {
Node* head;
};
实现文件的第二部分包含链表的四个支持函数的实现,第一个函数返回一个链表实例:
LinkedList* getLinkedListInstance() {
LinkedList* list = (LinkedList*)malloc(sizeof(LinkedList));
list->head = NULL;
return list;
}
接着是removeLinkedListInstance
函数的实现,如果有节点的话,它会释放链表中的所有节点,然后释放链表本身。如果节点引用的数据包含指针,这个实现可能会产生内存泄漏。一种解决方案是传递一个释放数据成员的函数:
void removeLinkedListInstance(LinkedList* list) {
Node *tmp = list->head;
while(tmp != NULL) {
free(tmp->data); // 潜在的内存泄漏!
Node *current = tmp;
tmp = tmp->next;
free(current);
}
free(list);
}
addNode
函数把第二个参数传入的数据添加到第一个参数指定的链表中。系统会为每个节点分配内存,然后将其和用户的数据关联起来。在这个实现中,总是将链表的节点添加到头部:
void addNode(LinkedList* list, Data data) {
Node *node = (Node*)malloc(sizeof(Node));
node->data = data;
if(list->head == NULL) {
list->head = node;
node->next = NULL;
} else {
node->next = list->head;
list->head = node;
}
}
removeNode
函数返回跟链表中第一个节点关联的数据。我们会调整头指针,让其指向链表中下个节点。接着返回数据,释放旧节点,将数据返回堆中。
注意 用户使用这种方法无需记住释放链表节点,从而避免了内存泄漏。这是隐藏实现细节的巨大优势:
Data removeNode(LinkedList* list) { if(list->head == NULL) { return NULL; } else { Node* tmp = list->head; Data* data; list->head = list->head->next; data = tmp->data; free(tmp); return data; } }
为了说明这个数据结构的使用方法,我们会重用6.1节中开发的Person
结构体及其函数。下面的代码会把两个人添加到链表中,然后删除。首先调用getLinkedListInstance
函数来获取链表。接着,用initializePerson
函数创建一个Person
实例并用addNode
函数将其添加到链表中。displayPerson
函数会打印由removeNode
函数返回的人。最后释放链表:
#include "link.h";
...
LinkedList* list = getLinkedListInstance();
Person *person = (Person*) malloc(sizeof(Person));
initializePerson(person, "Peter", "Underwood", "Manager", 36);
addNode(list, person);
person = (Person*) malloc(sizeof(Person));
initializePerson(person, "Sue", "Stevenson", "Developer", 28);
addNode(list, person);
person = removeNode(list);
displayPerson(*person);
person = removeNode(list);
displayPerson(*person);
removeLinkedListInstance(list);
这种方法有几个有趣的地方。我们只能在link.c文件中创建_linkedList
结构体的实例,这是因为如果没有完整的结构体声明就无法使用sizeof
操作符。比如说,如果你试图在main
函数中为这个结构体分配内存,如下所示,会得到一个语法错误:
LinkedList* list = (LinkedList*)malloc(sizeof(LinkedList));
产生的语法错误类似下面这样:
error: invalid application of ‘sizeof’ to incomplete type ‘LinkedList’
类型不完整是因为编译器看不到link.c文件中的实际定义。它只能看到_linkedList
结构体的类型定义,而看不到结构体的实现细节。
我们不允许用户看到链表内部结构以及使用链表内部结构,并且会对用户隐藏结构体的任何变化。
只有四个支持函数的签名对用户是可见的,否则,用户就无法利用或修改实现细节。我们封装了链表结构及其支持函数,从而减轻了用户的负担。