当前位置: 移动技术网 > IT编程>开发语言>C/C++ > C语言开发函数库时利用不透明指针对外隐藏结构体细节

C语言开发函数库时利用不透明指针对外隐藏结构体细节

2018年10月19日  | 移动技术网IT编程  | 我要评论

兽人之华音,搜搜更懂你,冯长革简介

1 模块化设计要求库接口隐藏实现细节

 

作为一个函数库来说,尽力减少和其调用方的耦合,是最基本的设计标准。c语言,作为经典“程序=数据结构+算法”的践行者,在实现函数库的时候,必然存在大量的结构体定义,接口函数需要对这些结构体进行操作。同时,程序设计的模块化要求库接口尽量少的暴露其实现细节,接口参数尽量使用基本数据类型,尽量避免在形参中暴露库内结构体的定义。

 

2 隐藏结构体的两种方法

 

以笔者粗浅的认识,有两种最常用的方法,可以实现库内结构体定义的隐藏:接口函数形参使用结构体指针,接口函数形参使用句柄。

 

2.1 通过结构体指针引用结构体

 

为了说明方便,先给出使用vc++写的一段例子代码。

 

库接口头文件 mysdk.h

#pragma once

#ifdef mysdk_export
#define mysdk_api __declspec(dllexport)
#else
#define mysdk_api __declspec(dllimport)
#endif

typedef struct _window window; /*预先声明*/

#ifdef __cplusplus
extern "c" {
#endif

    mysdk_api window* createwindow();
    mysdk_api void showwindow(window* pwin);

#ifdef __cplusplus
}
#endif

库实现文件mysdk.c

#define mysdk_export

#include "mysdk.h"
#include <stdlib.h>

struct _window
{
    int width;
    int height;
    int x;
    int y;
    unsigned char color[3];
    int isshow;
};

mysdk_api window* createwindow()
{
    window* p = malloc(sizeof(window));
    if (p) {
        p->width = 400;
        p->height = 300;
        p->x = 0;
        p->y = 0;
        p->color[0] = 255;
        p->color[1] = 255;
        p->color[2] = 255;
        p->isshow = 0;
    }
    return p;
}
mysdk_api void showwindow(window* pwin)
{
    pwin->isshow = 1;
}

库使用者代码

#include <stdio.h>
#include "../mydll/mysdk.h"
#pragma comment(lib, "../debug/mydll.lib")

int main(int argc, char** argv)
{
    window* pwin = createwindow();
    showwindow(pwin);

    return 0;
}

其中mysdk.h和mysdk.c是库的实现; main.cpp是调用方程序实现。双方使用了相同的接口头文件mysdk.h。

 

但是从使用者角度,main.cpp里面只知道库中有名为window的一种结构体类型,但是却不能知道此机构体的实现细节(定义)。由于c/c++编译器是延迟依赖型编译器,只要源代码中没有涉及到window结构体内存布局的代码,编译时不需要知道window的完整定义,但是仍然能够检查类型名称的正确性,比如如果客户端代码如下则会被编译器检查出问题:

    int* p = 0;
    showwindow(p);

编译器虽然不知道showwindow(pwin)中pwin指向的结构体的实现细节,但是仍然能够确保实参类型为window*,这也方便了调用方检查错误。

 

2.2 通过“句柄”(handle)来引用结构体

 

最先接触句柄的概念,是在win32api中。可以断定windows的内部定义了大量的结构体,如线程对象、进程对象、窗口对象、….。但是接口win32api中却很少提供这些结构体的定义,调用者通过一个称为“句柄”的值来间接引用要使用的结构体对象。

 

win32api 中的句柄 

例如,如下win32api

   hwnd hwnd = createwindoww(szwindowclass, sztitle, ws_overlappedwindow,
      cw_usedefault, 0, cw_usedefault, 0, nullptr, nullptr, hinstance, nullptr);

  showwindow(hwnd, ncmdshow);
  updatewindow(hwnd);

窗口类型在windows中一定是一个非常复杂的结构体,为了隐藏其实现细节,微软采取了窗口句柄的概念来间接引用窗口结构体对象。为了实现这种对应关系,库内部必须维护句柄和结构体对象的对应关系。

 

linux api中的句柄 

句柄的概念也广泛的应用在linux平台api中。如

 int open(const char *pathname, int flags);
 ssize_t read(int fd, void *buf, size_t count);

在linux内部,文件一定是通过一个复杂的结构体来表示,但是在api中使用了一个简单整数对其进行引用,避免了向调用者暴露文件结构体的细节。

 

opengl api中的句柄 

句柄同样应用到了opengl库中。如

void winapi glgentextures(
   glsizei n,
   gluint  *textures
);
void winapi glbindtexture(
   glenum target,
   gluint texture
);

纹理在opengl库内部也是一个复杂的结构体,同样使用句柄的概念对外隐藏了实现细节。

 

3 句柄和指针的比较

 

3.1 句柄的优势与不足

 

句柄看起来真的不错,那么局部到底是如何映射到对应的结构体的呢?一个最容易想到的答案就是:直接把结构体对象的内存地址作为句柄。然而实际上,大多数的库实现都不是这么做的。之所以不直接把内存地址作为句柄的值,我个人认为有如下几个原因:

 

从保护角度,内存地址更容易被hack。知道了结构体的内存地址,就能够读取这块内存的内容,从而为猜测结构体细节提供了方便。

 

从程序稳定性角度,对于库内部维护的对象,调用者只应该通过接口函数来访问,如果调用者得到了对象的内存地址,那么就有可能有意或无意的进行直接修改,从而影响库的稳定运行。

 

从可移植性角度,指针类型在32位和64位系统中具有不同的长度,这样就需要为定义两个名称重复的接口函数,造成各种不便。而例如opengl,使用int型作为句柄类型,则可以一个接口函数跨越多个平台。

 

从简化接口头文件角度,使用指针至少需要事先声明结构体类型,如 struct window; 而使用基本数据类型作为句柄,无需这样做。

 

句柄存在的不足有:

 

编译器无法识别具体的结构体类型 

由于句柄的数据类型实际上是基本数据类型,所以编译器只能进行常规的检查,不能识别具体的结构体类型。如

   security_attributes sa;
   handle h = createmutex(&sa, true, l"mutex");
   readfile(h, null, 0, 0, 0);

上述代码编译器并不会报错,因为互斥体对象和文件对象都是使用相同的句柄类型。

 

效率可能稍差 

毕竟存在一个 根据句柄值-查找内存指针的过程,可能会稍稍影响运行效率。

3.2 指针的优势与不足

 

其实指针和句柄是相对的,句柄的不足就是指针的优势,句柄的优势也是指针的不足。

 

4 如何选择

 

对于大型跨平台库的设计,采用句柄;对于专用小型库,采用指针。

 

就我目前的项目而言,是一个小型的c库工程,库的目标群体也相对单一,所以本着简单够用的原则,我选择了使用指针的方式对外隐藏库内结构体的实现细节。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网