C++中typedef和类型别名

在日常的开发编码中,偶尔会遇到一些看似简单的一些知识点,在一般的使用过程中并不觉得有什么问题,但是一旦出现在某些相对复杂的场景下,就会发现自己仍然存在使用上的一些盲区。有点类似于我们背单词的情况,一般只知道单词常用的意思,在某些语句中当单词使用它不常用的释义时,就感觉难以理解,需要重新查阅。

C++ typedef的使用看似十分简单和常用,但是仔细研究一下发现还是有许多需要了解的点,本文主要记录一下学习需要注意的细节。


1. 语法和使用场景

typedef是C/C++语言中保留的关键字,用来定义一种数据类型的别名。需要注意的是typedef并没有创建新的类型,只是指定了一个类型的别名而已。typedef定义的类型的作用域只在该语句的作用域之内, 也就是说如果typedef定义在一个函数体内,那么它的作用域就是这个函数。

typedef经常使用的场景包括以下几种:

  1. 指定一个简单的别名,避免了书写过长的类型名称
  2. 实现一种定长的类型,在跨平台编程的时候尤其重要
  3. 使用一种方便阅读的单词来作为别名,方便阅读代码

针对上面描述的情况做一个简单的说明:

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用来描述一个变量是“常量”,当用来修饰指针的时候它的含义有两种:

  1. 指针自己是常量,指针不能被修改(在C++11种被称为 Top-Level const)
  2. 指针指向的变量是常量,指针指向的变量不能修改(在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*类型的形参,返回值类型是intint (*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能完成的,并且可以做的更多,如你所见的模板别名。


参考文献

  1. Wikipedia typedef
  2. 结构体
  3. typedef和const之间的trap
  4. C++ typedef interpretation of const pointers
  5. [C++ templates: The Complete Guide –1.4 Some Remarks About Programming Style
    ]
  6. 关于typedef的用法总结
  7. 蓝色的味道知乎:Effective Modern C++ Note 02
发布了157 篇原创文章 · 获赞 220 · 访问量 81万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览