Cpp学习17 第 13 章 复合类型:枚举与结构体

铁名_IronName Lv5

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

program-defined (user-defined) types
如果 C++ 内置了分数类型,那就完美了——可惜没有。此外,还有数百种其他潜在有用的类型 C++ 没有包含,因为根本不可能预见到用户可能需要的所有类型(更不用说实现和测试这些类型了)。
C++并没有采用传统方法解决这类问题,而是采用了不同的方式:它允许创建全新的自定义类型,供我们在程序中使用!这类类型被称为用户自定义类型 。不过,正如我们将在本课后面讨论的那样,对于我们自己创建的、用于程序中的这类类型,我们更倾向于使用 “程序自定义类型 ”这个术语。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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;
}

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

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

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

每个使用程序自定义类型的代码文件都需要在使用前看到完整的类型定义。仅仅进行前向声明是不够的。这是为了让编译器知道应该为该类型的对象分配多少内存。
为了将类型定义传播到需要它们的代码文件中,程序定义的类型通常定义在头文件中,然后通过 #include 指令包含在任何需要该类型定义的代码文件中。这些头文件通常与程序定义的类型同名(例如,名为 Fraction 的程序定义类型会在 Fraction.h 中定义)。

类型定义部分不受单一定义规则 (ODR) 的约束。
类似的前向声明方式并不适用于类型,因为编译器通常需要看到完整的类型定义才能使用给定的类型。我们必须能够将完整的类型定义传播到每个需要它的代码文件中。

13.2 — 无作用域枚举

枚举 enumeration(也称为枚举类型  enumerated type枚举 enum )是一种复合数据类型,其值被限制为一组命名的符号常量(称为枚举器 enumerators )。

枚举类型最好通过示例来讲解,所以让我们定义一个无作用域(Unscoped)的枚举,它可以保存一些颜色值。下面我们将解释它的工作原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Define a new unscoped enumeration named Color
enum Color
{
// Here are the enumerators
// These symbolic constants define all the possible values this type can hold
// Each enumerator is separated by a comma, not a semicolon
red,
green,
blue, // trailing comma optional but recommended
}; // the enum definition must end with a semicolon

int main()
{
// Define a few variables of enumerated type Color
Color apple { red }; // my apple is red
Color shirt { green }; // my shirt is green
Color cup { blue }; // my cup is blue

Color socks { white }; // error: white is not an enumerator of Color
Color hat { 2 }; // error: 2 is not an enumerator of Color

return 0;
}

枚举类型名称以大写字母开头,枚举器名称以小写字母开头。

创建的每个枚举类型都被视为一个不同的类型 ,这意味着编译器可以将其与其他类型区分开来。由于枚举类型是不同的,因此定义为一个枚举类型一部分的枚举器不能用于另一个枚举类型的对象(编译错误)。

最佳实践

最好将枚举放在命名作用域区域(例如命名空间或类)内,这样枚举器就不会污染全局命名空间。

与枚举器进行比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

enum Color
{
red,
green,
blue,
};

int main()
{
Color shirt{ blue };

if (shirt == blue) // if the shirt is blue
std::cout << "Your shirt is blue!";
else
std::cout << "Your shirt is not blue!";

return 0;
}

13.3 — 无作用域枚举器 整型转换

1
2
3
4
5
6
7
8
9
10
11
enum Color
{
black, // 0
red, // 1
blue, // 2
green, // 3
white, // 4
cyan, // 5
yellow, // 6
magenta, // 7
};

可以显式定义枚举值。这些整数值可以是正数或负数,并且可以与其他枚举值相同。任何未定义的枚举值都比前一个枚举值大 1。

1
2
3
4
5
6
7
8
9
enum Animal
{
cat = -3, // values can be negative
dog, // -2
pig, // -1
horse = 5,
giraffe = 5, // shares same value as horse
chicken, // 6
};

