C++学习笔记(十) 内存机制与Allocator

本文探讨了C++中内存管理的底层机制,包括new与delete表达式、placement new、allocator类的作用及其实现细节。
本文章已经生成可运行项目,

C++为我们提供了安全的内存空间申请方式与释放方式,但是new与delete表达式却是把空间的分配回收与对象的构建销毁紧紧的关联在一起。实际上,作为与C语言兼容的语言,C++也为我们提供了更加底层的内存操作方式的。

谈C++就离不开STL,考虑一下vector<>类的机制,为了高效率的增加与删除元素,它并不会在我们每次进行添加或删除操作时进行内存的分配与回收,而是会提前预留下一片空间。我们通过size函数可以得到容器内元素的个数,通过capacity函数则可以得到该容器的实际大小。实际上每个容器都有自己的Allocator类,用于进行空间的分配与回收,对象的构造与销毁。下面的代码来自与《C++ primer》,是vector类的push_back函数的一种可能实现方式:

template <class T>
void Vector<T>::push_back(const T& t)
{
// are we out of space?
	if (first_free == end)
		reallocate(); // gets more space and copies existing elements to it
	alloc.construct(first_free, t);
	++first_free;
}

first_free指向容器中第一个空闲的块,如果已经没有空闲块了,则通过reallocate函数重新分配。alloc是Alloctor<T>类的一个对象,调用其construct方法可在一个指定的区域构建对象,调用的是类型T的拷贝构造函数。在构造完成之后,让first_free指向下一个空闲块。

当我们使用new表达式,来调用拷贝构造函数时实际上时伴随着空间的分配的。那么Allocator是怎么做到的呢?实际上它是调用了C++的一个内置的操作符:

void *operator new(size_t); // allocate an object
void *operator new[](size_t); // allocate an array
new (place_address) type
new (place_address) type (initializer-list)

这写重载操作符函数可以进行内存的分配以及在指定的内存空间进行对象的构造。需要注意的是它们并非new表达式,new表达式是不可以被重载的,实际上new表达式底层也就是调用了这些重载函数的。前两个用内存的分配,后两个则用于对象的构造。Alloctor<T>类的construct方法底层实现实际上就是一句调用而已:

new (first_free) T(const T& t);

我们再来看一下reallocate函数的实现:

template <class T> void Vector<T>::reallocate() {
// compute size of current array and allocate space for twice as many elements
	std::ptrdiff_t size = first_free - elements;
	std::ptrdiff_t newcapacity = 2 * max(size, 1);
// allocate space to hold newcapacity number of elements of type T
	T* newelements = alloc.allocate(newcapacity);
// construct copies of the existing elements in the new space
	uninitialized_copy(elements, first_free, newelements);
// destroy the old elements in reverse order
	for (T *p = first_free; p != elements; /* empty */ )
		alloc.destroy(--p);
// deallocate cannot be called on a 0 pointer
	if (elements)
// return the memory that held the elements
		alloc.deallocate(elements, end - elements);
// make our data structure point to the new elements
	elements = newelements;
	first_free = elements + size;
	end = elements + newcapacity;
}

这个实现就稍微复杂一点了,逻辑上我就不说了,着重说明一下其中几个函数的使用吧。首先是Alloctor<T>类的allocate成员函数的使用,它的作用是向系统申请指定个数的长度为sizeof(T)的连续空间,其底层实现是:

return operator new[](newcapacity * sizeof(T));

uninitialized_copy函数实际上是memory头文件中的一个函数,它的声明形式如下:

template <class InputIterator, class ForwardIterator>
         ForwardIterator
           uninitialized_copy ( InputIterator first, InputIterator last,
                                ForwardIterator result );

elements指针指向的是vector内部维护的线性表的首地址,该函数的调用实际上将elements与first_free所限定的区域里的对象拷贝到由newelements 所指向的新分配的的空间中去,其底层也是使用的是拷贝构造函数。

然后是关于Alloctor<T>类的destroy成员函数的分析,它有一个参数,指向需要销毁的对象的指针,该函数只进行对象的销毁,但不进行内存的回收。实际上这里就是简单的调用析构函数而已(不要质疑,析构函数确实可以通过指针直接调用的哦)。

