第 10 章:指针、STL 与纹理管理

第 10 章:指针、STL 与纹理管理:保留第三版项目代码讲解,并以源码—状态—输出切片、确定性轨迹和故障重放完成验收。

学习目标

  • 能解释“第 10 章:指针、STL 与纹理管理”如何区分拥有与观察指针,用 std::vector 管理可变实体集合,并把纹理生命周期提升到所有 Sprite 之上
  • 能逐项定位 指针(pointers)、标准模板库(standard template library)、容器(container)、纹理管理(texture management),说明它们位于源码、运行状态还是可见输出边界
  • 能按 创建所有者 → 加入 vector → 记录容量 → 更新实体 → 按序销毁 重放“生成一波僵尸”,持续检查“每个动态对象只有一个明确所有者,vector 变更后不继续使用可能失效的元素地址,纹理活得比 Sprite 久”
  • 能注入“保存 vector 元素指针后触发扩容,再通过旧地址更新僵尸”,从所有权图、vector size/capacity、扩容前后地址、析构日志及纹理/精灵寿命找到第一个不一致并用同输入恢复

第三版来源、工具链与版本边界

“第 10 章:指针、STL 与纹理管理”对齐 Packt 2024 年第三版的对应章节范围,并以官方公开代码仓库和 SFML 官方文档核对本页可公开验证的工程事实。本页是独立中文重写,不复现原书正文,也不把商品页、目录或代码仓库冒充完整原版。

在“第 10 章:指针、STL 与纹理管理”中,第三版示例使用 SFML 2.6.1 时代 API。本页保留与本章有关的 2.6 系列合同;SFML 3 的事件、角度、时长和构造接口差异不会被静默回填。升级工具链时必须单独记录本章迁移补丁,不能把版本不匹配误判为“指针(pointers)”概念错误。

  • Packt:Beginning C++ Game Programming, Third Edition:在“第 10 章:指针、STL 与纹理管理”中,核对 2024 年第三版、C++20、SFML、四个项目、21 个正式教学章节及章节次序;不把商品页当作正文全文。
  • PacktPublishing:第三版官方代码仓库:在“第 10 章:指针、STL 与纹理管理”中,核对 Timber、Pong、ZombieShooter、Run 四个项目的公开代码与资源组织;代码许可证不等于原书正文授权。
  • SFML 2.6 官方教程:在“第 10 章:指针、STL 与纹理管理”中,核对本书使用的 SFML 2.6 系列 Window、Graphics、View、VertexArray、Shader 与 Audio API 语义。

正式概念与运行状态合同

指针(pointers)

这个正式目录节点落在 所有权 边界:对象创建、指针职责与析构。在本页中,它参与“区分拥有与观察指针,用 std::vector 管理可变实体集合,并把纹理生命周期提升到所有 Sprite 之上”。验收时保存所有权图、vector size/capacity、扩容前后地址、析构日志及纹理/精灵寿命,不能只凭最终画面判断。

标准模板库(standard template library)

这个正式目录节点落在 集合变化 边界:vector 容量、插入、擦除和迭代。在本页中,它参与“区分拥有与观察指针,用 std::vector 管理可变实体集合,并把纹理生命周期提升到所有 Sprite 之上”。验收时保存每个动态对象只有一个明确所有者,vector 变更后不继续使用可能失效的元素地址,纹理活得比 Sprite 久,不能只凭最终画面判断。

容器(container)

这个正式目录节点落在 资源绑定 边界:Texture 所有者与 Sprite 观察关系。在本页中,它参与“区分拥有与观察指针,用 std::vector 管理可变实体集合,并把纹理生命周期提升到所有 Sprite 之上”。验收时保存所有权图、vector size/capacity、扩容前后地址、析构日志及纹理/精灵寿命,不能只凭最终画面判断。

纹理管理(texture management)

这个正式目录节点落在 所有权 边界:对象创建、指针职责与析构。在本页中,它参与“区分拥有与观察指针,用 std::vector 管理可变实体集合,并把纹理生命周期提升到所有 Sprite 之上”。验收时保存每个动态对象只有一个明确所有者,vector 变更后不继续使用可能失效的元素地址,纹理活得比 Sprite 久,不能只凭最终画面判断。

