C++中std::allocator的使用

fengbingchun發表於2017-12-31

標準庫中包含一個名為allocator的類,允許我們將分配和初始化分離。使用allocator通常會提供更好的效能和更靈活的記憶體管理能力。

        new有一些靈活性上的侷限,其中一方面表現在它將記憶體分配和物件構造組合在了一起。類似的,delete將物件析構和記憶體釋放組合在了一起。我們分配單個物件時,通常希望將記憶體分配和物件初始化組合在一起。因為在這種情況下,我們幾乎肯定知道物件應有什麼值。當分配一大塊記憶體時,我們通常計劃在這塊記憶體上按需構造物件。在此情況下,我們希望將記憶體分配和物件構造分離。這意味著我們可以分配大塊記憶體,但只在真正需要時才真正執行物件的建立操作(同時付出一定開銷)。一般情況下,將記憶體分配和物件構造組合在一起可能會導致不必要的浪費

        標準庫allocator類定義在標頭檔案memory中,它幫助我們將記憶體分配和物件構造分離開來它提供一種型別感知的記憶體分配方法,它分配的記憶體是原始的、未構造的。類似vector,allocator是一個模板。為了定義一個allocator物件,我們必須指明這個allocator可以分配的物件型別。當一個allocator物件分配記憶體時,它會根據給定的物件型別來確定恰當的記憶體大小和對齊位置。allocator支援的操作,如下:


        allocatro分配的記憶體是未構造的(unconstructed)。我們按需要在此記憶體中構造物件。在新標準庫中,construct成員函式接受一個指標和零個或多個額外引數,在給定位置構造一個元素。額外引數用來初始化構造的物件。類似make_shared的引數,這些額外引數必須是與構造的物件的型別相匹配的合法的初始化器。

        在早期版本的標準庫中,construct只接受兩個引數:指向建立物件位置的指標和一個元素型別的值。因此,我們只能將一個元素拷貝到未構造空間中,而不能用元素型別的任何其它建構函式來構造一個元素。還未構造物件的情況下就使用原始記憶體是錯誤的。為了使用allocator返回的記憶體,我們必須用construct構造物件。使用未構造的記憶體,其行為是未定義的。

        當我們用完物件後,必須對每個構造的元素呼叫destroy來銷燬它們。函式destroy接受一個指標,對執行的物件執行解構函式。我們只能對真正構造了的元素進行destroy操作。一旦元素被銷燬後,就可以重新使用這部分記憶體來儲存其它string,也可以將其歸還給系統。釋放記憶體通過呼叫deallocate來完成。我們傳遞給deallocate的指標不能為空,它必須指向由allocate分配的記憶體。而且,傳遞給deallocate的大小引數必須與呼叫allocate分配記憶體時提供的大小引數具有一樣的值。

        標準庫還為allocator類定義了兩個伴隨演算法,可以在未初始化記憶體中建立物件。它們都定義在標頭檔案memory中,如下:


        在C++中,記憶體是通過new表示式分配,通過delete表示式釋放的。標準庫還定義了一個allocator類來分配動態記憶體塊。分配動態記憶體的程式應負責釋放它所分配的記憶體。記憶體的正確釋放是非常容易出錯的地方:要麼記憶體永遠不會被釋放,要麼在仍有指標引用它時就被釋放了。新的標準庫定義了智慧指標型別------shared_ptr、unique_ptr和weak_ptr,可令動態記憶體管理更為安全。對於一塊記憶體,當沒有任何使用者使用它時,智慧指標會自動釋放它。現代C++程式應儘可能使用智慧指標。

        std::allocator是標準庫容器的預設記憶體分配器。你可以替換自己的分配器,這允許你控制標準容器分配記憶體的方式。

        以上內容主要摘自:《C++Primer(Fifth Edition 中文版)》第12.2.2章節

下面是從其他文章中copy的測試程式碼,詳細內容介紹可以參考對應的reference:

#include "allocator.hpp"
#include <iostream>
#include <memory>
#include <string>
#include <vector>