大多数情况下,枚举器的默认值正是你想要的,所以除非有特殊原因,否则不要提供自己的值。

最佳实践

将表示 0 的枚举器设置为最符合枚举默认含义的枚举器。如果没有合适的默认含义,请考虑添加一个值为 0 的“无效”或“未知”枚举器,以便明确记录该状态,并在适当的时候进行显式处理。

无作用域枚举值将隐式转换为整数值。

用于表示枚举器值的具体整数类型称为枚举的底层类型 (或类型)。
可以为枚举显式指定底层类型。底层类型必须是整型。例如,如果您在对带宽要求较高的环境中工作(例如通过网络发送数据),则可能需要为枚举指定一个较小的类型。(显然我不太会遇到这种情况)

编译器 不会 将整数隐式转换为非作用域枚举。所以用 static_cast<Enum>(x) 吧

13.4 — 枚举与字符串之间的转换

在下面的示例中,我们使用 switch 语句选择一个枚举值,并返回该枚举值对应的颜色字符串字面量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <string_view>

enum Color
{
black,
red,
blue,
};

constexpr std::string_view getColorName(Color color)
{
switch (color)
{
case black: return "black";
case red: return "red";
case blue: return "blue";
default: return "???";
}
}

int main()
{
constexpr Color shirt{ blue };

std::cout << "Your shirt is " << getColorName(shirt) << '\n';

return 0;
}

从 字符串 获取 枚举值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
#include <optional> // for std::optional
#include <string>
#include <string_view>

enum Pet
{
cat, // 0
dog, // 1
pig, // 2
whale, // 3
};

constexpr std::string_view getPetName(Pet pet)
{
switch (pet)
{
case cat: return "cat";
case dog: return "dog";
case pig: return "pig";
case whale: return "whale";
default: return "???";
}
}

constexpr std::optional<Pet> getPetFromString(std::string_view sv)
{
// We can only switch on an integral value (or enum), not a string
// so we have to use if-statements here
if (sv == "cat") return cat;
if (sv == "dog") return dog;
if (sv == "pig") return pig;
if (sv == "whale") return whale;

return {};
}

int main()
{
std::cout << "Enter a pet: cat, dog, pig, or whale: ";
std::string s{};
std::cin >> s;

std::optional<Pet> pet { getPetFromString(s) };

if (!pet)
std::cout << "You entered an invalid pet\n";
else
std::cout << "You entered: " << getPetName(*pet) << '\n';

return 0;
}

如果我们可以直接 std::cin >> pet 就更好了。我们将在接下来的 13.5 课——I/O 运算符重载简介中介绍这一点。

13.5 — I/O 运算符重载简介

类似地,C++ 也支持运算符重载 ,这允许我们定义现有运算符的重载,以便我们可以使这些运算符与我们程序定义的数据类型一起使用。

基本运算符重载相当简单:

  • 使用运算符的名称作为函数名来定义函数。
  • 为每个操作数(从左到右)添加一个适当类型的参数。其中一个参数必须是用户自定义类型(类类型或枚举类型),否则编译器会报错。
  • 将返回类型设置为任何合适的类型。
  • 使用 return 语句返回操作结果。

重载  operator<<  打印枚举器

实现 operator<< 的重载来打印 Color :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <string_view>

enum Color
{
black,
red,
blue,
};

constexpr std::string_view getColorName(Color color)
{
switch (color)
{
case black: return "black";
case red: return "red";
case blue: return "blue";
default: return "???";
}
}

// Teach operator<< how to print a Color
// std::ostream is the type of std::cout, std::cerr, etc...
// The return type and parameter type are references (to prevent copies from being made)
std::ostream& operator<<(std::ostream& out, Color color)
{
out << getColorName(color); // print our color's name to whatever output stream out
return out; // operator<< conventionally returns its left operand

// The above can be condensed to the following single line:
// return out << getColorName(color)
}

