10-7 类型定义与类型别名
typedef和type aliases均称呼类型别名
| 英文 | 专业翻译 | 示例 |
|---|---|---|
| typedef | 类型定义 | typedef unsigned int uint; |
| type aliases | 类型别名 | using uint = unsigned int; |
类型别名
在C++中,using是创建现有数据类型别名的关键字。创建类型别名时,需使用 using 关键字,后接别名名称,再接等号及原始数据类型。例如:
using Distance = double; // define Distance as an alias for type double
定义后,类型别名可在任何需要数据类型的地方使用。例如,我们可以创建一个使用别名名称作为类型的变量:
Distance milesToDestination{ 3.4 }; // defines a variable of type double
当编译器遇到类型别名名称时,会将其替换为别名对应的类型。例如:
#include <iostream>
int main()
{
using Distance = double; // define Distance as an alias for type double
Distance milesToDestination{ 3.4 }; // defines a variable of type double
std::cout << milesToDestination << '\n'; // prints a double value
return 0;
}
此代码输出:

在上例程序中,我们首先将Distance定义为double类型的别名。
接着定义名为milesToDestination的变量,其类型为别名Distance。由于编译器知晓Distance是类型别名,故会使用其别名类型double。因此milesToDestination变量实际编译为double类型变量,并在所有方面表现为double类型。
最后输出milesToDestination的值,其显示为double类型数值。
对于进阶读者
类型别名同样支持模板化,详见第13.14节——类模板参数推导(CTAD)与推导指南。
类型别名命名规范
历史上,类型别名的命名方式缺乏统一性。常见有三种命名约定(你将同时遇到这三种):
- 以“_t”后缀结尾的类型别名(“_t”是“type”的缩写)。标准库常采用此约定为全局作用域类型命名(如size_t和nullptr_t)。
该约定源自C语言,曾是自定义类型别名(有时也用于其他类型)的首选方案,但在现代C++中已逐渐失宠。需注意POSIX系统将“_t”后缀保留给全局作用域类型名,因此在POSIX系统中使用此约定可能引发类型命名冲突。
- 以“_type”结尾的类型别名。部分标准库类型(如std::string)采用此约定为嵌套类型别名命名(例如std::string::size_type)。
但许多此类嵌套类型别名完全不使用后缀(例如std::string::iterator),因此这种用法最多只能说是参差不齐。
- 不使用后缀的类型别名。
现代 C++ 中,自定义类型别名(或其他类型)的命名惯例是首字母大写且不使用后缀。大写字母有助于区分类型名与变量/函数名(后者首字母小写),并避免命名冲突。
采用此命名规范时,常见用法如下:
void printDistance(Distance distance); // Distance is some defined type
此处Distance是类型名,distance是形参名。由于C++区分大小写,此用法并无问题。
最佳实践
类型别名命名应以大写字母开头且不使用后缀(除非有特殊原因)。
作者注
本教程系列中后续部分仍会使用“_t”或“_type”后缀。欢迎在相关课程下留言,以便我们将其调整为符合最佳实践的规范。
类型别名并非独立类型
别名实际上并未定义新的独立类型(即与其他类型分离的类型),它只是为现有类型引入了新的标识符。类型别名与被别名的类型完全互换。
这使得某些语法正确但语义无意义的操作成为可能。例如:
int main()
{
using Miles = long; // define Miles as an alias for type long
using Speed = long; // define Speed as an alias for type long
Miles distance { 5 }; // distance is actually just a long
Speed mhz { 3200 }; // mhz is actually just a long
// The following is syntactically valid (but semantically meaningless)
distance = mhz;
return 0;
}

