typedef
一些大程序里面, 很多时候上来先用typedef定义了很多struct, enum或者union类型, 我们先看看typedef这个关键字
# 定义一种类型的别名
# typedef 定义基础类型,int, long
typedef 使用最多的地方是创建易于记忆的类型名,例如:
typedef int size;
typedef long byte_4; // 给已知数据类型long起个新名字,叫byte_4。
void measure(size * psz);
size array[4];
size len = file.getlength();
std::vector<size> vs;
2
3
4
5
6
这里定义了一个 int 的同义字,名为 size, typedef 并不创建新的类型。它仅为现有类型添加一个同义字。你可以在需要 int 的地方使用 size:
# typedef 定义复合类型,如指针和数组。
例如,像下面这样重复定义有 81 个字符元素的数组:
char line[81];
char text[81];
2
可以使用typedef定义相同类型和大小的数组,可以这样:
typedef char Line[81];
Line text, secondline;
getline(text);
2
3
也可以用typedef定义一个指针类型:
char* pa, pb; // 这是不对的,它只声明了一个指向字符变量的指针, 和一个字符变量;
typedef char * pstr; // 可以这样
pstr pa, pb;
int mystrcmp(pstr, pstr);
2
3
4
但是这里也有一些陷阱。标准函数 strcmp()有两个‘const char *’类型的参数。因此,它可能会误导我们象下面这样声明 mystrcmp():
int mystrcmp(const pstr, const pstr);
这是错误的,按照顺序,‘const pstr’被解释为‘char * const’(一个指向 char 的常量指针),而不是‘const char *’(指向常量 char 的指针)。这个问题很容易解决:
typedef const char * cpstr;
int mystrcmp(cpstr, cpstr); // 现在是正确的
2
记住: 不管什么时候,只要为指针声明 typedef,那么都要在 typedef 名称中加一个 const,以使得该指针所指数据为常量量(常量指针),而指针本身为常量(指针常量)。
# typedef定义struct类型
typedef struct tagMyStruct
{
int iNum;
long lLength;
} MyStruct;
2
3
4
5
这语句实际上完成以下两个操作:
1)定义一个新的struct
struct tagMyStruct
{
int iNum;
long lLength;
};
2
3
4
5
分析:tagMyStruct称为“tag”,即“标签”,实际上是一个临时名字,struct 关键字和tagMyStruct一起,构成了这个结构类型,不论是否有typedef,这个结构都存在。
我们可以用struct tagMyStruct varName来定义变量,但要注意,使用tagMyStruct varName来定义变量是不对的,因为struct 和tagMyStruct合在一起才能表示一个结构类型。
2)typedef为这个新的结构起了一个名字,叫MyStruct。
typedef struct tagMyStruct MyStruct;
因此,MyStruct实际上相当于struct tagMyStruct,我们可以使用MyStruct varName来定义变量。
3)规范做法:
struct tagNode
{
char *pItem;
struct tagNode *pNext;
};
typedef struct tagNode *pNode;
2
3
4
5
6
除了对struct, 在一些代码中经常看到对enum, union来进行typedef, 来形成整个程序的类型系统。
# typedef 有const的情况
typedef char* PSTR;
int mystrcmp(const PSTR, const PSTR);
2
const PSTR实际上相当于const char吗?不是的,它实际上相当于char const。
原因在于const给予了整个指针本身以常量性,也就是形成了常量指针char* const。
简单来说,当const和typedef一起出现时,typedef不再是简单的字符串替换。
下面的代码中编译器会报一个错误
typedef char * pStr;
char string[4] = "abc";
const char *p1 = string;
const pStr p2 = string;
p1++;
p2++; // error! const pStr p2并不等于const char * p2。
2
3
4
5
6
上面代码中const pStr p2和const long x本质上没有区别,都是对变量进行只读限制,只不过此处变量p2的数据类型是我们自己定义的而不是系统固有类型而已。const pStr p2的真正含义是:限定数据类型为char *的变量p2为只读,因此p2++错误。
# typedef 与 #define的区别
char* pa, pb; // 这是不对的,它只声明了一个指向字符变量的指针, 和一个字符变量;
// #define char* pstr // 如果是这样则是不对的, 因为#define只是等价替换祖父穿, 对于第二个pb则仍然为char类型
typedef char * pstr; // 可以这样
pstr pa, pb;
int mystrcmp(pstr, pstr);
2
3
4
5
#define只是在预处理的时候进行简单的字符串替换, typedef是重新在整个程序中定义一个新的类型
不仅仅如此:
- #define宏定义有一个特别的长处:可以使用 #ifdef ,#ifndef等来进行逻辑判断,还可以使用#undef来取消定义。
- typedef也有一个特别的长处:它符合范围规则,使用typedef定义的变量类型其作用范围限制在所定义的函数或者文件内(取决于此变量定义的位置),而宏定义则没有这种特性。
# 简化复杂的声明
在原来的声明里逐步用别名替换一部分复杂声明,如此循环,把带变量名的部分留到最后替换,得到的就是原声明的最简化版。举例:
- 原声明:
int *(*a[5])(int, char*); // 含有5个函数指针组成的数组
变量名为a,也可以直接用一个新别名 pFun 替换a就可以了:
typedef int *(*pFun)(int, char*);
pFun a[5]; // 原声明使用pFun表示
2
- 原声明:
void (*b[10]) (void (*)());
变量名为b,先替换右边部分括号里的,pFunParam为别名一:
typedef void (*pFunParam)();
再替换左边的变量b,pFunx为别名二:
typedef void (*pFunx)(pFunParam);
原声明的最简化版:
pFunx b[10];
- 原声明:
doube(*)() (*e)[9];
变量名为e,先替换左边部分,pFuny为别名一:
typedef double(*pFuny)();
再替换右边的变量e,pFunParamy为别名二
typedef pFuny (*pFunParamy)[9];
原声明的最简化版:
pFunParamy e;
- 原声明:
double(* (*pa)[9] )(); // 此蓝色部分为个人理解,未找到原文出处
typedef double(*pFun)(); //首先为上面表达式蓝色部分声明一个新类型
typedef pFun (*pFunParam)[9]; //整体声明一个新类型
pFunParam pa; //使用定义的新类型来声明对象,等价于double(*(*pa)[9])();
2
3
4
5
理解复杂声明可用的“右左法则”:
从变量名看起,先往右,再往左,碰到一个圆括号就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直到整个声明分析完。举例:
int (*func)(int *p);
首先找到变量名func,外面有一对圆括号,而且左边是一个*号,*这说明func是一个指针;然后跳出这个圆括号,先看右边,又遇到圆括号,这说明 (func)是一个函数,所以func是一个指向这类函数的指针,即函数指针,这类函数具有int类型的形参,返回值类型是int。
int (*func[5])(int *);
func 右边是一个[]运算符,说明func是具有5个元素的数组;func的左边有一个*,说明func的元素是指针(注意这里的不是修饰func,而是修饰 func[5]的,原因是[]运算符优先级比高,func先跟[]结合)。跳出这个括号,看右边,又遇到圆括号,说明func数组的元素是函数类型的指 针,它指向的函数具有int*类型的形参,返回值类型为int。
也可以归纳为2个固定的模式:
type (*)(....) //函数指针
type (*)[] //数组指针
2
上面讨论的 typedef 行为有点像 #define 宏,用其实际类型替代同义字。不同点是 typedef 在编译时被解释,因此让编译器来应付超越预处理器能力的文本替换。例如:
typedef int (*PF) (const char *, const char *);
这个声明引入了 PF 类型作为函数指针的同义字,该函数有两个 const char * 类型的参数以及一个 int 类型的返回值。如果要使用下列形式的函数声明,那么上述这个 typedef 是不可或缺的:
PF Register(PF pf);
Register() 的参数是一个 PF 类型的回调函数,返回某个函数的地址,其署名与先前注册的名字相同。做一次深呼吸。下面我展示一下如果不用 typedef,我们是如何实现这个声明的:
int (*Register (int (*pf)(const char *, const char *))) (const char *, const char *);
很少有程序员理解它是什么意思,更不用说这种费解的代码所带来的出错风险了。显然,这里使用 typedef 不是一种特权,而是一种必需。持怀疑态度的人可能会问:“OK,有人还会写这样的代码吗?”,快速浏览一下揭示 signal()函数的头文件
# 跨平台类型定义
定义一个叫 REAL 的浮点类型,为:
typedef long double REAL; // 在目标平台一上,让它表示最高精度的类型
typedef double REAL; // 在不支持 long double 的平台二上
typedef float REAL; // 在连 double 都不支持的平台三上,可以这样定义
2
3
当在不同的平台时,只要修改 typedef 本身就行,不用对代码其他部分作修改。这个微小的变动可以通过条件编译来自动实现。
标准库广泛使用了这个技巧,比如size_t。
此外,因为typedef是定义了一种类型的别名,不是简单的字符串替换(#define),所以它比宏更稳健(虽然用宏有时也可以完成以上的用途)。
# 注意事项:
# typedef是一个存储类的关键字
同auto、extern、mutable、static、register等一样,他们在语法上是一个存储类的关键字, 虽然它并不真正影响对象的存储特性,如:
typedef static int INT2; // error
typedef register int FAST_COUNTER; // error
2
编译将失败,会提示“指定了一个以上的存储类”。。因为符号 typedef 已经占据了存储类关键字的位置,在 typedef 声明中不能用 register(或任何其它存储类关键字)。s
Extra Reference: Using typedef to Curb Miscreant Code (opens new window) [使用 typedef 抑制劣质代码]