C++游戏编程入门读书笔记

本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

周末打麻将的时候和几个本科生一时兴起约了尬图书馆,第二天直接从寝室出发也没带书,准备去图书馆随便借点来看,我到了图书馆的时候他们还都在睡觉,看来我保住了研究生爱学习的良好形象,虽然打麻将什么的也是前晚熬夜打麻将是我带着他们去的(逃~)

去了图书加自习室的南馆四楼,计软分区,周末一天时间复习复习C++再好不过了。

《C++游戏编程入门》是之前是一本很基础但写的有很好的一本书,这本书虽说是游戏编程,但编写的都是控制台程序,说是游戏编程不如说是小程序例子下讲解C++,

书的内容不是很多,学过C++的大概三四个小时就能看完,

里面的例子很详细,全部都是C++的基础,很适合复习基础来食用,几乎每句代码都是一个知识点来展开谈,教科书式的晦涩语句也几乎没有,很多复杂的东西被几句话讲的很明白。

阅读本文可以帮助你迅速复习C++,了解和补充一些程序设计知识。

使用随机数

1
2
3
4
5
6
7
8
9
10
11
#include<cstdlib>//包含了随机数生成函数
#include<ctime>//包含time()函数
int main2() {
srand(static_cast<unsigned int>(time(0)));
int randomnumber = rand();
std::cout << randomnumber<<std::endl;
int die = (randomnumber % 10)+1;//产生1-10的随机数
std::cout << die << std::endl;
std::cin.get();
return 0;
}

使用string对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<iostream>
#include<string>
using namespace std;
int main() {
string word1 = "game";
string word2 = "over";//string的两种初始化方式
string word3(3,'!');//用多个字符对string进行初始化
string phrase = word1 + "" + word2 + word3;//string的拼接用了=运算重载,很是方便
cout << phrase << endl;
cout << "字符串大小:" << phrase.size() << endl;
cout << phrase[0] << endl;//按下表访问字符串中字符
phrase[0] = 'L';//按下标替换字符
for (unsigned int i = 0; i < phrase.size(); ++i) {//size()返回是unsigned int型
cout << "第" << i << "为" << phrase[i] << endl;
}
cout << phrase.find("over") << endl;//查找字符串位置,并返回查找单词首字母下标
if (phrase.find("nothiswords") == string::npos) {
//string::npos访问的常量表示string对象可能的最大长度,它比对象中任意可能的合法位置都要大,可以表示“一个不可能存在的位置”,表明不找不到字符串
cout << "'nothiswords'is not in the phrase" << endl;
}
phrase.erase(4, 5);//删除从第四个字符开始的五个字符
cout << phrase<<endl;
phrase.erase(4);//删除从第四个字符开始的所有字符
cout << phrase<<endl;
phrase.erase();//删除所有字符
cout << "phase:"<<phrase << endl;
if (phrase.empty()) {//判断句子是不是空了
cout << "phrase是空的"<<endl;
}
string a;
if (a.empty()) {//未初始化的string对象也是空的
cout << "a是空的";
}
cin.get();
return 0;
}

使用vector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<vector>
int main() {
vector<string> inventory;//尽管向量是动态的,但不可以使用下标运算符增加向量元素,例inventory[4] = "axe";是错的,要push_back
inventory.push_back("sword");
inventory.push_back("armor");
inventory.push_back("shield");
for (unsigned int i = 0; i < inventory.size(); ++i) {
cout << inventory[i] << endl;
}
inventory[0] = "axe";
//弹出一个元素
inventory.pop_back();
for (unsigned int i = 0; i < inventory.size(); ++i) {
cout << inventory[i] << endl;
}
//清除vector
inventory.clear();
//判断是否为空
if (inventory.empty())//向量等STL和字符串有很多相似之处
{
cout << "为空";
}
cin.get();
return 0;
}

其它STL容器

STL定义的容器分为两大类:顺序型和关联型。顺序型容器可以依次检索元素值,而关联型容器则基于键值检索元素值。

