章节目录

用户自定义类型简介

本节阅读量:

基本类型是C++语言核心的一部分,因此可以直接使用。如果要定义类型为int或double的变量,可以这样做:

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

由基本类型简单扩展而来的复合类型(包括函数、指针、引用和数组)也是如此:

1
2
3
4
void fcn(int) {}; // 定义类型为 void()(int) 的函数
int* ptr; // 定义类型为 'int 指针' 的变量
int& ref { x }; // 定义类型为 'int引用' 的变量
int arr[5]; // 定义有五个int的数组,类型为 int[5] (后续章节介绍)

这是可行的,因为C++语言已经知道这些类型名称(和符号)意味着什么——不需要提供或导入任何定义。

然而,考虑类型别名的情况,它允许为现有类型定义新名称。由于类型别名将新标识符引入程序,因此必须定义类型别名才能使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>

using length = int; // 定义 'length' 作为 int 的别名

int main()
{
    length x { 5 }; // 上面定义了 'length',所以这里可以使用
    std::cout << x << '\n';

    return 0;
}

如果省略length的定义,编译器将不知道length是什么,尝试使用length时会报错。length的定义并不会创建对象——它只是告诉编译器length是什么,以便以后可以使用。


什么是用户定义的类型?

上一章介绍了存储分数时遇到的挑战:分数具有概念上紧密关联的分子和分母。我们讨论了使用两个单独的整数分别存储分子和分母会带来的一些问题。

如果C++内置分数类型,那会很理想——但事实并非如此。C++没有内置数百种其他可能有用的类型,因为它不可能预测所有人可能需要的内容,更不用说逐一实现和测试它们。

相反,C++用另一种方式解决这类问题:允许我们创建全新的自定义类型!这样的类型通常称为用户定义类型(不过我们认为“程序定义类型”这个术语更准确——稍后会讨论二者差异)。C++有两类复合类型支持这种用法:枚举类型(包括非限定作用域枚举和限定作用域枚举)和类类型(包括struct、class和union)。


程序定义的类型

就像类型别名一样,程序定义的类型也必须在使用之前定义。

尽管我们还没有介绍结构体是什么,但下面的示例展示了自定义Fraction类型,以及如何使用该类型实例化对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 定义一个类型 Fraction,让编译器知道 Fraction 由什么组成
// (后续章节介绍 struct 如何使用)
// 这里只是说明 Fraction 的组成方式,并不实际创建一个对象
struct Fraction
{
	int numerator {};
	int denominator {};
};

// 现在可以使用 Fraction 类型
int main()
{
	Fraction f{ 3, 4 }; // 实例化一个类型 Fraction 的变量 f

	return 0;
}

在这个例子中,使用struct关键字来定义一个名为Fraction的新程序定义类型(在全局范围内,因此它可以在文件的其余部分使用)。这不会分配任何内存——它只是告诉编译器Fraction是什么样子的,所以可以稍后分配Fraction类型的对象。然后,在main()中,实例化(并初始化)一个名为f的Fraction类型的变量。

声明程序定义类型时,定义语句必须以分号结尾。忘记在类型定义末尾添加分号是常见错误,并且可能很难调试,因为编译器可能会在类型定义之后的行上报错。

在下一课中,我们将展示定义和使用程序定义类型的更多示例。


程序定义类型的命名惯例

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

由于类型名和变量名之间的相似性,新程序员有时会发现如下变量定义令人困惑:

1
Fraction fraction {}; // 实例化一个类型为 Fraction 的变量 fraction

这与任何其他变量定义没有什么不同:首先是类型(Fraction,因为Fraction以大写字母开头,所以我们知道它是程序定义类型),然后是变量名(fraction),最后是可选的初始值设定项。因为C++区分大小写,所以这里没有命名冲突!


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

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

为了将类型定义提供给需要它们的代码文件,通常会把程序定义类型放在头文件中,然后在需要该类型定义的代码文件里使用#include包含它。这些头文件通常与程序定义类型同名(例如,名为Fraction的程序定义类型会定义在fraction.h中)。

下面是一个示例,将Fraction类型移动到头文件(名为fraction.h)中,以便它可以被包含在多个代码文件中:

fraction.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#ifndef FRACTION_H
#define FRACTION_H

// 定义一个新类型 Fraction
// 这只是类型定义,并不实际创建对象
// 注意这是完整的声明,而不是前向声明
struct Fraction
{
	int numerator {};
	int denominator {};
};

#endif

fraction.cpp:

1
2
3
4
5
6
7
8
9
#include "fraction.h" // 在这里导入 Fraction 的定义

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

	return 0;
}

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

在前面,我们讨论了单定义规则,要求每个函数和全局变量在每个程序中只有一个定义。要在不包含定义的文件中使用给定的函数或全局变量,需要一个前向声明(通常通过头文件传播)。这是因为当涉及函数和非constexpr变量时,声明足以满足编译器的要求,然后链接器可以连接所有内容。

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

为了实现这一点,类型在一定程度上豁免于单定义规则:允许在多个代码文件中定义同一个类型。

您已经使用了该功能(可能没有意识到):如果程序有两个代码文件,它们都#include<iostream>,那么将把所有的输入/输出类型定义导入到这两个文件中。

有两个注意事项值得了解。首先,每个代码文件中仍然只能有一个类型定义(这通常不是问题,因为头文件保护会阻止重复包含)。其次,给定类型的所有类型定义都必须相同,否则将导致未定义的行为。


用户定义类型与程序定义类型

术语“用户定义类型”有时会出现在日常讨论中,也在C++语言标准中出现过(但没有被正式定义)。在非正式语境中,该术语通常表示“在您自己的程序中定义的类型”(例如上面的Fraction类型)。

C++语言标准以非常规的方式使用术语“用户定义类型”。在语言标准中,“用户定义类型”是由您、标准库或实现定义的任何类类型或枚举类型(例如,由编译器定义以支持语言扩展的类型)。也许与直觉相反,这意味着std::string(标准库中定义的类类型)被认为是用户定义的类型!

为了提供额外的区别,C++20语言标准定义了术语“程序定义类型”,以表示未定义为标准库、实现或核心语言的一部分的类类型和枚举类型。换句话说,“程序定义类型”仅包括由我们(或第三方库)定义的类类型和枚举类型。

因此,当只讨论我们为自己的程序定义的类类型和枚举类型时,我们更偏好使用“程序定义的”这个术语,因为它的含义更精确。

类别 含义 示例
基本类型 C++语言核心类型 int, std::nullptr_t
复合类型 由基本类型组合构建而成 int&, double*, std::string, Fraction
用户定义类型 类类型和枚举类型 (包含标准库与编译器实现的)(日常的使用的含义与程序定义类型相同) std::string, Fraction
程序定义类型 类类型和枚举类型 (排除标准库与编译器实现的) Fraction

12.14 第12章总结

上一节

13.1 非限定作用域枚举

下一节