// Define a new unscoped enumeration named Color enumColor { // 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
intmain() { // 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
intmain() { Pet myPet { black }; // compile error: black is not an enumerator of Pet Color shirt { pig }; // compile error: pig is not an enumerator of Color
FileReadResult readFileContents() { if (!openFile()) return readResultErrorFileOpen; if (!readFile()) return readResultErrorFileRead; if (!parseFile()) return readResultErrorFileParse;
voidsortData(SortOrder order) { switch (order) { case alphabetical: // sort data in forwards alphabetical order break; case alphabeticalReverse: // sort data in backwards alphabetical order break; case numerical: // sort data numerically break; } }
许多编程语言使用枚举来定义布尔值(Boolean)——毕竟,布尔值本质上只是一个包含两个枚举成员(false和true)的枚举类型!然而,在 C++ 中,true 和 false 被定义为关键字(keywords),而不是枚举成员(enumerators)。
enumColor { red, green, blue, // blue is put into the global namespace };
intmain() { Color apple { red }; // okay, accessing enumerator from global namespace Color raspberry { Color::red }; // also okay, accessing enumerator from scope of Color
对于非限定枚举(unscoped enumerations),C++ 标准并未指定应使用哪种特定的整数类型作为其底层类型,因此该选择是由具体实现定义的。大多数编译器会使用 int 作为底层类型(这意味着非限定枚举的大小与 int 相同),除非需要更大的类型来存储枚举成员的值。但是,你不应假设在所有编译器或平台上都是如此。可以显式指定枚举的基础类型。基础类型必须是整型。例如,如果你在一些带宽敏感的上下文中工作(例如,通过网络发送数据),你可能想为枚举指定一个更小的类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
#include<cstdint>// for std::int8_t #include<iostream>
// Use an 8-bit integer as the enum underlying type enumColor : std::int8_t { black, red, blue, };
intmain() { Color c{ black }; std::cout << sizeof(c) << '\n'; // prints 1 (byte)
intmain() { Pet pet { 2 }; // compile error: integer value 2 won't implicitly convert to a Pet pet = 3; // compile error: integer value 3 won't implicitly convert to a Pet
intmain() { Pet pet1 { 2 }; // ok: can brace initialize unscoped enumeration with specified base with integer (C++17) Pet pet2(2); // compile error: cannot direct initialize with integer Pet pet3 = 2; // compile error: cannot copy initialize with integer
pet1 = 3; // compile error: cannot assign with integer
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"???"; } }
intmain() { std::cout << "Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): ";
int input{}; std::cin >> input; // input an integer
if (input < 0 || input > 3) std::cout << "You entered an invalid pet\n"; else { Pet pet{ static_cast<Pet>(input) }; // static_cast our integer to a Pet std::cout << "You entered: " << getPetName(pet) << '\n'; }
return0; }
如果用户能输入一个代表枚举成员的字符串(例如 “pig”),而不是输入一个数字,那就更好了,我们可以将该字符串转换为适当的 Pet 枚举成员。但是,这样做有几个问题。首先,我们不能打开字符串,因此我们需要使用其他东西来匹配用户传入的字符串。这里最简单的方法是使用一系列 if 语句。其次,如果用户传入无效字符串,我们应该返回哪个 Pet 枚举成员?一种选择是添加一个枚举成员来表示 “none/invalid”,并返回它。但是,更好的选择是在此处使用 std::optional。
#include<algorithm>// for std::transform #include<cctype>// for std::tolower #include<iterator>// for std::back_inserter #include<string> #include<string_view>
// This function returns a std::string that is the lower-case version of the std::string_view passed in. // Only 1:1 character mapping can be performed by this function std::string toASCIILowerCase(std::string_view sv) { std::string lower{}; std::transform(sv.begin(), sv.end(), std::back_inserter(lower), [](char c) { returnstatic_cast<char>(std::tolower(static_cast<unsignedchar>(c))); }); return lower; }
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) }
intmain() { Color shirt{ blue }; std::cout << "Your shirt is " << shirt << '\n'; // it works!
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; }
intmain() { 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"; }
#include<iostream> intmain() { enum classColor// "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 classFruit { 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";
return0; }
此程序在第 19 行生成编译错误,因为限定枚举不会转换为任何可以与另一种类型进行比较的类型。
class 关键字(以及 static 关键字)是 C++ 语言中重载最多的关键字之一,根据上下文的不同,它可能具有不同的含义。尽管限定枚举使用 class 关键字,但它们不被视为“类类型”(保留给结构体、类和联合体)。
intmain() { enum classColor// "enum class" defines this as a scoped enum rather than an unscoped enum { red, // red is considered part of Color's scope region blue, };
std::cout << red << '\n'; // compile error: red not defined in this scope region std::cout << Color::red << '\n'; // compile error: std::cout doesn't know how to print this (will not implicitly convert to int)
if (shirt == Color::red) // this Color to Color comparison is okay std::cout << "The shirt is red!\n"; elseif (shirt == Color::blue) std::cout << "The shirt is blue!\n";
#include<iostream> #include<utility>// for std::to_underlying() (C++23)
intmain() { enum classColor { 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)
// Overload the unary + operator to convert an enum to the underlying type // adapted from https://stackoverflow.com/a/42198760, thanks to Pixelchemist for the idea // In C++23, you can #include <utility> and return std::to_underlying(a) instead template <typename T> constexprautooperator+(T a) noexcept { returnstatic_cast<std::underlying_type_t<T>>(a); }
intmain() { std::cout << +Animals::elephant << '\n'; // convert Animals::elephant to an integer using unary operator+
return0; }
输出:
1
3
在 C++20 中引入的 using enum 语句将所有枚举成员从枚举导入到当前作用域中。当与枚举类类型一起使用时,这允许我们访问枚举类枚举成员,而无需在每个枚举类的前缀前加上枚举类的名称。这在我们有许多相同、重复的前缀的情况下非常有用,例如在 switch 语句中:
constexpr std::string_view getColor(Color color) { usingenumColor; // 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"???"; } }
intmain() { Color shirt{ Color::blue };
std::cout << "Your shirt is " << getColor(shirt) << '\n';
int totalAge { joe.age + frank.age }; std::cout << "Joe and Frank have lived " << totalAge << " total years\n";
if (joe.wage > frank.wage) std::cout << "Joe makes more than Frank\n"; elseif (joe.wage < frank.wage) std::cout << "Joe makes less than Frank\n"; else std::cout << "Joe and Frank make the same amount\n";
// Frank got a promotion frank.wage += 5000.0;
// Today is Joe's birthday ++joe.age; // use pre-increment to increment Joe's age by 1
return0; }
8. 结构体聚合初始化
与普通变量非常相似,默认情况下不会初始化数据成员。请考虑以下结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#include<iostream>
structEmployee { int id; // note: no initializer here int age; double wage; };
intmain() { Employee joe; // note: no initializer here either std::cout << joe.id << '\n';
return0; }
因为我们没有提供任何初始化器,所以当 joe 实例化时,joe.id、joe.age 和 joe.wage 都将被取消初始化。然后,当我们尝试打印 joe.id 的值时,我们将得到未定义行为。
在通用编程中,聚合数据类型(aggregate data type,简称聚合)是指任何可以包含多个数据成员的类型。有些聚合类型允许成员具有不同的类型(例如结构体struct),而其他类型则要求所有成员必须是相同的类型(例如数组array)。在 C++ 中,聚合的定义更为狭窄,并且相对复杂得多。
简单来说,在 C++ 中,聚合(aggregate)要么是一个 C 风格的数组,要么是一个类类型(struct、class 或 union),并满足以下条件:
structEmployee { int id {}; int age {}; double wage {}; };
intmain() { Employee frank = { 1, 32, 60000.0 }; // copy-list initialization using braced list Employee joe { 2, 28, 45000.0 }; // list initialization using braced list (preferred)
Foo x = foo; // copy-initialization Foo y(foo); // direct-initialization Foo z {foo}; // direct-list-initialization
std::cout << x << '\n'; std::cout << y << '\n'; std::cout << z << '\n';
return0; }
这将打印:
1 2 3
1 2 3 1 2 3 1 2 3
9. 默认成员初始化
当我们定义一个结构体(或类)类型时,可以为每个成员提供一个默认初始化值,作为类型定义的一部分。对于未标记为 static 的成员,这个过程有时被称为非静态成员初始化(non-static member initialization)。初始化值称为默认成员初始化器(default member initializer)。下面是一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13
structSomething { int x; // no initialization value (bad) int y {}; // value-initialized by default int z { 2 }; // explicit default value };
intmain() { Something s1; // s1.x is uninitialized, s1.y is 0, and s1.z is 2
structSomething { int x; // no default initialization value (bad) int y {}; // value-initialized by default int z { 2 }; // explicit default value };
intmain() { 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
intmain() { Employee joe { 14, 32, 24.15 }; Employee frank { 15, 28, 18.27 };
// Print Joe's information printEmployee(joe);
std::cout << '\n';
// Print Frank's information printEmployee(frank);
return0; }
在上面的例子中,我们将整个 Employee 传递给 printEmployee()。
上述程序输出:
1 2 3 4 5 6 7
ID: 14 Age: 32 Wage: 24.15
ID: 15 Age: 28 Wage: 18.27
在前面的示例中,我们在将 Employee 变量 joe 传递给 printEmployee() 函数之前创建了该变量。在我们只使用一次变量的情况下,必须为变量命名并分隔该变量的创建和使用可能会增加复杂性。在这种情况下,最好改用临时对象。临时对象不是变量,因此它没有标识符。下面是与上述相同的示例,但我们已将变量 joe 和 frank 替换为临时对象:
intmain() { // Print Joe's information printEmployee(Employee { 14, 32, 24.15 }); // construct a temporary Employee to pass to function (type explicitly specified) (preferred)
std::cout << '\n';
// Print Frank's information printEmployee({ 15, 28, 18.27 }); // construct a temporary Employee to pass to function (type deduced from parameter)
Point3d getZeroPoint() { // We already specified the type at the function declaration // so we don't need to do so here again return { 0.0, 0.0, 0.0 }; // return an unnamed Point3d }
这被视为隐式转换。
11. 结构体杂项
在 C++ 中,结构体(以及类)可以包含其他程序自定义类型的成员。对此,有两种实现方式。首先,我们可以在全局作用域中定义一个程序自定义类型,然后将其用作另一个程序自定义类型的成员:
structEmployee { int id {}; int age {}; double wage {}; };
structCompany { int numberOfEmployees {}; Employee CEO {}; // Employee is a struct within the Company struct };
intmain() { Company myCompany{ 7, { 1, 32, 55000.0 } }; // Nested initialization list to initialize Employee std::cout << myCompany.CEO.wage << '\n'; // print the CEO's wage
return0; }
在上面的例子中,我们定义了一个 Employee 体,然后将其用作 Company 结构体中的成员。当我们初始化 Company 时,我们还可以使用嵌套初始化列表来初始化我们的 Employee。如果我们想知道 CEO 的薪水是多少,我们只需使用两次成员选择运算符:myCompany.CEO.wage。
其次,类型也可以嵌套在其他类型中,因此如果 Employee 类型仅作为 Company 的一部分存在,那么 Employee 类型可以嵌套在 Company 结构体中:
intmain() { std::cout << "The size of short is " << sizeof(short) << " bytes\n"; std::cout << "The size of int is " << sizeof(int) << " bytes\n"; std::cout << "The size of double is " << sizeof(double) << " bytes\n";
std::cout << "The size of Foo is " << sizeof(Foo) << " bytes\n";
return0; }
示例输出:
1 2 3 4
The size of short is 2 bytes The size of int is 4 bytes The size of double is 8 bytes The size of Foo is 16 bytes
注意 short + int + double 的大小是 14 字节,但 Foo 的大小是 16 字节!事实证明,我们只能说结构体的大小至少与它包含的所有变量的大小一样大。但它可能会更大!出于性能原因,编译器有时会在结构体中添加间隙(这称为填充)。
structTriangle { Point* a {}; Point* b {}; Point* c {}; };
intmain() { Point a {1,2}; Point b {3,7}; Point c {10,2};
Triangle tr { &a, &b, &c }; Triangle* ptr {&tr};
// ptr is a pointer to a Triangle, which contains members that are pointers to a Point // To access member y of Point c of the Triangle pointed to by ptr, the following are equivalent:
intmain() { 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)
return0; }
类模板可以包含一些使用模板类型的成员,而其他成员使用普通(非模板)类型。例如:
1 2 3 4 5 6
template <typename T> structFoo { T first{}; // first will have whatever type T is replaced with int second{}; // second will always have type int, regardless of what type T is };
intmain() { Pair<int, double> p1{ 1, 2.3 }; // a pair holding an int and a double Pair<double, int> p2{ 4.5, 6 }; // a pair holding a double and an int Pair<int, int> p3{ 7, 8 }; // a pair holding two ints
template <typename T, typename U> structPair { T first{}; U second{}; };
structPoint { int first{}; int second{}; };
template <typename T> voidprint(T p)// type template parameter will match anything { std::cout << '[' << p.first << ", " << p.second << ']'; // will only compile if type has first and second members }
Point p2 { 7, 8 }; print(p2); // matches print(Point)
std::cout << '\n';
return0; }
有一种情况可能会产生误导。请考虑以下版本的 print():
1 2 3 4 5 6 7 8 9 10 11 12
template <typename T, typename U> structPair// defines a class type named Pair { T first{}; U second{}; };
template <typename Pair> // defines a type template parameter named Pair (shadows Pair class type) voidprint(Pair p)// this refers to template parameter Pair, not class type Pair { std::cout << '[' << p.first << ", " << p.second << ']'; }
template <typename T, typename U> voidprint(std::pair<T, U> p) { // the members of std::pair have predefined names `first` and `second` std::cout << '[' << p.first << ", " << p.second << ']'; }
intmain() { std::pair<int, double> p1{ 1, 2.3 }; // a pair holding an int and a double std::pair<double, int> p2{ 4.5, 6 }; // a pair holding a double and an int std::pair<int, int> p3{ 7, 8 }; // a pair holding two ints
intmain() { 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)
return0; }
仅当不存在模板参数列表时,才会执行 CTAD。因此,以下两个都是错误:
1 2 3 4 5 6 7 8 9
#include<utility>// for std::pair
intmain() { std::pair<> p1 { 1, 2 }; // error: too few template arguments, both arguments not deduced std::pair<int> p2 { 3, 4 }; // error: too few template arguments, second argument not deduced
template <typename T, typename U> structPair { T first{}; U second{}; };
// Here's a deduction guide for our Pair (needed in C++17 only) // Pair objects initialized with arguments of type T and U should deduce to Pair<T, U> template <typename T, typename U> Pair(T, U) -> Pair<T, U>;
intmain() { Pair<int, int> p1{ 1, 2 }; // explicitly specify class template Pair<int, int> (C++11 onward) Pair p2{ 1, 2 }; // CTAD used to deduce Pair<int, int> from the initializers (C++17)
return0; }
首先,我们使用与我们的 Pair 类相同的模板类型定义。这是合理的,因为如果我们的推导指南要告诉编译器如何推导 Pair<T, U> 的类型,我们必须定义 T 和 U 是什么(模板类型)。其次,在箭头的右边,我们放置我们希望编译器帮助推导的类型。在这种情况下,我们希望编译器能够推导 Pair<T, U> 类型的对象的模板参数,所以我们在这里正是放入了这个内容。最后,在箭头的左边,我们告诉编译器应该查找哪种声明。在这种情况下,我们告诉编译器查找一个有两个参数(一个类型为 T,另一个类型为 U)的 Pair 声明。我们也可以将其写为 Pair(T t, U u)(其中 t 和 u 是参数的名称,但由于我们不使用 t 和 u,所以不需要给它们命名)。综上所述,我们告诉编译器,如果它看到一个有两个参数(分别为 T 和 U 类型)的 Pair 声明,它应该推导该类型为 Pair<T, U>。
就像函数参数可以具有默认参数一样,模板参数也可以被赋予默认值。当模板参数未明确指定且无法推导时,将使用这些参数。以下是对上述 Pair<T, U> 类模板程序的修改,类型模板参数 T 和 U 默认为 int 类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
template <typename T=int, typename U=int> // default T and U to type int struct Pair { T first{}; U second{}; };
template <typename T> structPair { T first{}; T second{}; };
// Here's our alias template // Alias templates must be defined in global scope template <typename T> using Coord = Pair<T>; // Coord is an alias for Pair<T>
// Our print function template needs to know that Coord's template parameter T is a type template parameter template <typename T> voidprint(const Coord<T>& c) { std::cout << c.first << ' ' << c.second << '\n'; }
intmain() { Coord<int> p1 { 1, 2 }; // Pre C++-20: We must explicitly specify all type template argument Coord p2 { 1, 2 }; // In C++20, we can use alias template deduction to deduce the template arguments in cases where CTAD works