int main()
{
Color shirt{ blue };
std::cout << "Your shirt is " << shirt << '\n'; // it works!

return 0;
}

重载 operator>> 以输入枚举器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <iostream>
#include <limits>
#include <optional>
#include <string>
#include <string_view>

enum Pet
{
cat, // 0
dog, // 1
pig, // 2
whale, // 3
};

constexpr std::string_view getPetName(Pet pet)
{
switch (pet)
{
case cat: return "cat";
case dog: return "dog";
case pig: return "pig";
case whale: return "whale";
default: return "???";
}
}

constexpr std::optional<Pet> getPetFromString(std::string_view sv)
{
if (sv == "cat") return cat;
if (sv == "dog") return dog;
if (sv == "pig") return pig;
if (sv == "whale") return whale;

return {};
}

// pet is an in/out parameter
std::istream& operator>>(std::istream& in, Pet& pet)
{
std::string s{};
in >> s; // get input string from user

std::optional<Pet> match { getPetFromString(s) };
if (match) // if we found a match
{
pet = *match; // dereference std::optional to get matching enumerator
return in;
}

// We didn't find a match, so input must have been invalid
// so we will set input stream to fail state
in.setstate(std::ios_base::failbit);

// On an extraction failure, operator>> zero-initializes fundamental types
// Uncomment the following line to make this operator do the same thing
// pet = {};

return in;
}

int main()
{
std::cout << "Enter a pet: cat, dog, pig, or whale: ";
Pet pet{};
std::cin >> pet;

if (std::cin) // if we found a match
std::cout << "You chose: " << getPetName(pet) << '\n';
else
{
std::cin.clear(); // reset the input stream to good
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "Your pet was not valid\n";
}

return 0;
}

13.6 — 作用域枚举(枚举类)

作用域枚举 (在 C++ 中通常称为枚举类 ,原因很快就会变得很明显)
要创建作用域枚举,我们使用关键字 enum class 。作用域枚举定义的其余部分与非作用域枚举定义相同。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
int main()
{
enum class Color // "enum class" defines this as a scoped enumeration rather than an unscoped enumeration
{
red, // red is considered part of Color's scope region
blue,
};

enum class Fruit
{
banana, // banana is considered part of Fruit's scope region
apple,
};

Color color { Color::red }; // note: red is not directly accessible, we have to use Color::red
Fruit fruit { Fruit::banana }; // note: banana is not directly accessible, we have to use Fruit::banana

if (color == fruit) // compile error: the compiler doesn't know how to compare different types Color and Fruit
std::cout << "color and fruit are equal\n";
else
std::cout << "color and fruit are not equal\n";

return 0;
}

该程序在第 19 行产生编译错误,因为作用域枚举无法转换为任何可以与其他类型进行比较的类型。

作用域枚举不会隐式转换为整数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <utility> // for std::to_underlying() (C++23)

int main()
{
enum class Color
{
red,
blue,
};

Color color { Color::blue };

std::cout << color << '\n'; // won't work, because there's no implicit conversion to int
std::cout << static_cast<int>(color) << '\n'; // explicit conversion to int, will print 1
std::cout << std::to_underlying(color) << '\n'; // convert to underlying type, will print 1 (C++23)

return 0;
}

也可以将整数 static_cast 为作用域枚举器,这在处理用户输入时非常有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

int main()
{
enum class Pet
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};

std::cout << "Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): ";

int input{};
std::cin >> input; // input an integer

Pet pet{ static_cast<Pet>(input) }; // static_cast our integer to a Pet

return 0;
}

除非有充分的理由,否则应优先使用作用域枚举而不是非作用域枚举。
尽管作用域枚举提供了诸多好处,但在 C++ 中仍然普遍使用非作用域枚举,因为在某些情况下,我们希望隐式转换为 int(进行大量的 static_casting 会很麻烦),而且我们不需要额外的命名空间。(?到底用哪个?)

