将C++类纳入存储池管理
近日在做一个项目,遇到一个比较棘手的问题。程序在执行一次任务的时候会new大量的处理类和各种Buffer,在任务执行完成后,这些类和Buffer会被释放掉,但会产生一些内存碎片,当时项目时间比较紧凑,所以没有考虑内存管理这方面的事情。程序在多次执行任务后,在new一些较大块内存的时候会出现分配内存失败的异常,导致任务失败,但整体程序占用内存其实并不高。个人猜测可能是内存碎片过多导致。
在处理问题的过程中,我想到,如果对内存进行统一管理,那就尽可能的减少了内存碎片的产生,降低了内存泄露的风险,如果以后有时间,还可以使用GC等内存管理技术来优化。
我们来做一些实验。
首先,我们分配一块Buffer,接下来所有要分配的内存都从那里取得。如果是基本数据类型,比如char*,int*等,还好处理,直接存储即可,但是类就比较麻烦了,这就涉及到malloc和new的一个不同点:malloc只负责分配内存,new除了分配内存,还对类进行了初始化。
看如下代码
class A
{
public:
A()
{
memcpy(m_s, "abcd\0", 5);
}
~A() {};
int GetA() { return m_a; }
int SetA(int a) { m_a = a; }
private:
int m_a = 5;
char m_s[5];
};
int main()
{
A* b = (A*)malloc(sizeof(A));
A* a = new A;
free(b);
delete a
return 0;
}
使用VS2019调试查看b 和 a的值
b中的值是无效的。如果我们直接将Buffer强转成类,效果跟使用malloc是一样的,无法对类成员进行初始化。
那么怎么对malloc出来的类成员进行初始化呢?
网上找到有几种做法:
第一种:根据类的内存布局直接赋值
int main()
{
A* b = (A*)malloc(sizeof(A));
A* a = new A;
int n = 3;
memcpy(((int*)b), &n, sizeof(int));
memcpy(((char*)b + sizeof(int)), "bcde\0", 5);
return 0;
}
调试结果发现,可以初始化成员变量
但这种方式有几个问题:第一,难以理解;第二,容易出错,必须得了解C++类的内存布局,万一类很复杂怎么办?万一有继承关系呢?
第二种:为每一个成员变量设置一个赋值函数
class A
{
public:
A()
{
memcpy(m_s, "abcd\0", 5);
}
~A() {};
int GetA() { return m_a; }
void SetA(int a) { m_a = a; }
void SetString(const char* s, size_t len)
{
memcpy(m_s, s, len);
}
private:
int m_a = 5;
char m_s[5];
};
int main()
{
A* b = (A*)malloc(sizeof(A));
b->SetA(3);
b->SetString("abcd\0", 5);
free b;
return 0;
}
结果如下:
这种比第一种方法可读,且安全性较高,是较为可行的一种方式。但使用起来有些不方便,如果成员变量有初始值也用不上,万一没有初始值会引起未定义行为呢?
第三种:先使用malloc分配,再使用 placement new 转换成指定类型
int main()
{
void* t = malloc(sizeof(A));
A* b = new(t)A;
//A* b = (A*)malloc(sizeof(A));
b->~A();
return 0;
}
结果是正确的
说实话,这种方式我也是第一次见,结果是正确的,但用起来怪怪的,就不多说什么了。
还有没有其它方法呢?我们在在程序实现过程中,遇到一个问题,方法总比困难多。有些方法,或者是没掌握,或者是不知道,也许是一时半会儿没想到而已。在这里,我也琢磨出一种方法来:
unsigned char* g_pBuffer = new unsigned char[100];
size_t g_nPos = 0;
class A
{
public:
A()
{
memcpy(m_s, "abcd\0", 5);
}
~A() {};
void* operator new(size_t size)
{
if (size == 0)
{
return NULL;
}
unsigned char* b = &g_pBuffer[g_nPos];
g_nPos += size;
return b;
}
void operator delete(void* ptr)
{
}
int GetA() { return m_a; }
int SetA(int a) { m_a = a; }
private:
int m_a = 5;
char m_s[5];
};
int main()
{
A* b = new A;
delete[] g_pBuffer;
return 0;
}
在这里,我使用了一个全局Buffer来做为存储池,要使用内存就从里面取。以前只是了解过operator new操作符,但没有在去认真用过。这次,我就想到用这种方式是否可以初始化类成员呢?结果是可行的
分配成功,成员变量也初始化了,b对象还不用释放,用完直接删除g_pBuffer就可以了,感觉很好。
如果里面有复杂的对象呢,比如STL对象,来试试
class A
{
public:
A()
{
memcpy(m_s, "abcd\0", 5);
}
~A() {};
void* operator new(size_t size)
{
if (size == 0)
{
return nullptr;
}
unsigned char* b = &g_pBuffer[g_nPos];
g_nPos += size;
return b;
}
void operator delete(void* ptr)
{
}
int GetA() { return m_a; }
int SetA(int a) { m_a = a; }
private:
int m_a = 5;
char m_s[5];
std::string m_str = "abcde";
std::vector<int> m_v = { 123, 245, 366 };
};
int main()
{
A* b = new A;
delete[] g_pBuffer;
g_nPos = 0;
return 0;
}
结果也符合要求
再看看g_pBuffer的内存布局
也行,vector自己会分配内存,所以暂时也想不到啥问题,实在有问题就用指针吧
如果有继承关系呢?
unsigned char* g_pBuffer = new unsigned char[100];
int g_nPos = 0;
class B
{
public:
B()
{
}
virtual ~B() {}
virtual void SetB(int b)
{
m_b = b;
}
private:
int m_b = 7;
std::string m_str = "CDEFG";
};
class A : public B
{
public:
//与上面一致
//.......
};
int main()
{
memset(g_pBuffer, 0, 100);
B* a = new A;
delete[] g_pBuffer;
g_nPos = 0;
return 0;
}
结果如下
也运行得不错 。
当然,代码有些粗糙,肯定会有BUG,但这就是我想达到的效果。这里需要注意一点的是,如果要使用自己的存储池管理,要使用的类都必须重载new 和 delete,我们可以把重载函数放到一个基类里面去,要管理的类都从这个类继承,不就OK了?
class Base_Alloc
{
public:
void* operator new(size_t size)
{
if (size == 0)
{
return nullptr;
}
unsigned char* b = &g_pBuffer[g_nPos];
g_nPos += size;
return b;
}
void operator delete(void* ptr)
{
}
};
class B : public Base_Alloc
{
.....
}
这样,我就可以做到想要管理的类从存储池分配,然后统一管理。上面的代码仅仅是实验性质,如果要真正用到项目中,还有很多工作要做。