C语言创建和使用不透明指针

不透明指针用来在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结构体的类型定义,而看不到结构体的实现细节。

我们不允许用户看到链表内部结构以及使用链表内部结构,并且会对用户隐藏结构体的任何变化。

只有四个支持函数的签名对用户是可见的,否则,用户就无法利用或修改实现细节。我们封装了链表结构及其支持函数,从而减轻了用户的负担。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程