c++ 第十二章:动态内存
1:对于一个c++程序,我们使用到的内存有栈内存
和静态内存
以及动态内存
,静态内存用来保存局部static,全局变量等对象,栈内存用来保存在函数中申请的非static对象,它们都是由编译器创建和销毁的。此外,我们在程序运行过程中,如果需要分配的动态内存,即c语言中的malloc,是从堆上分配的。但是释放也必须要我们手动释放,由于正确管理动态内存非常的棘手,c++为我们提供了管理动态内存的两个指针,shared_ptr和unique_ptr来让我们从正确释放动态内存这一件麻烦事中解放出来。
2:shared_ptr类
(1):基本操作
操作 | 含义 |
---|---|
shared_ptr sp | 空智能指针,指向一个T类型 |
if(sp) | 将sp作为条件,如果指向一个对象返回true |
*P | 解引用操作,得到指针指向的对象 |
p->mem | 等价于(*p).mem |
p.get() | 返回p中保存的指针,返回的是一个内置指针,不是智能指针 |
swap(p,q) | 交换p和q中的指针 |
make_shared(args) | 返回一个shared_ptr,指向一个动态分配的类型为T的对象,auto sp = make_shared() sp是一个shared_ptr 类型 |
shared_ptr p(q) | p是shared_ptr q 的拷贝,此操作会递增q中的计数器,q中的指针必须能转换成*T |
p = q | p和q都是shared_ptr,此操作会递减p的引用计数,递增q的引用计数,若p的引用计数变为0,则将其管理的原内存释放 |
p.use_count() | 返回与p共享对象的智能指针的数量 |
p.unique() | 如果p.use_count()为1,返回true,否则返回false |
(2):拷贝和赋值
每个shared_ptr都有一个关联的计数器,通常称之为引用计数,即表示的是现在的shared_ptr与多少个对象之间有联系,当引用计数变为0的时候,此shared_ptr指向的对象就会被释放。
增加引用计数的操作:拷贝shared_ptr,用a初始化b,a的引用计数增加。将shared_ptr作为参数传递或者作为函数的返回值。
减少引用计数的操作:给shared_ptr赋予新值或者shared_ptr被销毁。
#include<iostream>
#include<memory>
using std::cout;
using std::cin;
using std::endl;
int main(int argc,char *argv[])
{
auto sp = std::make_shared<int>(42);
auto q = std::make_shared<int>(50);
cout << *sp << endl;
//递增q指向的对象的引用计数,递减sp原来指向的对象的引用计数,此时sp引用计数为0,会被释放
sp = q;
return 0;
}
(3):shared_ptr自动销毁所管理的对象释放内存
shared_ptr自动释放相关联的内存是用析构函数完成销毁工作的。对于shared_ptr我们保证只要有任何shared_ptr对象引用它,它就不会被释放掉。因此处理无用的shared_ptr就变得至关重要,如果我们将shared_ptr存储在一个容器中,以后不再需要全部元素,而只使用其中的一部分,要记得用erase删除掉不需要的元素。
(4):使用动态内存的一个好处是允许多个对象共享相同的底层数据。也就是底层的数据只有一份,各个类之间共享。当引用计数变为0的时候,会被销毁。
看一个例子:
#include<iostream>
#include<memory>
#include<string>
#include<vector>
using std::cout;
using std::cin;
using std::endl;
class StrBlob {:
public:
typedef std::vector<std::string>::size_type size_type; //size_type是一个无符号整形数据
StrBlob();
StrBlob(std::initializer_list<std::string> il);
size_type size() const { return data->size(); }
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; //构造的是shared_ptr的data
void check(size_type i,const std::string &msg) const;
};
//StrBlob的两个构造函数
StrBlob::StrBlob() : data(std::make_shared<std::vector<std::string>>()) {} //通过make_shared来使用
StrBlob::StrBlob(std::initializer_list<std::string> il) : data(std::make_shared<std::vector<std::string>>(il)) {}
void StrBlob::check(size_type i,const std::string &msg) const
{
if(i > data->size()) {
throw std::out_of_range(msg);
}
}
//相当于自己重写front和back
std::string & StrBlob::front()
{
check(0,"front on empty StrBlob");
return data->front();
}
std::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();
}
int main(int argc,char *argv[])
{
StrBlob blob{"nihao","wo","jiao"};
blob.push_back("yangbodong");
cout << blob.back() << endl; //输出yangbodong
blob.pop_back();
cout << blob.back() << endl; //输出jiao
return 0;
}
3:new和delete
new:相当于c语言中的malloc,动态申请内存。
(1):申请空间
int *p1 = new int; //从动态内存中分配出来一块大小为int的空间,未命名。
int *p2 = new int(5); //p2指向的对象值为5
std::string *str = new std::string; //str指向的字符串为空。
std::string *str1 = new std::string("yangbodong"); //str1指向的内容为yangbodong
auto p1 = new auto(obj); //根据obj的类型推断出p1的类型
const int *pci = new const int(1024); //new返回的是一个指向const的指针。
(2):内存耗尽
正常情况下内存耗尽,new会失败,这时候它会抛出一个异常,类型为bad_alloc,但是我们可以用nothrow来阻止它抛出异常。我们称这种形式的new 为定位new。
int *p2 = new(nothrow) int; //如果分配失败,则new返回一个空指针。
delete:相当于c语言中的free,释放动态释放的内存。
int i,*pi1 = &i,*pi2 = nullptr;
double *pd = new double(33.33), *pd2 = pd;
delete(i); //delete传递的参数是指针
delete(pi1); //释放局部变量是未定义的
delete(pd); //正确,删除pd指向的空间
delete(pd2); //已经被删除,结果是未定义的
delete(pi2); //正确,删除一个没有指向的指针总是正确的
4:定义和改变shared_ptr的方法。
操作 | 含义 |
---|---|
shared_ptr p(u) | p管理内置指针q所指向的对象,q必须指向new分配的内存,且能转换成T*类型 |
shared_ptr p(u) | p从unique_ptr u那里接管了对象的所有权,将u置为空 |
shared_ptr p(q,d) | p从q接管了q所指向的对象的所有权,q必须能转换成T*类型,p调用d来代替delete |
shared_ptr p(p2,d) | p是shared_ptr p2的拷贝,p调用d来代替delete |
p.reset(),p.reset(q),p.reset(q,d) | 若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递了可选的参数内置指针q,会另p指向q,否则会将p置为空,若还传递了参数d,将会调用d而不是delete来释放q |
注意:
- 不要将智能指针和普通指针混合使用。
- 不要用get方法初始化另一个智能指针或者为另一个智能指针赋值。
5:shared_ptr的reset方法:我们可以用reset方法来将一个新的指针赋予一个shared_ptr
//通常我们会与unique来一起使用,先检查它原来是不是唯一用户,要是不是,我们调用reset将内容拷贝一份
if(!p.unique()) {
p.reset(new std::string(*p));
}
*p += newVal;
5:unique_ptr
操作 | 含义 |
---|---|
unique_ptr u1,unique_ptr u2 | 空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针,u2使用类型为D的可调用对象释放它的指针 |
u=nullptr | 释放u指向的对象,将u置为空 |
u.release() | u放弃对指针的控制权,返回指针,并且将u置为空 |
u.reset() u.reset(q) u.reset(nullptr) | 释放u指向的对象,如果提供了q,则将u指向q,否则置为空 |
注意:
- unique_ptr不支持拷贝和赋值操作。但是例外是可以拷贝或赋值一个将要被销毁的unique_ptr。比如从一个函数return一个unique_ptr。
- 可以向unique_ptr传递一个删除器。
6:weak_ptr
它是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。但是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。并且一旦最后一个指向对象的shared_ptr被释放,即使有weak_ptr指向对象。对象还是会被释放。
操作 | 含义 |
---|---|
weak_ptr w | 空的weak_ptr可以指向类型为T的对象 |
weak_ptr w(sp) | 与shared_ptr指向相同对象的weak_ptr。 |
w = p | p是一个shared_ptr或者weak_ptr,赋值后共享 |
w.reset() | 将w置为空 |
w.use_count() | 与w共享对象的shared_ptr的数量 |
w.expired() | 如果w.use_count()为0,返回true |
w.lock() | 如果expired为true,返回一个空的shared_ptr,否则返回一个指向w的对象的shared_ptr |
#include<iostream>
#include<memory>
using std::cout;
using std::cin;
using std::endl;
int main(int argc,char *argv[])
{
auto p = std::make_shared<int>(42);
cout << p.use_count() << endl; // 1
std::weak_ptr<int> wp(p);
cout << p.use_count() << endl; // 1
std::shared_ptr<int> wp1(p);
cout << p.use_count() << endl; // 2
//由于对象可能不存在,因此我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否存在,如果存在,lock返回一个指向共享对象的shared_ptr。
if(std::shared_ptr<int> np = wp.lock()) {
//此时,np和wp共享同一个shared_ptr
}
return 0;
}
7:new和数组
我们可以通过调用new申请一个数组的大小。new返回的是一个指向第一个元素的指针。
//初始化
int *pia = new int[10]; //10个未初始化的int
int *pia2 = new int[10](); //10个为0的int
std::string *psa = new std::string[10]; //10个空string
std::string *psa2 = new std::string[10](); //10个空string
std::string *psa3 = new std::string[10]("a","aa","aaa"); //前三个用初始化器初始化,后面的用值初始化。
注意:我们可以int *p = new int[0]
,返回的是合法的非空指针,对于零长度来说,此指针就像尾后指针,但是不可以用它来解引用,毕竟它不指向任何元素。
//释放动态数组
delete p; //p必须指向一个动态分配的对象或者为空
delete[] p;//p必须指向一个动态分配的数组或者为空
注意!!!!
要是对象,就用p,要是数组,一定要用[]p。
智能指针和动态数组
标准库中提供了一个可以管理new分配的数组的unique_ptr版本。为了用unique_ptr,我们必须在对象类型后面跟一对花括号。
std::unique_ptr<int[]> up(new int[10]); //unique_ptr指向的是一个拥有十个int元素的数组。
up.release(); //自动删除delete[]
注意:
- 当一个unique_ptr指向一个数组的时候,我们不能使用点和箭头来访问成员。
- shared_ptr不支持直接管理动态数组,要是非要使用shared_ptr管理,我们必须提供删除器。由于shared_ptr未定义下标运算符,因此我们需要用get获取一个内置指针,然后用它访问数组元素。
8:allocator类
1:
操作 | 含义 |
---|---|
allocator a | 定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存 |
a.allocate(n) | 分配一段原始的、未构造的内存,保存n个类型为T的对象 |
a.deallocate(p,n) | 释放从p中地址开始的内存,这块内存保存了n个类型为T的对象,p是一个由allocate返回的指针,且n必须是p创建时的大小. |
a.construct(p,args) | p必须是T*,指向一块原始内存,arg被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象 |
a.destroy(p) | 对p指向的对象调用析构函数 |
注意:
- 为了使用allocate返回的内存,我们必须用construct来构造对象,不能使用未构造的内存。
- 当我们用完对象,必须对每个构造的元素调用destroy来销毁它们。并且我们只能对真正构造了的元素进行destroy操作。
- 将我们使用完的内存归还给系统,释放内存通过deallocate完成。
2:拷贝和填充未初始化内存的算法
操作 | 含义 |
---|---|
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个对象 |
#include<iostream>
#include<memory>
#include<vector>
using std::cout;
using std::cin;
using std::endl;
int main(int argc,char *argv[])
{
std::allocator<int> alloc; //定义alloc
std::vector<int> vec{1,2,3,4,5};
//给p分配vec大小的2倍
auto p = alloc.allocate(vec.size()*2);
//将vec拷贝到q的前一半
auto q = std::uninitialized_copy(vec.begin(),vec.end(),p);
//后面的一半用42填
std::uninitialized_fill_n(q,vec.size(),42);
return 0;
}