C++内存管理一直是我比较困惑的问题。俗话说初生牛犊不怕虎,做点啥都new一个,然后delete一个。根本不知道底层会有怎么样的运行机制,慢慢地学习才知道以前学习中有一些东西是不可能在工业中应用的。所以想开辟一个坑为C++的内存管理机制。也希望通过这篇能够对C++内存管理机制有一个更加深刻全新的认识吧。
New、delete篇
在C++中已经将new 和 delete 认定为运算符。我们不用关心是如何进行申请内存如何释放内存。我们只需用它们就好了。但是有一些内容是我们必须要知道,因为只有这样我们才能把C++的内存管理应用到极致。
1.请看以下代码:
auto ch0 = new char; // 1
auto ch1 = new char(); // 2
这两行代码有什么区别呢?一个加了括号一个没有加。那么它们的运行是一样的么?显然是不一样的,1行是没有进行初始化的。而2行是进行了初始化。如果我们申请的变量在一开始没有决定如何进行初始化时,不妨就不要初始化。等程序真正要初始化的时候我们再进行初始。这样我们就会少进行一次赋值操作。
2.请看一下代码:
auto ch0 = new char[10];
auto stu = new Student[10];
delete ch0; //1
delete []ch0; // 2
delete stu; //3
delete []stu; //4
问题是上述几个delete中哪个是错误的呢?很多人都会说1和3是错误的,因为申请了数组而释放的时候要像2和4那样进行释放。其实第一个不会发生内存泄漏。因为只有在申请自定义的对象如果不用数组释放方法进行释放才会发生内存泄漏。而内置的变量时不会的。但是也要成对应用,规范的使用运算符
3.重载运算符new、delete
和重载其他运算符相同动机,有些时候我们需要进行重载new/delete为了更加方便操作程序自己定义的内存块。那么如何进行重载是有固定格式。
int operator new(size_t size)
{
return new(size_t); //进行内存申请操作;
}
void operator delete(void* p)
{
delete p; // 释放内存操作
}
通过以上三个基本知识我们现在可以进入真正的内存管理知识了,我将通过代码的形式一步步展现我们该如何利用new/delete做自己的是。第一个程序我们将介绍一下如何为自定义类型(类)定义自己的内存运算符。我定义了俩个类(模板类MemoryPool和要应用内存池的类Cat)和一个结构体Block。Block结构是用来存放内存的,是一个单向的列表。
/*类功能: 这个类主要是用来创建自定义类的内存池。Block结构是用来将类中的每个对象连接在一起。这样便于管理。*/
template <typename T>
class MemoryPool
{
public:
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef const T* const_pointer;
typedef const T& const_reference;
MemoryPool();
void* alloc(size_t);
void free(void* pt);
virtual ~MemoryPool();
private:
MemoryPool(const MemoryPool<value_type>); //禁止将内存池进行复制构造和赋值构造。
const MemoryPool<value_type> operator=(const MemoryPool<value_type>&);
static void expandBlock(int len=EXPANDSION_SIZE);
enum {EXPANDSION_SIZE = 50}; // 用来表示内存池的大小。
struct Block
{
value_type data;
Block* next;
Block() : next(nullptr){}
pointer operator ->()
{
return this->data;
}
const value_type operator*()
{
return *(data);
}
};
Block* head; // 内存池的头。
};
成员函数详细介绍:
这个类是一个内存池。所以不能够赋值和复制独此一份,这是一种设计模式Singleton模式。构造函数只允许无参构造函数,而无参构造函数是如下定义:
MemoryPool::MemoryPool()
{
expandBlock();
}
构造函数会调用一个私有函数expandBlock()来初始化内存池。具体如何初始化内存池就是申请预设大小的内存块。然后将内存块构建成链表。当用时在链表中取一块,如果不用了把内存块归还给内存池。
void MemoryPool::expandBlock(int len = EXPANDSION_SIZE)
{
this->head = new Block; // 注意:这个是没有初始化的,谁用谁搞;
for (int i = 0; i < len - 1; ++i)
{// 将新创建的块加入头部, 这样的效率比较快。
auto block = new Block;
block->next = head;
head = block;
}
}
这个代码比较容易理解我就不过多的解释了。
void* MemoryPool::alloc(size_t size)
{
if (head == nullptr)
{
expandBlock();
}
auto result = head;
head = head->next;
result->next = nullptr;
return result; // The first memory
// block is returned.
}
void MemoryPool::free(void* ptr)
{
auto newHead = static_cast<Block*>(ptr);
newHead->next = head;
head = newHead;
}
成员函数alloc()是其他类用来申请一个内存块的。上述函数的定义中如果head指针的值是nullptr时,说明没有进行初始化内存池或者已经把内存池中的内存块用完了,就需要再进行注入内存块,所以要调用expandBlck()。接下来的代码就是把内存池中的第一块内存提取出来,然后返回这个内存块。free()是用来释放的,因为形参是一个void*指针。大体意思是这个指针可以变成任何东东,但是别瞎搞。然后通过C++的类型转换static_cast
MemoryPool::~MemoryPool()
{
for (auto nextPtr = head; nextPtr != nullptr; nextPtr = head)
{
head = head->next;
delete nextPtr;
}
head = nullptr;
}
不解释!自己看!
一切准备妥当了。我们该怎么用呢?接下来我们就用这个内存池动态的申请内存块。
class Cat
{
public:
Cat(const char* na = nullptr, int ag = 1) : age(ag), name(na){}
static void newMemory() { freememory = new MemoryPool<Cat>;}
void* operator new(size_t size)
{
try{
if (freememory == nullptr)
{
newMemory();
}
}
catch (const exception& e)
{
cerr << e.what() << endl;
}
return freememory->alloc(size);
}
void operator delete(void* ptr){ freememory->free(ptr); }
void calling(){ cout << "Cat " << name << " is calling " << endl; }
static void deleteMemory(){ delete freememory;}
private:
const char* name;
int age;
static MemoryPool<Cat>* freememory;
};
MemoryPool<Cat>* Cat::freememory = nullptr;
int main(int argv, char* argc[])
{
newMemory(); // 初始化内存池。
Cat* cat1 = new Cat(“M”,2); // 申请一个Cat内存块。
delete cat1; // 归还给内存池;
Cat* cats = new Cat[50]; // 申请50个,这个时候内存池中没有内存块了;
cat1 = new Cat(“MM”,3);// 又申请一个内存池中会继续注入50个内存块;
delete []cats;// 归还内存块。没有真正的释放,这时内存池中有100个内存块
deleteMemory(); // 释放内存块。
return 0;
}