简化作用域枚举器到整数的转换,略。

using enum 语句 C++20

这在某些情况下非常有用,例如在 switch 语句中,否则我们可能会遇到许多相同的重复前缀:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <string_view>

enum class Color
{
black,
red,
blue,
};

constexpr std::string_view getColor(Color color)
{
using enum Color; // bring all Color enumerators into current scope (C++20)
// We can now access the enumerators of Color without using a Color:: prefix

switch (color)
{
case black: return "black"; // note: black instead of Color::black
case red: return "red";
case blue: return "blue";
default: return "???";
}
}

int main()
{
Color shirt{ Color::blue };

std::cout << "Your shirt is " << getColor(shirt) << '\n';

return 0;
}

添加了 using enum Color; 语句,这使得我们可以在不使用 Color:: 前缀的情况下访问这些枚举值。

13.7 — 结构体、成员和成员选择简介

1
2
3
4
5
6
struct Employee
{
int id {};
int age {};
double wage {};
};

结构体中的变量称为数据成员 (或成员变量 )。
需要提醒的是, Employee 只是一个类型定义——此时实际上并没有创建任何对象。

要访问特定的成员变量,我们需要在结构体变量名和成员名之间使用成员选择运算符 ( operator. )。例如,要访问 Joe 的年龄成员,我们可以使用 joe.age 。

13.8 — 结构体聚合初始化

什么是聚合体?

在一般编程中, 聚合数据类型 (也称为聚合体 )是指任何可以包含多个数据成员的类型。某些类型的聚合体允许成员具有不同的类型(例如结构体),而另一些类型则要求所有成员必须是同一类型(例如数组)。

仅包含数据成员的结构体是聚合体。

结构体的聚合初始化

聚合使用一种称为聚合初始化的初始化方式,它允许我们直接初始化聚合的成员。为此,我们提供一个初始化列表作为初始化器,它只是一个用花括号括起来的、以逗号分隔的值列表。

聚合初始化主要有两种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Employee
{
int id {};
int age {};
double wage {};
};

int main()
{
Employee frank = { 1, 32, 60000.0 }; // copy-list initialization using braced list
Employee joe { 2, 28, 45000.0 }; // list initialization using braced list (preferred)

return 0;
}

这些初始化形式均采用成员级初始化 ,这意味着结构体中的每个成员都按照声明顺序进行初始化。

重载 operator<< 以打印结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

struct Employee
{
int id {};
int age {};
double wage {};
};

std::ostream& operator<<(std::ostream& out, const Employee& e)
{
out << "id: " << e.id << " age: " << e.age << " wage: " << e.wage;
return out;
}

int main()
{
Employee joe { 2, 28 }; // joe.wage will be value-initialized to 0.0
std::cout << joe << '\n';

return 0;
}

结构体类型的变量可以是 const(或 constexpr),并且与所有 const 变量一样,它们必须进行初始化。

C++20 中带有指定初始化器的赋值

1
2
Employee joe { 1, 32, 60000.0 };
joe = { .id = joe.id, .age = 33, .wage = 66000.0 };

使用相同类型的另一个结构体初始化一个结构体。

1
2
3
4
5
Foo foo { 1, 2, 3 };

Foo x = foo; // copy-initialization
Foo y(foo); // direct-initialization
Foo z {foo}; // direct-list-initialization

13.9 — 默认成员初始化

定义结构体(或类)类型时,我们可以在类型定义中为每个成员提供默认初始化值。对于未标记为 static 成员,此过程有时称为非静态成员初始化 。该初始化值称为默认成员初始化器 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Something
{
int x; // no default initialization value (bad)
int y {}; // value-initialized by default
int z { 2 }; // explicit default value
};

