10-7 类型定义与类型别名

typedeftype 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;
}

此代码输出:

image

在上例程序中,我们首先将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;
}

image

尽管概念上我们期望 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;
}

该程序输出:

image

由于 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();
posted @ 2026-03-03 23:09  游翔  阅读(0)  评论(0)    收藏  举报