C语言最佳实践
上QQ阅读APP看书,第一时间看更新

1.4.4 善用类型定义

Linux内核强烈要求慎用类型定义(typedef),但在某些情形下使用类型定义可以带来很多便利。根据笔者多年的工作经验,应考虑在下列场合使用类型定义。

1.当需要隐藏类型的实现细节时

可以在函数库的接口定义中使用类型定义,尤其当需要隐藏类型的实现细节时。也就是说,使用接口的程序员不需要关心类型的内部细节。比如,在Win32 API中,存在很多称为句柄(handle)的类型,比如HWND表示窗口句柄,代表一个窗口对象的值。在内部实现中,窗口句柄可能是一个指针,也可能是一个表示索引的整数。使用HWND的程序员不需要关心窗口句柄的内部实现,也不允许应用程序通过窗口句柄直接访问内部的数据结构,而只需要传递某个API返回的句柄给其他API使用即可。这种情况是使用类型定义的绝佳场合。比如HWND就可以用一个和指针等宽的无符号整数类型(uintptr_t)来定义:

typedef uintptr_t HWND

假定在Windows操作系统的内部实现中,HWND可直接作为指针使用,那么在具体使用时,只要做一次强制类型转换即可,例如:

static void foo(HWND hWnd)
{
    WINDOW *pWin = (WINDOW *)hWnd;
 
    pWin->spCaption = strdup("Hello, world!");
 
    ...
}
2.对结构体指针类型使用类型定义

可以对结构体指针使用类型定义,并使用_p或者_t后缀,例如:

struct list_node {
    const char       *title;
    struct list_node *next;
};
 
typedef struct list_node *list_node_p;

使用_p后缀和_t后缀的区别是,当结构体的内部细节暴露在外时,意味着外部代码可以访问结构体内的成员,此时使用_p后缀;反之,当结构体的内部细节被隐藏时,意味着外部代码不可以访问结构体内的成员,此时结构体指针的作用类似于上面提到的句柄,对外部代码而言,结构体指针相当于一个普通的无符号整数值,因而使用_t后缀。

相比使用句柄的情形,若对结构体指针使用类型定义,则可以带来一个额外的优势:在内部使用时,不用进行强制类型转换。为此,我们在头文件中作如下声明和定义:

struct list_node;
typedef struct list_node *list_node_t;
 
/* Returns the title in the specific node */
const char *list_node_get_title(list_node_t node);

然后在内部的头文件或者源文件中,定义结构体的细节并实现相应的接口:

struct list_node {
    const char       *title;
    struct list_node *next;
};
 
const char *list_node_get_title(list_node_t node)
{
    return node->title;
}

对结构体指针使用类型定义,即使头文件中声明的结构体名称不变,我们也可以在不同的源文件中为结构体定义不同的内部细节。这将带来极大的灵活性,详见第6章。

另外,这种做法在C标准库中十分常见,比如C标准库中全部大写的FILEDIR等结构体,其内部细节不会暴露给应用程序。但用于描述目录项的结构体的细节则暴露给应用程序,并没有定义新的数据类型。

3.对枚举类型使用类型定义

对枚举类型使用类型定义并使用_k后缀,就可以和后缀为_t_p的类型区分开来。例如,下面的代码定义了一个名为purc_document_type_k的枚举类型来表示文档的类型:

typedef enum {
    PCDOC_K_TYPE_FIRST = 0,
    PCDOC_K_TYPE_VOID = PCDOC_K_TYPE_FIRST,
    PCDOC_K_TYPE_PLAIN,
    PCDOC_K_TYPE_HTML,
    PCDOC_K_TYPE_XML,
    PCDOC_K_TYPE_XGML,
 
    /* XXX: change this when you append a new operation */
    PCDOC_K_TYPE_LAST = PCDOC_K_TYPE_XGML,
} purc_document_type_k;
4.对结构体类型使用特别的命名规则

如果确实需要对结构体进行类型定义,则可以对类型定义名称采用全大写且不带下画线的命名法,以便提示它是一个结构体的类型定义名称,如LINKEDLIST。这样就不会与采用全小写加下画线形式的变量名或函数名,以及采用全大写形式但使用下画线的常量名或宏名产生混淆了。

typedef struct LINKEDLIST {
    const char         *title;
    struct linked_list *next;
} LINKEDLIST;

如果能接受驼峰命名规则,那么也可以使用首字母大写的驼峰命名法来定义结构体的类型名称,例如:

struct LinkedList {
    const char         *title;
    struct LinkedList  *next;
};
 
typedef struct LinkedList LinkedList;

但这里更推荐不使用后缀来定义结构体的类型名称,因为前面已经对整数类型、枚举类型和结构体指针类型使用了_t或者_k等后缀:

typedef struct linked_list {
    const char         *title;
    struct linked_list *next;
} linked_list;
 
typedef struct linked_list *linked_list_t;

在早期的C代码中,由于当时的编译器不允许新的类型名称和已有的结构体类型名称相同,因此我们经常会看到下面的代码:

struct _LINKEDLIST {
    const char         *title;
    struct linked_list *next;
};
 
typedef struct _LINKEDLIST LINKEDLIST;

或者

struct tagLINKEDLIST {
    const char         *title;
    struct linked_list *next;
};
 
typedef struct tagLINKEDLIST LINKEDLIST;

上述代码在结构体的类型名称中使用下画线和tag作为前缀以示区别,但现在已经不需要这样做了。

作为一个不建议自定义数据类型的例子,我们在新的C语言项目中,应避免对整数做类型定义。C99标准已经在<stdint.h>头文件中针对不同宽度的整数类型定义了新的数据类型,比如uint8_tintptr_tintmax_t等,因此我们没有必要再自行针对不同的整数类型自定义新的数据类型。