C++ 章节回顾与检测:数组、指针与多维数组详解

章节回顾

定长数组(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_wrapperconst std::reference_wrapper
  • 尽可能使用 static_assert 确保经 CTAD 推导的 constexpr std::array 拥有正确数量的初始值。


C 风格数组

C 风格数组继承自 C 语言,是 C++ 核心语言的一部分,因此拥有独立的声明语法:在标识符后使用方括号 [],可在其中可选地给出长度(std::size_t 类型的常量表达式)。

  • 亦为聚合类型,可用聚合初始化;若使用初始化器列表为全部元素赋初值,建议省略长度,由编译器计算。
  • 通过 [] 索引,下标可为带符号或无符号整数,或无作用域枚举,故不存在标准库容器因符号转换导致的索引问题。
  • 可声明为 constconstexpr

获取 C 风格数组长度

  • C++17:std::size() 返回无符号 std::size_t
  • C++20:std::ssize() 返回带符号的大整数类型(通常为 std::ptrdiff_t)。

在表达式中,C 风格数组通常隐式转换为指向其首元素的指针,此现象俗称“数组退化”(array decay)。

指针算术
允许对指针应用整数算术运算符(加、减、自增、自减)以产生新地址。给定指针 ptrptr + 1 返回内存中下一对象的地址(步长由所指类型决定)。

  • 若从数组起始索引,请使用下标,使索引与元素对齐。
  • 若基于某元素进行相对定位,可使用指针算术。

C 风格字符串
本质上是以 charconst 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 结构体,含 RankSuit 成员,并将枚举移入其中。

> 步骤:  
- 重载 `operator<<` 以两字符编码输出(例:黑桃 J → JS)。  
- 添加返回牌面值函数:A 计 11。  
- 在 `Card` 内添加静态常量 `std::array<Rank>` 与 `std::array<Suit>` 供遍历。

步骤:
创建 Deck 类,内含 52 张 Cardstd::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) (加分)实现以上功能,并显示初始手牌。

(解答略)

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"