验收项本页合同
最小正常场景预留足够容量后创建并加入固定数量实体
边界或恢复场景保存元素地址后插入直到 vector 扩容
必须保持每个动态对象只有一个明确所有者,vector 变更后不继续使用可能失效的元素地址,纹理活得比 Sprite 久
单一故障保存 vector 元素指针后触发扩容,再通过旧地址更新僵尸
可观察证据所有权图、vector size/capacity、扩容前后地址、析构日志及纹理/精灵寿命

先预测,再操作三个本页实验

实验一:从源码到可见结果

先预测“生成一波僵尸”会怎样穿过 所有权 → 集合变化 → 资源绑定,再切换场景和正式概念。每次操作都必须能回到同一初始状态。

Source · state · visible result

第 10 章:指针、STL 与纹理管理:可运行切片

区分拥有与观察指针,用 std::vector 管理可变实体集合,并把纹理生命周期提升到所有 Sprite 之上

选择输入或构建场景

定位正式概念

bcgp3-10 · 当前切片

指针(pointers)预留足够容量后创建并加入固定数量实体

边界 1所有权

对象创建、指针职责与析构

边界 2集合变化

vector 容量、插入、擦除和迭代

边界 3资源绑定

Texture 所有者与 Sprite 观察关系

可验收结果

集合大小正确,所有对象被更新且退出时只析构一次

实验二:逐步执行状态轨迹

依次执行 创建所有者 → 加入 vector → 记录容量 → 更新实体 → 按序销毁。每一步只选中一个阶段,并持续检查“每个动态对象只有一个明确所有者,vector 变更后不继续使用可能失效的元素地址,纹理活得比 Sprite 久”。

Deterministic state trace

第 10 章:指针、STL 与纹理管理:状态执行轨迹

逐步执行当前 1 / 5
始终保持

每个动态对象只有一个明确所有者,vector 变更后不继续使用可能失效的元素地址,纹理活得比 Sprite 久

本步证据

所有权图、vector size/capacity、扩容前后地址、析构日志及纹理/精灵寿命

实验三:单一故障与同输入恢复

注入“保存 vector 元素指针后触发扩容,再通过旧地址更新僵尸”,定位第一项不一致;撤销后用完全相同的“触发重新分配”重放。只有中间状态和最终输出一起恢复才算修复。

Fault injection · clean replay

第 10 章:指针、STL 与纹理管理:故障注入与恢复

单一故障:保存 vector 元素指针后触发扩容,再通过旧地址更新僵尸

1. 固定构建与输入一致

第 1 次重放使用同一源码、资源、初始状态和输入序列

2. 程序状态一致

每个动态对象只有一个明确所有者,vector 变更后不继续使用可能失效的元素地址,纹理活得比 Sprite 久

3. 诊断证据一致

所有权图、vector size/capacity、扩容前后地址、析构日志及纹理/精灵寿命

4. 恢复判断一致

同输入重放后,所有权、状态更新、可见输出和诊断证据重新一致

易错边界与工程取舍

从“一千只僵尸是否要复制一千张纹理”开始

Zombie Arena 要在运行期生成数量变化的敌人。第 10 章用

连接对象,用

管理可增长集合,并建立多只精灵共享一份 Texture 的资源协议。

地址、空指针与解引用

只有在对象存活且没有因移动、删除或容器重分配改变位置时才可使用。Zombie* p = &zombie 不复制 Zombie,也不让它活得更久。

Zombie zombie{spawnPosition};
Zombie* target{&zombie};
 
if (target != nullptr)
    target->update(dt, player.position());

适合表达“当前没有目标”。非空仍不足以证明有效:目标可能已销毁,留下数值非零的悬空指针。

失败不是可靠异常,而通常是未定义行为。函数参数若必定需要对象,引用比可空指针更直接;若允许没有目标,指针配合明确检查合适。

所有权:原始指针不等于 delete 责任

最好由类型表达。std::vector<Zombie> 直接拥有值对象;std::unique_ptr<Zombie> 表达独占动态所有权;std::shared_ptr 表达共享所有权但带来引用环和开销,不能因“不想思考生命周期”就默认使用。

