【C++】游戏开发--基础

文章目录

    • 使用调用堆栈进行内存存储
    • 谨慎使用递归
    • 使用指针存储内存地址
    • 在不同数据类型之间进行转换
    • 使用动态分配更有效地管理内存
    • 使用按位运算进行高级检查和优化

使用调用堆栈进行内存存储

C ++仍然是大多数游戏开发人员首选的语言,其主要原因是:
我们可以自己处理内存,在很大程度上控制内存的分配和取消分配。

因此,我们需要了解提供给我们的不同存储空间。

当数据“推送”到堆栈上时,堆栈会增加。随着数据从堆栈中“弹出”,堆栈会缩小。如果不先弹出放置在堆栈顶部的所有数据,就不可能从堆栈弹出特定的数据。
可以将其视为一系列从上到下对齐的隔室。堆栈的顶部是堆栈指针碰巧指向的任何隔离专区(这是一个寄存器)。
每个隔离专区都有一个顺序地址。这些地址之一保留在堆栈指针中。位于该魔术地址之下的所有内容(称为堆栈的顶部)都被视为处于堆栈中。
堆栈顶部上方的所有内容均视为已离开堆栈。将数据压入堆栈时, 将其放置在堆栈指针上方的隔离专区中,然后将堆栈指针移至新数据。
当数据从堆栈弹出时,通过将其向下移动堆栈来更改堆栈指针的地址。
准备工作
你需要在Windows计算机上安装Visual Studio的工作副本。

如何实现
C ++可能是目前最好的编程语言之一,其主要原因之一是它也是一种“低级语言“,因为我们可以操纵内存。
要了解内存处理,了解内存堆栈的工作方式非常重要:
1.打开Visual Studio。
2.创建一个新的C ++项目。
3.选择Win32控制台应用程序。
4.添加一个名为main.cpp的源文件或您想要命名该源文件的任何文件。 5.添加以下代码行:

#include 
#include 
using namespace std;
int countTotalBullets(int iGun1Ammo, int iGun2Ammo)
{
    return iGun1Ammo + iGun2Ammo;
}
int main()
{
    int iGun1Ammo = 3;
    int iGun2Ammo = 2;
    int iTotalAmmo = countTotalBullets(iGun1Ammo, iGun2Ammo);
    cout << "Total ammunition currently with you \
        is "<<iTotalAmmo;
        _getch();
}

当您调用countTotalBullets函数时,代码将分支到被调用的函数。
传入参数并执行函数主体。函数完成后,将返回一个值,并且控件将返回到调用函数。
但是,从编译器的角度来看,它如何真正起作用?

当你开始程序时,编译器会创建一个堆栈。
堆栈是为程序分配的特殊内存区域,用于保存程序中每个功能的数据。
堆栈是后进先出(LIFO)数据结构。想象一副纸牌;最后一张牌是拿出的第一张牌。当您的程序调用CountTotalBullets时,将建立一个堆栈框架。

堆栈框架是堆栈中专门用于管理该功能的区域。
这是非常复杂的并且在不同平台上有所不同,但是这些是基本步骤:

  1. CountTotalBullets的返回地址放在堆栈中。函数返回时,它将恢复 在此地址执行。
  2. 在堆栈上为您声明的返回类型留出空间。
  3. 该函数的所有参数都放在堆栈上。
  4. 程序分支到您的功能。
  5. 局部变量在定义时被压入堆栈。

谨慎使用递归

递归是一种编程设计形式,其中函数多次调用自身,以通过将一个大型解决方案集分解为多个小型解决方案集来解决问题。
代码大小肯定会缩短。但是,如果使用不当,则递归可以真正快速地填满调用堆栈,并且可能会耗尽内存。

递归可以使代码代码变得非常简洁,但也会导致一些严重的问题:
1.打开Visual Studio。
2.创建一个新的C ++项目。
3.选择Win32控制台应用程序。
4.添加一个名为main.cpp的源文件或您想要命名该源文件的任何文件。
5.添加以下代码行:

#include 
#include 
using namespace std;
int RecursiveFactorial(int number);
int Factorial(int number);
int main()
{
	long iNumber;
	cout << "Enter the number whose factorial you want
 	to find";
 	cin >> iNumber;
 	cout << RecursiveFactorial(iNumber) << endl;
 	cout << Factorial(iNumber);
 	_getch();
 	return 0;
}
int Factorial(int number)
{
 	int iCounter = 1;
 	if (number < 2)
 	{
 		return 1;
 	}
 	else
 	{
 		while (number>0)
 		{
 		iCounter = iCounter*number;
 		number -= 1;
 		}
 	}
 	return iCounter;
}
int RecursiveFactorial(int number)
{
 	if (number < 2)
 	{
 		return 1;
 	}
 	else
 	{
 		while (number>0)
 		{
 		return number*Factorial(number - 1);
 		}
 	}
}

从前面的代码中可以看到,两个函数都找到数字的阶乘。但是,当使用递归时,每次调用函数时堆栈的大小都会大大增加。每次调用和将数据压入堆栈时,都必须更新堆栈指针。使用递归时,随着函数调用自身,每次从自身内部调用函数时,堆栈大小都会不断增加,直到耗尽内存并产生死锁或崩溃为止。想象一下找到1000的阶乘。该函数将在自身内部被调用很多次。这是造成某些灾难的良方,我们应该在很大程度上避免这种编码做法。

如果找到大于15的数字的阶乘,则可以使用比int更大的数据类型,因为结果阶乘将太大而无法存储在int中。

使用指针存储内存地址

我们已经看到没有足够的内存对我们来说是一个问题。
但是,到目前为止,我们还无法控制分配多少内存以及为每个内存地址分配了什么内容。
使用指针,我们可以解决这个问题。
我认为,指针是C ++中最重要的主题。
如果你必须清楚C ++的概念,并且要成为C ++的优秀开发人员,那么你必须擅长使用指针。指针一开始看起来似乎很艰巨,但是一旦掌握了指针,指针就很容易使用。

我们将看到使用指针的简单性。一旦你习惯使用指针,我们就可以轻松地操作内存并将引用存储在内存中:
1.打开Visual Studio。
2.创建一个新的C ++项目。
3.选择Win32控制台应用程序。
4.添加一个名为main.cpp的源文件或您想要命名该源文件的任何文件。
5.添加以下代码行:

#include 
#include 
using namespace std;
int main()
{
 	float fCurrentHealth = 10.0f;
 	cout << "Address where the float value is stored: "
 	<< &fCurrentHealth << endl;
 	cout << "Value at that address: "
 	<< *(&fCurrentHealth) << endl;
 	float* pfLocalCurrentHealth = &fCurrentHealth;
 	cout << "Value at Local pointer variable:
 	"<<pfLocalCurrentHealth << endl;
 	cout << "Address of the Local pointer variable:
 	"<<&pfLocalCurrentHealth << endl;
 	cout << "Value at the address of the Local pointer
 	variable: "<<*pfLocalCurrentHealth << endl;
 	_getch();
 	return 0;
}

C ++程序员最强大的工具之一是直接操作计算机内存。指针是保存内存地址的变量。 C ++程序中使用的每个变量和对象都存储在内存中的特定位置。每个存储位置都有一个唯一的地址。内存地址将根据所使用的操作系统而有所不同。占用的字节数取决于变量类型:float = 4个字节,short = 2个字节:
【C++】游戏开发--基础_第1张图片
存储器中的每个位置均为1个字节。指针pfLocalCurrentHealth保存已存储fCurrentHealth的内存位置的地址。因此,当我们显示指针的内容时,我们将获得与包含fCurrentHealth变量的地址相同的地址。我们使用&运算符获取pfLocalCurrentHealth变量的地址。当我们使用*运算符引用指针时,我们得到存储在地址中的值。由于存储的地址与存储fCurrentHealth的地址相同,因此我们得到的值为10。

让我们考虑以下声明:

