CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • C++游戏编程入门(零基础学C++) 说明
  • 第1章 欢迎阅读
  • 第2章 变量、运算符与决策:精灵动画
  • 第3章 C++字符串、SFML时间:玩家输入与平视显示器
  • 第4章 循环、数组、switch语句、枚举和函数:实现游戏机制
  • 第5章 碰撞、音效与结束条件:让游戏可玩
  • 第6章 面向对象编程——开始开发《乒乓》游戏
  • 第7章 AABB碰撞检测与物理效果——完成乒乓球游戏
  • 第8章 SFML视图——开启丧尸射击游戏
  • 第9章 C++引用、精灵表和顶点数组
  • 第10章 指针、标准模板库和纹理管理
    • 学习指针
      • 指针语法
      • 声明指针
      • 初始化指针
      • 重新初始化指针
      • 解引用指针
      • 指针功能多样且强大
      • 动态分配内存
      • 将指针传递给函数
      • 声明和使用指向对象的指针
      • 指针和数组
      • 指针总结
    • 学习标准模板库
      • 什么是向量?
      • 声明向量
      • 向向量中添加数据
      • 访问向量中的数据
      • 从向量中删除数据
      • 检查向量的大小
      • 遍历向量中的元素
      • 什么是映射(map)?
      • 声明映射
      • 向映射中添加数据
      • 在映射中查找数据
      • 从映射中删除数据
      • 检查映射的大小
      • 检查映射中是否存在某个键
      • 遍历映射的键值对
    • auto关键字
    • 标准模板库(STL)总结
    • 总结
    • 常见问题解答
  • 第11章 编写TextureHolder类并创建一群僵尸
  • 第12章 碰撞检测、拾取物与子弹
  • 第13章 视图分层与平视显示器(HUD)的实现
  • 第14章 音效、文件输入输出与完成游戏制作
  • 第15章 快跑!
  • 第16章 声音、游戏逻辑、对象间通信与玩家
  • 第17章 图形、相机与动作
  • 第18章 为平台、玩家动画和控制功能编写代码
  • 第19章 构建菜单与实现下雨效果
  • 第20章 火球与声音空间化
  • 第21章 视差背景与着色器
目录

第10章 指针、标准模板库和纹理管理

# 第10章 指针、标准模板库和纹理管理

在本章中,我们将学习很多知识,同时在游戏开发方面也会取得不少进展。我们首先会学习C++的基础知识点——指针。指针是存储内存地址的变量,通常情况下,指针会存储另一个变量的内存地址。这听起来有点像引用,但我们会看到指针更为强大。我们将使用指针来处理数量不断增加的僵尸群体。

我们还会学习标准模板库(Standard Template Library,STL),它是一个类的集合,能让我们快速轻松地实现常见的数据管理技术。

在本章中,我们将涵盖以下主题:

  • 学习指针
  • 学习标准模板库

# 学习指针

在学习C++编程时,指针可能会让人感到挫败。不过,其概念本身很简单。

指针是存储内存地址的变量。

就是这么简单!没什么可担心的。可能让初学者感到挫败的是指针的语法,即我们用于处理指针的代码。我们将逐步讲解使用指针的代码的各个部分,然后你就可以开启掌握指针的学习过程。在后面的最终项目中,我们会学习智能指针,它在某些方面简化了我们即将学习的内容,但灵活性稍差。

在本节中,我们对指针的学习会超出本项目的实际需求。在下一个项目中,我们会更多地使用指针。即便如此,我们也只是触及指针这个主题的皮毛。

我很少建议通过死记硬背事实、数据或语法来学习,但记住与指针相关的简短却关键的语法可能是值得的。这能确保这些知识深深印在我们的脑海里,让我们永远不会忘记。之后,我们可以讨论为什么需要指针,并研究它们与引用的关系。打个比方可能有助于理解指针:

如果把变量类型比作房子,变量所存储的值比作房子里的物品,那么指针就是房子的地址。

