第十二章(动态内存)
1).动态分配对象的生存期和它们在哪里创建是没有关系的,只有显式地被释放时,这些对象才会被释放。 2).为了解决动态对象能够被正确释放的问题,设置了智能指针。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。 3).静态内存。
- 局部
static 变量。 - 类的
static 数据成员 - 全局变量
4).栈内存。保存局部变量。 5).静态内存和栈内存,由编译器自动创建和销毁。 6).程序的自由空间,或者堆。程序可以用堆,存储动态分配(程序运行时分配)的对象(它的创建和销毁由程序代码显式表示)。
/1.动态内存和指针
1).动态内存管理的运算符。
new ,在动态内存中为对象分配一个空间并返回一个指向该对象的指针。delete ,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
2).问题。
- 忘记释放,内存泄漏。
- 过早释放,使用非法的引用或者指针。s
3).解决。两个智能指针,和一个伴随类。
- 智能指针会自动释放所指向的对象。(这是与普通指针的差别)
shares_ptr ,允许多个指针同时指向一个对象。unique_ptr ,独占一个对象。- 伴随类。
weak_ptr ,它是一个弱引用。指向的是shared_ptr 所管理的对象。 - 这三个类型都定义在头文件
memory 中。
//1.shared_ptr类
1).智能指针实现的机制。依靠智能指针类里面的引用计数器成员。
- 可以认为每一个智能指针对象都有一个引用计数成员,指向同一个对象的智能指针的引用计数值一样。
- 当一个被引用对象的引用计数为0时,它的空间就会被自动释放。
引用计数何时增减。
- 当进行拷贝时,引用计数增加。例如,将它作为实参传递给形参时,进行了拷贝;当函数返回时,进行拷贝。
- 当一个局部
ptr 被销毁;或者一个ptr 被赋予新值时;引用计数器减少。
关于引用计数器。
- 引用计数器的具体实现由标准库决定。
- 可能时计数器,也可能是其他的数据结构。
2).介绍智能指针。
{
shard_ptr<string> p1;
shared_ptr<list<int>> p2;
}
- 使用
make_shared 函数构造智能指针并可以选择进行初始化。如果没有初始化,那么即使进行**值初始化。**这是分配和使用动态内存最安全的方法。该函数定义头文件memory 中。(使用时需要类型,类似于模板;与emplace 的类似,可以用参数来构造对象。)
{
auto p1 = make_shared<int>(42);
shared_ptr<string> p2 = make_shared<string>(10,'2');
shared_ptr<int> p3 = make_shared<int>();
}
{
initializer_list<string> li = {};
make_shared<vector<string>>(li);
}
4).智能指针支持的操作。
操作名称 | 相关介绍 |
---|
shared_ptr和unique——ptr 都支持的操作。 | | shared_ptr<T> sp; | 空智能指针。 | unique<T> up; | | p | 将p作为一个条件。非0返回true | *p | 解引用 | p->mem | 等价于(*p).mem | p.get() | 返回与p指向同一个对象的内置指针。 | swap(p,q) | 交换指针的值 | q.swap§ | |
5).shared_ptr 独有的操作。
操作名称 | 相关描述 |
---|
make_ptr<T>(agrs) | 返回一个指向动态分配的类型为T的对象的shared——ptr 。该对象用args 进行初始化。 | shared_ptr<T> p(q) | 进行拷贝初始化。q 中的指针必须可以转换为T* ,p 是shared_ptr 类型的指针。 | p = q | p,q都是shared_ptr ,并且它们所指向的类型必须可以进行相互转换。 | p.use_count() | 返回的是与p共享对象的智能指针的数量,可能很慢,主要用于调式。 | p.unique() | 如果,p.use_count()的数量为1,返回的是true ,否则返回的是false |
6).析构函数。
- 析构函数,是用来完成销毁工作的。
- 每一个类都有一个析构函数。它控制对象销毁时进行什么操作。一般用来释放对象所分配的资源。例如,释放内存。
- 当引用计数为0时,智能指针的析构函数就会销毁对象,释放它所占用的内存。
7).注意。以下情况引用计数不是为0。
{
void ues_factory(T args)
{
shared_ptr<F> p = make_ptr<F>(args);
return p;
}
}
8).当我们在一个容器中保存一些智能指针时,如果我们不需要这些指针了。只需要将他们erase 就可以进行删除。实际中这一点进行被遗忘。 9).为什么使用动态生存期的资源。
- 程序不知道自己需要使用多少的对象。例如,
vector ,我们随着用户输入才知道,需要多少的空间。容器的动态内存机制。 - 程序不知道所需对象的准确类型。(
template<typename T> ) - 程序需要在多个对象中共享数据。例如,一个
vector 拷贝给另一个vector ,虽然它们的内容是一样的,但是,它们不是共享的。一个vector 的生命期到了,它里面的元素也就释放了。我们要实现的是,当一个对象进行拷贝时,它所指向的底层数据是以被引用的方式被获取的,而不是底层数据的拷贝。
练习,
- 12.2,
const 版本的成员函数,返回值和this 类型都需要是const 。
//2.直接管理内存
1).使用new关键字创建指针。
- 可以选择是否值初始化。注意对于类类型,如果其内置类型数据成员没有类初始值,那么使用默认构造函数,内置类型还是未定义的。
- 对于类类型的是否值初始化都是调用它的默认构造函数。
- 可以选择进行显式地初始化。
- 注意,
new 关键字分配的空间是没有命名的,返回的是指针指向该对象。
{
int *p = new int;
string *q = new string;
int *p = new int();
string *q = new string();
int *p = new int(12);
int *p(new int(42));
string *q = new string("hello world!");
string *q = new string(10,'a');
vector<int> *p = new vector<int>{1,2,3,4,5,6};
}
2).使用auto 关键字进行自动识别。
{
auto p = new auto(obj);
auto q = new auto{a,b,c};
}
3).申请对于const 对象的指针。
const 指针的释放方式是一样的。delete p; 即可。
{
const string *p = new const string;
delete p;
}
const 对象必须进行初始化。可以是显式地,也可以是隐式地(例如,定义了默认构造函数的类类型。)
{
const int *p = new const int(12);
const string *q = new const string(" ");
const string *q = new const string;
}
4).关于定位new 与内存耗尽不能分配空间。
- 可以对定位
new 表达式传入额外参数, bad_alloc 和nothrow 都是定义在头文件new 中的。
{
int *p = new int;
int *p = new(nothrow) int;
}
5).delete 表达式
delete 执行两个操作,销毁给定指向的对象,然后释放内存。- 不可以对同一个空间进行多次释放。结果将会是未定义的。
- 只能对
new 关键字分配的内存或者空指针进行delete 。否则结果是未定义的。 - 编译器对于多次释放或者释放的是一个局部的变量无法判断,编译是通过的。
{
int i,*p = &i,*q = nullptr;
double *s = new double(33),*t = s;
delete i;
delete p;
delete q;
delete s;
delete t;
}
- 没有
delete ,即使指针被销毁了,它所指向的空间还是没有被释放。 - 释放后的指针,还是指向原来的位置,但是此时它就是一个空悬指针。会造成非法的访问。应该将他赋值为
nullptr 。 - 但是对于多个指针指向同一个对象的情况,上述方法只能修改个别指针。
练习,
{
bool b()
{
int *p = new(nothrow) int;
return p;
}
}
//3.shared_ptr和new的结合使用
1).shared_ptr 支持的操作。
操作名称 | 相关描述 |
---|
shared_ptr<T> p(q); | q是一个内置指针,p管理内置指针的空间。q必须是由new 分配的,并且可以转换为T* | shared_ptr<T> p(u); | p从unique_ptr 中接管对象,将u置为空。也就是unique_ptr 和shared_ptr 之间的转换。 | shared<T> p(q,d); | 同第一个操作,但是这里是使用可调用对象d来代替delete 。 | shared_ptr<T> p(p2,d) | p2是shared_ptr p2 类型的拷贝,区别就是使用d来代替delete | p.reset() | 如果p是唯一指向其对象的shared_ptr ,reset 会释放这个对象。并将p置为空。 | p.reset(q) | 此时指向的是内置指针q | p.reset(q,d) | 此时用d代替delete |
2).用内置指针初始化shared_ptr 。
- 注意接受内置指针参数的构造函数时
explicit 的。所以我们不可以对它进行拷贝初始化。因为不能进行隐式地转换。 - 对智能指针进行初始化,赋值的内置指针必须是指向动态内存的,因为智能指针就是默认使用
delete 的。 - 如果非要绑定其他类型的内置指针,我们需要自定义操作重载
delete 。
{
shared_ptr<int> p = new int(12);
shared_ptr<int> p(new int(12));
p.reset(new int(1024));
p = shared_ptr<int>(new int(12));
}
3).试图在函数返回值中使用内置指针对智能指针进行拷贝的错误。编译器不会执行从内置指针到智能指针的隐式转换。
{
shared_ptr<int> F(int q)
{
return new int(q);
return shared_ptr<int>(new int(q));
}
}
4).试图在传参时,将一个内置指针传递给智能指针导致指针内存的释放。
- 将一个内置指针的所有权交给智能指针时很危险的。你不知道什么时候他会释放内存。
{
int *p = new int(12);
void F(shared_ptr<int> q)
{
return;
}
F(p);
F(shared_ptr<int>(p));
int i = *p;
}
5).对一个内置指针绑定多个智能指针的错误。使用了get 函数。
- 永远不要使用
get 返回的内置指针对一个智能指针进行赋值。除非你可以保证他不会delete 。
{
shared_ptr<in> p(new int(12));
int *q = p.get();
{
shared_ptr<int>(q);
}
int i = *p;
}
6).reset 的使用。
{
if (!p.unique())
p.reset(new string(*p));
*p += " ";
}
7).归根结底就是,
- 智能指针只有在拷贝时才会增加引用计数。多次独立创建智能指针的错误就是因为每一个独立的智能指针的引用计数都是独立的。
练习,
{
shared_ptr<int> p(new int(12));
f(shared_ptr(p));
f(shared_ptr(p.get());
}
- 12.12
f(new int(12));//这是错误的,这也是需要隐式转换。 - 12.13,
get 返回的指针是可以delete ,但是会导致,shared_ptr 变为空悬指针。
//4.智能指针与异常
1).在一个函数f中,如果程序异常结束了,且在函数体里面没有捕获到这一个异常。那么,
- 局部变量智能指针会在退出函数体时正确地释放内存。
- 而由于
delete ,在异常抛出点之后,函数体退出时,它还没有运行到。使用内置类型的指针(局部变量)被销毁了。但是它的内存永远不会被释放。
2).虽然大多数的类有析构函数,来清理随想使用的资源。但是有一些是没有定义良好的析构函数的。**如果它有析构,就像内置类型一样,根本不需要我们进行释放。**例如,c和c++都使用的网络库。这种情况下,我们需要主动地去释放这些空间。但是,
3).解决,使用智能指针。但需要自定义函数(删除器)重载delete 。因为他没有delete 操作,不是new 产生的动态内存。(shared_ptr 默认是使用delete ,默认管理的是动态内存。)
{
void end_connection(connnection *p)
{
disconnection(*p);
}
destination d;
connection c = connect(&d);
shared_ptr<connection> p(&c,end_connection);
}
练习,
//5.unique_ptr
1).除了与shared_ptr 一样的操作,还支持以下操作。
操作名称 | 相关描述 |
---|
unique_ptr<T> u | 空的智能指针。使用delete 来释放空间 | unique_ptr<T,D> u | u会调用D代替delete | unique_ptr<T,D>u(d) | 空的指针,hi用类型为D的d来重载delete | u = nullptr | 释放u指向的空间,并置为空指针 | u.release() | u放弃控制权,并返回一个内置指针。u置为空指针。没有释放空间 | u.reset() | 释放空间,并且u置为空。,如果u为空则无需释放。 | u.reset(q) | 内置指针q。u释放所指向的空间,并且指向新的q。 | u.reset(nullptr) | 效果同u.reset() |
2).release 只是放弃控制权,没有释放内存。
{
p.release();
auto q = p.release();
}
3).对unique_ptr 的定义以及初始化,赋值。
- 可以认为它的引用计数只能为1;
unique_ptr 之间只能交换控制权,不可以相互赋值,初始化。- 不支持隐式转换。
{
unique<int> u = new int(1);
unique<int> u(new int(1));
unique<int> q(u);
unique<int> p;
p = u;
}
4).在函数的返回值是可以拷贝unique_ptr 的。编译器会知道要返回的对象将要被销毁。
{
unique_ptr<int> f(int p)
{
unique_ptr<int> ret(new int(p));
return ret;
return unique_ptr<int>(new int(p));
}
}
5).**自定义操作版本的unique_ptr 和shared_ptr 是不一样的。与算法是一样的。
- 重载
delete 操作,使得unique_ptr 的类型发生变化。
{
void f(destination d)
{
connection c = connect(&d);
unique_ptr<connect,decltype(end_connection)*> u(&c,end_connection);
}
}
6).移交控制权。
- 注意对于一个非
const 的unique_ptr 才可以进行移交控制权。
{
unique_ptr<string> p2(p1.release());
unique_ptr<string> p3(new string("test"));
p2.reset(p3.release());
}
7).关于auto_ptr
- 它是早期版本的一个类,有部分
unique_ptr 的特点。 - 不可以在容器中保存,也不能作为函数的返回值。
- 他仍是标准库的一部分。但是我们不会使用它。
练习,
- 12.16,当你试图拷贝或者赋值一个
unique_ptr 时,编译器给出的错误,并不一定是好理解的。 - 12.17,对一个
unique_ptr 使用一个普通的指针进行构造,合法。但是行为是未定义。
//6.weak_ptr
1).支持的操作。
操作名称 | 相关描述 |
---|
weak_ptr<T> w | 空指针 | weak_ptr<T> w(sp) | 与shared_ptr 指向相同的对像的weak_ptr 。T必须可以转换为sp的类型。 | w = p; | p可以是一个weak_ptr 或者是一个shared_ptr ,赋值后共享对象。 | w.reset() | 将w置为空指针 | w.use_count() | 与w共享的shared_ptr 的数量。 | w.expired() | 如果w.use_count() 为0返回true ,反之返回的是false | w.lock() | 如果w.expired() 返回true ,返回一个空的shared_ptr ,反之返回一个w的对象的shared_ptr |
2).定义以及初始化。
- 它不会控制对象的生存期。
- 只能用
shared_ptr 初始化weak_ptr - 创建时必须进行初始化。
{
auto p = make_shared<int>(42);
weak_ptr<int> q(p);
}
3).由于是弱引用,它的引用不计入引用计数中。因此它可能是无效的。使用时需要进行判断。使用local
{
if (shared_ptr q = wp.local())
{
}
}
/2.动态数组
1).解决一次为多个对象分配、释放内存的问题。 2).虽然可以操作,但是在新版本中,使用标准库容器,有很多优势。
- 不需要自己定义拷贝,赋值,析构
- 不用担心内存管理,简单高效
- 有更好的性能。
//1.new和数组
1).使用new ,创建一个动态的数组。
- 形式为
new int[]; - 注意虽然我们申请的是数组,但是
new 返回的并不是一个数组,而是一个指向数组首元素的指针。因此不可以使用begin 或者end 函数(因为begin 和end 是基于数组的维度实现的。),也自然地不可以使用范围for 循环。 - 与内置数组不一样,
[] 不要求是一个常量表达式
{
int *p = new int[get_size()];
}
{
typedef int arr[12];
int *p = new arr;
int *p = new int[12];
}
- 如果没有
() 将会执行默认初始化。如果有() 将会执行值初始化。**由于是数组,我们不可以在() 中有初始化器,**因此它们不可以使用auto 来通过编译器自动识别类型。
{
int *p = new int[12]();
string *p = new string[12];
string *p = new string[12]();
}
- 如果数量不足,剩下的元素进行值初始化。
- 如果数量超过。new表达式是错误的,不会分配内存,会抛出一个
bad_array_new_length 的异常。这个异常和bad_alloc 都定义在头文件new 中。
{
int *p = new int[12]{1,2,3,4};
string *p = new string[12]{"the","a",string(12,'1')};
}
2).动态分配一个大小为零的动态数组是合法的。
- 返回的是一个尾后指针。
- 但是不可以对这个指针进行解引用的操作。
- 可以对这个指针进行算术运算。(加上或者减去数,两个指针相减。)
{
size_t n = get_size();
int *p = new int[n]();
for (int *q = p;q != p+n;++q)
}
3).释放动态数组。
- 形式
delete []p; - 对于一个对象,加了
[] ;或者对于一个数组没有[] ,它们的行为都是没有定义的。但是编译器不会报错。 - 注意,编译器的释放顺序是,逆序,即最后一个元素先被释放,然后是到数第二个。
{
delete p;
delete []p;
}
4).使用智能指针管理动态数组。
- 对于,
unique_ptr ,改变了它的类型。unique_ptr<int[]> - 关于使用
release ;书上的例子是否有误。
{
unique_ptr<int[]> up(new int[12]);
}
unique_ptr<int[]> 类型改变,是因为它销毁内存空间时调用的是delete[]
5).unique_ptr 管理动态数组时支持的操作。
- 指向数组的
unique_ptr 不支持访问运算符号。.以及-> 。因为不是单个对象,是数组。 - 其他的操作一样的(包括之前介绍的)。
操作名称 | 相关描述 |
---|
unique_ptr<T[]> p | | unique_ptr<T[]> p(q) | q指向的是一个动态数组。类型为T | u[i] | 支持下标运算。u必须指向一个数组 |
6).使用shared_ptr 管理动态数组。
{
shared_ptr<int> p = (new int[12],[](int *p){delete []p};);
sp.reset();
}
shared_ptr 不支持下标运算,不支持算术运算。
{
for (size_t n = 0;n != 10;++n)
*(sp.get()+n) = n;
}
//2.allocator类
1).与new 和delete 的比较。
- 它与
new 以及delete 的不同在于,它将内存分配的对象创建分开。
- 避免了创建一些我们永远都不会用到的对象。
- 避免了在创建时进行初始化,我们需要使用时再一次赋值的消耗。
- 避免没有默认初始化的类不能使用动态内存。
- 但是注意
allocate 这样做,也要有一定的开销。
2).allocator支持的操作。
- 注意它也是一个类模板,定义在头文件
memory 。 - 我们只能对真正构造了对象的内存进行
destroy 。 - 试图使用没有构造对象内存的错误。
{
cout << *p << endl;
}
操作名称 | 相关描述 |
---|
allocator<T> a | 定义一个可以申请类型为T的内存空间的allocator 对象。 | a.allocate(n) | 为类型T的申请一个原始的没有构造的内存。可以保存n个对象。返回的是指向这段连续空间的首元素。 | a.deallocate(p,n) | 释放从p开始的n个内存空间。p必须是由allocate 的到的指针。n必须是p创建时的大小。在调用它之前必须先调用,destroy 清除创建的对象。 | a.construct(p,args) | p必须时一个T*类型的指针,指向一块原始的内存。 |
{
allocator<string> alloc;
auto p = alloc.allocate(n);
auto q = p;
alloc.construct(q++);
alloc.construct(q++,10,'c');
alloc.construct(q++,"hi");
--q;
while (q != p)
{
alloc.destroy(--q);
}
alloc.destroy(q);
alloc.deallocate(p,n);
}
3).拷贝以及填充未构造对象的算法。
算法 | 相关描述 |
---|
uninitialized_copy(b,e,d) | 输入范围拷贝到未构造的原始内存。d开始的内存应该足够大。 | uninitialized_copy(b,n,b2) | 输入范围未b开始的n个元素。输入范围必须是未构造的。因为它执行的是构造,不是拷贝,与copy不一样 ;并且返回的是指向下一个没有构造的元素的指针。这一点与copy 相似。 | uninitialized_fill(b,e,val) | b,e为未构造的内存空间。 | uninitialized_fill_n(b,n,val) | b开始的空间必须足够大。至少要有n个空间。 |
{
auto p = alloc.allocate(v.size()*2);
auto q = uninitialized_copy(v.begin(),v.end(),p);
uninitialized_fill_n(q,v.size(),42);
}
/3.使用标准库:文本查询程序
//1.文本查询程序设计
1).思路设计。
- 利用
vector<string> 存储文本中的每一行。 - 利用
istringstream 对输入的每一行进行分解。 - 利用
set 存储每一个单词出现的所有行号。 - 利用
map 将每一个单词和它的set 相互关联起来。 - 使用共享数据。
shared_ptr (设计两个类)
- 避免了数据的拷贝。
set ,和vector - 如果仅仅是通过迭代器或者指针,容易导致对象被销毁的非法访问。
2).编写使用这个类的程序,观察类是否具有我们预想的功能。
{
void runQuires(ifsstream &infile)
{
TextQuery tq(infile);
while (true)
{
cout << "enter the word to look for,or q to quit:";
string s;
if (!(cin >> s) || s == 'q') break;
print(cout,tq.query(s))<< endl;
}
}
}
//2.文本查询程序类的定义
1).编写TextQuery 类。
{
class QueryResult;
class TextQuery{
public:
using line_no = std::vector<string>::size_type;
TextQuery(std::ifstream&);
QueryResult query(const std::string&)const;
private:
std::shared_ptr<std::vector<std::string>> file;
std::map<string,std::shared_ptr<set<line_no>>> wm;
};
}
2).构造一个TextQuery 对象。(要求构造处file 和wm 数据成员。)
{
TextQuery::TextQuery(std::ifstream &is) : file(new vector<string>)
{
string text;
while (getline(is,tect))
{
file->push_back(text);
int n = file->size() - 1;
string word;
istringstream line(text);
while (line >> word)
{
auto &lines = wm[word];
if (!lines)
{
lines.reset(new set<ilne_no>);
lines->insert(n);
}
}
}
}
}
3).
{
class QueryResult{
friend std::ostream& print(std::ostream&,const string&);
public:
QueryResult(std::string s,
std::shared_ptr<std::vector<std::string>> f,
std::shared_ptr<std::set> p) :
sought(s),lines(p),file(f);
private:
std::string sought;
std::shared_ptr<std::set<line_no>> lines;
std::shared_ptr<std::vector<std::string>> file;
}
}
4).
{
QueryResult TextQuery::query(const string &sought) const
{
static shared_ptr<set<line_no>> nodata(new set<line_no>);
auto loc = wm.find(sought);
if (loc == wm.end())
return QueryResult(sought,nodata,file);
else
return QueryResult(sought,loc->second,file);
}
}
5).
{
ostream &print(ostream &os, const QueryResult &qr)
{
os << qr.sought << " occurs " << qr.lines->size() << " "
<< (qr.lines->size() > 1 ? "times" : "time") << endl;
for (auto num : *(qr.lines))
{
os << "\t(line " << num+1 << ") "
<< *((*(qr.file)).begin() + num) <<
endl;
}
return os;
}
}
|