动态内存和智能指针
静态内存:局部static对象,类static数据成员,定义在任何函数之外的变量
栈内存:保存定义在函数内的非static对象
堆(自由空间):动态分配的对象
头文件:memory
智能指针,自动的释放所指向的对象
shared_ptr:允许多个指针指向同一个对象
unique_ptr:独占所指向的对象
weak_ptr:一种弱引用,指向shared_ptr所管理的对象
shared_ptr类
shared_ptr<string> p1;
shared_ptr<vector<int>> p2;
使用前进行条件判断,保证p1不是指针,p1指向了一个空string
shared_ptr<string> p1; if(p1 && p1 -> empty()) { *p1 = "hello"; }
shared_ptr和unique_ptr都支持的操作
p -> mem; //获取内部成员 p.get(); //返回p中保存的指针,若智能指针释放了其对象,返回的指针所指向的对象也就消失了 swap(p,q); p.swap(q) //交换p,q
shared_ptr独有的操作
make_shared<T>(args); //返回一个shared_ptr,指向一个动态分配的类型为T的对象,用args初始化它 shared_ptr<T>p(q); //p是shared_ptr q的拷贝,会增加q中的计数器 p = q; //保证指针能够相互转换,此操作会递减p的引用计数,递增q的引用计数;p的引用计数为0会被删除 p.unique(); //p.use_count() == 1 p.use_count(); //返回与p共享对象的智能指针数量,很慢,用于调试
通常使用make_shared函数分配使用,若没传递任何参数,会默认进行值初始化
拷贝和赋值的过程本质上都是计数器调整的
shared_prt会调用析构函数来销毁对象
可以使用函数返回shared_ptr类型,return语句会向此函数的调用者返回一个拷贝,此时原对象随着函数的销毁而被销毁,但此时引用计数值并不为零,所以对应内存还会保留。
shared_ptr<int> shaptr_test(int x) { auto t = make_shared<int>(x); return t; } int main() { shared_ptr<int> p1 = shaptr_test(5); cout << *p1 << endl; }
所以,注意要保证在无用之后没有保留,否则会浪费内存。尤其容易忽略的是,把shared_ptr保存在vector中,对vector重排后,忘记erase掉无用元素。
使用动态内存的原因
(1)程序不知道自己需要使用多少对象:vector
(2)程序不知道所需对象的准确类型
(3)程序需要在多个对象间共享内存
在类中定义share_ptr成员,实现多个对象间的共享内存
直接管理内存
运算符new, delete
int *i = new int(1024); int *s = new string(5, '9'); vector<int> *v = new vector<int>{0,1,2}; int *i2 = new int(); //值初始化,*i2为0 int *i3 = new int; //默认初始化,*i3的值未定义
对于定义了自己的构造函数的类,值初始化都会通过默认的构造函数来初始化;对于内置类型才会有区别。
使用auto自动判断
auto p1 = new auto(obj); //p1指向一个与obj类型相同的对象,使用obj的值进行初始化
用new分配const对象
const int *p = new const int(10);
虽然不能改变const动态对象,但可以用delete来销毁
对于定义了默认构造函数的类类型,其const对象可以隐式初始化,其它则必须显式初始化
内存耗尽时,new会抛出std::bad_alloc异常
使用定位new传递额外参数nothrow,当分配失败时,返回一个空指针
int *p = new (nothow) int;
delete之后最好重置指针
在delete之后,很多机器上指针仍然保存着(已经释放了的)动态内存的地址,此时指针变成了空悬指针。所以,在指针要离开作用域前要释放它关联的内存,如果要保留指针,可以在delete之后重置为nullptr
只是有限的保护,若有多个指针指向同一地址则很难检测到。
new与shared_ptr结合使用
可以用new初始化shared_ptr
shared_ptr<int> p(new int(35));
接受指针参数的智能指针构造函数是explicit的,故不能讲一个内置指针隐式转换为智能指针,必须通过显式的绑定
定义和改变shared_ptr的方法
tr《T
shared_ptr<T> p(q); //p管理内置指针q所指向的对象,q必须指向new分配的内存,且能够转换为T*类型 shared_ptr<T> p(u); //p从unique_ptr u处接管对象的所有权,u被置空 shared_ptr<T> p(q, d); //p接管了q所指向对象的所有权,使用可调用对象d代替delete shared_ptr<T> p(p2,d); //p是shared_ptr p2的拷贝,区别是p将用可调用对象d来代替delete p.reset(); p.reset(q); p.reset(q, d); //若p是唯一指向对象的shared_ptr,reset会释放,或另p指向q,参数d用于提供delete操作
普通指针和智能指针混合使用易产生的错误
void process(shared_ptr<int> ptr) { /*……*/ } /*……*/ int *p(new int(100)); process(shared_ptr<int>(p)); int j = *p;
将临时的shared_ptr传入到process后,当调用结束时,临时对象将被销毁,引用次数变为0,p变成空悬指针,此时使用p的行为是未定义的。
当将一个shared_ptr绑定到一个普通指针时,相当于管理责任的移交,最好不要再使用内置指针。
同样的,shared_ptr定义了一个get函数,返回一个内置指针,应用场景是:向不能使用智能指针的代码传递一个内置指针,这里需要注意不要被delete。特别是,永远不要用get初始化另一个智能指针。
在改变底层对象之前,检查自己是否是当前对象仅有的用户
if(!p.unique()) q.reset(new int(*p)); *p += val;
智能指针和异常
使用智能指针,即使程序块由于异常而过早结束,也能确保在内存不再需要时将其释放。
内置指针则做不到。
对于那些没有定义析构函数的类,通常要求用户显式地释放所使用的任何资源,这时如果使用shared_ptr来避免内存泄露会默认的使用delete。但对于有些资源并不适用,比如网络连接,这时可以采用自定义的函数来代替delete。
void end_connection(connection *p) { disconnect(*p); } /* ……*/ void f(destination &d) { connection c = connect(&d); shared_ptr<connection> p (&c, end_connection); /* ……*/ }
此时,即便是由于异常而退出,也可以保证connection会被正确光比
unique_ptr
定义
只能将其绑定到一个new返回的指针上
unique_ptr<int> p1; unique_ptr<int> p2(new int(10));
unique_ptr的对象是独有的,所以不支持普通的拷贝或赋值。
//使用类型为D的可调用对象d代替delete unique_ptr<T> u1; unique_ptr<T, D> u2; unique_ptr<T, D> u(d); u = nullptr; //释放并置空 u.release(); //放弃控制权,返回当前保存的指针,置空 //释放原对象,将u指向q的对象 u.reset(); u.reset(q); u.reset(nullptr);
仅使用release不会释放内存,而且会丢失原指针,如:p.release()
传递和返回unique_ptr
不能拷贝unique_ptr的规则有一个例外:可以拷贝或赋值一个将要被销毁的unique_ptr。
典型的例子是从函数返回一个unique_ptr
auto_ptr
auto_ptr有拷贝语义,拷贝后源对象变得无效;unique_ptr则无拷贝语义,但提供了移动语义
auto_ptr不可作为容器元素,unique_ptr可以作为容器元素
auto_ptr不可指向动态数组(尽管不会报错,但不会表现出正确行为),unique_ptr可以指向动态数组
weak_ptr
不控制所指向对象生存期的智能指针,指向一个由shared_ptr管理的对象,不改变shared_ptr的引用计数,一旦shared_ptr被销毁,weak_ptr没有任何阻碍的效果。
使用方法
weak_ptr<T> w; weak_ptr<T> w(sp); //与shared_ptr sp指向相同对象 w = p; w.reset(); //w置空 w.use_count(); w.expired(); //w.use_count() == 0 w.lock(); //若expired为true,返回空shared_ptr,否则返回一个指向w对象的shared_ptr
访问前注意判定
if(shared_ptr<int> sp = w.lock()) { /*……*/ }
动态数组
一次为很多对象分配内存
new表达式,allocator类
更多的情况还是使用容器
new和数组
int *p = new int[get_size()]; //p指向第一个int
方括号中的大小必须是整型,但不必是常量
由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end
关于初始化
int *pi = new int[10]; //10个未初始化 int *pi = new int[10](); //10个值初始化
还可以使用列表初始化,不能用auto分配数组
有趣的是,可以动态分配一个空数组,使用时除了不能解引用可以进行其它操作,类似于一个尾后迭代器
char arr[0]; //错误 char *cp = new char[0]; //正确,但不能解引用
释放数组
delete [] arr;
即使是使用别名来定义一个数组,释放时也必须加上[],但是在vs下测试可以不加上[]
typedef int arr[10]; //arr为10个int的数组的类型别名 int *p = new arr(); delete []p; //方括号理论上必须
顺序是按照驻足中元素的逆序
智能指针和动态数组
可以管理new分配的数组的unique_ptr版本
unique_ptr<int[]> up_arr(new int[10]); up_arr.release(); //自动用delete[]销毁指针
可以使用下标直接访问
如果希望使用shared_ptr管理一个动态数组,必须提供一个删除器
shared_ptr<int[]> up_arr(new int[10](), [](int *p) {delete[] p;}); up_arr.reset();
传递给一个lambda作为删除器。若未提供删除器,默认情况shared_ptr使用delete来销毁对象,这与释放动态数组时没有加[]产生的问题一样。
shared_ptr未定义下标运算符,且不支持指针的算术运算,所以访问只能通过get之后再加减(vs下不支持)
allocator类
将内存分配和构造对象分离,避免浪费。
头文件memory
是一个模板,分配的内存是原始的、未构造的。
基本操作
allocator<T> a; a.allocate(n); //分配内存,保存n个类型为T的对象,返回起始位置指针 //释放从指针p出开始的内存,共保存了n个对象,p必须是先前由allocate返回的指针,n必须为当初构造时的大小 a.deallocate(p, n); //args被传递给T的构造函数,用于在p指向的内存中构造一个对象 a.construct(p ,args); a.destory(p); //对p指向的对象析构
分配内存
allocator<string> alloc; auto p = alloc.allocate(10); auto q = p; alloc.construct(p++); //空字符串 alloc.construct(p++, 5, 'h'); //5个h alloc.construct(p++, 'hello');
早期版本中不能采用其它构造函数来构造一个元素,只有两个参数:指针,参数类型的值
逐个destory,但内存并没有被释放
while(q != p) { alloc.destory(--p); }
拷贝和填充
uninitialized_copy(b, e, b2); //从迭代器b和e间拷贝元素到迭代器b2指定的未构造的原始内存中,假定b2足够大 uninitialized_copy(b, n, b2); //拷贝n个元素uninitialized_fill(b, e, t); //在迭代器b和e指定的原始内存范围中创建对象,值为t的拷贝 uninitialized_fill(b, n, t); //拷贝n个,假定足够大