在上一章讨论引用时,我们了解到,当向函数传递值或从函数返回值时,实际上是创建了一个全新的变量,不过这个变量与之前的变量完全相同,也就是对传递给函数或从函数返回的值进行了复制。

此时,指针听起来可能有点像引用。这是因为它们确实有一些相似之处。然而,指针要灵活和强大得多,并且有其独特的用途。这些独特用途需要特殊的语法来实现。我们先来看看指针的语法。

# 指针语法

有两个主要运算符与指针相关。第一个是取地址运算符:

&
1

第二个是解引用运算符:

*
1

现在我们来看看如何在使用指针时运用这些运算符。

你首先会注意到,取地址运算符和引用运算符长得一样。这让立志成为C++游戏程序员的人更加头疼,因为这两个运算符在不同的上下文中作用不同。从一开始就了解这一点很重要。如果你盯着一段涉及指针的代码,感觉自己好像要抓狂了,要知道:

你完全没疯!你只是需要仔细查看代码的上下文细节。

既然你已经知道,遇到不明白、不直观的地方不是你的错,指针本身就不是那么一目了然的,仔细查看上下文就能明白代码的意图。

在知道了使用指针要比之前学的语法更细心,并且了解了这两个运算符(取地址运算符和解引用运算符)之后,我们现在可以开始看看真正的指针代码了。

在继续之前,务必记住这两个运算符。

# 声明指针

要声明一个新指针,我们使用解引用运算符,同时指定指针所指向变量的类型。在进一步讨论指针之前,先看看下面的代码:

// Declare  a pointer  to  hold
//  the  address  of a  variable  of  type  int
int* pHealth;
1
2
3

上述代码声明了一个名为pHealth的新指针,它可以存储int类型变量的地址。注意,我说的是“可以存储int类型变量的地址”。和其他变量一样,指针也需要初始化才能正常使用。

pHealth这个名字和其他变量名一样,可以随意取。

通常的做法是给指针变量名加上前缀p,这样更容易记住我们正在处理的是指针,也能将其与普通变量区分开来。

解引用运算符周围的空白是可选的,因为C++在语法上不太在意空格。不过,建议保留空白,这样有助于提高代码的可读性。看看下面三行代码,它们的作用完全相同。

我们在前面的例子中看到了下面这种格式,解引用运算符紧挨着类型:

int* pHealth;
1

下面的代码在解引用运算符两侧都有空白:

int * pHealth;
1

下面的代码则是解引用运算符紧挨着指针名:

int *pHealth;
1

了解这些不同的写法很有必要,这样当你在网上看到代码时,就能明白它们其实是一样的。在本书中,我们始终采用第一种写法,即解引用运算符紧挨着类型。

就像普通变量只能成功存储相应类型的数据一样,指针也应该只存储相应类型变量的地址。

一个指向int类型的指针不应该存储String、Zombie、Player、Sprite、float或其他任何类型变量的地址,只能存储int类型变量的地址。

接下来看看如何初始化指针。

# 初始化指针

下面,我们来看看如何将一个变量的地址存入指针中。看看下面的代码:

// A  regular  int  variable  called  health
int health = 5;
// Declare  a pointer  to  hold  the  address  of
//  a  variable  of  type  int
int* pHealth;

//  Initialize pHealth  to  hold  the  address  of health,
//  using  the  "address  of"  operator
pHealth = &health;
1
2
3
4
5
6
7
8
9

在上述代码中,我们声明了一个名为health的int类型变量,并将其初始化为5。虽然之前从未讨论过,但这个变量肯定存储在计算机内存的某个地方,它一定有一个内存地址,这是合乎逻辑的。

我们可以使用取地址运算符来获取这个地址。仔细看上面代码的最后一行,我们用health的地址初始化pHealth:

pHealth = &health;
1

现在,我们的pHealth指针存储了普通int类型变量health的地址。

用C++术语来说,我们称pHealth指向health。