尽管概念上我们期望 Miles 和 Speed 具有不同含义,但二者本质上都是 long 类型的别名。这意味着 Miles、Speed 和 long 完全可以互换使用。事实上,当我们将类型为Speed的值赋给类型为Miles的变量时,编译器仅识别为将long类型值赋给long类型变量,不会报错。
由于编译器不会阻止此类类型别名的语义错误,我们称别名不具备类型安全性type safe。尽管如此,它们仍具有实用价值。
警告
必须谨慎避免混用语义上应区分的别名值。
附带说明...
某些语言支持强类型定义strong typedef(或强类型别名strong type alias)的概念。强类型定义实际上创建了具有原始类型所有属性的新类型,但若尝试混用别名类型与强类型定义的值,编译器将抛出错误。截至C++20,C++尚未直接支持强类型定义(尽管第13.6节——作用域枚举(枚举类)中介绍的枚举类具有类似特性),但已有不少第三方C++库实现了类似强类型定义的行为。
类型别名的作用域
由于作用域是标识符的属性,类型别名标识符遵循与变量标识符相同的作用域规则:在代码块内定义的类型别名具有块作用域,仅限于该块内使用;而在全局命名空间定义的类型别名具有全局作用域,可在文件结尾前使用。在上例中,Miles 和 Speed 仅可在 main() 函数内使用。
若需在多个文件中使用类型别名,可将其定义在头文件中,并通过#include引入至需要该定义的代码文件:
mytypes.h:
#ifndef MYTYPES_H
#define MYTYPES_H
using Miles = long;
using Speed = long;
#endif
通过此方式#include的类型别名将被导入全局命名空间,从而获得全局作用域。
类型定义
typedef(即“类型定义type definition”的缩写)是创建类型别名的一种传统方式。创建typedef别名需使用typedef关键字:
// The following aliases are identical
typedef long Miles;
using Miles = long;
出于向后兼容性考虑,C++仍保留typedef,但在现代C++中已基本被类型别名取代。
typedef存在若干语法问题。首先,容易混淆应先定义类型定义的名称typedef name还是被别名类型的名称aliased type name。以下哪种写法正确?
typedef Distance double; // incorrect (typedef name first)
typedef double Distance; // correct (aliased type name first)
很容易搞反。幸运的是,在这种情况下,编译器会发出警告。
其次,当类型更复杂时,typedef的语法会变得臃肿。例如,下面是一个难以阅读的typedef,以及一个等效(且稍易阅读)的类型别名:
typedef int (*FcnType)(double, char); // FcnType hard to find
using FcnType = int(*)(double, char); // FcnType easier to find
在上面的typedef定义中,新类型(FcnType)的名称被埋在定义的中间部分,而在类型别名中,新类型的名称与定义的其余部分由等号分隔。
第三,名称“typedef”暗示正在定义新类型,但事实并非如此。typedef仅仅是一个别名。
最佳实践
优先使用类型别名而非typedef。
命名规范
C++标准将类型定义(typedef)和类型别名(type aliases)的名称统称为“typedef名称typedef names”。
何时应使用类型别名?
既然我们已经介绍了类型别名是什么,接下来就来谈谈它们的用途。
使用类型别名实现平台无关编程
类型别名的主要用途之一是隐藏平台特有的细节。在某些平台上,整型(int)占用2字节,而在其他平台上则占用4字节。因此,在编写平台无关代码时,使用整型存储超过2字节的信息可能存在潜在风险。
由于char、short、int和long未明确标注其大小,跨平台程序通常会使用类型别名来定义包含类型位数的别名。例如:int8_t 表示8位有符号整数,int16_t 表示16位有符号整数,int32_t 表示32位有符号整数。此类类型别名能有效避免错误,并清晰揭示变量大小的预设假设。
为确保每个别名类型解析为正确大小的类型,此类类型别名通常需配合预处理器指令使用:
#ifdef INT_2_BYTES
using int8_t = char;
using int16_t = int;
using int32_t = long;
#else
using int8_t = char;
using int16_t = short;
using int32_t = int;
#endif
在整型仅占2字节的机器上,可通过编译器/预处理器设置将INT_2_BYTES进行#define定义,此时程序将采用顶部类型别名集进行编译。而在整型占4字节的机器上,若未定义INT_2_BYTES,则会使用底部类型别名集。通过这种方式,只要正确定义 INT_2_BYTES,int8_t 将解析为 1 字节整数,int16_t 将解析为 2 字节整数,int32_t 将解析为 4 字节整数(使用 char、short、int 和 long 的组合,具体取决于程序编译目标机器的特性)。
固定宽度整数类型(如 std::int16_t 和 std::uint32_t)以及 size_t 类型(均在第 4.6 节——固定宽度整数与 size_t 中介绍)实际上只是对各种基本类型的别名。
这也是为什么当你使用 std::cout 打印 8 位固定宽度整数时,很可能会得到字符值。例如:
#include <cstdint> // for fixed-width integers
#include <iostream>
int main()
{
std::int8_t x{ 97 }; // int8_t is usually a typedef for signed char
std::cout << x << '\n';
return 0;
}
该程序输出:

