目前写的程序都只使用过静态内存和栈内存。静态内存用来保存局部static
对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static
对象。分配在静态或栈内存中的对象由编译器自动创建和销毁,对于栈对象,仅在其定义的程序块运行时才存在:static对象在使用之前分配,在程序结束时销毁
除了静态内存和栈内存,每个程序还拥有一个内存池–自由空间或堆。程序用堆来存储动态分配的对象:在程序运行时分配的对象。 动态对象的生存期由程序来控制,也就是当动态对象不再使用时,必须显示地销魂它们。
3.1动态内存与智能指针
new
:在动态内存中为对象分配空间并返回一个指向该对象的指针。delete
:接受一个动态对象的指针,销毁该对象,释放与之关联的内存。忘记释放内存会导致内存泄露,而在尚有指针引用内存的情况下释放会导致产生引用非法内存的指针。
智能
指针的出现是为了更安全的管理动态对象,类似常规指针,区别在于它会负责自动释放所指向的对象。新标准库提供了两种智能指针:1)shared_ptr
:允许多个指针指向同一个对象;2)unuque_ptr
:独占指向的对象。此外还有一个weak_ptr
,指向shared_ptr
所管理的对象。三种类型定义在memory
中。
3.1.1shared_ptr类
智能指针也是模板,默认初始化的智能指针中保存一个空指针,使用方式与普通指针类似,解引用一个智能指针返回它所指向的对象。
make_shared函数
此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
。
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10,'9');
shared_ptr<int> p5 = make_shared<int>(); // 值为0
auto p6 = make_shared<vector<string>>(); // 指向一个动态分配的空vector<string>
类似于顺序容器的emplace
成员,make_shared
用其参数来构造给定类型的对象,比如调用make_shared<string>
传递的参数必须与string
某一个构造函数相匹配。
shared_ptr的拷贝和赋值
当进行拷贝或者赋值操作时,每个shared_ptr
都会记录有多少个其他shared_ptr
指向相同的对象, 可以理解它内部有一个计数器(引用计数)。
auto p = make_shared<int>(42); // 42有一个引用者
auto q(p); // 现在42有两个引用者
一旦一个shared_ptr的计数器变为0就会被自动释放
auto r = make_shared<int>(4);
r = 1; // 4已经没有对象指向它,计数器值为0,会自动释放
shared_ptr
被销毁时是通过一个特殊的成员函数:析构函数完成销毁工作。==shared_ptr的析构函数会递减它所指向的对象的引用计数,如果引用计数变为0,析构函数就会销毁对象。==此外,当动态对象不再被使用时,shared_ptr
类会自动释放动态对象。如下面一个例子:
shared_ptr<Foo> factory(T arg) {
...
return make_shared<Foo>(arg);
}
factory
返回一个shared_ptr
,所以我们可以确保它分配的对象会在恰当的时刻被释放。
void use_factory(T arg) {
shared_ptr<Foo> p = factory(arg);
}
可以看到p
是一个局部变量,当它离开了作用域,指向的内存就会被自动释放掉(要先递减判断计数器是否为0,如果为0 就释放)。
使用了动态生存期的资源的类
程序使用动态内存的三个原因:
- 程序不知道自己需要使用多少对象(容器类)
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据(例子:StrBlob)
一般来说,由一个容器分配的元素只有当容器存在时才会存在,但是某些类分配的资源具有与原对象相独立的生存期。下面看一个例子:StrBlob
,我们想达到拷贝对象与原对象共享一个底层数据,所有当我们删除其中一个对象时,里面的数据是不能被销毁的。
定义StrBlob
class StrBlob {
public:
typedef std::vector<std::string>::size_type size_type; // size_type = vector<string>
StrBlob(); // 默认构造函数
StrBlob(std::initializer_list<std::string> il); // 列表初始化构造函数
size_type size() const { return data->size(); } // 返回一个vector<string>的元素大小
bool empty() const { return data->empty(); } // 检查是否为空
void push_back(const std::string &t) {data->push_back(t);} // 添加元素
void pop_back(); // 删除元素
std::string &front();
std::string &back();
private:
std::shared_ptr<std::vector<std::string>> data;
void check(size_type i, cosnt std::string &msg) const; // 在对容器的元素进行操作时,检查元素是否存在
}
StrBlob::StrBlob() : data(make_shared<vector<string>>()) {}
StrBlob::StrBlob(initializer_list<string> il) :
data(make_shared<vector<string>>(il)) {}
void check(size_type i, cosnt std::string &msg) const {
if( i >= data->size())
throw out_of_range(msg);
}
3.2动态数组
3.2.1new和数组
使用new
分配一个对象数组,需要定义分配的数量,new
分配要求数量的对象并返回指向第一个对象的指针。
int *pia = new int[get_size()]; // pia指向第一个int
==分配一个数组会得到一个元素类型的指针,==由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin
或end
,同样也不能用范围for
语句来处理动态数组中的元素。
动态分配一个空数组是合法的
如果下面代码中的get_size()
返回零,任然可以正常工作,虽然不能创建一个大小为0的静态数组对象,但是当n = 0
时,调用new[n]
是合法的。
size_t n = get_size();
int *p = new int[n];
for(int *q = p; q != p + n; ++ q)
char arr[0]; // 错误
char *cp = new char[0]; // 正确,但是cp不能解引用
释放动态数组
动态数组被释放时,数组中的元素按照逆序销毁。
delete [] pa; // []告诉编译器它指向一个对象数组的第一个元素
智能指针和动态数组
标准库提供了一个可以管理new
分配的数组的unique_ptr
版本。
unique_ptr<int[]> up(new int[10]);
up.release(); // 自动调用delete[]销毁其指针
与unique_ptr
不同,shared_ptr
不直接支持管理动态数组,如果需要其管理一个动态数组,必须提供自己定义的删除器。
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; }); // 使用lambda
sp.reset();
shared_ptr
不直接支持动态数组管理这一特性会影响访问数组中的元素
for(size_t i = 0; i != 10; ++ i)
*(sp.get() + i) = i; // 使用get获取一个内置指针。
shared_ptr
未定义下标运算符,而且智能指针类型不支持指针算术运算,为了访问数组中的元素,必须用get
获取一个内置指针。
3.2.2allocator类
new
将内存分配和对象构造组合在了一起,delete
将对象析构和内存释放组合在了一起,造成了灵活性上的局限。一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。
string *const p = new string[n]; // 构造n个空string
string s;
string *q = p; // q指向第一个string
while( cin >> s && q != p + n)
*q ++ = s; // 输入s赋值到q
const size_t size = q - p; // 获得输入string的数量
delete []p; // 释放p
在上面代码中,new
表达式分配并初始化n个string
,但是我们可能用不完n个string
,此外对于那些确定要用的对象,我们在初始化之后立即赋予了它们新值,每个使用的元素都被赋值了两次,第一次在默认初始化,随后是在赋值时,更重要的是那些没有默认构造函数的类不能动态分配数组。
allocator
类可以将内存分配和对象构造分离,提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。使用allocator
对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置
allocator<string> alloc;
auto const p = alloc.allocate(n); // 分配n个未初始化的string
allocator分配未构造的内存
construct
成员函数接受一个指针和零个或多个额外参数,在给定的位置构造一个元素,额外参数用来初始化构造的对象。为了使用allocate返回的内存,必须使用construct构造对象,使用未构造的内存,行为是未定义的。
auto q = p; / q指向最后构造的元素之后的位置
alloc.construct(q ++); // *q是一个空字符串
alloc.construct(q ++, 10, 'c'); // *q为ccccccccc
alloc.construct(q ++, "hi"); // *q为hi
当用完对象后,必须对每个构造的元素调用destroy
来销毁它们。
while (q != p)
alloc.destroy(--q);
在元素被销毁后,就可以重新使用这部分内存来保存其他string
,或者归还给系统。
alloc.deallocate(p, n)
拷贝和填充未初始化内存的算法
stroy`来销毁它们。
while (q != p)
alloc.destroy(--q);
在元素被销毁后,就可以重新使用这部分内存来保存其他string
,或者归还给系统。
alloc.deallocate(p, n)