欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++中typedef和类型别名

程序员文章站 2024-03-23 12:36:58
...

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

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中就采用了这种方式,定义了许多固定大小的整型,如下图所示:

C++中typedef和类型别名

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
相关标签: C++ typedef using