接下来是关于Alloctor<T>类的deallocate成员函数的分析,它有两个参数,第一个指向线性表的首地址,第二个参数指明要内存回收的对象的个数,注意,这里只进行内存回收,而不会进行对象的销毁,其底层使用的是delete重载函数,同new重载函数一样,它也不是我们所熟知的delete表达式,而delete表达式其底层则是调用了delete重载函数来释放内存的,先来看delete有哪些重载函数:

void *operator delete(void*); // free an object
void *operator delete[](void*); // free an array

这两个版本是分别用来释放单个对象以及数组对象的。

小结

C++底层为我们提供了内存分配与回收,对象创建与释放的单独的手段,它们是operator new, placement new, operator delete以及析构函数(不存在placement delete)。通过这四种手段,我们可以灵活的进行资源的合理管理,但它们是属于较低层次的手段,STL为我们提供了Allocator类能很好的为我们做这一切,所以,如无必要,我们最好使用Allocator类来进行内存的管理。进行这种级别的内存管理,处理在效率上的提升外,还能有效的减少内存碎片的诞生,这在某些内存资源有限(例如嵌入式设备)的情况下将是一种非常有效的方式。

参考文献:

《C++ Primer》 Stanley B.Lippman  Barbara E.Moo

本文已生成可运行项目
### 关于侯捷 C++ 系列课程学习笔记 #### 内存管理和 `std::allocator` 在探讨内存管理时,`std::allocator` 是一个重要的概念。它提供了一种通用的方式来进行动态内存分配和释放操作[^1]。通过使用 `std::allocator`,可以更灵活地控制容器内部对象的创建销毁。 ```cpp #include <memory> using namespace std; int main() { allocator<int> alloc; const int n = 1000; int* p = alloc.allocate(n); // 构造元素 uninitialized_fill_n(p, n, 0); // 销毁并回收内存 destroy_n(p, n); alloc.deallocate(p, n); } ``` 这段代码展示了如何利用 `std::allocator` 来手动管理一块整型数据的空间,并对其进行初始化以及最终清理工作。 #### 使用迭代器简化输入处理 对于从标准输入流读取数值并将这些值存储至目标容器的操作,可以通过组合 `istream_iterator` 和算法库中的 `copy()` 函数来实现简洁高效的解决方案[^2]: ```cpp #include <iostream> #include <iterator> #include <vector> #include <algorithm> using namespace std; int main(){ vector<double> c; istream_iterator<double> eos; // 表示结束标记 istream_iterator<double> iit(cin); // 创建基于cin的输入迭代器 copy(iit, eos, back_inserter(c)); // 将所有输入复制到向量c中 } ``` 此程序片段能够方便地接收来自用户的多个浮点数作为输入,并自动将其追加到指定的目标集合内而无需显式循环结构。 #### 移动语义的重要性及其应用 当涉及到像 `std::vector` 这样的序列式容器时,确保自定义类型的移动构造函数和赋值运算符被声明为 `noexcept` 至关重要。这不仅有助于提高性能,还可能影响某些 STL 容器的行为逻辑,比如扩容机制会优先考虑调用 noexcept 的版本以减少异常风险[^3]。 ```cpp class MyClass { public: MyClass(MyClass&& other) noexcept : member(std::move(other.member)) {} MyClass& operator=(MyClass&& other) noexcept { if (this != &other){ member = std::move(other.member); } return *this; } private: string member; }; ``` 上述类实现了无抛出保证的右值引用重载方法,从而使得该类型更适合用于频繁变动大小的数据结构之中。 #### 初始化列表 (`initializer_list`) 及其特性 最后,在介绍现代 C++ 特性的过程中不得不提到 `initializer_list`。这是一种特殊的模板参数形式,允许我们采用花括号语法传递一组同质化的初始值给构造函数或其他接受此类参数的地方[^4]。值得注意的是,尽管看起来像是拥有自己的副本,实际上 initializer_list 并不持有任何实际的对象实例——它仅仅是指向原始数组的一个视图而已。 ```cpp template<typename T> void print(const initializer_list<T>& ilist){ for(auto elem : ilist) cout << elem << " "; } // 调用方式如下所示: print({1, 2, 3}); ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值