我们可以将pHealth传递给一个函数,这样函数就能处理health,就像我们使用引用时那样。

如果指针的用途仅限于此,那就没必要使用指针了。下面我们来看看如何重新初始化指针。

# 重新初始化指针

与引用不同,指针可以重新初始化,使其指向不同的地址。看看下面的代码:

// A  regular  int  variable  called  health
int health = 5;
int score = 0;
// Declare  a pointer  to  hold  the  address
//  of a  variable  of  type  int
int* pHealth;
//  Initialize pHealth  to  hold  the  address  of health
pHealth = &health;
//  Re-initialize pHealth  to  hold  the  address  of score
pHealth = &score;
1
2
3
4
5
6
7
8
9
10

现在,pHealth指向了int类型变量score。

当然,我们给指针取的名字pHealth现在有点不太合适,或许叫pIntPointer更合适。这里关键要理解的是,我们可以进行这样的重新赋值操作。

到目前为止,除了指向(存储内存地址),我们实际上还没有让指针发挥其他作用。下面看看如何访问指针所指向地址存储的值,这会让指针真正派上用场。

# 解引用指针

我们知道指针存储内存地址。在声明并初始化指针后,如果在游戏的抬头显示(HUD)中输出这个地址,它可能看起来像这样:9876。

这只是一个值,一个代表内存地址的值。在不同的操作系统和硬件类型上,这些值的范围会有所不同。在本书的语境中,我们永远不需要直接操作地址,我们只关心指针所指向地址存储的值是什么。

变量实际使用的地址是在游戏执行(运行时)时确定的,所以在编写游戏代码时,我们无法知道变量的地址,也就无法知道指针中存储的值。

我们可以使用解引用运算符来访问指针所指向地址存储的值:

*
1

没错,这和解引用运算符与声明指针时使用的符号完全一样。上下文很重要。下面的代码既直接操作了一些变量,也通过指针进行了操作。试着理解一下,然后我们再详细讲解:

**警告!**下面的代码没什么实际意义(此处为双关语,pointless既有“无意义的”之意,也有“没有指针”之意),只是用来演示指针的使用。

// Some  regular  int  variables
int score = 0;
int hiScore = 10;

// Declare  2 pointers  to  hold  the  addresses  of  int
int* pIntPointer1;
int* pIntPointer2;
//  Initialize pIntPointer1  to  hold  the  address  of score
pIntPointer1 = &score;
//  Initialize pIntPointer2  to  hold  the  address  of hiScore
pIntPointer2 = &hiScore;
// Add  10  to  score  directly
score += 10;
// Score  now  equals  10
// Add  10  to  score  using pIntPointer1
*pIntPointer1 += 10;
//  score  now  equals  20. A  new  high  score
// Assign  the  new  hi  score  to  hiScore  using  only pointers
*pIntPointer2 = *pIntPointer1;
//  hiScore  and  score  both  equal  20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在上述代码中,我们声明了两个int类型变量score和hiScore,并分别初始化为0和10。接着,我们声明了两个指向int类型的指针pIntPointer1和pIntPointer2,在声明的同时将它们分别初始化为存储(指向)score和hiScore变量的地址。

接着,我们用常规方式给score加10,即score += 10。然后可以看到,通过对指针使用解引用运算符,我们可以访问指针所指向地址存储的值。下面的代码改变了pIntPointer1所指向变量存储的值:

// Add  10  to  score  using pIntPointer1
*pIntPointer1 += 10;
//  score  now  equals  20, A  new  high  score
1
2
3

上述代码的最后一部分对两个指针进行解引用操作,将pIntPointer1所指向的值赋给pIntPointer2所指向的变量:

// Assign  the  new  hi-score  to  hiScore  with  only pointers
*pIntPointer2 = *pIntPointer1;
//  hiScore  and  score  both  equal  20
1
2
3

现在,score和hiScore都等于20。

# 指针功能多样且强大