const float * pfNumber1 
float * const pfNumber2 
const float * const pfNumber3

所有这些声明都是有效的。但是它们是什么意思呢?
第一个声明指出pfNumber1是一个指向常量浮点数的指针。
第二个声明指出pfNumber2是一个指向浮点数的常量指针。
第三个声明指出pfNumber3是指向常量整数的常量指针。

此处列出了引用与这三种const指针之间的主要区别:
const指针可以为NULL
引用没有自己的地址
指针具有它自己的地址,并将其指向的值的地址作为其值

在不同数据类型之间进行转换

转换是将某些数据更改为不同类型的数据的转换过程。我们可以在内置类型或我们自己的数据类型之间进行转换。
某些转换是由编译器自动完成的(如在函数传递参数时float转为double),程序员无需干预。
这种转换称为隐式转换。其他必须由程序员直接指定的转换称为显式转换

有时我们可能会收到有关数据丢失的警告。我们应该注意这些警告,并考虑这可能对我们的代码产生不利影响。当接口需要特定类型的数据时,通常会使用强制类型转换,但我们要向其提供其他类型的数据。

使用C,我们可以将任何内容转换为所有内容。但是,C ++为我们提供了更好的控件。

通常,即使在C ++中,程序员也使用C样式转换,但是不建议这样做。 C ++为我们提供了针对不同情况的自身转换样式,应使用:

1.打开Visual Studio。
2.创建一个新的C ++项目。
3.选择Win32控制台应用程序。
4.添加一个名为main.cpp的源文件或您想要命名该源文件的任何文件。
5.添加以下代码行:

#include 
#include 
using namespace std;
int main()
{
 	int iNumber = 5;
 	int iOurNumber;
 	float fNumber;
 	//No casting. C++ implicitly converts the result
 	into an int and saves
 	//into a float
 	fNumber = iNumber/2;
 	cout << "Number is " << fNumber<<endl;
 	//C-style casting. Not recommended as this is not type safe
 	fNumber = (float)iNumber / 2;
 	cout << "Number is " << fNumber<<endl;
 	//C++ style casting. This has valid constructors to make the
	casting a safe one
 	iOurNumber = static_cast<int>(fNumber);
 	cout << "Number is " << iOurNumber << endl;
 	_getch();
 	return 0;
}

C ++中有四种类型的转换运算符,具体取决于我们要转换的内容:static_castconst_castreinterpret_castdynamic_cast。现在,我们来看一下static_cast。在讨论动态内存和类之后,我们将介绍其余的三种转换技术。
从较小的数据类型转换为较大的类型称为提升,并且保证不会丢失数据
但是,从较大的数据类型转换为较小的数据类型称为降级,并且可能导致数据丢失。发生这种情况时,编译器通常会向您发出警告,您应该注意这一点。

让我们看一下前面的例子。我们已经初始化了一个值为5的整数。接下来,我们已经初始化了一个浮点变量,并存储了结果5除以2(即2.5)。但是,当我们显示变量fNumber时,我们看到显示的值为2。原因是C ++编译器隐式转换5/2的结果并将其存储为整数。
因此,它正在计算类似于int(5/2)的结果,即int(2.5),计算结果为2。
因此,为了实现所需的结果,我们有两个选择。

第一种方法是C样式的显式强制转换,因为它没有类型安全检查,因此根本不建议使用。 C样式转换的格式为(resultant_data_type)(表达式),在这种情况下,它类似于float(5/2)。我们明确告诉编译器将表达式的结果存储为浮点数。

第二种方法,以及一种更具C ++风格的转换方法,是使用static_cast操作。这有合适的构造函数 指示该转换是类型安全的。 static_cast操作的格式为static_cast(表达式)。 编译器检查强制转换是否安全,然后执行类型强制转换操作。

使用动态分配更有效地管理内存

