章节目录

用户自定义类型简介

本节阅读量:

基本类型被定义为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 非限定作用域枚举

下一节