我们可以用指针做更多的事情。下面只是一些指针的实用用法。

# 动态分配内存

到目前为止,我们看到的所有指针都指向内存地址,而这些内存地址的作用域仅限于创建指针的函数内部。所以,如果我们声明并初始化一个指向局部变量的指针,当函数返回时,指针、局部变量以及内存地址都会消失,因为它们超出了作用域。

到目前为止,我们一直使用在游戏执行前就确定好的固定数量的内存。此外,我们使用的内存由操作系统控制,在调用函数和函数返回时,变量会被创建和销毁。我们需要的是一种使用内存的方式,这种内存的作用域会一直存在,直到我们不再使用它。我们希望能够访问属于自己且由自己管理的内存。

当我们声明变量(包括指针)时,它们位于内存中的一个区域,这个区域被称为栈(stack)。在第4章中,我们讨论了栈是如何通过添加和移除函数及其相关参数和局部变量来工作的。还有另一个内存区域,虽然它由操作系统分配和控制,但可以在运行时进行分配,这个区域被称为堆(heap)。

堆上的内存没有特定函数的作用域限制。从函数返回并不会删除堆上的内存。

这赋予了我们强大的能力。由于能够访问仅受运行游戏的计算机资源限制的内存,我们可以设计包含大量对象的游戏。就我们而言,我们想要一大群僵尸。然而,正如蜘蛛侠的叔叔会毫不犹豫提醒我们的那样:“能力越大,责任越大。”

让我们看看如何使用指针来利用堆上的内存,以及在使用完后如何将这些内存释放回操作系统。要创建一个指向堆上值的指针,我们需要一个指针:

int* pToInt = nullptr;
1

在上面这行代码中,我们以之前见过的方式声明了一个指针,但由于我们没有将它初始化为指向某个变量,所以将它初始化为nullptr。这样做是一个好习惯。想象一下,在你甚至不知道指针指向什么的情况下就解引用它(更改它所指向地址处的值),这在编程中就好比在射击场,给人蒙上眼睛,让其转圈,然后让他们射击。将指针指向空(nullptr),我们就不会因它而造成任何危害。

当我们准备在堆上请求内存时,我们使用new关键字,如下代码所示:

pToInt = new int;
1

现在,pToInt保存了堆上一块大小刚好能容纳一个int值的内存空间的地址。

任何已分配的内存会在程序结束时被返回。然而,重要的是要意识到,除非我们释放这块内存,否则在游戏执行过程中它永远不会被释放。如果我们持续从堆中获取内存却不归还,最终堆内存会耗尽,游戏就会崩溃。

偶尔从堆中获取int大小的内存块,不太可能导致内存耗尽。但是,如果我们的程序中有一个请求内存的函数或循环,并且这个函数或循环在整个游戏中频繁执行,最终游戏会变慢,然后崩溃。此外,如果我们在堆上分配了大量对象却没有正确管理,这种情况可能很快就会发生。

下面这行代码将之前由pToInt指向的堆上的内存归还(删除):

delete pToInt;
1

现在,之前由pToInt指向的内存不再归我们使用,我们必须谨慎处理。虽然内存已经归还操作系统,但pToInt仍然保存着这块不再属于我们的内存的地址。

下面这行代码确保pToInt不能用于尝试操作或访问这块内存:

pToInt = nullptr;
1

如果一个指针指向无效地址,它被称为野指针或悬空指针。如果你尝试解引用一个悬空指针,运气好的话,游戏会崩溃,你会得到一个内存访问冲突错误。运气不好的话,你会制造出一个极难发现的漏洞。此外,如果我们使用的堆上内存的生命周期超出了某个函数,我们必须确保保留一个指向它的指针,否则就会造成内存泄漏。也就是说,内存将保持分配状态,但我们无法再访问它。C++智能指针可以避免这些情况,通常是最合适的选择,但如果不先理解普通指针,就很难学习智能指针。而且,有些操作只能通过普通指针来完成。

