程序定义(用户定义)类型简介

基本数据类型是 C++ 核心语言的一部分,因此可直接使用。例如,若要定义 intdouble 类型的变量,只需声明即可:

int x;      // 定义基本类型 'int' 的变量
double d;   // 定义基本类型 'double' 的变量

对于基本类型的简单扩展(包括函数、指针、引用和数组)也是如此:

void fcn(int) {}; // 定义类型为 void(int) 的函数
int* ptr;         // 定义复合类型 '指向 int 的指针' 的变量
int& ref{ x };    // 定义复合类型 '引用 int' 的变量(初始化为 x)
int arr[5];       // 定义类型为 int[5] 的 5 个整数数组(后续章节讲解)

这些类型之所以能直接使用,是因为 C++ 语言已知晓这些类型名称(及符号)的含义,无需额外提供或导入定义。

然而,考虑类型别名(10.7 课《类型别名与 typedef》引入)的情况:它为现有类型赋予新名称。由于类型别名引入了新标识符,因此必须先定义才能使用:

#include <iostream>

using Length = int; // 定义标识符为 'Length' 的类型别名

int main() {
    Length x{ 5 }; // 因为已在上方定义,故可使用 'Length'
    std::cout << x << '\n';

    return 0;
}

若省略 Length 的定义,编译器将不知 Length 为何物,进而报错。Length 的定义不创建对象,仅告知编译器 Length 的含义,以便后续使用。

什么是用户定义 / 程序定义类型?

回顾 12.1 课《复合数据类型简介》中提到的分数存储问题:分子与分母紧密相关,若 C++ 内置分数类型,那将完美解决需求,但遗憾的是,C++ 并未提供。实际上,由于无法预知所有需求(更别提实现与测试),C++ 标准库中缺失许多潜在有用的类型。

为解决此类问题,C++ 提供了一种替代方案:允许创建全新的自定义类型,供程序使用!这些类型称为用户定义类型。然而,后续我们将更倾向于使用程序定义类型这一术语,特指我们为自身程序创建的类型。

C++ 提供两种复合类型用于创建程序定义类型:

  1. 枚举类型(包括非限定枚举与限定枚举)
  2. 类类型(包括结构体、类与联合体)

定义程序定义类型

与类型别名类似,程序定义类型也需先定义并命名,才能使用。其定义称为类型定义

关键要点

程序定义类型必须有名称和定义,才能使用。其他复合类型则无此要求。

函数不算用户定义类型(尽管它也需要名称与定义才能使用),因为被命名与定义的是函数本身,而非其类型。我们自定义的函数称为用户定义函数

虽然尚未讲解结构体,但以下示例展示了自定义 Fraction 类型的定义及其对象的实例化:

// 定义名为 Fraction 的程序定义类型,使编译器知晓 Fraction 的结构
// 后续章节将讲解结构体的详细用法
// 此处仅定义 Fraction 的外观,不创建对象
struct Fraction {
    int numerator{};    // 分子
    int denominator{};  // 分母
};

// 现在可使用 Fraction 类型
int main() {
    Fraction f{ 3, 4 }; // 实例化并初始化名为 f 的 Fraction 对象

    return 0;
}

本例中,我们使用 struct 关键字在全局作用域定义了名为 Fraction 的新程序定义类型,以便后续在文件中任意位置使用。这不会分配内存,仅向编译器描述 Fraction 的结构,以便后续创建 Fraction 类型的对象。随后,在 main() 函数中,我们实例化(并初始化)了一个名为 fFraction 类型变量。

程序定义类型的定义必须以分号结尾。遗漏分号是常见错误,且因编译器可能在类型定义之后的行报错,故调试难度较大。

警告
别忘了在类型定义末尾加分号。

我们将在下一课(13.2《非限定枚举》)展示更多定义与使用程序定义类型的示例,并从 13.7 课《结构体简介》开始讲解结构体。

命名程序定义类型

按惯例,程序定义类型以大写字母开头,不加后缀(例如 Fraction,而非 fractionfraction_tFraction_t)。

最佳实践

程序定义类型的名称应以大写字母开头,不加后缀。

新手有时会因类型名称与变量名称相似而对以下变量定义感到困惑:

Fraction fraction{}; // 实例化名为 fraction 的 Fraction 类型变量

这与其他变量定义无异:首先是类型(Fraction,因其首字母大写,故知其为程序定义类型),其次是变量名(fraction),最后是可选的初始化器。由于 C++ 区分大小写,此处并无命名冲突。

在多文件程序中使用程序定义类型

每个使用程序定义类型的代码文件都必须看到完整的类型定义,才能使用。前向声明是不够的。这是为了让编译器知晓为该类型对象分配多少内存。

为将类型定义传播到需要的代码文件,程序定义类型通常定义于头文件中,并通过 #include 导入到需要该类型定义的每个代码文件。这些头文件通常与程序定义类型同名(例如,名为 Fraction 的程序定义类型定义于 Fraction.h)。

最佳实践
仅在一个代码文件中使用的程序定义类型,应在该文件中尽可能靠近首次使用点定义。
在多个代码文件中使用的程序定义类型,应定义于与程序定义类型同名的头文件中,并按需通过 #include 导入到每个代码文件。

若将 Fraction 类型移至名为 Fraction.h 的头文件,以便在多个代码文件中包含,它将如下所示:

Fraction.h

#ifndef FRACTION_H
#define FRACTION_H

// 定义名为 Fraction 的新类型
// 仅定义 Fraction 的外观,不创建对象
// 注意:这是完整定义,非前向声明
struct Fraction {
    int numerator{};
    int denominator{};
};

#endif

Fraction.cpp

#include "Fraction.h" // 将 Fraction 定义导入此代码文件

// 现在可使用 Fraction 类型
int main() {
    Fraction f{ 3, 4 }; // 实际创建名为 f 的 Fraction 对象

    return 0;
}

类型定义部分豁免单一定义规则(ODR)

在 2.7 课《前向声明与定义》中,我们讨论了单一定义规则要求每个函数和全局变量在程序中只能有一个定义。若要在不含定义的文件中使用给定函数或全局变量,需提供前向声明(通常通过头文件传播)。这可行是因为对于函数和非 constexpr 变量,声明足以满足编译器,链接器随后可连接一切。

然而,类似方式的前向声明对类型不适用,因为编译器通常需要看到完整的类型定义才能使用给定类型。我们必须能够将完整的类型定义传播到每个需要它的代码文件。

为此,类型部分豁免单一定义规则:允许在多个代码文件中定义给定类型。

你可能已无意中使用过此功能:若程序有两个代码文件均 #include <iostream>,则相当于将所有输入/输出类型定义导入到这两个文件中。

需注意两点:

  1. 每个代码文件中仍只能有一个类型定义(通常不是问题,因为头文件保护可防止此情况)。
  2. 给定类型的每个定义都必须完全相同,否则将导致未定义行为。

术语:用户定义类型 vs 程序定义类型

“用户定义类型”一词常在日常对话中出现,也在 C++ 标准中提及(但未明确定义)。在日常对话中,它通常指“在自身程序中定义的类型”(如上文的 Fraction 示例)。

C++ 标准以非传统方式使用“用户定义类型”一词。在标准中,“用户定义类型”是任何由你、标准库或实现(例如,编译器为支持语言扩展而定义的类型)定义的类类型或枚举类型。或许令人意外的是,这意味着 std::string(标准库中定义的类类型)也被视为用户定义类型!

为提供更多区分,C++20 标准引入了“程序定义类型”一词,指非标准库、实现或核心语言

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"