namespace allocator_ {

////////////////////////////////////////////////
// reference: C++ Primer(Fifth Edition) 12.2.2
int test_allocator_1()
{
	std::allocator<std::string> alloc; // 可以分配string的allocator物件
	int n{ 5 };
	auto const p = alloc.allocate(n); // 分配n個未初始化的string

	auto q = p; // q指向最後構造的元素之後的位置
	alloc.construct(q++); // *q為空字串
	alloc.construct(q++, 10, 'c'); // *q為cccccccccc
	alloc.construct(q++, "hi"); // *q為hi

	std::cout << *p << std::endl; // 正確:使用string的輸出運算子
	//std::cout << *q << std::endl; // 災難:q指向未構造的記憶體
	std::cout << p[0] << std::endl;
	std::cout << p[1] << std::endl;
	std::cout << p[2] << std::endl;

	while (q != p) {
		alloc.destroy(--q); // 釋放我們真正構造的string
	}

	alloc.deallocate(p, n);

	return 0;
}

int test_allocator_2()
{
	std::vector<int> vi{ 1, 2, 3, 4, 5 };

	// 分配比vi中元素所佔用空間大一倍的動態記憶體
	std::allocator<int> alloc;
	auto p = alloc.allocate(vi.size() * 2);
	// 通過拷貝vi中的元素來構造從p開始的元素
	/* 類似拷貝演算法,uninitialized_copy接受三個迭代器引數。前兩個表示輸入序列,第三個表示
	這些元素將要拷貝到的目的空間。傳遞給uninitialized_copy的目的位置迭代器必須指向未構造的
	記憶體。與copy不同,uninitialized_copy在給定目的位置構造元素。
	類似copy,uninitialized_copy返回(遞增後的)目的位置迭代器。因此,一次uninitialized_copy呼叫
	會返回一個指標,指向最後一個構造的元素之後的位置。
	*/
	auto q = std::uninitialized_copy(vi.begin(), vi.end(), p);
	// 將剩餘元素初始化為42
	std::uninitialized_fill_n(q, vi.size(), 42);

	return 0;
}

////////////////////////////////////////////////////////////
// reference: http://www.modernescpp.com/index.php/memory-management-with-std-allocator
int test_allocator_3()
{
	std::cout << std::endl;

	std::allocator<int> intAlloc;

	std::cout << "intAlloc.max_size(): " << intAlloc.max_size() << std::endl;
	int* intArray = intAlloc.allocate(100);

	std::cout << "intArray[4]: " << intArray[4] << std::endl;

	intArray[4] = 2011;

	std::cout << "intArray[4]: " << intArray[4] << std::endl;

	intAlloc.deallocate(intArray, 100);

	std::cout << std::endl;

	std::allocator<double> doubleAlloc;
	std::cout << "doubleAlloc.max_size(): " << doubleAlloc.max_size() << std::endl;

	std::cout << std::endl;

	std::allocator<std::string> stringAlloc;
	std::cout << "stringAlloc.max_size(): " << stringAlloc.max_size() << std::endl;

	std::string* myString = stringAlloc.allocate(3);

	stringAlloc.construct(myString, "Hello");
	stringAlloc.construct(myString + 1, "World");
	stringAlloc.construct(myString + 2, "!");

	std::cout << myString[0] << " " << myString[1] << " " << myString[2] << std::endl;

	stringAlloc.destroy(myString);
	stringAlloc.destroy(myString + 1);
	stringAlloc.destroy(myString + 2);
	stringAlloc.deallocate(myString, 3);

	std::cout << std::endl;

	return 0;
}

//////////////////////////////////////////////////////
// reference: http://en.cppreference.com/w/cpp/memory/allocator
int test_allocator_4()
{
	std::allocator<int> a1;   // default allocator for ints
	int* a = a1.allocate(1);  // space for one int
	a1.construct(a, 7);       // construct the int
	std::cout << a[0] << '\n';
	a1.deallocate(a, 1);      // deallocate space for one int

	// default allocator for strings
	std::allocator<std::string> a2;

	// same, but obtained by rebinding from the type of a1
	decltype(a1)::rebind<std::string>::other a2_1;

	// same, but obtained by rebinding from the type of a1 via allocator_traits
	std::allocator_traits<decltype(a1)>::rebind_alloc<std::string> a2_2;

	std::string* s = a2.allocate(2); // space for 2 strings

	a2.construct(s, "foo");
	a2.construct(s + 1, "bar");

	std::cout << s[0] << ' ' << s[1] << '\n';

	a2.destroy(s);
	a2.destroy(s + 1);
	a2.deallocate(s, 2);

	return 0;
}

} // namespace allocator_

GitHub: https://github.com/fengbingchun/Messy_Test 

相關文章