现在,我们可以声明指针并让它们指向堆上新分配的内存。我们可以通过解引用指针来操作和访问它们指向的内存。我们也可以在使用完内存后将其返回给堆,并且我们还知道如何避免出现悬空指针。

让我们看看指针的更多优点。

# 将指针传递给函数

为了将指针传递给函数,我们需要编写一个在函数原型中带有指针的函数,如下代码所示:

void myFunction(int *pInt) {
    // 解引用并增加指针所指向地址处存储的值
    *pInt++;
    return; 
}
1
2
3
4
5

上述函数只是解引用指针,并将指针所指向地址处存储的值加1。

现在,我们可以使用这个函数,并显式地传递一个变量的地址或指向变量的另一个指针:

int someInt = 10;
int* pToInt = &someInt; 
myFunction(&someInt);
// someInt现在等于11

myFunction(pToInt);
// someInt现在等于12
1
2
3
4
5
6
7

如上述代码所示,在函数内部,我们通过变量的地址或指向该变量的指针来操作调用代码中的变量,因为这两种方式本质上是一样的。

指针也可以指向类的实例。

# 声明和使用指向对象的指针

指针不仅适用于普通变量。我们还可以声明指向用户自定义类型(如我们的类)的指针。以下是声明一个指向Player类型对象的指针的方法:

Player player;
Player* pPlayer = &player;
1
2

我们甚至可以直接从指针访问Player对象的成员函数,如下代码所示:

// 调用Player类的一个成员函数
pPlayer->moveLeft();
1
2

注意这里细微但关键的区别:通过指向对象的指针而不是直接通过对象来访问函数时,使用的是->操作符。

在C++中,->操作符被称为成员访问操作符,有时也简称为箭头操作符。它用于通过指向类的指针来访问类的成员。->操作符是一种简写表示法,用于同时解引用指向对象的指针并访问该对象的成员。

在这个项目中,我们不需要使用指向对象的指针,但在使用之前,我们会更深入地探讨它们,这将在最后一个项目中进行。在讨论全新的内容之前,让我们再了解一个关于指针的新话题。

# 指针和数组

数组和指针有一些共同之处。数组名是一个内存地址。更具体地说,数组名是数组中第一个元素的内存地址。换句话说,数组名指向数组的第一个元素。理解这一点的最好方法是继续阅读并查看以下示例。

我们可以创建一个指向数组所存储类型的指针,然后使用该指针,其语法与使用数组的语法完全相同:

// 声明一个int类型的数组
int arrayOfInts[100];
// 声明一个指向int的指针,并使用数组arrayOfInts第一个元素的地址对其进行初始化
int* pToIntArray = arrayOfInts;
// 像使用arrayOfInts一样使用pToIntArray
arrayOfInts[0] = 999;
// arrayOfInts的第一个元素现在等于999
pToIntArray[0] = 0;
// arrayOfInts的第一个元素现在等于0
1
2
3
4
5
6
7
8
9

这也意味着,一个函数如果其原型接受一个指针,那么它也接受指针所指向类型的数组。在构建越来越庞大的僵尸群时,我们将利用这一特性。

关于指针和引用之间的关系,实际上编译器在实现引用时使用了指针。这意味着引用只是一个便捷工具(在“底层”使用指针)。你可以把引用想象成自动变速箱,在城市中驾驶很方便;而指针则像是手动变速箱,虽然更复杂,但如果使用得当,能带来更好的效果、性能和灵活性。

# 指针总结

指针有时有点复杂。实际上,我们对指针的讨论只是一个入门介绍。熟悉指针的唯一方法就是尽可能多地使用它们。为了完成这个项目,你只需要理解关于指针的以下几点:

  • 指针是存储内存地址的变量。
  • 我们可以将指针传递给函数,以便在被调用函数中直接操作调用函数作用域内的值。
  • 数组名保存着数组第一个元素的内存地址。我们可以将这个地址作为指针传递,因为它本质上就是一个指针。
  • 我们可以使用指针指向堆上的内存。这意味着我们可以在游戏运行时动态分配大量内存。

