C++中内存管理
明确“内存实际使用”与”内存分配”的概念
Vector容器中内存结构
Size:vector容器全部内存中真正已经初始化使用了的内存大小;
Resize成员函数:调整vector总内存中真实使用的内存区域的大小(未显式初始化区域使用默认初始化方式);
Capacity:vector容器的容量指的是我们一共给vector容器预留的总的内存空间,当然了总的内存空间包括“没有被使用的(未被初始化前不可以访问)“和”已经使用了的(这部分内存已经被初始化)“;
Reverse成员函数:这个成员函数是用来调节vector总的内存空间的,即内存容量。
为什么vector的内存容量会提高程序运行效率?
我们知道:vector容器的底层是用new操作符不断的申请空间用delete不断地释放空间,但是频繁的申请和释放必然导致低效率。此时这些IT大牛们想出来一个方式可以有效地规避这个低效的工作方式:
① 我先根据我需要使用的内存空间去内存区域找一个比这个内存空间还大的区域,此时vector的内存容量就出现了,那么多的内存空间足够我们装下我们的目标数据;
② 你如果要使用内存的话,你就从已经开辟的内存空间中去拿;
③ 当内存容量,即vector容器总的容量不够时,我们就在动态的申请一块更大的内存空间去使用,这就重复了我们的第一步的工作,重复这几个步骤。
注:虽然vector容器是STL中效率最低的容器,但是vector容器的使用方式以及在内存上的构造和我们数组很相似,我也可以说vector容器是“C风格数组的最终进化模式“。虽然vector容器这个”超级C风格数组“已经被优化到极致,但是由于vector容器仍然保留了传统C风格数组的本质属性:需要连续的存储空间,因此在实际使用时,效率还是偏低。
Allocator内存分配器的使用
Allocator内存分配器的结构
pointer allocate (size_type n, allocator<void>::const_pointer hint=0) |
构造内存区域 |
void deallocate (pointer p, size_type n) |
销毁p指针指向的n个字节的内存空间 |
template <class U, class... Args> void construct (U* p, Args&&... args); |
调用p指向类类型的构造函数; Arg参数就是一个初始化p指针指向对象的值 |
template <class U> void destroy (U* p); |
调用p指针指向的析构函数对内存区上的类对象进行析构操作 |
我们应注意以下几点:
① 调用析构函数并不是将相应的内存区域释放掉,而是将类对象从相应内存区域上抹去,也就是说当我们调用析构函数时,内存区域仍旧完好无损但是内存区域上面的数据已荡然无存;
② 单纯的申请的一块“纯“内存空间(没有被任何初始化操作的内存空间)上的所有数据均没有意义,也就是说我们申请的这块内存区域不属于任何数据类型,因此我们应用void*无类型指针去指向它。
请仔细看如下程序:
#include <iostream>
using namespace std;
#include <string>
#include <memory>
class Person
{
private:
string name;
int age;
public:
Person(const Person& obj)
{
cout << "调用构造函数" << endl;
this->name = obj.name;
this->age = obj.age;
}
Person(const string& name, const int& age)
{
this->name = name;
this->age = age;
}
~Person()
{
cout << "调用析构函数" << endl;
}
};
int main()
{
allocator<Person> ALLOC;
void* ptr = ALLOC.allocate(sizeof(Person)); // 分配内存,但没调用构造函数
ALLOC.construct((Person*)ptr, Person("张三", 19)); // 在已分配内存空间上,调用析构函数
ALLOC.destroy((Person*)ptr); // 针对于ptr指针指向的一个Person类对象,去调用析构函数销毁这块内存空间上的Person类对象
ALLOC.deallocate((Person*)ptr, sizeof(Person)); // 销毁ptr指针所指向的sizeof(Person)个内存区域
}
运行结果:
中间那个析构函数是由于第33行的“Person(“张三”,19)“这个匿名变量(临时变量)在第34行时被析构,才出现的这个命令行窗口第二行输出的这个”调用析构函数“的标志。
① 命令行第一个“调用构造函数“应该是由于”在已有的内存区域中调用了构造函数,即调用constructor函数将ptr指针指向的类类型区域初始化“产生的;
② 命令行最后一行的“调用析构函数“标志应该是由于”第34行代码的执行,即destroy函数的实行使得ptr指针指向的~Person()类对象的析构函数被执行“产生的;
③ 销毁内存区的操作,即deallocate函数的执行并不会去调用类对象的析构函数。
内存释放流程的形象比喻
相当于“一座山(山就是内存区域)上有很多树木(这些树木就是类对象),当人们拿着斧头(斧头相当于析构函数,析构函数会影响类对象但是不会影响这块内存区域)树木被砍伐了之后,山依旧会存在(类对象被析构了之后,内存区域仍旧存在,除非人为手动deallocate销毁内存区域),但是“当我们拿着铲子(deallocate销毁内存区域的命令)去移走这座大山”时,山上的树木并没有被砍伐,也就是说我把底层的内存区域连根拔起,上面的建筑上面的一切销毁与否没有任何区别。
Allocator内存分配器与vector容器的联系
代码示例:
Vector.hpp
#include <iostream>
#include <math.h>
#include "MyAlloc.hpp"
using namespace std;
template <typename T, template<typename T> class ALLOC = MyAlloc>
class Vector
{
private:
MyAlloc<T> MemoryManager;
T* Element;
int ElementCapacity;
int ElementSize;
public:
Vector();
Vector(const Vector& obj);
int CalCapacity(double ArraySize);
int Size();
void Resize(int NewSize);
int Capacity();
void Reserve(int NewCapacity);
T& At(int Order);
T* Begin();
T* End();
void Push_back(const T& obj);
void Pop_back();
~Vector();
};
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
void Vector<T, ALLOC>::Reserve(int NewCapacity)
{
T* TempElement = nullptr;
if (NewCapacity > this->ElementCapacity)
{
TempElement = this->MemoryManager.Allocate(NewCapacity);
for (int i = 0; i < this->ElementSize; i++)
{
this->MemoryManager.Constructor(TempElement + i, *(this->Element + i));
this->MemoryManager.Destroy(this->Element + i);
}
this->MemoryManager.Deallocate(this->Element, this->ElementCapacity);
this->Element = TempElement;
this->ElementCapacity = NewCapacity;
}
else if (NewCapacity < this->ElementCapacity && NewCapacity > 0)
{
this->MemoryManager.Deallocate(this->Element + NewCapacity, this->ElementCapacity - NewCapacity);
this->ElementCapacity = NewCapacity;
}
else
{
throw out_of_range("NewCapacity is over valid range!");
}
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
void Vector<T, ALLOC>::Resize(int NewSize)
{
T* TempElement = nullptr;
if (NewSize > this->ElementCapacity)
{
TempElement = MemoryManager.Allocate(CalCapacity(NewSize));
for (int i = 0; i < this->ElementSize; i++)
{
MemoryManager.Constructor(TempElement + i, *(this->Element + i));
this->MemoryManager.Destroy(this->Element + i);
}
this->MemoryManager.Deallocate(this->Element, this->ElementCapacity);
this->Element = TempElement;
this->ElementSize = NewSize;
this->ElementCapacity = CalCapacity(NewSize);
}
else if (NewSize > this->ElementSize && NewSize < this->ElementCapacity)
{
for (int i = this->ElementSize; i < NewSize; i++)
{
this->MemoryManager.Constructor(this->Element + i, T());
}
this->ElementSize = NewSize;
}
else if (NewSize < this->ElementSize)
{
for (int i = NewSize; i < this->ElementSize; i++)
{
this->MemoryManager.Destroy(this->Element + i);
}
this->ElementSize = NewSize;
}
else
{
throw out_of_range("NewSize is over valid range!");
}
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
T* Vector<T, ALLOC>::End()
{
if (this->ElementSize == 0)
{
throw out_of_range("Empty!");
}
return this->Element + this->ElementSize;
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
T* Vector<T, ALLOC>::Begin()
{
if (this->ElementSize == 0)
{
throw out_of_range("Empty!");
}
return this->Element;
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
void Vector<T, ALLOC>::Pop_back()
{
if (this->ElementSize == 0)
{
throw out_of_range("Empty!");
}
MemoryManager.Destroy(this->Element + this->ElementSize - 1);
this->ElementSize--;
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
void Vector<T, ALLOC>::Push_back(const T& obj)
{
T* TempElement = nullptr;
if (this->ElementCapacity == this->ElementSize)
{
TempElement = MemoryManager.Allocate(this->CalCapacity(this->ElementSize + 1));
for (int i = 0; i < this->ElementSize; i++)
{
MemoryManager.Constructor(TempElement + i, *(this->Element + i));
MemoryManager.Destroy(this->Element + i);
}
MemoryManager.Constructor(TempElement + this->ElementSize, obj);
MemoryManager.Deallocate(this->Element, this->ElementCapacity);
this->ElementSize++;
this->ElementCapacity = this->CalCapacity(this->ElementSize);
this->Element = TempElement;
}
else
{
MemoryManager.Constructor(this->Element + this->ElementSize, obj);
this->ElementSize++;
}
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
T& Vector<T, ALLOC>::At(int Order)
{
if (this->ElementSize == 0)
{
throw invalid_argument("Order is an invalid argument!");
}
return *(this->Element + Order);
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
Vector<T, ALLOC>::~Vector()
{
for (int i = 0; i < this->ElementSize; i++)
{
MemoryManager.Destroy(this->Element + i);
}
this->ElementSize = 0;
MemoryManager.Deallocate(this->Element, this->ElementCapacity);
this->ElementCapacity = 0;
this->Element = nullptr;
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
int Vector<T, ALLOC>::Capacity()
{
return this->ElementCapacity;
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
int Vector<T, ALLOC>::Size()
{
return this->ElementSize;
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
int Vector<T, ALLOC>::CalCapacity(double ArraySize)
{
return (int)exp2(ceil(log2(ArraySize)));
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
Vector<T, ALLOC>::Vector(const Vector& obj)
{
this->ElementCapacity = obj.Size();
this->ElementSize = obj.Size();
this->Element = MemoryManager.Allocate(this->ElementCapacity);
for (int i = 0; i < this->ElementSize; i++)
{
MemoryManager.Constructor(this->Element + i, obj.At(i));
}
}
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>
Vector<T, ALLOC>::Vector()
{
this->Element = nullptr;
this->ElementCapacity = 0;
this->ElementSize = 0;
}
MyAlloc.hpp
#include <iostream>
using namespace std;
template <class T>
class MyAlloc
{
public:
T* Allocate(size_t Size);
void Deallocate(T* ptr, size_t Size);
void Destroy(T* ptr);
void Constructor(T* ptr, const T& val);
};
template <class T>
void MyAlloc<T>::Constructor(T* ptr, const T& val)
{
ptr = new(ptr) T(val);
}
template <class T>
void MyAlloc<T>::Destroy(T* ptr)
{
ptr->~T();
}
template <class T>
void MyAlloc<T>::Deallocate(T* ptr, size_t Size)
{
::operator delete(ptr, sizeof(T)*Size);
}
template <class T>
T* MyAlloc<T>::Allocate(size_t Size)
{
char* ptr = new char[sizeof(T)*Size];
return (T*)ptr;
}
Main.cpp
#include "Vector.hpp"
#include <iostream>
using namespace std;
int main()
{
Vector<int> obj;
obj.Push_back(10);
cout << obj.At(0) << endl;
obj.Reserve(10);
obj.Resize(2);
cout << obj.Size() << endl;
cout << obj.Capacity() << endl;
}
也许自定义的Vector容器代码略长有些难读,但是原理十分简单:无非就是首先开辟一块内存,然后不断地向内存区域中添加数据,如果内存空间不够就在申请一块内存更大的区域来使用。
使用new和delete进行管理
普通new
形式:int* p = new int;
此时不能通过p是否为nullptr来判断内存是否开辟成功,而是需要通过bad_alloc来捕获异常。
#include <iostream>
#include <exception>
using namespace std;
int main()
{
int* ptr = nullptr;
try
{
ptr = new int[0x7fffffff];
delete ptr;
}
catch (const bad_alloc& exp)
{
cout << exp.what() << endl;
}
}
(nothrow) new
形式:int *p = new (nothrow) int(20);
此时指针已经退化为C语言中通过malloc开辟内存得到的指针,是可以通过判空来验证是否成功开辟内存。
#include <iostream>
#include <exception>
using namespace std;
int main()
{
int* ptr = nullptr;
if ((ptr = new (nothrow) int[0x7fffffff]) == nullptr)
{
printf("内存申请失败!");
exit(-1);
}
delete ptr;
}
我们也可以重载针对于某个类类型或结构体类型的operator new运算符(使用malloc和free为底层):
#include <iostream>
#include <exception>
#include <string>
using namespace std;
class Person
{
private:
string name;
int age;
public:
Person()
{
this->name = "无";
this->age = 0;
}
Person(const string& name, const int& age)
{
this->name = name;
this->age = age;
}
Person(const Person& obj)
{
this->name = obj.name;
this->age = obj.age;
}
void* operator new (size_t Size)
{
Person* ptr = nullptr;
ptr = (Person*)malloc(sizeof(Person));
if (ptr == nullptr)
{
printf("Bad Allocate!");
exit(-1);
}
return (void*)ptr;
}
void operator delete(void* ptr)
{
if (ptr == nullptr)
{
return;
}
free(ptr);
}
};
int main()
{
Person* ptr = new Person;
delete(ptr);
}
申请指向常量内存的指针的new
形式:const int* p = new const int(20);
#include <iostream>
#include <exception>
using namespace std;
int main()
{
int var = 10;
const int* ptr = new const int(var);
cout << *ptr << endl;
delete ptr;
}
定位new(placement new)
形式:int data = 0;
int *p = new (&data) int(20);
作用:
placement new可以实现在一块指定的内存上(这块内存可以由任意方式分配)构造对象(调用对象的构造函数)。
#include <iostream>
#include <exception>
using namespace std;
int main()
{
char* ptr[20]; // 由于char类型所占空间是一个字节,因此我们可以用char类型数组来申请n个字节的存储空间
int *p = new (ptr)int(20);
cout << *p << endl;
cout << *(int*)ptr << endl;
cout << p << " " << ptr << endl; // 我们看见p指针和ptr指针指向的内存空间一致
}
输出结果:
在上述程序中,我们也许会问到“为什么要把ptr指针转换为int*类型的指针再解引用?”,是这样的,ptr原本的数据类型是char*字符型指针,ptr指向的是一个字节的存储空间,但是我们在ptr指向的内存区域上建立的是一个int类型的对象,因此我们必须从ptr开始读取一个sizeof(int)个字节的存储空间才可以读取到这块内存区域上的数据。
注意:
我们用ptr这个char*类型的指针申请的连续内存空间是建立在stack栈区上的,因此我们不需要调用allocator内存分配器中的deallocate函数来销毁这块内存区域。如果要将内存建立在heap堆区中,我们可以进行如下操作:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
void* ptr = nullptr;
allocator<int> ALLOC;
ptr = ALLOC.allocate(sizeof(int)); // 申请内存空间是以字节为单位进行申请的
ALLOC.construct((int*)ptr, int(10)); // 在内存区域上进行初始化
cout << *(int*)ptr << endl; // 使用这片内存区域上的数据
ALLOC.destroy((int*)ptr); // 销毁构建在内存区域上的数据
ALLOC.deallocate((int*)ptr, sizeof(int)); // 销毁这片内存区域
}
注意:
我这里申请内存使用的是void*数据类型,因为我们申请内存空间时不知道在这篇内存区域上建立的是什么类型的数据,而且void*指针因为可以强制类型转换为任意数据类型的指针,因此被称为“C语言中的万能指针”。但是我们使用void*类型指针ptr时一定要注意“在上述程序中,我们要在内存区域上建立一个int类型的数据进行存储,那么void*类型的指针ptr一定要转换为int*类型才可以,就相当于你真对于那种类型就将void*指针转换为对应数据类型的指针”。
用new和delete实现一个自定义内存分配器allocator
MyAlloc.hpp
#include <iostream>
using namespace std;
template <class T>
class MyAlloc
{
public:
T* Allocate(size_t Size);
void Deallocate(T* ptr, size_t Size);
void Destroy(T* ptr);
void Constructor(T* ptr, const T& val);
};
template <class T>
void MyAlloc<T>::Constructor(T* ptr, const T& val)
{
ptr = new(ptr) T(val);
}
template <class T>
void MyAlloc<T>::Destroy(T* ptr)
{
ptr->~T();
}
template <class T>
void MyAlloc<T>::Deallocate(T* ptr, size_t Size)
{
::operator delete(ptr, sizeof(T)*Size);
}
template <class T>
T* MyAlloc<T>::Allocate(size_t Size)
{
char* ptr = new char[sizeof(T)*Size];
return (T*)ptr;
}
Main.cpp
#include <iostream>
#include "MyAlloc.hpp"
using namespace std;
int main()
{
MyAlloc<int> ALLOC;
int* ptr = ALLOC.Allocate(sizeof(int)); // 申请内存区域
ALLOC.Constructor(ptr, int(10)); // 在内存区域上放置数据
cout << *ptr << endl; // 使用内存区域上的数据
ALLOC.Destroy(ptr); // 销毁内存区域上的数据
ALLOC.Deallocate(ptr, sizeof(int)); // 销毁整片内存区域
}