由于 std::int8_t 通常是 signed char 的 typedef,变量 x 很可能被定义为 signed char。而 char 类型会将值以 ASCII 字符形式打印,而非整数值。
使用类型别名使复杂类型更易于阅读
虽然我们目前只处理过简单的数据类型,但在高级C++中,类型可能变得复杂且冗长,需要在键盘上手动输入。例如,你可能会看到函数和变量这样定义:
#include <string> // for std::string
#include <vector> // for std::vector
#include <utility> // for std::pair
bool hasDuplicates(std::vector<std::pair<std::string, int>> pairlist)
{
// some code here
return false;
}
int main()
{
std::vector<std::pair<std::string, int>> pairlist;
return 0;
}
在需要使用该类型的地方到处输入 std::vector<std::pair<std::string, int>> 既繁琐又容易出错。使用类型别名会方便得多:
#include <string> // for std::string
#include <vector> // for std::vector
#include <utility> // for std::pair
using VectPairSI = std::vector<std::pair<std::string, int>>; // make VectPairSI an alias for this crazy type
bool hasDuplicates(VectPairSI pairlist) // use VectPairSI in a function parameter
{
// some code here
return false;
}
int main()
{
VectPairSI pairlist; // instantiate a VectPairSI variable
return 0;
}
好多了!现在我们只需输入 VectPairSI 而不是 std::vector<std::pair<std::string, int>>。
不必担心你还不了解std::vector、std::pair或这些奇怪的尖括号。你真正需要理解的是:类型别名能让你将复杂类型赋予更简洁的名称,从而提升代码可读性并减少输入量。
这可能是类型别名最理想的用法。
利用类型别名来记录值的含义
类型别名还能帮助代码文档化与理解。
对于变量,我们有变量标识符来帮助记录变量的用途。但考虑函数的返回值情况。诸如 char、int、long、double 和 bool 之类的数据类型描述了函数返回的值的类型,但我们更常需要了解返回值的具体含义。
例如,考虑以下函数:
int gradeTest();
我们能看出返回值是个整数,但这个整数代表什么?字母评分?错题数量?学生ID号?错误代码?谁知道呢!整数类型本身并不能说明太多。运气好的话,函数文档可能存在于某个可查阅的位置;运气不佳时,就只能通过阅读代码来推测其用途了。
现在我们使用类型别名实现等效版本:
using TestScore = int;
TestScore gradeTest();
TestScore 的返回类型使其更明确地表明该函数返回的是代表考试成绩的类型。
根据我们的经验,仅为记录单个函数的返回类型而创建类型别名并不值得(建议改用注释说明)。但若有多个函数传递或返回此类类型,创建类型别名则可能值得考虑。
使用类型别名简化代码维护
类型别名还能让你在无需修改大量硬编码类型的情况下,改变对象的底层类型。例如,若你原本使用short类型存储学生ID号,后来却发现需要改用long类型,就必须翻遍大量代码将short替换为long。此时要分辨哪些short类型对象用于存储ID号、哪些用于其他用途,恐怕相当困难。
然而,若使用类型别名,则修改类型只需更新类型别名即可(例如将 StudentId = short; 改为 StudentId = long;)。
虽然这看似是个不错的优势,但每次改变类型时都需谨慎,因为程序的行为也可能随之改变。当将类型别名转换为不同类型家族中的类型时(例如整数转为浮点数,或有符号数转为无符号数),这种情况尤为明显!新类型可能存在比较运算或整数/浮点数除法问题,以及旧类型不曾出现的问题。若将现有类型转换为其他类型,必须对代码进行全面重新测试。
缺点与结论
尽管类型别名具备某些优势,但它们也会在代码中引入另一个需要理解的标识符。若这种额外标识符未能通过提升可读性或可理解性来抵消其影响,那么类型别名反而弊大于利。
不当使用的类型别名可能将常见类型(如std::string)隐藏在需要查阅的自定义名称背后。在某些情况下(例如智能指针,我们将在后续章节中讨论),模糊类型信息也会妨碍理解该类型的预期行为。
因此,类型别名应主要用于能显著提升代码可读性或维护性的场景。这既是科学也是艺术。当类型别名能在代码中广泛复用而非局限于少数位置时,其价值最为突出。
最佳实践
谨慎使用类型别名,仅当其能显著提升代码可读性或维护性时才采用。
测验时间
给定以下函数原型:
int printData();
将整型返回值转换为名为PrintError的类型别名。同时包含类型别名声明和更新后的函数原型。
显示答案
using PrintError = int;
PrintError printData();

浙公网安备 33010602011771号