int main()
{
Something s1; // No initializer list: s1.x is uninitialized, s1.y and s1.z use defaults
Something s2 { 5, 6, 7 }; // Explicit initializers: s2.x, s2.y, and s2.z use explicit values (no default values are used)
Something s3 {}; // Missing initializers: s3.x is value initialized, s3.y and s3.z use defaults

return 0;
}
最佳实践

为所有成员提供默认值。这样即使变量定义中没有包含初始化列表,也能确保成员被初始化。

对于聚合,优先使用值初始化(使用空花括号初始化器)而不是默认初始化(不使用花括号)。

1
2
Fraction f1;          // f1.numerator value initialized to 0, f1.denominator defaulted to 1
Fraction f2 {}; // f2.numerator value initialized to 0, f2.denominator defaulted to 1

13.10 — 传递和返回 结构体

使用结构体而非单个变量的一大优势在于,我们可以将整个结构体传递给需要操作其成员的函数。为了避免复制,结构体通常按引用传递(通常是常量引用)。

13.11 — 结构杂项

具有程序定义成员的结构体

1
2
3
4
5
6
7
8
9
10
11
12
struct Employee
{
int id {};
int age {};
double wage {};
};

struct Company
{
int numberOfEmployees {};
Employee CEO {}; // Employee is a struct within the Company struct
};
1
2
3
4
5
6
7
8
9
10
11
12
struct Company
{
struct Employee // accessed via Company::Employee
{
int id{};
int age{};
double wage{};
};

int numberOfEmployees{};
Employee CEO{}; // Employee is a struct within the Company struct
};

大多数情况下,我们希望结构体(和类)拥有所有权。最简单的实现方法是确保每个数据成员都有一个所有权类型(例如,不是查看器、指针或引用)。
这就是为什么字符串数据成员几乎总是 std::string 类型(所有者),而不是 std::string_view 类型(查看器)。

结构体的大小 至少 等于它所包含的所有变量的大小之和。但它也可能更大!出于性能考虑,编译器有时会在结构体中添加一些空隙(这称为填充 )。
可以通过按大小递减的顺序定义成员来最大限度地减少填充。C++编译器不允许重新排列成员,因此必须手动完成此操作。

13.12 — 指针和引用的 成员选择

结构体指针的成员选择

为了使语法更简洁,C++ 提供了一个从指针中选择成员的运算符(->) (有时也称为箭头运算符 ),可用于从指向对象的指针中选择成员:

1
2
Employee* ptr{ &joe };
std::cout << ptr->id << '\n'; // Better: use -> to select member from pointer to object

链式 operator->

如果通过 operator-> 访问的成员是指向类类型的指针,则可以在同一个表达式中再次应用 operator-> 来访问该类类型的成员。

13.13 — 类模板

就像函数模板是用于实例化函数的模板定义一样, 类模板是用于实例化 类(class)类型的模板定义。

把 pair 类重写成一个类模板, max() 是以函数模板的形式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

template <typename T>
struct Pair
{
T first{};
T second{};
};

template <typename T>
constexpr T max(Pair<T> p)
{
return (p.first < p.second ? p.second : p.first);
}

int main()
{
Pair<int> p1{ 5, 6 };
std::cout << max<int>(p1) << " is larger\n"; // explicit call to max<int>

Pair<double> p2{ 1.2, 3.4 };
std::cout << max(p2) << " is larger\n"; // call to max<double> using template argument deduction (prefer this)

return 0;
}

类模板可以包含一些使用模板类型的成员,以及一些使用普通(非模板)类型的成员。

类模板也可以包含多种模板类型。

std::pair

由于处理成对数据很常见,C++ 标准库包含一个名为 std::pair 的类模板(位于 <utility> 头文件中),其定义与上一节中具有多个模板类型的 Pair 类模板完全相同。

在多个文件中使用类模板

与函数模板类似,类模板通常定义在头文件中,以便可以被任何需要它们的代码文件包含。模板定义和类型定义都不受“只能定义一次”规则的限制,因此不会造成问题。
 
 pair.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef PAIR_H
