章节回顾
定长数组(fixed-size arrays,或固定长度数组)要求在实例化时已知长度,且之后不可更改。C 风格数组与 std::array
均属于定长数组;动态数组可在运行时改变大小,std::vector
即动态数组。
std::array
的长度必须为常量表达式,通常使用整数字面量、constexpr
变量或无作用域枚举值。std::array
是聚合类型,无构造函数,使用聚合初始化。- 只要可行,应将
std::array
定义为constexpr
;若非constexpr
,请优先考虑std::vector
。 - 利用类模板实参推断(CTAD)可由初始值自动推断
std::array
的元素类型与长度。 std::array
的模板声明形如:长度形参template<typename T, std::size_t N> // N 为非类型模板参数 struct array;
N
的类型为std::size_t
。
获取 std::array
长度
- 通过成员函数
size()
获取(返回无符号size_type
)。 - C++17 起可使用非成员函数
std::size()
(对std::array
内部调用size()
,返回无符号size_type
)。 - C++20 起可使用非成员函数
std::ssize()
,返回带符号的大整数类型(通常为std::ptrdiff_t
)。
以上三者返回 constexpr 值,除非实参是通过引用传递的std::array
;该缺陷在 C++23 已由 P2280 修正。
索引 std::array
- 使用下标运算符
[]
,无边界检查;越界将导致未定义行为。 - 使用成员函数
at()
执行运行时边界检查;鉴于通常应在索引前完成边界检查或采用编译期检查,建议避免使用at()
。 - 使用函数模板
std::get<I>()
,以非类型模板实参I
作为索引,进行编译期边界检查。
函数模板形参
- 通过函数模板将不同元素类型和长度的
std::array
传参:template <typename T, std::size_t N> // 或 C++20 的 template <typename T, auto N>
返回值
- 按值返回
std::array
会复制数组及全部元素;若数组较小且复制开销低,此行为可接受,否则可考虑使用输出参数。
聚合初始化与大括号省略
- 使用聚合初始化结构体、类或数组时,若未逐一指定元素类型,需额外一对大括号,这是聚合初始化的特性;而标准库其它容器因使用列表构造函数,无需双重括号。
- 聚合类型支持“大括号省略”规则,允许在满足条件时省略多层大括号;通常对单个标量值或显式给出类型的类/数组可省括号。
数组与引用
不可直接创建“引用的数组”;但可使用
std::reference_wrapper
实现可修改的左值引用数组。operator=
可重设std::reference_wrapper
所引对象。std::reference_wrapper<T>
可隐式转换为T&
。- 成员函数
get()
返回T&
,便于修改被引对象。 - 辅助函数
std::ref()
与std::cref()
用于快捷创建std::reference_wrapper
及const std::reference_wrapper
。
尽可能使用
static_assert
确保经 CTAD 推导的constexpr std::array
拥有正确数量的初始值。
C 风格数组
C 风格数组继承自 C 语言,是 C++ 核心语言的一部分,因此拥有独立的声明语法:在标识符后使用方括号 []
,可在其中可选地给出长度(std::size_t
类型的常量表达式)。
- 亦为聚合类型,可用聚合初始化;若使用初始化器列表为全部元素赋初值,建议省略长度,由编译器计算。
- 通过
[]
索引,下标可为带符号或无符号整数,或无作用域枚举,故不存在标准库容器因符号转换导致的索引问题。 - 可声明为
const
或constexpr
。
获取 C 风格数组长度
- C++17:
std::size()
返回无符号std::size_t
。 - C++20:
std::ssize()
返回带符号的大整数类型(通常为std::ptrdiff_t
)。
在表达式中,C 风格数组通常隐式转换为指向其首元素的指针,此现象俗称“数组退化”(array decay)。
指针算术
允许对指针应用整数算术运算符(加、减、自增、自减)以产生新地址。给定指针 ptr
,ptr + 1
返回内存中下一对象的地址(步长由所指类型决定)。
- 若从数组起始索引,请使用下标,使索引与元素对齐。
- 若基于某元素进行相对定位,可使用指针算术。
C 风格字符串
本质上是以 char
或 const char
为元素的 C 风格数组,同样会发生退化。
多维数组
- 维度(dimension):选取元素所需的索引数量。
- 仅一个维度称为一维数组;数组的数组称为二维数组;多于一个维度者统称多维数组。
- 展平(flattening):将多维数组降维(通常降至一维)的过程。
- C++23 引入
std::mdspan
,为连续元素序列提供多维数组视图接口。
测验
问题 1
指出下列代码片段中的错误并给出修正方法。
a)
#include <array>
#include <iostream>
int main()
{
std::array arr { 0, 1, 2, 3 };
for (std::size_t count{ 0 }; count <= std::size(arr); ++count)
std::cout << arr[count] << ' ';
std::cout << '\n';
}
b)
#include <iostream>
void printArray(int array[])
{
for (int element : array)
std::cout << element << ' ';
}
int main()
{
int array[] { 9, 7, 5, 3, 1 };
printArray(array);
}
c)
#include <array>
#include <iostream>
int main()
{
std::size_t length{};
std::cin >> length;
std::array<int, length> scores; // 错误:长度非常量
}
(解答略)
问题 2
实现“Roscoe 的药水商店”程序,功能如下:
- 玩家初始拥有 80–120 之间的随机金币。
- 显示药水列表(编号、名称、价格)。
- 玩家可反复输入编号购买或输入
q
退出。 - 处理无效输入与余额不足。
- 退出时打印库存与剩余金币。
按步骤完成:
> 步骤:
创建 `Potion` 命名空间,内含枚举 `Type` 表示药水类型;两个 `std::array`:整型数组保存价格,`std::string_view` 数组保存名称;并实现 `shop()` 用于打印列表。
步骤:
创建Player
类存储姓名、库存与金币;实现欢迎、告别文本;随机化金币。
> 步骤:
加入购买逻辑,处理非法输入;退出后打印库存。
(解答略)
问题 3
构建纸牌游戏所需组件:
> 步骤:
定义枚举 `Rank`(A–K)与 `Suit`(梅花、方块、红心、黑桃)。
步骤:
创建Card
结构体,含Rank
与Suit
成员,并将枚举移入其中。
> 步骤:
- 重载 `operator<<` 以两字符编码输出(例:黑桃 J → JS)。
- 添加返回牌面值函数:A 计 11。
- 在 `Card` 内添加静态常量 `std::array<Rank>` 与 `std::array<Suit>` 供遍历。
步骤:
创建Deck
类,内含 52 张Card
的std::array
:
- 默认构造函数初始化牌组;
dealCard()
返回下一张牌(值返回),发完断言;shuffle()
使用std::shuffle
洗牌并重置发牌索引。
(解答略)
---
#### 问题 4
利用 `Card` 与 `Deck` 实现简化二十一点(Blackjack):
规则概述:
- 庄家 1 张、玩家 2 张起始;
- 玩家回合:可反复 hit(要牌)或 stand(停牌);
- A 固定计 11;爆牌(>21)即输;
- 玩家结束后庄家补牌至 ≥17;
- 比较点数,高者胜;平局算庄家胜。
步骤:
创建Player
结构体(仅记录分数)。
实现函数:发 1 张给庄家,2 张给玩家,返回谁分高。
> 步骤:
添加 `Settings` 命名空间存放爆牌阈值与庄家停牌线。
实现庄家回合逻辑。
步骤:
实现玩家回合:输入h
/s
控制要牌或停牌。
(解答略)
---
#### 问题 5
a) 说明如何修改程序以支持 A 既可当 1 又可当 11。
b) 说明如何处理平局(双方同分且未爆牌)。
c) (加分)实现以上功能,并显示初始手牌。
(解答略)