使用迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int main8() {
vector<string> inventory;
inventory.push_back("sword");
inventory.push_back("armor");
inventory.push_back("shield");
vector<string>::iterator myiter;//声明了一个名为myiter的迭代器
//何为迭代器?迭代器是标识容器中的某个特定元素的值,给定一个迭代器,可以访问元素的值,给定正确类型的迭代器,就可以修改其值。
//迭代器还可以通过常见的算数运算符在元素之间移动。
//可以将迭代器想象成贴在容器中某个特定元素上的便签,迭代器虽然不是元素本身,但它是应用元素的一种方式。
vector<string>::const_iterator iter;//声明了一个名为iter的常量迭代器
//除了不能用来修改其引用元素外,常量迭代器与常规迭代器几乎一样。由常量迭代器与常规迭代器几乎一样。可以将常量迭代器想象成提供了制度访问权限
//C++有很多权限上的严格限制,这也是C++的特色,使代码意图清晰,也更安全。
cout << "Your items:" << endl;//用迭代器循环打印
for (iter = inventory.begin(); iter != inventory.end(); ++iter) {
cout << *iter << endl;
}
//用迭代器去修改元素的值
myiter = inventory.begin();
*myiter = "axe";
cout << "Now your items:" << endl;//用迭代器循环打印
for (iter = inventory.begin(); iter != inventory.end(); ++iter) {
cout << *iter << endl;
}
cout << (*myiter).size() << endl;//可以把迭代器当做向量等容器的指针,用法和数组指针都极为类似
cout << myiter->size() << endl;
//插入值操作,插入的地方值后移
inventory.insert(inventory.begin(), "crossbow");
cout << "Now your items:" << endl;//用迭代器循环打印
for (iter = inventory.begin(); iter != inventory.end(); ++iter) {
cout << *iter << endl;
}
//清除元素
inventory.erase((inventory.begin() + 2));//erase接受一个迭代器作为实参
cout << "Now your items:" << endl;//用迭代器循环打印
for (iter = inventory.begin(); iter != inventory.end(); ++iter) {
cout << *iter << endl;
}
cin.get();
return 0;
}

使用算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<iostream>
#include<vector>
#include<algorithm>
#include<cstdlib>
#include<ctime>
using namespace std;
int main() {
//使用 STL 内置算法
vector<int>::const_iterator iter;//常量迭代器
vector<int> scores;
scores.push_back(1200);
scores.push_back(1500);
scores.push_back(1900);
scores.push_back(111);
cout << "scores :" << endl;
for (iter = scores.begin(); iter != scores.end(); ++iter)
{
cout << *iter << endl;
}
//使用查询算法
cout << "Finding a score.\n";
int score;
cout << "Please enter a score\n";
cin >> score;
iter = find(scores.begin(), scores.end(), score);
if (iter != scores.end()) {
cout << "score found\n";
}
else
{
cout << "not found\n";
}
//随机打乱顺序,游戏中很有用噢,可以用来洗牌,也可以用来打乱某一关遭遇的敌人顺序
srand(static_cast<unsigned int>(time(0)));//用时间作为随机种子
random_shuffle(scores.begin(), scores.end());
for (iter = scores.begin(); iter != scores.end(); ++iter)
{
cout << *iter << endl;
}
//对元素进行升序排序
sort(scores.begin(), scores.end());
for (iter = scores.begin(); iter != scores.end(); ++iter)
{
cout << *iter << endl;
}
cin.get();
cin.get();
return 0;
}

理解代码重用

  • 提高产量 。通过重用已有的代码和一些元素的基础上(例如游戏引擎),可以迅速构建出完整的项目。
  • 改善质量。 经过测试和已经确认无bug的代码进行重用,可以使程序稳定性增强。
  • 改善性能。 一旦写出了或者改进后的高性能的代码,对其重用可以避免再造轮子,也避免了低性能代码的产生。

函数指定默认参数

在函数中的默认参数设置时,一旦在参数列表里指定了一个默认参数,则必须为余下的所有参数指定默认参数。

此形参后面的所有参数都要设置上默认参数

因此下面的原型是合法的:

1
void func(int a , int b , int c = 100 , int d = 20 )

而下面这个是非法的:

1
void func(int a , int b = 2 , int c = 3 , int d )

函数重载

实现函数重载,需要使用不同的形参列表为同一个函数编写不同的定义。

注意,仅仅是返回类型不同的函数,将导致编译错误。

例如:

下面是错误的:

1
2
int func(int)
float func(int)

下面是正确的:

1
2
int func(int)
float func(float)

程序规划

在编写一切代码之前,例如编写游戏,游戏设计者已在概念书、设计文档和原型上花费数不清的时间。一旦设计工作完成,程序员便开始他们的工作——更多的规划。只有在程序员写下他们自己的技术设计之后,他们才开始认真的编码。

一般流程:

  • 编写伪代码

    在抽象层面考虑代码,伪代码的每一行应当是一个函数调用。

    之后,所有要做的则是编写伪代码所暗示的函数。

    例如一个控制台版本的XXOO游戏,伪代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    create an empty xxoo board
    display the game instructions
    determine who goes first
    display the board
    while nobody has won and it's not a tie //tie代表平局局
    if it's the human's turn
    get the human move
    update the board with the human's move
    otherwise
    calculate the computer's move //这里是计算出电脑该怎么行动,因为电脑是需要自己编写的AI,要根据人类的棋局选择下一步放在哪里
    update the board with the computer's move
    display the board
    switch turns //转换选手
    congratulate the winner or declare a tie
  • 数据表示

    有了一个不错的规划,但仍然很抽象,需要真正定义其中的元素。

    例如:如何表示游戏棋盘?如何表示棋子和一招棋?

    选择恰当的数据结构,去定义抽象的事物。

  • 创建函数列表

    伪代码暗示了所需要的不同函数,为他们创建一个列表,并确定各自功能、拥有的参数和返回值。

    | 函数 | 描述 |
    | —————————————- | —————————————- |
    | void instructions(); | 显示游戏操作指南 |
    | char askYesNo(string question); | 接受一个问题,返回“y”或“n” |
    | int askNumber(string question, int high, int low = 0); | 询问一定范围内的数字。接受一个问题、一个范围上限和一个范围下限。返回low到high之间的数字 |
    | char humanPiece(); | 确定玩家的棋子。返回X或O |
    | char opponent(char piece); | 返回给定棋子的对应棋子。 |
    | void displayBoard(const vector& board); | 在屏幕上显示当前棋盘。 |
    | char winner(const vector& board); | 确定游戏的胜者。返回X、O、或T(和棋)或N(还没有哪一方胜出) |
    | bool isLegal(const vector& board, int move); | 判断输入的数字是否合法 |
    | int humanMove(const vector& board, char human); | 获取人类玩家的下棋。接受一个棋盘与人类玩家的棋子作为参数,返回玩家下棋的数字位置。 |
    | int computerMove(vector board, char computer); | 获取计算机玩家的下棋。接受一个棋盘与人类玩家的棋子作为参数,返回玩家下棋的数字位置。 |
    | void announceWinner(char winner, char computer, char human); | 宣布最后结果。 |

使用常量指针

声明一个常量指针,无法修改存储在常量指针中的地址

1
2
int score =100
int * const ps =&score;//ps是常量,不可以变动,而*ps可以改变

注意,和所有常量一样,第一次声明常量指针时候必须初始化

1
int *const p; //illegal !,因为常量p声明之后就无法修改,再也无法赋值了,所以,声明时要初始化

提示:在C++中尽管可以使用指针,但还是尽可能的使用引用,引用在语法上比指针更加简洁,并且让代码更易读懂。

使用指向常量的指针

1
const int* p //指向int型常量的指针p

指针本身可以变得,但是被指的对象为常量,不可改变。

使用指向常量的常量指针

1
2
const int a=1;
const int* const p=&a;

注意

使用指针时候要注意野指针问题。

特别是函数返回类型为一个指针时,例如:

1
2
3
4
5
6
string* badpointer()
{
string local ="this a string";
string* plocal =&local;
return plocal;
}

这段代码可能导致程序崩溃,因为返回的指针所指向的字符串在函数结束之后不复存在。成为野指针。

静态数据成员

  • 静态变量在函数调用之间保留其值
  • 类中定义的公有静态数据成员,可以在程序任意一处以加上类域的方式访问:Critter::s,只有静态成员变量可以这样访问!
  • 静态数据成员私有化的时候,就只能和其它私有数据成员一样,只能在类成员函数之中对其进行访问。