程序员通常处理内存的五个区域:全局名称空间,寄存器,代码空间,堆栈和自由存储。初始化数组时,必须定义元素数。这导致很多内存问题。大多数时候,并非分配给我们的所有元素都被使用,有时我们需要更多的元素。为了帮助解决此问题,在使用空闲存储运行.exe文件时,C ++促进了内存分配。空闲存储区是可用于存储数据的大容量内存,有时也称为
我们可以在空闲存储上请求一些空间,这将为我们提供一个可用于存储数据的地址。

我们需要将该地址保留在指针中。
程序结束之前,不会清除空闲存储。
程序员有责任释放其程序使用的任何空闲存储内存。空闲存储的优点是无需预先分配 所有变量。
我们可以在运行时决定何时需要更多的内存。 内存已保留,并且在显式释放之前一直保持可用。
如果在函数中保留了内存,则当控制从该函数返回时,该内存仍然可用。 这是比全局变量更好的编码方式。 只有有权访问指针的函数才能访问存储在内存中的数据,并且它为该数据提供了受严格控制的接口。

在游戏中,大多数内存是在运行时动态分配的,因为我们不确定应该分配多少内存。
分配任意数量的内存可能导致更少的内存或内存浪费:
1.打开Visual Studio。
2.创建一个新的C ++项目。
3.添加一个名为main.cpp的源文件或您想要命名该源文件的任何文件。
4.添加以下代码行:

#include 
#include 
#include 
using namespace std;
int main()
{
 	int iNumberofGuns, iCounter;
 	string * sNameOfGuns;
 	cout << "How many guns would you like to purchase? ";
 	cin >> iNumberofGuns;
 	sNameOfGuns = new string[iNumberofGuns];
 	if (sNameOfGuns == nullptr)
 	cout << "Error: memory could not be allocated";
 	else
 	{
 		for (iCounter = 0; iCounter<iNumberofGuns; iCounter++)
 		{
 		cout << "Enter name of the gun: ";
 		cin >> sNameOfGuns[iCounter];
 		}
 	cout << "You have purchased: ";
 	for (iCounter = 0; iCounter<iNumberofGuns; iCounter++)
 		cout << sNameOfGuns[iCounter] << ", ";
 	delete[] sNameOfGuns;
 	}
 	_getch();
 	return 0;
}

您可以使用new关键字为空闲存储分配内存; new后跟要分配的变量的类型。这使编译器知道需要分配多少内存。

在我们的示例中,我们使用了字符串。 new关键字返回一个内存地址。
该内存地址已分配给指针sNameOfGuns。我们必须将地址分配给指针,否则地址将丢失。

使用new运算符的格式为 datatype * pointer = new datatype。因此,在我们的示例中,我们使用了sNameOfGuns = new string [iNumberofGuns]
如果新分配失败,它将返回空指针(NULL)。我们应该经常检查指针分配是否成功。否则,我们将尝试访问尚未分配的部分内存,并且可能会从编译器中获取错误,如以下屏幕截图所示,您的应用程序将崩溃

完成存储后,必须在指针上调用delete。删除将内存返回到免费存储。请记住,指针是局部变量。如果在其中声明了指针的函数超出范围,则免费存储中的内存不会自动释放。静态内存和动态内存之间的主要区别在于,静态内存的创建/删除是自动处理的,而动态内存必须由程序员创建和销毁。

delete []运算符向编译器发出信号,它需要释放数组。如果不使用括号,则仅删除数组中的第一个元素。这将导致内存泄漏。内存泄漏确实很糟糕,因为这意味着有些内存空间尚未释放。请记住,内存是有限的空间,因此最终您将遇到麻烦。当我们使用delete []时,编译器如何知道它必须从内存中释放n个字符串?仅当您知道指针sNameOfGuns时,运行时系统才会将项目数存储在可以检索的位置。有两种流行的技术可以做到这一点。

这两种方法都由商业编译器使用,两者都有权衡,而且都不是完美的:
方法1:
过度分配数组并将项目数放在第一个元素的左侧。这是两种技术中较快的一种,但是对程序员说delete sNameOfGuns而不是delete []的问题更为敏感。 sNameOfGuns。
方法2:
使用关联数组,将指针作为键,并将项目数作为值。 这是两种技术中较慢的一种,但是对程序员说delete sNameOfGuns而不是delete [] sNameOfGuns的问题不太敏感。

