Cocos2d-x 的内存管理
既然选择了C++作为游戏开发的语言, 手动的管理内存是难以避免的, 而Cocos2d-x的仿Objctive-C的内存管理方式, 事实上让问题变得更加复杂(仅仅是移植更加方便了), 因为你需要很好的掌握两种语言的内存管理方式, 并且在使用时头脑清晰, 不能混用, 不然等待你的就是无穷的噩梦, 因为引用计数的原因, 问题比起纯粹的C++内存泄漏还要难以定位的多.
这里统一的整理一下目前在Cocos2d-x中的内存管理相关的方法和问题. 为了让思路更加清晰, 我提及的不仅仅是具体在Cocos2d-x中怎么用, 也包含一些为啥在Cocos2d-x中应该这么用.
并且, 因为以下讲到的每个点要讲的详细了, 其实都可以写一整篇文章了, 没法在短时间内详述, 这篇文章也就仅仅作为一个引子而已.
C++的内存管理
C语言的malloc, free
因为C++基本兼容C, 所以C语言的malloc和free也是支持的. 简单的说用法就是malloc后记得free即可.
#include <stdio.h>
#include <stdlib.h>
const size_t kBufferSize = 16;
void test() {
char *buffer = (char*)malloc(kBufferSize);
memset(buffer, 0, sizeof(char) * kBufferSize);
sprintf(buffer, "%s", "Hello World\n");
printf(buffer);
free(buffer);
}
int main(int, char**) {
test();
return 0;
}
当然, 还有realloc和calloc这两个平时较少用的内存分配函数, 这里不提了, 在C语言时代, 我们就是用malloc和free解决了我们的内存问题. 重复的对同一段内存进行free是不安全的, 同时, 为了防止free后, 还拿着指针使用(即野指针), 我们一般使用将free后的内存置空.
因为这种操作特别的多, 我们常常会弄一个宏, 比如Cocos2d-x中的
#define CC_SAFE_FREE(p) if(p) { free(p); p = 0; }
C++的new, delete, new[], delete[]
为什么malloc和free在C++时代还不够呢? malloc,free在C++中使用有如下的缺点:
malloc和free并不知道class的存在, 所以不会调用对象的构造函数和析构函数.
malloc返回的是void*, 不符合C++向强类型发展的趋势.
于是, BS在C++中增加了new-delete组合以替代malloc, free. 其实new, delete基本上都是用malloc, free实现的更高层函数, 只是增加了构造和析构的调用而已. 所以在平时使用的时候并无区别, 但是因为malloc其实是记录了分配的长度的, 长度以字节为单位, 所以一句malloc对应即可, 不管你是malloc出一个char, 一个int, 还是char的数组或者int的数组, 单位其实都是字节. 而长度一般记录在这个内存块的前几个字节, 在Windows中, 甚至可以使用_msize函数从指针中取出malloc出的内存长度.
而new的时候有两种情况, 可以是一个对象, 也可以是对象的数组, 并且, 没有统一的记录长度. 使得我们需要通过自己记住什么时候new出了一个对象, 什么时候new出了多个对象. 这个问题最近云风还吐槽过. 并且增加了delete[]用于删除new出多个对象的情况. 让问题更加容易隐藏不被发现的是, 当你讲delete用于new[]分配的内存时, 实际相当于删除了第一个对象, 是完全正确的语法, 不会报任何错误, 你只能在运行出现诡异问题的时候再去排查.
这个问题实际是语言设计的决策问题. 在BS这个完美主义者这里, C++语言的设计原则之一就是零负荷规则 — 你不会为你所不使用的部分付出代价. 并且在这里这个规则执行的显然是要比C语言还要严格. 当你只分配一个对象的时候, 为啥要像分配多个对象时一样, 记录一个肯定为1的计数呢? 估计BS是这么想的. 于是我们只好用人工来区分delete和delete[]. 某年某月, 我看到某人说C++是实用主义, 我笑了, 然后我看到C++的一条设计原则是”不一味的追求完美”, 我哭了……
#include <stdio.h>
class Test {
public:
Test() {
test_ = __LINE__;
printf("Test(): Run Code in %d\n", __LINE__);
}
~Test() {
test_ = __LINE__;
printf("~Test(): Run Code in %d\n", __LINE__);
}
void print() {
printf("%d\n", test_);
}
private:
int test_;
};
void test() {
Test *temp = new Test;
delete temp;
Test *temps = new Test[2];
delete []temps;
Test *error_delete_temps = new Test[2];
delete error_delete_temps;
}
int main(int, char **) {
test();
return 0;
}
上面的代码最后用delete删除了使用new[]分配的内存, 但是编译时即使开启-wall选项, 你也不会看到任何的警告, 运行时也不会报错, 你只会发现析构函数少运行了一次. 实际上就是发生了内存泄漏.
C/C++内存管理的实际使用
上面虚构的例子也就是看看语法, 真实的使用情景就要复杂了很多. 最重要的原则之一就是谁分配谁释放原则. 这个原则即使是用malloc, free也一样需要遵循.
在C语言上的表现形态大概如下:
只管用, 不管分配
典型的例子就是C语言的文件读取API:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
这里的ptr buffer并不由fread内部分配, 而是由外部传进来, fread只管使用这段buffer, 并且假设这段buffer的size 大于等于传进来的size. 通过这样的方式, fread本身逃避了malloc, free的责任, 也就不用关心内存该怎么管理的问题. 好处是fread的实现得到简化, 坏处是外部使用的负担增加了~~~
类似的API还有sprintf, fgets等, 而strcat这种API也算是这种类型的变化.
这种类型的API的一个最大问题在于很多时候不好确定buffer到底该多大, 于是在一些时候, 还有一个复杂的用法, 那就是在第一次传特定参数调用函数时, 函数仅传出需要buffer的大小, 分配了buffer后, 第二次调用才真正实现函数的功能.
这种API虽然外部的使用麻烦, 但是在绝大部分时候, 已经是C语言的最佳API设计方法.
管分配, 也管删除
只管用, 不管分配的API设计方式在仅仅需要一个buffer, 或者简单的结构缓存的时候基本已经够用, 但也不是万能的, 在想要隐藏内部实现时, 往往就没法使用这样的API设计, 比如Windows API中一系列与核心对象创建相关的方法, 如CreateThread, CreateProcess等函数返回的都是一个核心对象的handle, 然后必须要使用CloseHandle来释放, handle具体的对应什么结构, 分配了多少内存, 此时并不由我们关心, 我们只需要保证一个CreateXXX, 就一定要CloseHandle即可.
这种API的设计方式, 还常见于我们熟知的工厂模式, 工厂模式提供Create和Delete的接口, 就要比只提供Create接口, 让外部自己去Delete要好的多, 此时外部不用关心对象的删除方式, 将来需要在创建对象时进行更多操作(比如对对象进行引用计数管理), 外部代码也不需要更改. 我在网上随便一搜, 发现绝大部分关于工厂模式的代码中都没有考虑这个问题, 需要特别注意.
这里看一个我们常见的开源游戏引擎Ogre中的例子: (Ogre1.8.1 OgreFileSystem.h)
/** Specialisation of ArchiveFactory for FileSystem files. */
//class _OgrePrivate FileSystemArchiveFactory : public ArchiveFactory
class _OgreExport FileSystemArchiveFactory : public ArchiveFactory
{
public:
virtual ~FileSystemArchiveFactory() {}
/// @copydoc FactoryObj::getType
const String& getType(void) const;
/// @copydoc FactoryObj::createInstance
Archive *createInstance( const String& name )
{
return OGRE_NEW FileSystemArchive(name, "FileSystem");
}
/// @copydoc FactoryObj::destroyInstance
void destroyInstance( Archive* arch) { delete arch; }
};
这里这个FileSystemArchiveFactory就是典型的例子. 虽然这里的destroyInstance仅仅是一个delete, 但是还是这么设计了接口.
单独的缓存
一般而言, 所有与内存管理相关的C语言API设计都应该使用上面提到的两种方案, 其他的方案基本都是错的(或者有问题的), 但是因为各种考虑, 还是有些很少见的设计, 这里把一些可能出现的设计也列出来.
所谓的单独缓存的设计, 指的是一段代码内部的确需要缓存, 但是不由外部传入, 而是自己直接分配, 只是每次的调用都使用这一份缓存, 不再释放.
比如C语言中对错误字符串的处理:
char * strerror ( int errnum );
strerror这个API的设计咋一看就有蹊跷, 什么样的设计能传入一个整数, 然后返回一个字符串呢? 字符串的存储空间哪里来的? 一个简单的例子就能看出这样设计的问题:
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main ()
{
char *error1 = strerror(EPERM);
printf ("%s\n", error1);
char *error2 =
补充:移动开发 , 其他 ,