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

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库工程,库的目标群体也相对单一,所以本着简单够用的原则,我选择了使用指针的方式对外隐藏库内结构体的实现细节。

时间: 2024-10-24 07:20:20

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

程序猿之---C语言细节21(#define和typedef区别、结构体细节)

主要内容:#define和typedef区别.结构体细节 #include <stdio.h> #define INT_D int* #define CHAR_D char int main() { /*#define 与 typedef区别*/ typedef int* INT_T; typedef char CHAR_T; INT_T a,b; // a.b都为int型指针 INT_D c,d; // c为int型指针.d为int整型 unsigned CHAR_D e; // #defi

c语言指针数组和结构体的指针

指向数组的指针,先初始化一个数组,使用传统方式遍历 1 void main() 2 { 3 int a[5] = { 1,2,3,4,5 }; 4 for (int i = 0; i < 5; i++) 5 { 6 printf("%d,%x\n", a[i], &a[i]); 7 printf("%d,%x\n",*(a+i),a+i); //等价 a[i] , &a[i] 8 } 9 } a就是数组a的首地址,即元素1的地址, a + 1

黑马程序员---C基础12【结构体数组】【结构体指针】【结构体嵌套】【作为函数参数】【枚举类型】

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- [结构体数组] 1.结构体数组: 结构体数组的每一个元素都是具有相同结构类型的下标结构变量:(可以表示一个群体,一个班学生档案) 2.结构数组定义: 定义格式: struct    结构名{ 成员列表: }数组名[数组长度]: 1 1)定义结构体的同时定义数组: 2 3 struct stu{ 4 5 int num; 6 7 char name[10]; 8 9 int age; 10 11

利用动态排序,对学生结构体的各类信息进行排序

#import <Foundation/Foundation.h> //创建一个学生变量 typedef struct student{ char name[20];  //姓名 int  age;       //年龄 float  weight;  //体重 float  height;  //身高 float  score;   //分数 }Stu; //为什么使用动态排序,动态排序的好处:所有的排序函数if语句之后的比较条件不一样,其余的所有代码都是相同的,把相同的内容放在一个函数里,

c语言:通过指向结构体变量的指针变量输出结构体变量中成员的信息

通过指向结构体变量的指针变量输出结构体变量中成员的信息. 解:程序: #include<stdio.h> #include<string.h> int main() { struct Student { long int num; char name[20]; char sex[10]; float score; }; struct Student stu_1;//定义struct Student类型的变量stu_1 struct Student *p; p = &stu_

【C语言】使用结构体和malloc函数时的一些错误。

使用结构体错误: #include <stdio.h> struct STU { char *name; int score; }stu,*pstu; int main () { strcpy(stu.name,"bit-tech"); strcpy(pstu->name,"bit-tech"); return 0; } 错误一:strcpy(stu.name,"bit-tech"); 结构体中的成员name是一个指针,声明结构

Go语言开发(四)、Go语言面向对象

Go语言开发(四).Go语言面向对象 一.结构体和方法 1.结构体的定义 在结构体中可以为不同项定义不同的数据类型.结构体是由一系列具有相同类型或不同类型的数据构成的数据集合.结构体定义需要使用type和struct语句.struct语句定义一个新的数据类型,结构体有中有一个或多个成员.type语句设定了结构体的名称.结构体的格式如下: type struct_variable_type struct { member definition; member definition; ... mem

开发函数计算的正确姿势 —— 爬虫

在 <函数计算本地运行与调试 - Fun Local 基本用法> 中,我们介绍了利用 Fun Local 本地运行.调试函数的方法.但如果仅仅这样简单的介绍,并不能展现 Fun Local 对函数计算开发的巨大效率的提升. 这一次,我们拿一个简单的场景来举例子--开发一个简单的爬虫函数(代码参考函数计算控制台模板),介绍如何以正确姿势,从零开始,开发一个自动伸缩.按调用次数收费的 serverless 爬虫应用. 开发步骤我们将这个完整的应用拆分成多步,并且在每一步完成后,我们都会进行相应的运

php中mysqli函数库常用函数

在使用php5.6中的mysql函数库时,php会输出一条建议使用mysqli的提示,于是就学习了mysqli mysqli与mysql操作大致相同,少了选择数据库的函数,把这项功能放到的链接数据库的函数里. 常用函数: 参考:http://www.w3school.com.cn/php/php_ref_mysqli.asp