13-1 程序定义(用户定义)类型介绍

由于基本类型是 C++ 核心语言的一部分,因此可以立即使用。例如,如果我们想定义一个类型为 intdouble的变量,我们可以直接这样做:

int x; // define variable of fundamental type 'int'
double d; // define variable of fundamental type 'double'

对于基本类型(包括函数、指针、引用和数组)的简单扩展——复合类型来说,情况也是如此:

void fcn(int) {}; // define a function of type void(int)
int* ptr; // define variable of compound type 'pointer to int'
int& ref { x }; // define variable of compound type 'reference to int' (initialized with x)
int arr[5]; // define an array of 5 integers of type int[5] (we'll cover this in a future chapter)

之所以可行,是因为 C++ 语言已经知道这些类型的类型名称(和符号)的含义——我们不需要提供或导入任何定义。

然而,考虑一下类型别名(在第10.7 课——类型定义和类型别名中介绍)的情况,它允许我们为现有类型定义一个新名称。由于类型别名会在程序中引入一个新的标识符,因此必须先定义类型别名才能使用它:

#include <iostream>

using Length = int; // define a type alias with identifier 'Length'

int main()
{
    Length x { 5 }; // we can use 'length' here since we defined it above
    std::cout << x << '\n';

    return 0;
}

如果我们省略 Length 的定义,编译器就不知道 Length是什么,当我们尝试使用该类型定义变量时就会报错。Length 的定义并不会创建对象——它只是告诉编译器 Length是什么,以便稍后可以使用。

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

在上一章(12.1——复合数据类型简介)的引言中,我们提出了存储分数的挑战,因为分数的分子和分母在概念上是相互关联的。在那节课中,我们讨论了使用两个独立的整数分别存储分数的分子和分母所面临的一些挑战。

如果 C++ 内置了分数类型,那就完美了——可惜没有。此外,还有数百种其他潜在有用的类型 C++ 没有包含,因为根本不可能预见到用户可能需要的所有类型(更不用说实现和测试这些类型了)。

C++并没有采用传统方法解决这类问题,而是采用了不同的方式:它允许创建全新的自定义类型,供我们在程序中使用!这类类型被称为用户自定义类型user-defined types。不过,正如我们将在本课后面讨论的那样,对于我们自己创建的、用于程序中的这类类型,我们更倾向于使用“程序自定义类型program-defined types这个术语。

C++ 有两种不同类型的复合类型,可用于创建程序定义的类型:

  • 枚举类型(包括无作用域枚举和有作用域枚举)
  • 类类型(包括结构体、类和联合体)。

定义程序定义类型

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

关键见解
程序定义类型必须先有名称和定义才能使用。其他复合类型则两者都不需要。
函数不被视为用户自定义类型(即使它们在使用前需要名称和定义),因为被赋予名称和定义的是函数本身,而不是函数的类型。我们自己定义的函数则被称为用户自定义函数。

虽然我们还没有讲解什么是结构体,但这里有一个例子,展示了自定义 Fraction 类型的定义以及如何使用该类型实例化一个对象:

// Define a program-defined type named Fraction so the compiler understands what a Fraction is
// (we'll explain what a struct is and how to use them later in this chapter)
// This only defines what a Fraction type looks like, it doesn't create one
struct Fraction
{
	int numerator {};
	int denominator {};
};

// Now we can make use of our Fraction type
int main()
{
	Fraction f { 3, 4 }; // this actually instantiates a Fraction object named f

	return 0;
}

在这个例子中,我们使用关键字struct定义了一个名为 Fraction 的新程序定义类型(在全局作用域中,因此可以在文件的其他任何位置使用)。这不会分配任何内存——它只是告诉编译器 Fraction 的类型是什么,以便我们稍后可以分配该Fraction类型的对象。然后,在 main() 内部,我们实例化(并初始化)一个名为 fFraction 类型的变量。

程序定义的类型定义必须以分号结尾。类型定义末尾缺少分号是程序员常见的错误,而且很难调试,因为编译器可能会在类型定义之后的行报错。

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

我们将在下一课( 13.2——无作用域枚举)中展示更多定义和使用程序定义类型的示例,并在第13.7 课——结构体、成员和成员选择简介中介绍结构体。

命名程序定义类型

按照惯例,程序定义的类型以大写字母开头命名,并且不使用后缀(例如Fraction,而不是fraction,fraction_t或Fraction_t)。

最佳实践
程序定义的类型名称应以大写字母开头,并且不要使用后缀。

