一、new 及局限性
1.1 new单个对象
对一个对象执行new运算将会:
- 在自由空间分配对象空间
- 执行默认构造函数构造对象
- 返回一个指向无名空间的对象指针
对于内置类型或组合类型(指针、引用)其对象的值是未定义的,对于有默认初始化的类将会执行默认构造。此外,我们可以在对象名后加上小括号,括号的内容是实例化的对象,通常可以是临时对象。
A * pa=new A(1,2,3);//利用A(int,int,int)构造方法构造这个对象
int * pint=new int(4);//临时的4常量构造
1.2 new对象数组
A* pa=new A[10]
,和单个对象一样,数组中的每个对象都将会默认执行默认构造,如果想要对各个对象特定构造函数的话就会用到初始化列表这个方法:
class A
{
public:
A() { std::cout << "A()" << std::endl; }
A(int a) { std::cout << "A(a)" << std::endl; }
~A() { std::cout << "~A()" << std::endl; }
};
A* paArrint = new A[10]{ A(1),A(2),A(3),A(4)};
delete []paArrint;
如果你没有对某个元素给特定实例化的对象,那么编译器仍然会给定默认初始化实例以完成new数组过程。
1.3 局限性
无论是单个对象还是内容是对象的数组,其空间分配和初始化是绑定在一起执行的,你需要多少对象,我就分配多少空间,然而有些时候我们并不知道我们需要多少对象。
假设我们想要动态创建一个不多于100个的string对象数组,因为实现不知道需要多少对象,所以我们使用了下面的语句创建这个数组:
string * new string[100];
//下面可能对其中的string对象进行赋值
实际使用可能只用到了一个,也可能用到了100个,但是为了保证任务要求我们不得不创建了100个string空间并进行了构造。当实际使用的对象小于100个对象时会出现:
- 不必要的空间分配
- 不必要的对象构造
- 不必要的赋值构造
- 要求类必须要有默认构造函数
- 资源不能单独回收
只说说第三点,因为实际的string构造方法不一定是默认构造,所以你提前进行默认构造是多余的,后面仍然要根据实际进行赋值。
为了解决这些问题,标准库alllcator类就出现了。
二、std::allocator 是一个用于分配内存的类模版
std::allocator
定义在头文件#include<memory>
中,和new
不同,他将空间分配和对象构造分离,是一种内存感知内存分配方法,它能够根据对象类型来选择恰当的内存对齐位置和大小。其内存的特点是:分配的内存是原始的、未构造的!
#include <memory>
int main() {
std::allocator<int> allocator;
int* p = allocator.allocate(5); // 分配5个int的内存
for (int i = 0; i < 5; ++i) {
allocator.construct(&p[i], i); // 在内存上构造int
}
for (int i = 0; i < 5; ++i) {
std::cout << p[i] << std::endl; // 输出:0 1 2 3 4
}
for (int i = 0; i < 5; ++i) {
allocator.destroy(&p[i]); // 调用析构函数(在这种情况下,对于简单的int类型,这实际上什么都不做)
}
allocator.deallocate(p, 5); // 释放内存
}
使用这个类首先要构造一个allocator对象,下面是他的成员方法:
2.1 构造allocator对象
std::allocator<T>::allocator;
构造了一个allocator对象用于对T对象进行空间分配。
利用allocator分配空间
T* allocate( std::size_t n, const void * hint);
通过调用allocate方法构造了长度为n的T对象数组的指针。此时的指针不可使用,因为没有进行构造。需要使用方法construct之后才能使用。
利用deallocator回收空间
void deallocate( T* p, std::size_t n );
通过deallcate方法回收了指向p的n个对象空间。这个n必须等于创建时的长度大小。
利用construct构造一个对象
template< class U, class... Args >
void construct( U* p, Args&&... args ); //(since C++11)(deprecated in C++17) (removed in C++20)
construct按照参数args在指向T的原始内存中构造对象。请注意是一个对象!经过这个步骤这个对象才算可以真正被使用。如果你想析构掉对象而不想回收内存,那么你可以使用destory方法。
利用destory析构一个对象
template< class T >
void destroy( T* p );//(since C++11)(deprecated in C++17)(removed in C++20)
destory将会析构一个指向U的对象,请注意需要保证对象是被构造才能调用这个函数。
三、一个实例
class A
{
public:
A(int i) { std::cout << "A() = " << i << std::endl; m_i = i; }
~A() { std::cout << "~A() = " << m_i << std::endl; }
int m_i=0;
};
int main()
{
std::allocator<A> alloc;
auto const p = alloc.allocate(10);
auto q = p;
for(int i=0;i<10;i++)
{
alloc.construct(q++, i);
}
while(q!=p)
alloc.destroy(--q);
return 0;
}
四、拷贝和填充未初始化内存算法
对于一个对象,可以对未分配的内存调用construct,对于多个对象你可以通过迭代器进行范围指定,一次性构造多个对象。对于完全相同的多个对象,你可以使用unintialized_fill或unintialized_fill_n进行重复值构造。
unintialized_copy(b,e,b2);
unintialized_copy_n(b,n,b2);
unintialized_fill(b,e,t);
unintialized_fill_n(b,n,t);
一个例子:
std::allocator<A> alloc;
auto const p = alloc.allocate(5);
auto q = p;
std::vector<A> vecA{ A(1),A(2),A(3),A(4),A(5) };
std::uninitialized_copy(vecA.begin(), vecA.end(), p);//利用已经存在的vector来初始化这个未初始化的内存
for (int i = 0; i < 5; i++)
{
std::cout << (q++)->m_i << endl;
}
while (q != p)
alloc.destroy(--q);//因为按照前面这么增,q指向的是不存在的内存,因此要先--回去
复习以下自增和自减少的区别:
int i = 0;
std::cout << (i++);//先用i再自增,编译器从左往右先看到的是i,是一个操作数,所以直接用了。然后才是++。
int j = 0;
std::cout << (++j);//先自增再用j,因为从左往右先看到的是++,不是操作数,直到他看到了j对象,但是此时已经加上1了。