了解错误消息使用调试版本时,您可能会在调试过程中注意到内存中的以下值:
0xCCCCCCCC:这是指在堆栈上分配但尚未初始化的值。
0xCDCDCDCD:这表示已在堆中分配了内存,但尚未初始化(干净内存)。 0xDDDDDDDD:这意味着内存已从堆中释放(死内存)。
0xFEEEFEEE:这是指从免费存储中释放的值。
0xFDFDFDFD:“无人区”围栏,在调试模式下放置在堆内存的边界。这些绝对不能被覆盖,如果被覆盖,则可能意味着程序正在尝试以超出数组最大大小的索引访问内存。

使用按位运算进行高级检查和优化

在大多数情况下,除非需要编写一些压缩算法,否则程序员无需太担心位,并且在进行游戏时,我们永远不知道何时会发生诸如此类的情况。出现了。为了对以这种方式压缩的文件进行编码和解码,您实际上需要在位级别提取数据。最后,您可以使用位操作来加速程序或执行巧妙的技巧。但是,并不总是建议这样做。

通过直接与内存交互,按位操作也是优化代码的一种好方法:
1.打开Visual Studio。
2.创建一个新的C ++项目。
3.添加一个名为main.cpp的源文件或您想要命名该源文件的任何文件。
4.添加以下代码行:

#include 
#include 
using namespace std;
void Multi_By_Power_2(int iNumber, int iPower);
void BitwiseAnd(int iNumber, int iNumber2);
void BitwiseOr(int iNumber, int iNumber2);
void Complement(int iNumber4);
void BitwiseXOR(int iNumber,int iNumber2);
int main()
{
 	int iNumber = 4, iNumber2 = 3;
 	int iPower = 2;
 	unsigned int iNumber4 = 8;
 	Multi_By_Power_2(iNumber, iPower);
  BitwiseAnd(iNumber,iNumber2);
 BitwiseOr(iNumber, iNumber2);
 BitwiseXOR(iNumber,iNumber2);
 Complement(iNumber4);
 _getch();
 return 0;
}

void Multi_By_Power_2(int iNumber, int iPower)
{
 	cout << "Result is :" << (iNumber << iPower)<<endl;
}
void BitwiseAnd(int iNumber, int iNumber2)
{
 	cout << "Result is :" << (iNumber & iNumber2) << endl;
}
void BitwiseOr(int iNumber, int iNumber2)
{
 	cout << "Result is :" << (iNumber | iNumber2) << endl;
}
void Complement(int iNumber4)
{
 	cout << "Result is :" << ~iNumber4 << endl;
}
void BitwiseXOR(int iNumber,int iNumber2)
{
 	cout << "Result is :" << (iNumber^iNumber2) << endl;
}

左移位运算符等效于将一个数字的所有位向左移动指定的位数。
在我们的示例中,我们要发送给函数Multi_By_ Power_2的数字是4和3。
二进制表示形式4是100,
因此,如果将最高有效位(即1)向左移动三位,则得到10000,它是16的二进制数。
因此,左移等效于2 ^ shift_arg的整数除法,即4 * 2 ^ 3,也就是16。类似地,右移运算等效于2 ^ shift_arg的整数除法。 。现在让我们考虑我们要打包数据以便压缩数据。考虑以下示例:

int totalammo,type,rounds;

我们将全部子弹存放在枪支中;枪的类型,但只能是步枪或手枪;以及它可以发射的每发子弹总数。
当前,我们使用三个整数值来存储数据。但是,我们可以将所有前面的数据压缩为一个整数,从而压缩数据:

int packaged_data;
packaged_data = (totalammo << 8) | (type << 7) | rounds;

如果我们假设以下符号:

totalammon:A
tyoe:T
rounds:R

数据中的最终表示将如下所示:

AAAAAAATRRRRRRR

你可能感兴趣的:(笔记,c++)