首先,此贴不讨论任何网络游戏WG的内容,所以请不要问我某某游戏如何过XX,如何写XX,XX哪里不对,以及XX为什么不起作用等类似问题,此贴所述的知识和技术请自行使用单机游戏尝试,例如:植物大战僵尸。
其次,本人早已不再进行任何WG相关的研究与开发,最早接触WG开发只是因为兴趣,并不曾以此牟利,现在做正规软件开发,所以不参与任何具体到某游戏中XX姿势的讨论,最多只讲与其相关的原理性知识,另外如果有C++和WIN32相关的问题我很乐意解答。
最后,此贴所述为C++和内存相关的知识,适合对二者都有一定了解的人阅读,对其中一些基本概念我不会深入讲解,所以没有相关基础的同学请选择性阅读,另外本贴纯属一时兴起,随便写写,言辞不加修饰,思路未做整理,不接受任何鸡蛋里挑骨头的行为。
以下是正文:
此贴诞生的原因要从我在本吧看到的一份代码截图说起:
这份代码如果要看懂需要这么几个基本条件:
1. 了解C/C++基本语法
2. 理解C/C++指针的概念
3. 理解数据在内存中的存储形式
4. 理解C/C++对象内存布局
我在此对第234点做简单的解释:
在现代操作系统中,因为内存分页机制的存在,程序所使用的内存空间是线性的,对于一个32位的应用程序,其地址空间范围为0-0xFFFFFFFF,大小共4GB(即2^32字节)
在C/C++语言中对于整型变量和字符型是直接以数据的常规来存储的,整形的100在内存中就是100,而对于浮点型数据float、double、long double而言,他们的存储形式是由编译器决定的,目前最常用的标准是IEEE754标准,此标准规定的浮点数在内存中的存储形式如下图:
关于IEEE754标准的详细信息请自行网络搜索或看标准Paper,这里只需要知道,这两种基本数据类型在内存中的存储方式是不同的,所以一切妄图通过以整型格式去读取浮点型格式数据的行为,最终只会得到无意义的数值,相反同理。举例比如:
int a = 10;
float b = *(float*)&a;
执行完毕后,b的值将是根据浮点数标准相关规定得出的无意义的值,因此对于一个值,我们必要根据其本身的数据类型,使用对应类型的指针来进行存取操作(此例为int型,float同理):
int a = 10;
int b = *(int*)&a;
懂一些编程的人会说:你这是脱裤子放屁,为什么不直接 int b = a 呢?
因为此代码中有a的定义,编译器可以直接通过变量名字a来寻址到a的内存,而在编写WG时,我们不可能去到游戏的上下文中进行开发,除非你有游戏的源代码(有源代码谁还写WG了)。
因此你所知的a仅能通过一个表示其地址的数值来体现,比如0x12345678,假定此为a的地址,那么对a的读取操作为 int b = *(int*)0x12345678,写入操作为 *(int*)0x12345678 = 20。
以上是第23点的简单讲解,再给完全没理解指针的同学补充一些:
指针自身的类型就是指针类型,不属于整型或浮点型或字符型或其他任何型。
指针在32位进程中的大小为4字节,和int/long类型的大小一致,而且指针在内存中的存储形式也和int/long相同,因此可以相互转化,但这种转化不是自动的(隐式的),需要使用强制转换,C风格的强制转换只有一种,形式即(type)object。
类似0x1234567或者100的这种直接写在源代码里的数值,他有个名字叫字面量(字面值)(0x开头的为16进制字面值),默认的不超过4字节范围的字面量其类型为int,与指针类型不同,因此若要将0x12345678作为指针使用,需要使用强制类型转换,即(int*)0x12345678,此即为一个指向int型数据的指针。对于访问一个指针所指数据的操作,使用*操作符,即解引用,*(int*)0x12345678这个表达式的结果即为10。
&为取地址操作符,使用形式为&object,此表达式结果为指向该变量的指针,类型为指向object的类型的指针,比如:float x = 3.14,那么&x的结果就是一个float*类型的指针,其指向数据为x存储的数据,对其解引用 *&x 结果就是3.14。
另外指向不同类型的指针本身的类型也不相同,因此也需要强制转换,对于上面最开始的例子,(float*)&a就体现了这点,&a的类型是int*,要将其当作float*来使用就需要这样的强制转换。
对于第4点,需要理解C/C++对象的内存模型,最基础的来说就是struct(结构体),举例以下结构体定义:
struct S {
int a;
int b;
};
这是一个最简单的例子,类型为S的结构体包含2个成员a和b,因为是包含关系,所以其大小为所有成员大小的和(此处不考虑内存对齐),因此S的大小是4+4=8字节。
又因为一个结构体的存储方式是连续且有序的,所以在S所在的8字节中,前4字节是a的数据,后4字节是b的数据,其内存结构图如下:
对结构体的访问最基本的做法是 . 操作符,即成员访问操作符,比如访问S的成员b:
S s;
s.b = 42;
首先定义了一个S类型的对象s,然后使用.操作符访问S的b成员(不是s的b成员,只有类型才拥有成员,s是对象),并将其赋值为42,这是最基本最常用的操作方式。
接下来考虑取s的地址,保存在类型为S*的指针p里,并通过p将a也赋值为42:
S* p = &s;
p->a = 42;
这里遇到了一个新的操作符 -> ,其名也为成员访问操作符,行为也和.一致,只不过他作用的对象是指针类型,因此 s.a 和 p->a 是结果是一致的。
继续我们再来看一种访问b的方式:
int i = (int)p;
*(int*)(i + 4) = 58;
先来看第一句,(int)p 即将指向s的指针p强制转换为int型,上文说过指针和整数类型不同但数据存储方式相同,并且上文我们也做过将int型转为int*型指针的操作了,此处可以反过来将S*类型的指针转为int型。
记住凡是指针都可以转换为int型,不论其是指向int型或是指向float型亦或是指向S型的,经过强制转换后,我们得到了一个数值意义上的地址,保存在i中。
第二局代码稍微复杂,我们一点点来看,首先i+4是什么,i 是对象 s 的地址,前文也讲了结构体和成员是包含关系,所以S大小为8字节,前4字节是a,后4字节是b。
那么这样一个表达式 i+0,其结果便是a的地址,又因为a是整数型大小4字节,所以i往后数4个字节i+4便是b的地址。
拿到了b的地址,但因为其是由i+4运算来的int型数据,如果我们要访问b的话,就要先转化为int*类型的指针,即(int*)(i+4),用*解引用,*(int*)(i+4)这条表达式即是b,最后对其赋值58。
以上概念对于从未接触过相关内容的同学来说可能有些难于理解,对此我爱莫能助,内存和指针的本质需要时间来消化,只能说多看书多写代码。
其次,本人早已不再进行任何WG相关的研究与开发,最早接触WG开发只是因为兴趣,并不曾以此牟利,现在做正规软件开发,所以不参与任何具体到某游戏中XX姿势的讨论,最多只讲与其相关的原理性知识,另外如果有C++和WIN32相关的问题我很乐意解答。
最后,此贴所述为C++和内存相关的知识,适合对二者都有一定了解的人阅读,对其中一些基本概念我不会深入讲解,所以没有相关基础的同学请选择性阅读,另外本贴纯属一时兴起,随便写写,言辞不加修饰,思路未做整理,不接受任何鸡蛋里挑骨头的行为。
以下是正文:
此贴诞生的原因要从我在本吧看到的一份代码截图说起:
这份代码如果要看懂需要这么几个基本条件:
1. 了解C/C++基本语法
2. 理解C/C++指针的概念
3. 理解数据在内存中的存储形式
4. 理解C/C++对象内存布局
我在此对第234点做简单的解释:
在现代操作系统中,因为内存分页机制的存在,程序所使用的内存空间是线性的,对于一个32位的应用程序,其地址空间范围为0-0xFFFFFFFF,大小共4GB(即2^32字节)
在C/C++语言中对于整型变量和字符型是直接以数据的常规来存储的,整形的100在内存中就是100,而对于浮点型数据float、double、long double而言,他们的存储形式是由编译器决定的,目前最常用的标准是IEEE754标准,此标准规定的浮点数在内存中的存储形式如下图:
关于IEEE754标准的详细信息请自行网络搜索或看标准Paper,这里只需要知道,这两种基本数据类型在内存中的存储方式是不同的,所以一切妄图通过以整型格式去读取浮点型格式数据的行为,最终只会得到无意义的数值,相反同理。举例比如:
int a = 10;
float b = *(float*)&a;
执行完毕后,b的值将是根据浮点数标准相关规定得出的无意义的值,因此对于一个值,我们必要根据其本身的数据类型,使用对应类型的指针来进行存取操作(此例为int型,float同理):
int a = 10;
int b = *(int*)&a;
懂一些编程的人会说:你这是脱裤子放屁,为什么不直接 int b = a 呢?
因为此代码中有a的定义,编译器可以直接通过变量名字a来寻址到a的内存,而在编写WG时,我们不可能去到游戏的上下文中进行开发,除非你有游戏的源代码(有源代码谁还写WG了)。
因此你所知的a仅能通过一个表示其地址的数值来体现,比如0x12345678,假定此为a的地址,那么对a的读取操作为 int b = *(int*)0x12345678,写入操作为 *(int*)0x12345678 = 20。
以上是第23点的简单讲解,再给完全没理解指针的同学补充一些:
指针自身的类型就是指针类型,不属于整型或浮点型或字符型或其他任何型。
指针在32位进程中的大小为4字节,和int/long类型的大小一致,而且指针在内存中的存储形式也和int/long相同,因此可以相互转化,但这种转化不是自动的(隐式的),需要使用强制转换,C风格的强制转换只有一种,形式即(type)object。
类似0x1234567或者100的这种直接写在源代码里的数值,他有个名字叫字面量(字面值)(0x开头的为16进制字面值),默认的不超过4字节范围的字面量其类型为int,与指针类型不同,因此若要将0x12345678作为指针使用,需要使用强制类型转换,即(int*)0x12345678,此即为一个指向int型数据的指针。对于访问一个指针所指数据的操作,使用*操作符,即解引用,*(int*)0x12345678这个表达式的结果即为10。
&为取地址操作符,使用形式为&object,此表达式结果为指向该变量的指针,类型为指向object的类型的指针,比如:float x = 3.14,那么&x的结果就是一个float*类型的指针,其指向数据为x存储的数据,对其解引用 *&x 结果就是3.14。
另外指向不同类型的指针本身的类型也不相同,因此也需要强制转换,对于上面最开始的例子,(float*)&a就体现了这点,&a的类型是int*,要将其当作float*来使用就需要这样的强制转换。
对于第4点,需要理解C/C++对象的内存模型,最基础的来说就是struct(结构体),举例以下结构体定义:
struct S {
int a;
int b;
};
这是一个最简单的例子,类型为S的结构体包含2个成员a和b,因为是包含关系,所以其大小为所有成员大小的和(此处不考虑内存对齐),因此S的大小是4+4=8字节。
又因为一个结构体的存储方式是连续且有序的,所以在S所在的8字节中,前4字节是a的数据,后4字节是b的数据,其内存结构图如下:
对结构体的访问最基本的做法是 . 操作符,即成员访问操作符,比如访问S的成员b:
S s;
s.b = 42;
首先定义了一个S类型的对象s,然后使用.操作符访问S的b成员(不是s的b成员,只有类型才拥有成员,s是对象),并将其赋值为42,这是最基本最常用的操作方式。
接下来考虑取s的地址,保存在类型为S*的指针p里,并通过p将a也赋值为42:
S* p = &s;
p->a = 42;
这里遇到了一个新的操作符 -> ,其名也为成员访问操作符,行为也和.一致,只不过他作用的对象是指针类型,因此 s.a 和 p->a 是结果是一致的。
继续我们再来看一种访问b的方式:
int i = (int)p;
*(int*)(i + 4) = 58;
先来看第一句,(int)p 即将指向s的指针p强制转换为int型,上文说过指针和整数类型不同但数据存储方式相同,并且上文我们也做过将int型转为int*型指针的操作了,此处可以反过来将S*类型的指针转为int型。
记住凡是指针都可以转换为int型,不论其是指向int型或是指向float型亦或是指向S型的,经过强制转换后,我们得到了一个数值意义上的地址,保存在i中。
第二局代码稍微复杂,我们一点点来看,首先i+4是什么,i 是对象 s 的地址,前文也讲了结构体和成员是包含关系,所以S大小为8字节,前4字节是a,后4字节是b。
那么这样一个表达式 i+0,其结果便是a的地址,又因为a是整数型大小4字节,所以i往后数4个字节i+4便是b的地址。
拿到了b的地址,但因为其是由i+4运算来的int型数据,如果我们要访问b的话,就要先转化为int*类型的指针,即(int*)(i+4),用*解引用,*(int*)(i+4)这条表达式即是b,最后对其赋值58。
以上概念对于从未接触过相关内容的同学来说可能有些难于理解,对此我爱莫能助,内存和指针的本质需要时间来消化,只能说多看书多写代码。