指针还有更多的用法。在习惯使用普通指针之后,我们将在最后一个项目中学习智能指针。

在我们再次开始编写《僵尸竞技场》项目的代码之前,还有一个主题需要介绍。

# 学习标准模板库

标准模板库(Standard Template Library,STL)是一组数据容器以及操作存储在这些容器中数据的方法。更确切地说,它是一种存储和操作不同类型的C++变量和类的方式。

我们可以把不同的容器看作是经过定制且更高级的数组。STL是C++的一部分,它不像SFML那样是一个需要单独设置的可选组件。

STL是C++的一部分,因为它的容器以及操作这些容器的代码是许多类型的代码和应用程序都需要使用的基础。

简而言之,STL实现的代码几乎是每个C++程序员在某个时候(很可能是经常)都会用到的。

如果我们自己编写代码来存储和管理数据,不太可能像编写STL的人那样高效。

所以,通过使用STL,我们可以确保使用尽可能优质的代码来管理数据。甚至SFML也使用STL。例如,在底层,VertexArray类就使用了STL。

我们需要做的就是从可用的容器类型中选择合适的类型。STL提供的容器类型包括:

  • 向量(Vector):这就像是增强版的数组,它可以处理动态调整大小、排序和搜索操作。这可能是最有用的容器。接下来我们会看一些向量的代码。
  • 列表(List):一种允许对数据进行排序的容器。
  • 映射(Map):一种关联容器,允许用户将数据存储为键值对。在这种容器中,一个数据作为查找另一个数据的“键”。映射也可以增长和收缩,并且可以进行搜索。在学习向量之后,我们将学习映射,然后使用映射。
  • 集合(Set):一种确保每个元素都唯一的容器。在《僵尸竞技场》游戏中,我们将使用映射。

如果你想了解一下STL为我们省去的复杂工作,可以看看这个教程,它实现了列表的基本功能。注意,该教程只是实现了列表最基本的功能:http://www.sanfoundry.com/cpp-program-implement-single-linked-list/ 。

我们很容易看出,如果学习STL,我们将节省大量时间,并最终开发出更好的游戏。让我们更深入地了解如何使用向量实例,然后再看看映射,以及映射在《僵尸竞技场》游戏中的用途。

# 什么是向量?

在C++中,向量(vector)是一种动态数组,它允许我们存储和操作一组元素。它提供了一个灵活且可调整大小的容器,类似于数组,但具有更多的功能,使其成为管理数据集的强大工具。

# 声明向量

要声明一个向量,我们使用标准模板库(STL)中的向量模板类。下面是声明一个int类型向量的示例:

// 将vector头文件添加到项目中
#include <vector>

vector<int> numbers;
1
2
3
4

在这个例子中,numbers是一个可以存储int类型数据的向量。和数组一样,向量可以用于存储任何数据类型的元素。

# 向向量中添加数据

让我们向向量中添加一些整数:

numbers.push_back(42);
numbers.push_back(73);
numbers.push_back(10);
1
2
3

现在,我们的向量numbers包含三个整数:42、73和10。

# 访问向量中的数据

我们可以使用类似数组的语法来访问向量中的元素:

int firstNumber = numbers[0]; // 访问第一个元素(42)
int secondNumber = numbers[1]; // 访问第二个元素(73)
1
2

并且我们可以从向量中删除数据。

# 从向量中删除数据

从向量中删除元素可以使用多种方法。例如,要删除第一个元素:

numbers.erase(numbers.begin());
1

现在,numbers只包含两个元素:73和10。这是因为numbers.begin()指向第一个元素,而erase函数正如其名,会删除指向的元素。所有这些函数都可用,是因为numbers是vector的一个实例。

# 检查向量的大小

要查看向量中包含多少个元素,我们可以使用size方法:

int size = numbers.size(); // size现在为2
1

