在日常的开发编码中,偶尔会遇到一些看似简单的一些知识点,在一般的使用过程中并不觉得有什么问题,但是一旦出现在某些相对复杂的场景下,就会发现自己仍然存在使用上的一些盲区。有点类似于我们背单词的情况,一般只知道单词常用的意思,在某些语句中当单词使用它不常用的释义时,就感觉难以理解,需要重新查阅。
C++ typedef的使用看似十分简单和常用,但是仔细研究一下发现还是有许多需要了解的点,本文主要记录一下学习需要注意的细节。
1. 语法和使用场景
typedef是C/C++语言中保留的关键字,用来定义一种数据类型的别名。需要注意的是typedef并没有创建新的类型,只是指定了一个类型的别名而已。typedef定义的类型的作用域只在该语句的作用域之内, 也就是说如果typedef定义在一个函数体内,那么它的作用域就是这个函数。
typedef经常使用的场景包括以下几种:
- 指定一个简单的别名,避免了书写过长的类型名称
- 实现一种定长的类型,在跨平台编程的时候尤其重要
- 使用一种方便阅读的单词来作为别名,方便阅读代码
针对上面描述的情况做一个简单的说明:
1.1 减少书写类型的长度
这一使用方式比较常见的就是在C语言中定义结构体,在C语言中定义结构体的方式如下:(包含3种)
///第一种方式
struct MyStruct {
int data1;
char data2;
};
//之后定义变量
struct MyStruct a, b;
///第二种方式(声明的同时定义)
struct MyStruct {
int data1;
char data2;
}a, b;
///第三种方式(不需要提供结构体名字,直接定义)
struct {
int data1;
char data2;
}a, b;
为了简化书写,可以使用typedef来简化(这种方式在Windows API中非常的常见,使用过Win32 API编程的读者应该深有体会),比如Windows定义四边形区域RECT结构的代码:
typedef struct _RECT {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT, *PRECT;
使用typedef之后,可以将上面的代码修改为:
///第一种情况
struct MyStruct {
int data1;
char data2;
};
typedef struct MyStruct newtype;
newtype a, b;
///第二种情况
typedef struct MyStruct {
int data1;
char data2;
} newtype;
newtype a, b;
///第三种情况
typedef struct {
int data1;
char data2;
} newtype;
newtype a, b;
需要注意的是上面描述的是C语言的使用方法,在C++中定义struct之后再次声明变量时,不需要使用struct关键字。
1.2 实现一种定长的类型
在C/C++中并没有规定int类型的长度,因此在不同的机器上很有可能表示int类型变量所占用的字节数是不同的,这样有时候会带来一些问题。为了解决这些问题,很多时候可以定义一个固定大小的类型(比如32位的Int类型)Int32,用来表示固定长度的整型。这样在任意的机器上都可以使用这个Int32的类型,保证它的长度是固定的32位(至于它底层使用的真正类型,会根据具体机器上使用对应的类型),在OpenGL中就采用了这种方式,定义了许多固定大小的整型,如下图所示:
1.3 实现一种有意义可读的别名
例如下面这段代码,速度和分数都是使用int类型来表示,函数调用的时候传入的也是整型,把速度变量传入到原本接收成绩的参数,并不会有错误。
int current_speed ;
int high_score ;
void congratulate(int your_score) {
if (your_score > high_score)
...
}
为了区别开这两种类型(只是从阅读代码者角度来看,对编译器来说二者并无不同),可以使用typedef来定义两个不同的别名:
typedef int km_per_hour ;
typedef int points ;
km_per_hour current_speed ;
points high_score ;
void congratulate(points your_score) {
if (your_score > high_score)
...
}
这样代码阅读起来更加清晰一些。
另外需要注意的是:当使用typedef定义了points(km_per_hour)为int类型,在使用int类型定义的时候都可以使用这两个别名来替换,但并不是二者可以完全替换,当定义unsigned int等类型时,使用points(km_per_hour)替换int是错误的。
unsigned int a; // Okay
unsigned km_per_hour b; // 编译报错
long int c; // Okay
long km_per_hour d; // 编译报错
2. 其他情况下typedef的使用
typedef在定义复杂类型时,有时候会变得难于理解,有很多时候这些使用方式看起来并没有什么严格上的逻辑可言,需要使用者自己熟悉。
2.1 定义指针
定义指针的方式如下所示:
//语法: typedef 指针类型 别名
typedef char* CHARS;
这行代码定义了 CHARS是一个char类型的指针,也就是说CHARS的类型是char*。
当引入指针的修饰符之后,情况立刻变得复杂起来,以const关键字为例,在C++中const用来描述一个变量是“常量”,当用来修饰指针的时候它的含义有两种:
- 指针自己是常量,指针不能被修改(在C++11种被称为 Top-Level const)
- 指针指向的变量是常量,指针指向的变量不能修改(在C++11种被称为Low-Level const)
下面三个变量分别了这几种指针:
const double pi = 3.1415;
double p = 3.0;
const double * ptoConst = π //指向const double的指针(ptoConst可以指向其他const变量)
double * const constp = &p; //指向double类型的常量指针(可以解引用修改p的值)
const double * const constptoconst = π //指向const double类型的const指针
在C++ templates一书中的前言部分,提到了下面几种typedef的定义:
typedef char* CHARS;
typedef CHARS const CPTR_1;
typedef char* const CPTR_2;
typedef const CHARS CPTR_3;
typedef const char* CPTR_4;
以上这几个变量表示的含义依次是:
1. CHARS == char*
2. CPTR_1 == char* const [Top-Level const]
3. CPTR_2 == char* const [Top-Level const]
4. CPTR_3 == char* const [Top-Level const]
5. CPTR_4 == char const * (const char *) [Low-Level const]
如何知道这些类型到底是什么,只需要抓住几点:
1. typedef 并不是#define,不能简单的替换字符来理解
2. typedef 将中间变量类型作为一个整体来看,也就是:typederf xxx 别名,这里面xxx如果包含修饰符,那么修饰符要整体看待,也就是整个xxx才是一种类型。(以CPTR_3为例,CHARS是指针类型,那么给它加上const,也就是整体xxx是 const类型的指针,于是CPTR_3的释义是:const类型的char*指针(指针是const类型的:Top-Level const)
C++ templates一书中提倡将 const修饰的变量写在 const 所修饰的变量前面,也就是:
//const的变量是int类型的
const int i = 10;
int const i = 10; //(推荐写法)
//const的变量是int*类型的
int* const pi = &i;//(int*是const的,也就是说是指针是const,Top-Level const)
//指向const int的指针 [Low-Level const]
int const *pi = &i;//(推荐写法)
const int *pi = &i;
它认为如果这样去书写,那么在有const参与typedef的时候可以用简单的替换就能表达类型的含义。(参考CPTR_1和CPTR_2)。我们也可以这样去做,但是有许多代码都习惯用const T* 来表示Low-Level const,在阅读代码的时候理解就可以了。
2.2 定义数组
使用typedef定义数字,使用下面的语法:
typedef char arrType[6]; //arrType是一个包含6个参数的数组类型
我个人在学习过程中经常写错成: typedef char[6] arrType; 这种写法是不对的,定义数组在C/C++中使用的是:
int s[6];而不是 int[6] s;
2.3 定义函数指针
typedef在C/C++中还有一个非常常见的使用场景是定义函数指针,语法如下:
typedef int (*MathFunc)(float, int);
定义了一个MathFunc类型的函数指针,这个函数返回值是int类型,包含两个参数(float,int)
基本上函数指针了解这种语法结构就可以了,大部分的函数指针都类似。
2.4 更加复杂的定义
在typdef可以定义更加复杂的类型,这种情况遇到的不是很多,有时候多层的嵌套会将代码演变的异常复杂,可读性变差。虽然不推荐这么写,但是对于他人写的代码要能理解。关于复杂定义的方式可以阅读参考文献6中的描述:
理解复杂声明可用的“右左法则”:从变量名看起,先往右,再往左,碰到一个圆括号就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直到整个声明分析完。举例:
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。
3. C++11下新的选择(使用using定义别名)
C++11 中扩展了using的使用场景(C++11之前using主要用来引入命名空间名字 如:using namespace std;),可以使用using定义类型的别名:
使用语法如下:
using 别名 = xxx(类型);
通过语法可以看出,using声明别名的顺序和typedef是正好相反:typedef首先是类型,接着是别名,而using使用别名作为左侧的参数,之后才是右侧的类型,例如上面的类型定义:
typedef int points;
using points = int; //等价的写法
在定义诸如函数指针等类型时,使用using的方式更加自然和易读:
typedef void (*FP) (int, const std::string&);
using FP = void (*) (int, const std::string&); //等价的using别名
另外using可以在模板别名中使用,但是typedef不可以:
template <typename T>
using Vec = MyVector<T, MyAlloc<T>>;
// usage
Vec<int> vec;
若使用typedef的方式改写如下:
template <typename T>
typedef MyVector<T, MyAlloc<T>> Vec;
// usage
Vec<int> vec;
当你使用编译器编译的时候,将会得到类似:error: a typedef cannot be a template的错误信息。
那么,为什么typedef不可以呢?在 n1449 中提到过这样的话:”we specifically avoid the term “typedef template” and introduce the new syntax involving the pair “using” and “=” to help avoid confusion: we are not defining any types here, we are introducing a synonym (i.e. alias) for an abstraction of a type-id (i.e. type expression) involving template parameters.” 所以,我认为这其实是标准委员会他们的观点与选择,在C++11中,也是完全鼓励用using,而不用typedef的。
那么,如果我们想要用typedef做到这一点,应该怎么办呢?如Meyers所述以及一些STL的做法,那就是包装一层,如:
template <typename T>
struct Vec
{
typedef MyVector<T, MyAlloc<T>> type;
};
// usage
Vec<int>::type vec;
正如你所看到的,这样是非常不漂亮的。而更糟糕的是,如果你想要把这样的类型用在模板类或者进行参数传递的时候,你需要使用typename强制指定这样的成员为类型,而不是说这样的::type是一个静态成员亦或者其它情况可以满足这样的语法,如:
template <typename T>
class Widget
{
typename Vec<T>::type vec;
};
然而,如果是使用using语法的模板别名,你则完全避免了因为::type引起的问题,也就完全不需要typename来指定了。
template <typename T>
class Widget
{
Vec<T> vec;
};
一切都会非常的自然,所以于此,非常推荐using,而非typedef。
归根到底就是一句话,在C++11中,请使用using,而非typedef,如标准中7.1.3.2 所述:
A typedef-name can also be introduced by an alias-declaration. The identifier following the using keyword becomes a typedef-name and the optional attribute-specifier-seq following the identifier appertains to that typedef-name. It has the same semantics as if it were introduced by the typedef specifier. In particular, it does not define a new type and it shall not appear in the type-id.
它可以完成typedef能完成的,并且可以做的更多,如你所见的模板别名。
参考文献
- Wikipedia typedef
- 结构体
- typedef和const之间的trap
- C++ typedef interpretation of const pointers
- [C++ templates: The Complete Guide –1.4 Some Remarks About Programming Style
]- 关于typedef的用法总结
- 蓝色的味道知乎:Effective Modern C++ Note 02