std::vector<std::unique_ptr<Zombie>> polymorphicHorde;
polymorphicHorde.push_back(std::make_unique<FastZombie>(spawn));

当前 Zombie 若是同一具体类型,优先 vector<Zombie>:连续存储、所有权简单、少一次间接访问。只有需要运行时多态且对象身份必须稳定时,才考虑 unique_ptr 容器。不要手写 new[]/delete[] 管群体。

解决释放责任,不自动解决指向对象的所有观察者失效。

vector:大小、容量与重分配

适合一批同类型 Zombie。reserve(n) 只预留容量,不创建元素;访问 horde[0] 前仍需 size()>0

std::vector<Zombie> horde;
horde.reserve(maximumZombies);
 
for (std::size_t index{0}; index < initialCount; ++index)
    horde.emplace_back(randomSpawn(randomEngine));
 
for (Zombie& zombie : horde)
    zombie.update(dt, player.position());

是最常见生命周期陷阱。reserve 足够容量可推迟重分配,但后续超过容量仍会发生,不能把元素地址当永久 id。

迭代器与删除阶段

删除 vector 元素会移动后续元素,使被删位置及之后的迭代器、引用和指针失效。不要在范围 for 内直接 erase 当前元素。

#include <algorithm>
 
std::erase_if(horde, [](const Zombie& zombie) {
    return !zombie.alive();
});

C++20 erase_if 清楚表达过滤。更新阶段可先标记死亡,所有碰撞和事件处理完成后统一删除;删除后不再使用本帧保存的元素地址。若其它系统需要长期身份,给 Zombie 分配稳定 id 并通过索引表查询,而不是暴露 vector 地址。

算法中的 lambda 按引用捕获外部状态时也受生命周期约束。只在同步调用期使用通常安全;存进异步任务前必须明确捕获副本或所有权。

指针、引用和迭代器失效表

push_back 未重分配时现有元素地址保持,发生重分配时全部失效;erase 使删除点及之后失效;clear 使所有元素引用失效;vector 销毁后全部失效。reserve 若增大 capacity 本身就可能重分配。

这些规则属于容器契约,不应靠一次调试地址“看起来没变”判断。启用标准库调试模式和 AddressSanitizer 可发现部分错误,但设计上应缩短观察指针生命周期:在一次循环内取得、使用,容器修改前丢弃。

即使内存尚未被覆盖也不合法。

纹理管理:加载一次,稳定借用

先解决原则,第 11 章再封装 TextureHolder。生成一千只僵尸时,不应一千次从磁盘解码并上传相同纹理;所有 Zombie Sprite 可绑定同一个 Texture。

sf::Texture zombieTexture;
if (!zombieTexture.loadFromFile("assets/graphics/zombie.png"))
    throw std::runtime_error{"failed to load zombie texture"};
 
std::vector<Zombie> horde;
horde.reserve(maximumZombies);
for (std::size_t index{0}; index < count; ++index)
    horde.emplace_back(zombieTexture, randomSpawn(randomEngine));

要求 Texture 比所有 Sprite 活得更久。声明顺序可让 texture 先构造、horde 后构造,从而销毁时 horde 先、texture 后;更大项目由资源库显式控制。

资源容器的地址稳定性

不能随便用 vector<Texture> 再保存元素引用:扩容会移动 Texture 对象,Sprite 仍指旧地址。节点式 std::map<std::string, sf::Texture> 的元素引用在插入其他项时保持稳定,删除对应项仍会失效。unordered_map 重哈希使迭代器失效,但标准保证元素引用和指针不因 rehash 失效;擦除目标仍失效。

std::map<std::string, sf::Texture> textures;
auto [it, inserted] = textures.try_emplace("zombie");
if (inserted && !it->second.loadFromFile("assets/graphics/zombie.png")) {
    textures.erase(it);
    throw std::runtime_error{"zombie texture load failed"};
}
const sf::Texture& zombieTexture{it->second};

加载失败必须回滚空条目,否则下一次查询误以为资源已缓存。返回 const 引用只读借用,资源库禁止在精灵仍存活时 erase。第 11 章会把这些不变量封装成 TextureHolder。