由于类型名称和变量名称非常相似,新手程序员有时会对如下的变量定义感到困惑:

Fraction fraction {}; // Instantiates a variable named fraction of type Fraction

这与其他变量定义并无不同:类型(Fraction)在前(因为 Fraction 首字母大写,所以我们知道它是程序自定义类型),然后是变量名(fraction),最后是可选的初始值设定项。由于 C++ 区分大小写,所以这里不存在命名冲突!

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

每个使用程序自定义类型的代码文件都需要在使用前看到完整的类型定义。仅仅进行前向声明是不够的。这是为了让编译器知道应该为该类型的对象分配多少内存。

为了将类型定义传播到需要它们的代码文件中,程序定义的类型通常定义在头文件中,然后通过 #include 指令包含在任何需要该类型定义的代码文件中。这些头文件通常与程序定义的类型同名(例如,名为 Fraction 的程序定义类型会在 Fraction.h 中定义)。

最佳实践:
仅在一个代码文件中使用的程序定义类型,应在该代码文件中尽可能靠近首次使用点进行定义。
在多个代码文件中使用的程序定义类型应该在与程序定义类型同名的头文件中定义,然后根据需要将其包含到每个代码文件中。

如果我们把 Fraction 类型移到一个头文件(名为 Fraction.h)中,以便它可以被包含在多个代码文件中,那么它的样子如下:

Fraction.h:

#ifndef FRACTION_H
#define FRACTION_H

// Define a new type named Fraction
// This only defines what a Fraction looks like, it doesn't create one
// Note that this is a full definition, not a forward declaration
struct Fraction
{
	int numerator {};
	int denominator {};
};

#endif

Fraction.cpp

#include "Fraction.h" // include our Fraction definition in this code file

// Now we can make use of our Fraction type
int main()
{
	Fraction f{ 3, 4 }; // this actually creates a Fraction object named f

	return 0;
}

类型定义部分不受单一定义规则 (ODR) 的约束。

在 2.7 课——前向声明和定义中,我们讨论了“单一定义规则”要求每个函数和全局变量在每个程序中只能有一个定义。要在不包含定义的文件中使用某个函数或全局变量,我们需要进行前向声明(通常通过头文件传递)。这样做的原理是,对于函数和非条件变量,声明足以满足编译器的要求,链接器随后可以将所有内容连接起来。

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

为了实现这一点,类型可以部分豁免于单一定义规则:允许在多个代码文件中定义给定的类型。

你已经运用了这种能力(可能没有意识到):如果你的程序有两个代码文件,并且它们都…… #include ,那么你正在将所有输入/输出类型定义导入到这两个文件中。

有两点需要注意。首先,每个代码文件仍然只能有一个类型定义(这通常不是问题,因为头文件保护机制会阻止这种情况发生)。其次,给定类型的所有类型定义必须完全相同,否则会导致未定义行为。

命名规则:用户定义类型与程序定义类型

“用户自定义类型”这个术语有时会在日常对话中出现,C++ 语言标准中也有提及(但没有定义)。在日常对话中,这个术语通常指的是“在你自己程序中定义的类型”(例如上面提到的 Fraction 类型示例)。

C++ 语言标准对“用户自定义类型”一词的使用方式比较特殊。在语言标准中,“用户自定义类型”指的是任何由你、标准库或实现定义的类类型或枚举类型(例如,编译器为了支持语言扩展而定义的类型)。或许有些出乎意料,这意味着std::string(标准库中定义的类类型)也被视为用户自定义类型!

为了进一步区分,C++20 语言标准对“程序定义类型”一词进行了清晰的定义,指的是那些并非作为标准库、实现或核心语言的一部分而定义的类类型和枚举类型。换句话说,“程序定义类型”仅包括由我们(或第三方库)定义的类类型和枚举类型。

因此,当我们只谈论为我们自己的程序定义的类类型和枚举类型时,我们会更喜欢“程序定义”这个术语,因为它有更精确的定义。

类型Type 意义Meaning 示例Examples
基本的Fundamental C++核心语言中内置的基本类型 int,std::nullptr_t
复合的Compound 根据其他类型定义的类型 int&、double*、std::string、Fraction
用户定义User-defined 类类型或枚举类型(包括标准库或实现中定义的类型)(在非正式用法中,通常指程序定义的类型) std::string,Fraction
程序定义Program-defined 类类型或枚举类型 (不包括标准库或实现中定义的类型) Fraction
posted @ 2025-12-20 10:23  游翔  阅读(2)  评论(0)    收藏  举报