静态成员函数

  • 静态成员函数是为整个类而存在的函数。

static int get_s();

  • 静态成员函数主要用来使用静态数据成员。
1
2
3
int Critter::get_s(){
return s;
}
  • 静态成员函数不能访问非静态数据成员!

    因为静态成员函数是为整个类而存在的,而与具体的该类的某个实例无关。

  • 从外部调用调用类需要用类域名限定它:Critter::get_s()

STL的reserve()

1
2
3
vector<string> str_list;
int spaces=10;
str_list.reserve(spaces);

函数reserve()将字符串的容量设置为至少size. 如果size指定的数值要小于当前字符串中的字符数(亦即size < this→size()), 容量将被设置为可以恰好容纳字符的数值. reserve()以线性时间(linear time)运行。

它最大的用处是为了避免反复重新分配缓冲区内存而导致效率降低,或者在使用某些STL操作(例如std::copy)之前保证缓冲区够大。

对于指针和数组的关系

指针和数组名之间的关系记住这几个公式即可:

1
int array[10];
  • $$array \equiv \&array[0]$$
  • $array+k\equiv \&array[k]$
  • $*array\equiv array[0]$
  • $*(array+k)\equiv array[k]$

避免内存泄露

两种典型的导致内存泄露的例子:

1
2
3
4
void leak1()
{
int* drip1 = new int(30);
}

分配了个内存块,函数没有返回内存指针,没办法释放,内存泄露。

1
2
3
4
5
6
void leak2()
{
int* drip2 = new int(50);
drip2 = new int(100);
delete drip2;
}

此时没有任何指针指向堆中存储50那块内存,程序无法释放它,内存泄露。

深拷贝和浅拷贝

1
简单的来说就是,在有指针的情况下,浅拷贝只是增加了一个指针指向已经存在的内存,而深拷贝就是增加一个指针并且申请一个新的内存,使这个增加的指针指向这个新的内存,采用深拷贝的情况下,释放内存的时候就不会出现在浅拷贝时重复释放同一内存的错误!

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Critter{
pubilc:
Critter(const string& name=" ");//构造函数
Critter(const Critter& c);//拷贝构造
Critter& Critter::operator=(const Critter& c);//重载=运算符
~Critter();//析构
private:
string* m_pName;
};
Critter::Critter(const string& name)
{
m_pName=new string(name)
}
Critter::Critter(const Critter& c)
{
m_pName=new string(*(c.m_pName));
}
Critter& Critter::operator=(const Critter& c)
{
if (this != &c)
{
delete m_pName;
m_pName = new string(*(c.m_pName));
}
return *this;
}
Critter::~Critter()
{
delete m_pName;
}

类的访问权限

  • public成员可以被程序中的所有代码访问。
  • protected成员只能被本类与特定派生类访问,这取决于继承的访问级别。
  • private成员只能被本类成员访问,即它们不能被任何派生类直接访问。

虚基类成员函数

对于任何继承的基类成员函数,如果期望在派生类中对其重写,则应当使用关键字virtual将其声明为虚函数。

尽管可以重写非虚成员函数,但这可能会导致一些意外行为,一个较好的准则是将任何要重写的基类成员函数声明为虚函数。

重写函数与重载函数

注意:在重写一个基类的重载成员函数时,基类成员函数的所有重载版本都会被覆盖掉,意味着访问其他版本的成员函数的唯一方式是显式的地调用基类成员函数。因此,如果要重写重载成员函数,最好重写重载函数的每个版本。

在派生类中使用重载运算符与拷贝构造函数

重载运算符与拷贝构造函数不会从基类继承过来,所以在派生类中需要重新处理。

在派生类中重载赋值运算符时,通常需要调用基类中的赋值运算符成员函数,方法是使用基类名称作为前缀显式调用。例:

Boss是从Enemy继承而来,那么Boss中定义的重载赋值运算符成员函数可以这样开始:

1
2
3
4
5
6
7
Boss& operator= (const Boss & b)
{
Enemy::opertor=(b);
}
Boss(const Boss& b):Enemy(b)
{//这里再处理余下部分数据...
}

纯虚函数

当类包含至少一个纯虚函数时,该类为抽象类。

坚持原创技术分享,您的支持将鼓励我继续创作!