C/C++中的重命名

常用的重命名方式

从最简单的理解入手,C/C++中的typedef和宏定义都是能够完成对一个变量或者一段代码进行重命名,以便程序能够识别新的名称,使得程序更易读。

例如将int重命名为Int32来标注不同位数系统中的类型长度,或者用宏定义来设置一些常用语句(比如:比较大小函数)。以及用typedef重命名函数指针以达到更清晰得理解回调函数的作用等等。

#define NKEYS (sizeof keytab / sizeof(struct key))
#define MAX(a, b) ((a)>(b)?(a):(b))
#define bool char
typedef bool (*OnClick)(int,int);

有些人会直接将宏定义和typedef理解成同一个概念。但实际上两者区别很大,如果用宏定义的方式去理解typedef,就容易掉入const限定符的陷阱。我个人认为抛开这个重命名的功能不谈,只从重命名后分配内存的角度来分析typedef,就更容易将其与宏定义区分开。

宏定义的重命名模式

宏定义的重命名模式实际上等价于在C/C++的预处理中进行字符串替换。且宏定义最主要的特征就是:

  1. 使用时也不产生内存分配
  2. 不做类型检查
  3. 宏替换会使编译时间变长,但不占用运行时间(这也是为什么有人会为了防止函数调用而使用宏定义)

上述特点也就导致经常可以看到有人建议:在用#define时,应该在能加上括号的地方都加上。这就是为了防止在字符串进行替换后,不同程序环境导致被替换部分与原程序在==优先级==上产生变化。从该情况来说,宏定义更贴近于大家最常说的“重命名”功能。

如何用内存理解typedef的重命名

typedef更类似于重新定义了一种类型,也就是重新定义一种内存分配方式

举例来说,在32位系统下typedef long Int32就是重新定义了一个和long一样在内存中分配32位的变量类型,且其名称为Int32。而C语言中最常见的在结构体前直接加typedef的方式也可以这样理解。例如:

typedef int Int16
typedef long Int32
typedef long long Int64

typedef struct UserData{
    int ID; //用户唯一ID标示
    char* name;//用户名
    int userLevel;//用户的会员等级
    ···
}UserData,*pUserData;

大括号内部的结构体成员不但定义了该结构体的内存分配方式。同时为typedef定义(或者说重命名)的新变量类型定义了同样的内存分配方式。而大括号后面的名字是使用typedef定义的相同内存分配方式的新变量类型的名称。这两个例子用颜色来表示可能会更加明显:

用内存分配新类型图示

我认为使用这种方式来理解typedef会减少很多仅将它等同于宏定义别名这种理解方式时会产生的一些错误写法。其中,比较常见的就是使用typedef时的const陷阱。

如何利用该角度解释const陷阱

用到const的场景假设

我们不妨来假设一个简单的应用场景:

  • 这里有一个使用C/C++语言编写的简易UI函数库。
  • 它像现在各类UI框架一样可以使用回调函数来实现一些基础的交互功能(例如:弹出一个确认弹窗,打开一个新界面,关闭当前界面)。
  • 且假设它的头文件click.h包含如下内容:
typedef struct UserData* pUserData;//读写用户数据的指针类型
...
//两种常用的函数指针声明
typedef bool (*OnClick)(); //点击时触发的函数指针类型
typedef bool (*OnClick2Open)(const pUserData);//点击时打开某信息窗的函数指针类型

//函数库自带的一些基础函数
bool CloseCurrentWin();//关闭当前窗口的函数声明
bool PopupWin(const pUserData);//弹出一个以用户数据初始化形成的弹窗
...

  • 场景说明:
    • 其中pUserData是一个自定义结构体类型的指针,它的功能是存储特定类型的用户数据。其定义形如typedef struct UserData* pUserData,这里的UserData代表了用户的==只读==数据,所以需要用const来限定。
    • 上述限定可以保证:在pUserData指向的某一用户数据内容时,无法通过pUserData指针更改该用户的数据。但用户本人在更改时,系统调用的是形如pOwnData一类的可读可写接口。从而保证了信息的读写安全。

误用const时与该场景的潜在冲突

上述函数库的写法会导致一个问题:使用typedef struct UserData* pUserData来重命名时,会导致该类型的指针作为上述函数的实参时,并不能起到只读的作用,而是导致指针无法指向别的区域。
因为const是有两种情况的:

//假设我们有一个变量,用来读取用户的会员等级,但不能更改会员等级
//该变量和上面的权限需求是一致,即:只读数据,但不改数据,且可改变指针指向不同的用户数据

//这两种写法等价,限定了只读数据,但可以指向别的用户的数据区
const int * pUserLevel;
int const * pUserLevel'

//这种写法,将不可更改该指针的指向,即:const限定的是指针本身
int * const pUserLevel;

基于这两种情况,已知我们在调用上述的库函数时,需要的是第一、二种只读的const限定模式。但在使用typedef时,如果我们写了typedef struct UserData* pUserData。此时,该变量指针将成为一个常指针,无法改变其指向的数据区域。即:

typedef struct UserData* pUserData 的写法,在函数bool PopupWin(const pUserData)等价于:bool PopupWin(UserData * const pUserData),这将导致该指针无法指向别的数据区(第三种情况),而不是对指向的不同数据区使用只读权限(第一二种情况)。
如果将typedef理解成宏定义那样的字符替换,就会将实参代入时理解为bool PopupWin(const UserData* pUserData),但实际上使用typedef替换后实际上等同于bool PopupWin(UserData* const pUserData),两者的效果截然不同。

从该角度解释const限定符为什么属于第三种情况

这就是前述所说的,typedef的重命名不等同于宏定义的字符串替换,产生上述错误的根本,就是将typedef定义的东西用字符替换的方式放到函数中理解。


而使用第二部分阐述的用内存分配的方式来理解,就更不容易出错。将typedef struct UserData* pUserData看做是新定义了一种在内存中占用4字节的指针类型,且其名称为pUserData。此时,将该类型的变量再次放入函数调用中理解:bool PopupWin(const pUserData),就可以理解到该const限定的是指针本身的值,即指针的指向为常量,而不是在限定它指向区域为只读,因为const将指针限制为了常量,而没有管指针指向的位置。

const陷阱的解决方法

所以,为了避免这种情况的出现,我们可以使用:

···C
typedef struct UserData* pUserData;//读写指针
typedef const struct UserData* cpUserData;//只读指针
···
typedef bool (*OnClick2Open)(cpUserData);//点击时打开某信息窗的函数指针类型
···

来防止上述的const陷阱。而该问题也更能说明typedef与宏定义之间重命名之间的区别。



C C++

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!