上述代码使用了size函数,该函数返回向量中的元素数量,并将结果存储在int类型的变量size中。

# 遍历向量中的元素

我们可以使用循环遍历向量中的所有元素。下面是使用普通for循环的示例:

for (vector<int>::iterator it = numbers.begin(); it != numbers.end(); it++) {
    *it += 1; // 每个元素加1
}
1
2
3

不过,我们可以使用auto关键字简化代码:

for (auto it = numbers.begin(); it != numbers.end(); ++it) {
    *it += 1; // 每个元素加1
}
1
2
3

auto关键字通过让编译器为我们推断类型,有助于减少代码的冗长性。vector<int>::iterator it是循环变量,初始化为numbers.begin()。只要这个变量不等于numbers.end(),我们就不断使用it++进行递增。正如我们在讨论映射时会看到的,这些循环的格式非常灵活。这种简洁的语法提高了代码的可维护性,因为程序员不再需要显式指定复杂的迭代器类型,从而使循环结构更简洁、更直观,这对程序员来说绝对是一个好处。

向量因其动态调整大小的能力和简单的语法而功能多样,在C++中被广泛使用。它们提供了一种方便的方式来高效管理数据集。在最后一个项目中,我们将使用向量来管理一组游戏对象。但首先,让我们看看映射。

# 什么是映射(map)?

映射是一种动态可调整大小的容器。我们可以轻松地添加和删除元素。与标准模板库(STL)中的其他容器相比,映射类的特殊之处在于我们访问其中数据的方式。

映射实例中的数据以键值对的形式存储。想象一下你登录账户的场景,可能需要用户名和密码。映射非常适合通过用户名查找,并检查相关密码的值。

映射也非常适用于存储诸如账户名和账号,或者公司名称和股价等信息。

请注意,当我们使用标准模板库中的映射时,可以自行决定构成键值对的值的类型。这些值可以是字符串实例和整数实例,比如账号;也可以是字符串实例和其他字符串实例,比如用户名和密码;还可以是用户自定义类型,例如对象。

下面通过一些实际代码来帮助我们熟悉映射。

# 声明映射

我们可以这样声明一个映射:

map<string, int> accounts;
1

上面这行代码声明了一个名为accounts的新映射,它的键是字符串对象,每个键都对应一个整数值。

现在,我们可以存储字符串类型的键和整数值的键值对了。接下来看看如何实现。

# 向映射中添加数据

我们直接向accounts映射中添加一个键值对:

accounts["John"] = 1234567;
1

现在,映射中有了一个可以通过“John”这个键访问的条目。下面的代码再向accounts映射中添加两个条目:

accounts["Smit"] = 7654321;
accounts["Larissa"] = 8866772;
1
2

我们的映射中现在有三个条目了。接下来看看如何访问账号。

# 在映射中查找数据

我们可以用添加数据时的相同方式来访问数据,即通过键来访问。例如,我们可以将“Smit”这个键所存储的值赋给一个新的整数accountNumber,如下所示:

int accountNumber = accounts["Smit"];
1

现在,整数变量accountNumber存储的值为7654321。对于存储在映射实例中的值,我们能对该类型数据做的任何操作,都可以对其进行。

# 从映射中删除数据

从映射中删除值也很简单。下面这行代码删除“John”这个键及其关联的值:

accounts.erase("John");
1

我们再看看映射的其他一些操作。

# 检查映射的大小

我们可能想知道映射中有多少个键值对。下面这行代码就能实现:

int size = accounts.size();
1

现在,整数变量size的值为2。这是因为我们删除了“John”,accounts中只剩下“Smit”和“Larissa”对应的值。

# 检查映射中是否存在某个键

映射最相关的特性是它能通过键来查找值。我们可以像这样测试某个特定的键是否存在:

if(accounts.find("John") != accounts.end()) {
    // 这段代码不会运行,因为“John”已被删除
}
if(accounts.find("Smit") != accounts.end()) {
    // 这段代码会运行,因为“Smit”在映射中
}
1
2
3
4
5
6