热重载与关闭顺序

开发期热重载不能先销毁旧 Texture 再构造新对象。若 SFML 支持在同一 Texture 对象上重新 loadFromFile,地址保持但内容更新仍要处理失败:先加载候选,成功后 move-assign 是否保持 Sprite 关联要按库语义验证;更稳妥是资源句柄层通知 Sprite 重新绑定。

关闭顺序从借用者到拥有者:先停止游戏更新,销毁 Zombie/Sprite,再销毁纹理库和窗口上下文。音频、字体也遵循类似图。进程退出看似会回收内存,但明确顺序能避免析构期间访问已销毁资源。

纹理键要规范化路径和颜色空间等变体,同一路径不同相对写法不应重复加载。缓存没有上限也会增长;关卡切换要按资源组释放,但只有确认无借用后才能删。

构建僵尸群的完整流程

关卡开始先确定 count 与上限,reserve 后 emplace Zombie;每个对象借用已加载纹理并拥有位置、速度和血量。每帧用引用遍历更新和绘制,碰撞阶段只标记死亡,帧尾统一 erase。任何长期目标使用稳定 id 查询。

生成位置必须在竞技场内、与 Player 保持最小距离并有最大尝试次数,避免随机条件不可满足时无限循环。构造中途抛异常时 vector 自动销毁已完成 Zombie;纹理由更外层资源库继续拥有。

性能测试分别记录加载次数、draw calls、update 时间与内存。共享 Texture 解决重复资源,不自动解决一千次 draw;后续纹理一致的实体可进一步批处理,但不要牺牲正确生命周期换表面 FPS。

先预测:保存 Zombie* target = &horde[0] 后持续 push_back,reserve 容量被超过会怎样?vector 重分配移动全部元素,target 悬空;应使用稳定 id 重新查询或选择满足身份需求的存储模型。

小结

  • 指针可空可改指向但不表达所有权,解引用必须证明目标仍存活且地址未失效
  • 同类型 Zombie 优先由 vector 按值拥有,需要多态身份时再用 unique_ptr 等明确所有权
  • vector 重分配使全部元素观察地址失效,erase 使删除点及之后失效,删除集中在安全阶段
  • 纹理只加载一次并由稳定资源层拥有,多个 Sprite 共享借用而不复制图像
  • 资源容器要审计地址稳定和 erase 规则,加载失败回滚,使用期禁止删除
  • 关闭时先销毁 Sprite/实体借用者,再销毁 Texture 所有者;热重载也必须保持或重建绑定

练习

问题 1 指针非空为什么仍可能不能解引用?列出 vector 的三个失效场景。

问题 2 一千只同类型僵尸应选择 vector<Zombie>vector<unique_ptr<Zombie>> 还是裸指针数组?说明条件。

问题 3 纹理缓存为何必须地址稳定?加载失败、热重载和关闭分别怎样处理?

名词解释

本章出现的专业名词,用大白话再讲一遍。

指针

保存对象或函数地址的值,可为空可改指向,不自动表示所有权。

标准模板库

C++ 标准库中的通用容器、算法、迭代器和其他组件集合。

对象地址
对象在有效存储期内占据的位置。
空指针

不指向任何对象的 nullptr 值,只能检查不能解引用。

解引用
经指针访问有效目标对象的操作。
所有权

规定谁维持对象生命、何时销毁和怎样转移责任的协议。

std::unique_ptr

独占拥有动态对象并在作用域结束自动释放的可移动智能指针。

std::vector

拥有元素的动态连续序列容器,区分 size 与 capacity。

重分配

vector 更换连续存储并移动元素的过程,会使旧元素地址失效。

迭代器

抽象容器位置并支持遍历、受容器修改失效规则约束的对象。

悬空指针

目标已销毁或地址失效后仍保留的不可解引用指针。

纹理管理

集中加载、复用和释放 Texture 并保证 Sprite 借用期的协议。

纹理共享

多个 Sprite 关联同一个长生命周期 Texture 并各自保存变换状态。

资料与写作方式声明

本章以Beginning C++ Game Programming, Third Edition, Chapter 10权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…