
什么是RAll?RAII [1] (Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
二、为什么要使用RAII? 上面说到RAII是用来管理资源、避免资源泄漏的方法。那么,用了这么久了,也写了这么多程序了,口头上经常会说资源,那么资源是如何定义的?在计算机系统中,资源是数量有限且对系统正常运行具有一定作用的元素。比如:网络套接字、互斥锁、文件句柄和内存等等,它们属于系统资源。由于系统的资源是有限的,就好比自然界的石油,铁矿一样,不是取之不尽,用之不竭的,所以,我们在编程使用系统资源时,都必须遵循一个步骤:
1 申请资源;
2 使用资源;
3 释放资源。
第一步和第二步缺一不可,因为资源必须要申请才能使用的,使用完成以后,必须要释放,如果不释放的话,就会造成资源泄漏。
RAII 代表资源获取即初始化。这个词语背后的想法:对于任何获取的资源,都应该初始化一个对象,该对象将拥有该资源并在析构函数中关闭它。智能指针是 RAII 的一个突出示例。它们有助于避免内存泄漏。以下库提供智能指针和其他工具来帮助您更轻松地管理内存。
-
Boost.SmartPointers 定义了智能指针。其中一些是由 C++11 标准库提供的。其他仅在 Boost 中可用。
-
Boost.PointerContainer 定义容器来存储动态分配的对象——使用 new 创建的对象。因为这个库中的容器在析构函数中使用 delete 来销毁对象,所以不需要使用智能指针。
-
Boost.ScopeExit 可以将 RAII 惯用语用于任何资源。虽然 Boost.SmartPointers 和 Boost.PointerContainer 只能与指向动态分配对象的指针一起使用,但使用 Boost.ScopeExit 不需要使用特定于资源的类。
-
Boost.Pool 与 RAII 无关,但与内存管理有很大关系。这个库定义了许多类来更快地为您的程序提供内存。
- 1. Boost.SmartPointers
- 2. Boost.PointerContainer
- 3. Boost.ScopeExit
- 4. Boost.Pool
内容列表
- Sole Ownership
- Shared Ownership
- Special Smart Pointers
库 Boost.SmartPointers 提供各种智能指针。它们帮助您管理动态分配的对象,这些对象锚定在智能指针中,这些指针在析构函数中释放动态分配的对象。因为析构函数是在智能指针范围结束时执行的,所以可以保证释放动态分配的对象。例如,如果您忘记调用 delete,则不会发生内存泄漏。
标准库自 C++98 起包含智能指针 std::auto_ptr,但自 C++11 起,std::auto_ptr 已被弃用。在 C++11 中,标准库中引入了新的更好的智能指针。 std::shared_ptr 和 std::weak_ptr 源自 Boost.SmartPointers,在这个库中被称为 boost::shared_ptr 和 boost::weak_ptr。没有对应的 std::unique_ptr。但是,Boost.SmartPointers 提供了四个额外的智能指针——boost::scoped_ptr、boost::scoped_array、boost::shared_array 和 boost::intrusive_ptr——它们不在标准库中。
五、指针:Boost.PointerContainer 库 Boost.PointerContainer 提供专门用于管理动态分配对象的容器。例如,在 C++11 中,您可以使用 std::vector
Example 2.1. Using boost::ptr_vector
#include
#include
int main()
{
boost::ptr_vector v;
v.push_back(new int{1});
v.push_back(new int{2});
std::cout << v.back() << '\n';
}
类 boost::ptr_vector 基本上像 std::vector
示例 2.2。 boost::ptr_set 具有直观正确的顺序
#include
#include
#include
#include
#include
#include
int main()
{
boost::ptr_set s;
s.insert(new int{2});
s.insert(new int{1});
std::cout << *s.begin() << '\n';
std::set, boost::indirect_fun>> v;
v.insert(std::unique_ptr(new int{2}));
v.insert(std::unique_ptr(new int{1}));
std::cout << **v.begin() << '\n';
}
Example 2.2
说明使用专用容器的另一个原因。该示例将 int 类型的动态分配变量存储在 boost::ptr_set 和 std::set 中。 std::set 与 std::unique_ptr 一起使用。
使用 boost::ptr_set,元素的顺序取决于 int 值。 std::set 比较 std::unique_ptr 类型的指针,而不是指针引用的变量。要使 std::set 根据 int 值对元素进行排序,必须告知容器如何比较元素。在示例 2.2 中,使用了 boost::indirect_fun(由 Boost.PointerContainer 提供)。使用 boost::indirect_fun,std::set 被告知不应该根据 std::unique_ptr 类型的指针对元素进行排序,而是根据指针所指的 int 值。这就是示例显示 1 两次的原因。
除了 boost::ptr_vector 和 boost::ptr_set 之外,还有其他容器可用于管理动态分配的对象。这些附加容器的示例包括 boost::ptr_deque、boost::ptr_list、boost::ptr_map、boost::ptr_unordered_set 和 boost::ptr_unordered_map。这些容器对应于标准库中众所周知的容器。
示例 2.3。来自 Boost.PointerContainer 的容器插入器
#include
#include
#include
#include
#include
int main()
{
boost::ptr_vector v;
std::array a{{0, 1, 2}};
std::copy(a.begin(), a.end(), boost::ptr_container::ptr_back_inserter(v));
std::cout << v.size() << '\n';
}
Boost.PointerContainer 为其容器提供插入器。它们在命名空间 boost::ptr_container 中定义。要访问插入器,您必须包含头文件 boost/ptr_container/ptr_inserter.hpp。
示例 2.3 使用函数 boost::ptr_container::ptr_back_inserter(),它创建了一个 boost::ptr_container::ptr_back_insert_iterator 类型的插入器。此插入器被传递给 std::copy() 以将数组 a 中的所有数字复制到向量 v。因为 v 是 boost::ptr_vector 类型的容器,它需要动态分配的 int 对象的地址,所以插入器使用堆上的新地址并将地址添加到容器中。
除了 boost::ptr_container::ptr_back_inserter() 之外,Boost.PointerContainer 还提供了 boost::ptr_container::ptr_front_inserter() 和 boost::ptr_container::ptr_inserter() 函数来创建相应的插入器。
练习
使用成员变量 name、legs 和 has_tail 创建一个包含多个动物类型对象的程序。将对象存储在 Boost.PointerContainer 的容器中。根据腿按升序对容器进行排序,并将所有元素写入标准输出。
六、Boost.ScopeExit
库 Boost.ScopeExit 使得在没有资源特定类的情况下使用 RAII 成为可能。
Example 3.1. Using BOOST_SCOPE_EXIT
#include
#include
int *foo()
{
int *i = new int{10};
BOOST_SCOPE_EXIT(&i)
{
delete i;
i = 0;
} BOOST_SCOPE_EXIT_END
std::cout << *i << '\n';
return i;
}
int main()
{
int *j = foo();
std::cout << j << '\n';
}
Boost.ScopeExit 提供了宏 BOOST_SCOPE_EXIT,它可以用来定义一些看起来像本地函数但没有名字的东西。但是,它确实有一个括号中的参数列表和大括号中的块。
必须包含头文件 boost/scoped_exit.hpp 才能使用 BOOST_SCOPE_EXIT。
宏的参数列表包含来自外部范围的变量,这些变量应该可以在块中访问。变量通过副本传递。要通过引用传递变量,它必须以 & 符号作为前缀,如示例 3.1 中所示。
如果变量在参数列表中,则块中的代码只能从外部范围访问变量。
BOOST_SCOPE_EXIT 用于定义一个块,该块将在定义块的范围结束时执行。在示例 3.1 中,使用 BOOST_SCOPE_EXIT 定义的块在 foo() 返回之前执行。
BOOST_SCOPE_EXIT 可用于从 RAII 中受益,而无需使用特定于资源的类。 foo() 使用 new 创建一个 int 变量。为了释放变量,使用 BOOST_SCOPE_EXIT 定义了一个调用 delete 的块。即使函数由于异常而提前返回,也保证执行此块。在示例 3.1 中,BOOST_SCOPE_EXIT 与智能指针一样好。
请注意,变量 i 在 BOOST_SCOPE_EXIT 定义的块末尾设置为 0。然后 i 由 foo() 返回并写入 main() 中的标准输出流。但是,该示例不显示 0。j 设置为随机值 - 即 int 变量在内存被释放之前所在的地址。 BOOST_SCOPE_EXIT 后面的块获得了对 i 的引用并释放了内存。但由于该块是在 foo() 的末尾执行的,因此将 0 分配给 i 为时已晚。 foo() 的返回值是在 i 设置为 0 之前创建的 i 的副本。
如果您使用 C++11 开发环境,则可以忽略 Boost.ScopeExit。在这种情况下,您可以在 lambda 函数的帮助下使用没有资源特定类的 RAII。
示例 3.2。 Boost.ScopeExit 与 C++11 lambda 函数
#include
#include
template
struct scope_exit
{
scope_exit(T &&t) : t_{std::move(t)} {}
~scope_exit() { t_(); }
T t_;
};
template
scope_exit make_scope_exit(T &&t) { return scope_exit{
std::move(t)}; }
int *foo()
{
int *i = new int{10};
auto cleanup = make_scope_exit([&i]() mutable { delete i; i = 0; });
std::cout << *i << '\n';
return i;
}
int main()
{
int *j = foo();
std::cout << j << '\n';
}
定义其构造函数接受函数的类 scope_exit。该函数由析构函数调用。此外,还定义了一个辅助函数 make_scope_exit(),它可以在无需指定模板参数的情况下实例化 scope_exit。
在 foo() 中,一个 lambda 函数被传递给 make_scope_exit()。 lambda 函数看起来像示例 3.1 中 BOOST_SCOPE_EXIT 之后的块:地址存储在 i 中的动态分配的 int 变量通过删除被释放。然后将 0 分配给 i。
该示例与前一个示例执行相同的 *** 作。不仅 int 变量被删除,而且 j 在写入标准输出流时也没有设置为 0。
示例 3.3。 BOOST_SCOPE_EXIT 的特点
#include
#include
struct x
{
int i;
void foo()
{
i = 10;
BOOST_SCOPE_EXIT(void)
{
std::cout << "last\n";
} BOOST_SCOPE_EXIT_END
BOOST_SCOPE_EXIT(this_)
{
this_->i = 20;
std::cout << "first\n";
} BOOST_SCOPE_EXIT_END
}
};
int main()
{
x obj;
obj.foo();
std::cout << obj.i << '\n';
}
示例 3.3 定义其构造函数接受函数的类scope_exit。该函数由解析构造函数调用。另外,实例化还定义了一个辅助函数make_scope_exit(),它可以在单独指定模板参数的情况下使用scope_exit。
在 foo() 中,一个 lambda 函数被传递给 make_scope_exit()。 lambda 函数看起来像示例 3.1 中 BOOST_SCOPE_EXIT 之后的块:存储在 i 中的动态变量分配的 int 被释放。然后将 0 分配给一世。
该示例之前的示例执行相同的 *** 作。但是,一个int变量被删除,并且j在写入标准输出流时也没有设置为0。
示例 3.3。 BOOST_SCOPE_EXIT 的特点
Example 3.3 介绍了BOOST_SCOPE_EXIT的一些特性:
-
当BOOST_SCOPE_EXIT用于定义作用域中的多个块时,这些块按相反的顺序执行。示例3.3先显示,后显示。
-
如果没有变量传递给BOOST_SCOPE_EXIT,则需要指定void。括号不能为空。
-
如果在成员函数中使用BOOST_SCOPE_EXIT,并且需要传递指向当前对象的指针,则必须使用this_,而不是this。
显示 first, last, 和20顺序.
练习
将std::unique_ptr和用户定义的删除程序替换为BOOST_SCOPE_EXIT:
#include
#include
#include
struct CloseFile
{
void operator()(std::FILE *file)
{
std::fclose(file);
}
};
void write_to_file(const std::string &s)
{
std::unique_ptr file{
std::fopen("hello-world.txt", "a") };
std::fprintf(file.get(), s.c_str());
}
int main()
{
write_to_file("Hello, ");
write_to_file("world!");
}
七、Boost.Pool
Boost.Pool是一个库,其中包含一些用于管理内存的类。虽然C++程序通常使用new动态分配内存,但如何提供内存的细节取决于标准库和 *** 作系统的实现。使用Boost。例如,池可以加速内存管理,以更快地为程序提供内存。
Boost.Pool不会更改新 *** 作系统或 *** 作系统的行为。促进池可以工作,因为首先从 *** 作系统请求受管内存,例如使用new。从外部来看,您的程序已经分配了内存,但在内部,还不需要内存,而是移交给Boost。池来管理它。
Boost.Pool以相同大小对内存段进行分区。每次从Boost请求内存时。池中,库将访问下一个空闲段并从该段为您分配内存。然后将整个段标记为已使用,无论该段实际需要多少字节。
这种内存管理概念称为简单隔离存储。这是Boost.Pool支持的唯一概念。如果必须经常创建和销毁许多相同大小的对象,这一功能尤其有用。在这种情况下,可以快速提供和释放所需的内存。
Boost.Pool提供类boost::simple_segregated_storage来创建和管理隔离内存。boost::simplesegregatedstorage是一个低级类,通常不会直接在程序中使用。它仅在示例4.1中用于说明简单的隔离存储。Boost的所有其他课程。池在内部基于boost::simple_segregated_storage。
Example 4.1. Using boost::simple_segregated_storage
#include
#include
#include
int main()
{
boost::simple_segregated_storage storage;
std::vector v(1024);
storage.add_block(&v.front(), v.size(), 256);
int *i = static_cast(storage.malloc());
*i = 1;
int *j = static_cast(storage.malloc_n(1, 512));
j[10] = 2;
storage.free(i);
storage.free_n(j, 1, 512);
}
头文件boost/pool/simple_segregated_storage。必须包含hpp才能使用类模板boost::simple_segregated_storage。示例4.1将std::size_t作为模板参数传递。此参数指定传递给boost::simple_segregated_storage成员函数的数字应使用哪种类型,例如引用段的大小。此模板参数的实际相关性相当低。
更有趣的是在boost::simple_segregated_storage上调用的成员函数。首先,调用add_block()将1024字节的内存块传递到存储器。内存由向量v提供。传递给add_block()的第三个参数指定内存块应分区为段,每个段256个字节。因为内存块的总大小为1024字节,所以由存储器管理的内存由四个段组成。
对malloc()和malloc_n()的调用从存储器请求内存。当malloc()返回一个指向空闲段的指针时,malloc_n()则返回指向一个或多个连续段的指针,这些连续段在一个块中提供所需的字节数。示例4.1使用malloc_n()请求一个512字节的块。此调用使用两个段,因为每个段是256字节。在调用malloc()和malloc_n()之后,存储器只剩下一个未使用的段。
在示例的末尾,使用free()和free_n()释放所有段。在这两次调用之后,所有段都可用,可以用malloc()或malloc_n()再次请求。
通常不直接使用boost::simple_segregated_storage。促进Pool提供了其他类,这些类可以自动分配内存,而不需要您自己分配内存并将其传递给boost::simple_segregated_storage。
Example 4.2. Using boost::object_pool
#include
int main()
{
boost::object_pool pool;
int *i = pool.malloc();
*i = 1;
int *j = pool.construct(2);
pool.destroy(i);
pool.destroy(j);
}
Example 4.2 用类 boost::object_pool, 它被定义在 boost/pool/object_pool.hpp.
与boost::simple_segregated_storage不同,boost::object_pool知道将存储在内存中的对象的类型。示例4.2中的pool是int值的简单隔离存储。池管理的内存由段组成,例如,每个段的大小为int–4字节。
另一个区别是您不需要提供内存来提升::object_pool。boost::objectpool自动分配内存。在示例4.2中,对malloc()的调用使池为32个int值分配一个内存块。malloc()返回一个指针,指向int值可以精确放入的这32个段中的第一个。
请注意,malloc()返回一个int*类型的指针。与示例4.1中的boost::simple_segregated_storage不同,不需要强制转换运算符。
construction()类似于malloc(),但通过调用构造函数初始化对象。在示例4.2中,j指的是用值2初始化的int对象。
请注意,当调用construct()时,池可以从32个段的池中返回一个空闲段。对construct()的调用不会使示例4.2从 *** 作系统请求内存。
示例4.2中调用的最后一个成员函数是destroy(),它释放一个int对象。
示例4.3.使用boost::object_pool更改段大小
#include
#include
int main()
{
boost::object_pool pool{32, 0};
pool.construct();
std::cout << pool.get_next_size() << '\n';
pool.set_next_size(8);
}
可以向boost::object_pool的构造函数传递两个参数。第一个参数设置在调用malloc()或construct()请求第一个段时boost::object_pool将分配的内存块的大小。第二个参数设置要分配的内存块的最大大小。
如果频繁调用malloc()或construct(),以至于使用了内存块中的所有段,那么对其中一个成员函数的下一次调用将导致boost::object_pool分配一个新内存块,该内存块将是前一个内存块的两倍大。每次boost::object_pool分配新内存块时,大小都会加倍。objectpool可以管理任意数量的内存块,但它们的大小将成倍增长。第二个构造函数参数允许您限制增长。
boost::object_pool的默认构造函数的作用与示例4.3中对构造函数的调用相同。第一个参数将内存块的大小设置为32个int值。第二个参数指定没有最大大小。如果传递了0,boost::object_pool可以无限期地将内存块的大小加倍。
示例4.3中对construct()的调用使池分配32个int值的内存块。池最多可以提供32个对malloc()或construct()的调用,而无需从 *** 作系统请求内存。如果需要更多内存,下一个要分配的内存块将有空间容纳64个int值。
get_next_size()返回要分配的下一个内存块的大小。set_next_size()允许您设置下一个内存块的大小。在示例4.3中,get_next_size()返回64。调用set_next_ssize=()将下一个内存块的大小从64更改为8个int值。使用set_next_size(),可以直接更改下一个内存块的大小。如果只想设置最大大小,请通过第二个参数将其传递给构造函数。
使用boost::singleton_pool,boost。池在boost::simple_segregated_storage和boost::object_Pool之间提供一个类(参见示例4.4)。
Example 4.4. Using boost::singleton_pool
#include
struct int_pool {};
typedef boost::singleton_pool singleton_int_pool;
int main()
{
int *i = static_cast(singleton_int_pool::malloc());
*i = 1;
int *j = static_cast(singleton_int_pool::ordered_malloc(10));
j[9] = 2;
singleton_int_pool::release_memory();
singleton_int_pool::purge_memory();
}
boost::singletonpool在boost/pool/singleton_pool.hpp中定义。此类类似于boost::simple_segregated_storage,因为它还希望将段大小作为模板参数,而不是要存储的对象的类型。这就是为什么ordered_malloc()和malloc()等成员函数返回void*类型的指针,必须显式转换。
这个类也类似于boost::object_pool,因为它自动分配内存。下一个内存块的大小和可选的最大大小作为模板参数传递。这里boost::singleton_pool与boost::object_pool:不同:您不能在运行时更改boost:∶singleton _pool中下一个内存块的大小。
如果要管理多个内存池,可以使用boost::singleton_pool创建多个对象。传递给boost::singleton_pool的第一个模板参数是一个标记。标记是用作内存池名称的任意类型。示例4.4使用结构int_pool作为标记,以突出显示singleton_int_pook是一个管理int值的池。由于标记,多个单例可以管理不同的内存池,即使大小的第二个模板参数相同。标记除了创建boost::singleton_pool的单独实例之外,没有其他用途。
boost::singletonpool提供了两个成员函数来释放内存:release_memory()释放当前未使用的所有内存块,purge_memory(。调用purge_memory()将重置boost::singleton_pool。
release_memory()和purge_memory()将内存返回给 *** 作系统。要将内存返回给boost::singleton_pool而不是 *** 作系统,请调用成员函数,如free()或ordered_free()。
boost::object_pool和boost::singletonpool允许您显式请求内存。这可以通过调用成员函数来实现,例如malloc()或construct()。促进Pool还提供类boost::Pool_allocator,您可以将其作为分配器传递给容器(参见示例4.5)。
Example 4.5. Using boost::pool_allocator
#include
#include
int main()
{
std::vector> v;
for (int i = 0; i < 1000; ++i)
v.push_back(i);
v.clear();
boost::singleton_pool::
purge_memory();
}
boost::pool_allocator在boost/pool/pool_aalloc.hpp中定义。该类是一个分配器,通常作为第二个模板参数从标准库传递给容器。分配器提供容器所需的内存。
pool_allocator基于boost::singletonpool。要释放内存,您必须使用标记访问boost::singleton_pool,并调用purge_memory()或release_memory(。此标记由Boost定义。Pool,由boost::Pool_allocator用于内部boost::singleton_Pool。
当示例4.5第一次调用push_back()时,v访问分配器以获取请求的内存。因为使用了分配器boost::pool_allocator,所以分配了一个内存块,其中有32个int值的空间。v接收指向该内存块中具有int大小的第一个段的指针。每次后续调用push_back()时,都会使用内存块中的另一段,直到分配器检测到需要更大的内存块。
请注意,在使用purge_memory()释放内存之前,应该对容器调用clear()(参见示例4.5)。调用purge_memory()会释放内存,但不会通知容器不再拥有内存。调用release_memory()的危险性较小,因为它只释放未使用的内存块。
促进Pool还提供了一个名为boost::fast_Pool_allocator的分配器(参见示例4.6)。
示例4.6.使用boost::fast_pool_allocator
#define BOOST_POOL_NO_MT
#include
#include
int main()
{
typedef boost::fast_pool_allocator allocator;
std::list l;
for (int i = 0; i < 1000; ++i)
l.push_back(i);
l.clear();
boost::singleton_pool::
purge_memory();
}
这两个分配器的使用方式相同,但如果您请求的是连续的段,则应首选boost::pool_allocator。如果逐个请求段,则可以使用boost::fastpool_allocator。大大简化:对于std::vector使用boost::pool_allocator,对于std:::list使用boost::fast_pool_allocater。
示例4.6说明了哪些模板参数可以传递给boost::fast_pool_allocator。boost::poolallocator接受相同的参数。
boost::defaultuserallocatornewdelete是一个用new分配内存块并用delete[]释放内存块的类。您还可以使用boost::default_user_allocator_malloc_free,它调用malloc()和free()。
boost::details::pool::default_mutex是一个类型定义,设置为boost:∶mutex或boost:;details:::pool::null_mutex。mutex是默认类型,它支持多个线程从分配器请求内存。如果宏BOOST_POOL_NO_MT的定义如示例4.6所示,则BOOST支持多线程。池已禁用。示例4.6中的分配器使用空互斥体。
示例4.6中传递给boost::fast_pool_allocator的最后两个参数设置了第一个内存块的大小和要分配的内存块的最大大小。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)