#define PAIR_H

template <typename T>
struct Pair
{
T first{};
T second{};
};

template <typename T>
constexpr T max(Pair<T> p)
{
return (p.first < p.second ? p.second : p.first);
}

#endif

13.14 — 类模板论证推导 (CTAD) 和推导指南

Class template argument deduction (CTAD)
从 C++17 开始,当从类模板实例化对象时,编译器可以从对象初始化器的类型推断出模板类型(这称为类模板参数推导 ,简称 CTAD )。

1
2
3
4
5
6
7
8
9
#include <utility> // for std::pair

int main()
{
std::pair<int, int> p1{ 1, 2 }; // explicitly specify class template std::pair<int, int> (C++11 onward)
std::pair p2{ 1, 2 }; // CTAD used to deduce std::pair<int, int> from the initializers (C++17)

return 0;
}

略。CTAD 不适用于非静态成员初始化;CTAD 不适用于函数参数
还是少用推导吧…

13.15 — 别名模板

在其他情况下,我们可能需要为模板类定义类型别名,但并非所有模板参数都包含在别名中(而是由类型别名的用户提供)。为此,我们可以定义别名模板 ,该模板可用于实例化类型别名。

1
2
template <typename T>
using Coord = Pair<T>;

也就是把 Pair<T> 换成 Coord<T>。感觉用处不大,还很麻烦…
不如 using Coord = Pair<int>

“别名模板”是给“类模板”起外号,专门用来简化复杂泛型代码的;而“具体别名”是给“具体类型”起外号,专门用来简化业务代码的。

quiz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
enum class MonsterType
{
ogre,dragon,orc,giant_spider,slime,
};

struct Monster
{
MonsterType type{ MonsterType::ogre };
std::string name{"unknown"};
int health{ 100 };
};

constexpr std::string_view getMonsterTypeName(MonsterType monster)
{
switch (monster)
{
case MonsterType::ogre: return "Ogre";
case MonsterType::dragon: return "Dragon";
case MonsterType::orc: return "Orc";
case MonsterType::giant_spider: return "Giant Spider";
case MonsterType::slime: return "Slime";
default: return "???";
}
}

void printMonster(const Monster& m)
{
std::cout << "This " << getMonsterTypeName(m.type) << " is named "
<< m.name << " and has " << m.health << " health.\n";
}

参考答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct Monster
{
// Define our different monster types as an enum
enum Type
{
ogre,
dragon,
orc,
giant_spider,
slime,
};

Type type{};
std::string name{}; // the Monster should be an owner of its name
int health{};
};

// Return the name of the monster's type as a string
// Since this could be used elsewhere, it's better to make this its own function
constexpr std::string_view getMonsterTypeString(Monster::Type type)
{
switch (type)
{
case Monster::ogre: return "Ogre";
case Monster::dragon: return "Dragon";
case Monster::orc: return "Orc";
case Monster::giant_spider: return "Giant Spider";
case Monster::slime: return "Slime";
}

return "Unknown";
}

Question #3 涉及推导CTAD。不做了。

passed.

13.y — 使用语言参考

更好的选择是先查阅参考指南。与教程不同,教程往往侧重于最重要的主题,并使用非正式/通俗易懂的语言来简化学习,而参考指南则使用正式的术语来精确描述 C++。正因如此,参考资料往往内容全面、准确,但……也更难理解。
如何使用 cppreference
Cppreference 并非官方文档来源,而是一个维基。
C++ 的唯一官方来源是标准 ( GitHub 上的免费草案),这是一份正式文档,不易用作参考。

  • 标题: Cpp学习17 第 13 章 复合类型:枚举与结构体
  • 作者: 铁名_IronName
  • 创建于 : 2026-08-11 09:51:58
  • 更新于 : 2026-08-11 13:27:44
  • 链接: https://blog.ironname.top/2026/Cpp/Cpp学习17/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论