在上面的代码中,!= accounts.end()用于判断某个键是否存在。如果在映射中没有找到所搜索的键,那么accounts.end()就是if语句的结果。

下面看看如何通过遍历映射来测试或使用其中的所有值。

# 遍历映射的键值对

我们已经知道如何使用for循环遍历数组中的所有值。但如果想对映射做类似操作该怎么办呢?

下面的代码展示了如何遍历accounts映射中的每一个键值对,并将每个账号的值加1:

for (map<string,int>::iterator it = accounts.begin(); it != accounts.end(); ++ it) {
    it->second += 1; 
}
1
2
3

for循环的条件部分可能是上面代码中最值得关注的地方。条件的第一部分是最长的。如果我们把map<string,int>::iterator it = accounts.begin()拆开来看,就更容易理解了。

map<string,int>::iterator是一种类型。我们正在声明一个适用于键值对为字符串和整数的映射的迭代器。

迭代器的名称是it。我们将accounts.begin()返回的值赋给it。现在,迭代器it保存了accounts映射中的第一个键值对。

for循环条件的其余部分工作方式如下:it != accounts.end()表示循环会持续到到达映射的末尾,而++it会在每次循环时将迭代器移动到映射中的下一个键值对。

在for循环内部,it->second用于访问键值对中的值,+= 1则将该值加1。注意,我们可以用it->first来访问键(即键值对的第一部分)。

你可能已经注意到,设置遍历映射的循环的语法相当繁琐。C++有一种方法可以简化这种情况。

# auto关键字

for循环条件中的代码相当繁琐,尤其是map<string,int>::iterator这部分。和关于向量(vector)的教程一样,我们可以使用auto来简化代码。使用auto关键字,我们可以改进上面的代码:

for (auto it = accounts.begin(); it != accounts.end(); ++ it) {
    it->second += 1; 
}
1
2
3

auto关键字指示编译器自动为我们推断类型。在编写下一个类时,这将特别有用。

# 标准模板库(STL)总结

正如本书中介绍的几乎所有C++概念一样,标准模板库(STL)是一个庞大的主题。有专门的书籍只讲解标准模板库。不过,目前我们所学的知识已经足够用来构建一个使用标准模板库映射来存储SFML纹理(Texture)对象的类。这样,我们就可以通过使用文件名作为键值对的键来检索或加载纹理。

随着学习的深入,我们会明白为什么要采用这种更复杂的方式,而不是像之前那样继续直接使用纹理类。

# 总结

在本章中,我们学习了指针,了解到指针是存储特定类型对象内存地址的变量。随着本书内容的推进,指针的强大功能将逐渐展现,其重要意义也会越发明显。

我们还使用指针创建了大量的僵尸,这些僵尸可以通过指针访问,而指针实际上与数组的第一个元素是相同的。

我们学习了标准模板库(STL),特别是映射类。我们实现了一个类,它可以存储所有纹理,并提供对纹理的访问。

在下一章中,我们将运用本章所学的知识。我们将使用指针和数组实现一群僵尸,并探索一种通过映射来处理精灵纹理的巧妙方法。我们还会更深入地探讨面向对象编程(OOP),并使用静态函数,静态函数是类的一种函数,无需类的实例即可调用。

# 常见问题解答

以下是一些你可能会想到的问题:

  • 问:指针和引用有什么区别?
  • 答:指针就像是增强版的引用。指针可以更改,以指向不同的变量(内存地址),还可以指向堆上动态分配的内存。
  • 问:数组和指针是怎么回事?
  • 答:数组实际上是指向其第一个元素的常量指针。
第9章 C++引用、精灵表和顶点数组
第11章 编写TextureHolder类并创建一群僵尸

← 第9章 C++引用、精灵表和顶点数组 第11章 编写TextureHolder类并创建一群僵尸→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式