全局对象在启动时分配,结束时销毁
局部自动对象在进入时创建,离开块时销毁
static对象在第一次使用前分配,程序结束时销毁
除了自动和static对象外,C++支持动态内存分配。动态分配的对象生存期与创建的位置无关,只有显式地被释放,才会销毁
为了安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配地对象。当一个对象应该被释放时,指向它地智能指针可以确保自动地释放它。
静态内存保存局部static对象、类static数据成员以及定义在任何函数之外地变量。
栈内存用来保存定义在函数内的非static对象。
静态内存或栈内存中的对象由编译器自动创建或销毁。
除了静态内存和栈内存,每个程序还有一个内存池,称为自由空间或堆。程序用堆来存储动态分配的对象,即在程序运行时分配的对象。
动态对象的生存期有程序控制,如果不再使用必须显式销毁。
12.1 动态内存与智能指针
动态内存通过运算符:new。在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化
delete接受一个动态对象的指针,销毁该对象,并释放内存。
如果忘记释放就会内存泄露,如果尚有指针引用就释放会产生引用非法内存的指针。
智能指针shared_ptr允许多个指针指向同一个对象
unique_ptr独占所指向的空间。
weak_ptr伴随类,是弱引用,指向share_ptr所管理的对象。
都定义在memory头文件中。
12.1.1 shared_ptr
智能指针也是模板,创建时必须提供指针可以指向的类型。
share_ptr<string> p1;
share_ptr<list<int>> p2;
默认初始化保证着空指针。使用与指针类似,解引用返回它指向的对象。如果在条件判断中使用智能指针,就检测是否为空
//如果p1不为空,检查它是否指向一个空string
if (p1 & p1->empty())
*p1 = "hi";
更多操作见表12.1
make_shared函数
最安全的分配和使用动态内存的方法是调用make_shared函数
在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
要用make_shared先指定要创建的对象的类型。
shared_ptr<int> p3 = make_shared<int>(42);
//指向"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10, '9');
//指向一个值初始化的int,0
shared_ptr<int> p5 = make_shared<int>();
可以用auto来保存make_shared的结果
auto p6 = make_shared<vector<string>>();
shared_ptr的拷贝和赋值
当拷贝或赋值时,每个shared_ptr都会记录有多少个其它shared_ptr指向相同的对象
可以认为每个shared_ptr都有一个关联的计数器,称为引用计数
当拷贝时计数器递增,销毁或赋值时递减
一旦计数器变为0,就自动释放自己所管理的对象
auto r = make_shared<int>(42); //r指向的int只有一个引用
r = q; //给r赋值,原来的对象没有引用者,自动销毁
shared_ptr 自动销毁所管理的对象
通过析构函数销毁的。每个类都有析构函数,控制此类型的对象销毁时做什么操作
shared_ptr的析构函数会递减它所指向的对象的引用计数,如果变为0则析构函数销毁对象,并释放内存
使用动态生存期的资源的类
程序使用动态内存出于一下三种原因之一:
1.程序不知道自己需要使用多少对象(容器)
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据
容器类都出于第一个原因使用动态内存,分配的资源都与对应对象的生存期一致,拷贝一个vector时,vector和副本中的元素是相互分离的
vectot<string> v1;
{
vector<string> v2 = {"a", "an"};
v1 = v2;
}
//v2被销毁
//v1有三个元素,是原来的拷贝
某些类分配的资源具有与原对象相独立的生存期.
某个类Blob,b2,b2被销毁后,b2中的元素不被销毁,b1指向最初b2创建的元素
定义StrBlob类
实现一个新的集合类型的最简单方法是使用某个标准库容器来管理元素,借助标准库类型来管理元素所使用的内存空间,这里用vector
但不能在Blob对象内直接保存vector,因为一个对象的成员在对象销毁时也会被销毁.因此保存在动态内存中,每个设置一个shared_ptr来管理vector.
class StrBlob{
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
size_type size() const { return data->size(); };
bool empty() const { retrun data->empty(); };
//添加和删除元素
void push_back(const std::string &t){ data->push_back(t);};
void pop_back();
//元素访问
std::string& front();
std::string& back();
private:
std::shaed_ptr<std::vector<std::string>> data;
//如果data不合法,抛出一个异常
void check(size_type i, const std::string &msg) const;
};
构造函数
初始化data成员
StrBlob::StrBlob() : data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(initializer_list<string> il) :
data(make_shared<vector<string>>(il)) { }
访问成员函数
void StrBlob::check(size_type i, const string &msg) const
{
if (i >= data->size())
throw out_of_range(msg);
}
访问成员首先check,如果成功继续利用底层vector
string& StrBlob::front()
{
check(0, "front on empty StrBlob");
retrun data->front();
}
string& StrBlob::back()
{
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back()
{
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
StrBlob使用默认版本的拷贝复制和销毁成员函数来对此类型的对象进行操作。只有一个数据成员share_ptr类型的,当最后一个指向它的StrBlob对象被销毁时,它会随之被自动销毁。
12.1.2直接内存管理
new分配内存
delete删除内存
使用new动态分配和初始化对象
在自由空间分配的内存是无名的,new无法为对象命名,而返回的是一个指针:
int *pi = new int;
默认分配的对象是默认初始化的,不过也可以使用一些初始化:
int *pi = new int(1024);
string *ps = new string(10, '9');
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8};
也可以对默认分配的对象进行值初始化,用空括号:
int *pi = new int; //默认初始化;*pi的值未定义
int *pi = new int(); //值初始化为0
对于定义了构造函数的类类型(如string),值初始化没有意义,不管什么形式都会通过默认构造函数来初始化
对于内置类型,默认初始化是未定义的,值初始化有定义好的值
对动态分配对象进行值初始化通常是好的
当括号中仅有单一初始化器时可以使用auto:
auto p1 = new auto(obj); //可以,p1的类型是一个指针,指向从obj推断出的类型
auto p2 = new auto{a,b,c}; //错误
动态分配和const对象
用 new分配const对象是合法的
const int *pci = new const int(1024); //分配并初始化一个const int
const string *pcs = new const string; //空string
一个动态分配的const对象必须初始化,对于有默认构造函数的类类型,可以用隐式初始化.
new返回的指针是一个指向const的指针
内存耗尽
用光了所有可用内存,new就会失败,抛出bad_alloc的异常
可以改变使用new的方式来阻止它抛出异常
int *p1 = new int; //如果失败抛出异常
int *p2 = new (nothrow) int; //失败new返回空指针
这种称为定位new,运行向new传递额外的参数.bad_alloc和nothrow都定义在头文件new中
释放动态内存
防止内存耗尽,在动态内存使用完后,必须delete来释放
delete接受一个指针
delete p;
指针值和delete
delete的指针必须是指向动态分配的内存或者是一个空指针。
释放一个并非new分配的内存,或这将相同的指针值释放多次,都是未定义的。
int i, *pi1 = &i, &pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; //错误,i不是指针
delete pi1; //未定义,pi1指向的是局部变量
delete pd; //正确
delete pd2; //未定义pd2指向的内存以及被释放
delete pi2 //正确
const对象的值不能被改变,但本身也是可以销毁的
const int *pci = new const int(1024);
delete pci;
动态对象的生存期直到被释放为止
对于一个由内置指针类型来管理的动态对象,直到被显示释放之前它都是存在的。
返回指向动态内存的指针的函数必须要记得释放:
Foo* factory(T arg)
{
return new Foo(arg);
}
void use_factory(T arg)
{
Foo *p = factory(arg)''
}
p离开了作用域,但是指针指向的内存没有被释放,销毁的是局部变量p,是一个指针,而不是智能指针
一旦函数结束,程序就没办法再释放这个内存了。要么delete,要么再return
使用new和delete的三个问题
1.忘记释放,造成内存泄露
2.使用以及释放的对象
3.同一个内存释放两次,会破坏自由空间
delete之后重置指针值
delete之后指针值就变为无效,但很多机器上仍然保存这动态内存的地址,delete以后指针变成了空悬指针.
未初始化指针的所有确定空悬指针也有,避免方法是在指针要离开作用域之前释放掉它所关联的内存,或者释放之后将nullptr赋予指针
这只是提供了有限保护
可能有多个指针指向相同的内存,delete之后重置指针的方法只对这个指针有效,其它指针会变无效。
12.1.3 shared_ptr和new结合使用
如果不初始化智能指针它就会初始化为一个空指针,我们还可以用new返回的指针来初始化智能指针:
shared_ptr<int> p2(new int(42)); //p2指向一个值为42的int
接受指针参数的智能指针构造函数是explicit的,因此不能将一个内置指针隐式转换,必须使用直接初始化
shared_ptr<int> p1 = new int(42); //错误
shared_ptr<int> p2(new int(42)); //使用了直接初始化形式
同理返回一个shared_ptr的函数不能在返回语句中隐式转换一个普通指针
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,指针指针会默认使用delete.
也可以绑定的一个指向其它类型的资源的指针,但必须提供自己的操作来代替delete.
不要混合使用普通指针和智能指针
shared_ptr可以协调对象的析构,但只局限于自身的拷贝之间
void process(shared_ptr<int> ptr)
{
//使用ptr
}
ptr离开作用域,被销毁
使用此函数的正确方法是传递给它一个shared_ptr
shared_ptr<int> p(new int(42));
process(p);
int i = *p; //正确
如果混用会错误
int *x(new int(1024));
process(x); //错误,不能将int*转换为shared_ptr<int>
process(shared_ptr<int>(x)); //合法,但内存会被释放
int j = *x; //x是一个悬空指针
调用结束后计数变为0,指针被销毁,指向的内存被释放
也不要用get初始化另一个智能指针或为智能指针赋值
指针智能定义了get函数,返回一个内置指针,指向智能指针管理的对象。
使用情况:向不能使用智能指针的代码传递指针,这个指针不能delete
虽然编译器不报错,但也不能把智能指针绑定到get返回的指针
其它shared_ptr操作
1.可以用reset来将一个新指针赋予一个shared_ptr:
p = new int(1024);
p.reset(new int(1024));
reset会更新计数
2.unique检查是否是当前对象仅有的用户,与reset配合
if(!p.unique())
p.reset(new string(*p)); //如果不是唯一就分配新的拷贝
*p += newVal; //现在唯一,可以改变对象的值
12.1.4 智能指针和异常
如果一个类没有析构函数,退出前忘记调用某个显示关闭的函数,也可以用shared_ptr类似的来管理
定义一个函数来代替delete,创建shared_ptr是传递一个删除器函数的参数
void end_connection(connection *p) { disconnect(*p); )
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
当P被销毁,自动调用end_connection
正确使用智能指针的规范
1.不使用相同的内置指针值初始化(或reset)多个智能指针
2.不delete get()返回的指针
3.不适用get初始化或reset令一个智能指针
4.如果使用get返回的指针,记住当最后一个对应的智能指针被销毁后,它就无效了
5.如果使用的智能指针管理的资源不是new分配的资源,传递给它一个删除器
12.1.5 unique_ptr
unique_ptr某个时刻只能有一个unique_ptr执行的对象。
当unique_ptr被销毁,对象也被销毁。
unique_ptr定义时需要绑定到一个new返回的指针
unique_ptr<double> p1;
unique_ptr<int> p2(new int(42)); //p2指向42
unique_ptr不支持普通拷贝或赋值,但可以通过release或reset将指针的所有权从一个unique_ptr转到另一个unique_ptr
unique_ptr<string> p2(p1.release()); //p1转移p2
p2.reset(p3.release()); //p3转移p2,p2释放原来的内存
reset()释放
release会切断联系,必须用另一个指针转移
传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr:
unique_ptr<int> clone(int p)
{
return unique_ptr<int>(new int(p));
}
//也可以返回一个局部对象的拷贝
unique_ptr<int> clone(int p)
{
unique_ptr<int> ret(new int(p));
return ret;
}
向unique_ptr传递删除器
unique_ptr也可重载默认的删除器,但管理方式不同,会影响unique_ptr类型以及如何构造该类型的对象
// p指向一个类型为objT的对象,并用一个类型为delT的对象释放objT
unique_ptr<objT, delT> p (new objT, fcn);
例:
void f(destination &d)
{
connection c = connect(&d);
unique_ptr<connection, decltype(end_connection)*>
p(&c, end_connection);
}
12.1.6 weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,指向一个shared_ptr管理的对象。不会更改引用计数,若shared_ptr被销毁,对象就释放。是弱共享对象
要创建weak_ptr,需要用shared_ptr来初始化:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);
由于对象可能不存在,不能之间调用weak_ptr,需要用lock
如果存在返回shared_ptr
if(shared_ptr<int> np = wp.lock()); //如果np不为空则成立
核查指针类
为StrBlob类定义一个伴随指针类。
保存一个weak_ptr,指向StrBlob的data成员
通过使用weak_ptr,不会影响一个给定的StrBlob所指向的vector的生存期,但可以阻止用户访问一个不再存在的vector的企图
class StrBlobPtr {
public:
StrBlobPtr(): curr(0) { }
StrBlobPtr(StrBlob &a, size_t sz = 0):
wptr(a.data), curr(sz) { }
std::string& deref() const;
StrBlobPtr& incr();
private:
std::shared_ptr<std::vector<std::string>>
check(std::size_t, const std::string&) const;
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr;
}
std::shared_ptr<std::vector<std::string>>
check(std::size_t, const std::string& msg) const
{
auto ret = wptr.lock();
if (!ret)
throw std::runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw std::out_of_range(msg);
return ret;
}
deref成员调用check,检查使用vector是否安全以及curr是否在合法范围内
std::string& StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
如果check成功,p就是一个shared_ptr,指向一个vector,用下标运算符提取并返回curr位置的元素
StrBlobPtr& StrBlobPtr::incr()
{
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
当然,为了访问data成员,我们的指针必须声明为StrBlob的friend,还要为StrBlob类定义begin和end操作,返回一个指向它自身的StrBlobPtr:
class StrBlobPtr;
class StrBlob
{
friend class StrBlobPtr;
StrBlobPtr begin() { return StrBlobPtr(*this); }
StrBlobPtr end()
{ auto ret = StrBlobPtr(*this, data->size()); return ret; }
};
12.2 动态数组
一次分配一个对象数据的方法
1.new可以分配并初始化一个对象数组
2.allocator类允许我们分配和初始化分离
当一个应用需要可变数量的对象时,使用vector总是最好的.
使用容器的类可以使用默认版本的拷贝、赋值和析构,分配动态数组的类则必须定义自己版本的操作,在拷贝赋值和销毁对象时管理所管理内存
12.2.1 new和数组
int *pia = new int[get_size()]; //调用get_size确定分配多少个int
或用类型别名
typedef int arrT[42];
int *p = new arrT; //42个int的数组
分配一个数组会得到一个元素类型的指针
用new分配数组时,并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。分配的内存不是一个数组类型,因此不能调用begin或end,也不能用for
初始化动态分配对象的数组
new分配的对象都是默认初始化,可以对数组的元素用空括号进行值初始化
int *pia = new int[10]; //10个为初始化的int
int *pia2 = new int[10](); //10个初始化为0的int
可以用元素初始化器
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
如果初始化器的数目小于元素数目,则剩余的进行值初始化。
如果数目大于元素数目,则new失败,不分配内存.
用空括号进行值初始化,但不能在括号中给出初始化器,所以不能用auto分配数组。
动态分配一个空数组是合法的
可以用任意表达式来确定要分配的对象和数目:
size_t n = get_size(); //数目
int* p = new int[n];
for(int *q = p; p != p + n; ++q)
{
//处理数组
}
如果n为0,不能创建一个大小为0的静态数组对象,但调用new是合法的,但不能解引用,返回一个车合法的非空指针。
char arr[0]; //错误
char *cp = new char[0]; //正确
释放动态数组
用delete,在指针前加一个方括号
delete [] pa;
数组中的元素按逆序销毁,如果忽略方括号,行为是未定义的
智能指针和动态数组
unique_ptr管理动态数组必须在对象类型后面加括号
unique_ptr<int[]> up(new int[10]);
up.release(); //自动用delete[]销毁其指针
当unique_ptr指向一个数组时,不能使用点和箭头运算符,用下标运算符
for (size_t i = 0; i != 10; ++i)
up[i] = i;
shared_ptr不支持管理动态数组,如果要用必须提供自己定义的删除器
shared_ptr<int> sp(new int[10], [](int *p) {delete [] p;});
sp.reset(); //使用定义的lambda释放数组
shared_ptr也不能之间访问数组
for (size_t i = 0; i != 10; ++i)
*(sp.get() + i ) = i;
12.2.2 allocatro类
一般情况下将内存分配和对象构造组合在一起可能会导致不必要的浪费
string *const p = new string[n];
string s;
string *q = p; //*q指向第一个p
while (cin >> s && q != p + n)
*q++ = s;
const size_t size = q - p;
delete[] p;
new表达式分配并初始化了n个string,但可能用不到,创建了一些永远用不到的对象。
而且对于用的对象,也是在初始化之后立即赋予了它们新值。
每个元素都被赋值了两次,第一次是默认初始化,第二次是赋值
没有默认构造函数的类不能动态分配数组
allocator类
定义在memort中,帮助我们将内存分配和对象构造分离开。
提供一种类型感知的内存分配方法,它分配的内存是原始的,未构造的。
allocator也是模板,要给定类型
allocator<string> alloc;
auto const p = alloc.allocate(n); //分配n个未初始化的string
allocator分配未构造的内存
需要在此内存中构造对象,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素
额外参数用来初始化构造的对象。必须是与构造的对象的类型相匹配的合法的初始化器:
auto q = p; //q指向最后构造的元素之后的位置
alloc.construct(q++); //*q为空字符串
alloc.construct(q++, 10, 'c'); //*q为cccccccccc
alloc.construct(q++, "hi"); //*q为hi!
还未构造对象的情况下不能使用原始内存
使用完后需要对每个构造元素调用destroy来销毁它们。
destroy接受一个指针,对指向的对象进行析构
while (q != p)
alloc.destroy(--q);
一旦元素被销毁,就可以重新使用这部分的内存来保存其它string,也可以归还给系统,用deallocate
alloc.deallocate(p, n);
拷贝和填充未初始化内存的算法
allocator类定义了两个伴随算法
uninitialized_copy(b, e, b2) //从迭代器b和e范围中拷贝到迭代器b2指定的未构造的原始内存中
uninitialized_copy_n(b, n, b2) //从迭代器b指向的元素开始,拷贝n个到b2
uninitialized_fill(b, e, t) //在b和e指定的原始内存范围中创建对象,值为t的拷贝
uninitialized_fill_n(b, n, t) //从b地址创建n个对象t
例:一个int的vector,分配一块比vector元素所占空间大一倍的动态内存,将其拷贝到前一半,后一半用给定值填充
auto p = alloc.allocate(vi.size() *2 );
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
uninitialized_fill_n(q, vi.size(), 42);
12.3 使用标准库:文本查询程序
实现一个简单的文本查询程序作为总结
在一个给定文件中查询单词,结果是单词在文件中出现的次数及所在行的列表。
12.3.1 文本查询程序设计
需求
逐行读取输入文件,并将每一行分解为独立的单词
生成输出时,能提前每个单词所关联的行号,行号必须按升序出现且无重复,必须能打印给定行号的内容
如何实现:
1.用vector
来保存整个输入文件的一份拷贝,一行一个元素,行号作为下标
2.使用一个istringstream来分解每行的单词
3.使用set来保存每个单词在输入文本出现的行号,保证每行只出现一次并按升序排列
4.用map来把单词和出现的行号set关联起来,方便提取任意单词的set
数据结构
定义一个保存输入文件的类TextQuery,包含一个vecto保存文本r和一个map关联单词和行号
这个类会有一个用来读取给定输入文件的构造函数和一个执行查询的操作
查询操作:查找map成员,检查给定的单词是否出现
返回这些结果可以定义一个类QueryResult,来保存查询结果.有一个print函数来打印。
在类直接数据共享
用shared_ptr来管理queryResult和TextQuery数据的关系
使用TextQuery类
void runQueries(ifstream &infile)
{
TextQuert tq(infile); //保存文件并建立查询map
while(true){
cout << "enter word to look for, or q to quit: ";
string s;
if(!(cin >> s) || s == "q") break; //若到文件尾或q时终止
print(cout, tq.quert(s)) << endl;
}
}
12.3.2 文本查询程序类的定义
class QueryResult;
class TextQuery {
public:
using line_no = std::vector<std::string>::size_type;
TextQuery(std::ifstream&);
QueryResult query(const std::string&) const;
private:
std::shared_ptr<std::vector<std::string>> file;
//每个单词到它所在行号的集合的映射
std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
}
构造函数
TextQuery::TextQuery(ifstream &is): file(new vector<string>)
{
string text;
while (getline(is, text))
{
file->push_back(text);
int n = file->size() - 1;
istringstream line(text);
string word;
while (line >> word)
{
auto &lines = wm[word];
if (!lines)
lines.reset(new set<line_no>);
lines->insert(n);
}
}
}
QueryResult
class QueryResult{
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
QueryResult(std::strings, std::shared_ptr<std::set<line_no>> p,
std::shared_ptr<std::vector<std::string>> f) :
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;
}
query函数
TextQuert::quert(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 QuertResult(sought, nodata, file);
else
return QuertResult(sought, loc->second, file);
}
打印结果
ostream &print(ostream &os, const QueryResult &qr)
{
os << qr.sought << " occurs " << qr.lines->size() << " "
<< make_plural(qr.lines->size(), "time", "s") << endl;
for (auto num : *qr.lines)
os << " (line" << num + 1 << ")"
<< *(qr.file->begin() + num) << endl;
return os;
}