C++支持动态分配对象,它的生命周期与它们在哪里创建无关,只有当显示的被释放时,这些对象才会被销毁。分配在静态或栈内存中的对象由编译器自动创建和销毁。
new在动态内存中为对象分配空间并返回一个指向该对象的指针,并调用构造函数构造对象;delete接受一个动态对象指针,调用析构函数销毁该对象,并释放与之相关的内存。
那么new、delete和malloc、free有什么区别和联系呢?
1、new/delete是C++的操作符,而malloc与free是C++/C 语言的标准库函数,不在编译器控制权限之内。
2、new做两件事,一是分配内存,二是调用类的构造函数;同样,delete会调用类的析构函数和释放内存。而malloc和free只是分配和释放内存。
3、new建立的是一个对象,而malloc分配的是一块内存;new建立的对象可以用成员函数访问,不要直接访问它的地址空间;malloc分配的是一块内存区域,用指针访问,可以在里面移动指针;new出来的指针是带有类型信息的,而malloc返回的是void指针。
4、new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。
5、new自动计算需要分配的空间,而malloc需要手工计算字节数
6、new是类型安全的,而malloc不是,比如:
int* p = new float[2]; // 编译时指出错误 int* p = malloc(2*sizeof(float)); // 编译时无法指出错误
new operator 由两步构成,分别是 operator new 和 construct
7、operator new对应于malloc,但operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上。而malloc无能为力
8、new将调用constructor,而malloc不能;delete将调用destructor,而free不能。
9、malloc/free要库文件支持,new/delete则不要。
new动态分配内存
new无法为其分配的对象命名,而是返回一个指向该对象的指针;默认情况下,动态分配的对象是默认初始化,因此内置对象和组合对象是未初始化的。
int *p1 = new int;//动态分配一个未初始化的无名对象 string *p2 = new string;//默认初始化为空string
养成一个为动态分配的对象初始化的好习惯。
如果提供括号包围的初始化器,则可以通过初始化器来推断对象类型,就可以使用auto,但是这是当括号中仅有单一初始化器的时候才能使用auto。
动态分配一个const对象必须进行初始化。
auto p1 = new auto("haha"); const int *p2 = new const int(10);
默认情况下,new操作符时内存不足会抛出一个bad_alloc的异常。
但是还有定位new的形式,使得此时不抛出异常,而返回一个空指针。定位new允许我们想new中传递额外的参数。
int *p = new (nothrow) int;//如果失败,返回空指针
delete释放内存
通常,编译器是不能分辨一个指针是指向的静态对象还是动态对象,所以下面的操作是错的,但是编译期检查不出来。
int i = 10; int *p = &i; delete p;//释放局部变量,错误
注意:new和delete经常出现的三种错误
- 忘记释放内存;
- 使用已经释放的内存;
- 同一块内存释放两次。
delete之后重置指针为空是一个好习惯,悬空指针很危险,通常上面的第二个错误就是悬空指针引起的;但是这样也只是提供有限的保护。
一、智能指针
动态分配内存容易出现问题,因为确保在正确的时间释放内存很困难,我们可能经常忘记释放的内存。于是C++11的标准库中提供了两种智能指针来管理动态对象,它们自己负责动态释放所指向的对象。
分别是:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。
shared_ptr和unique_ptr都支持的操作如下:
操作 | 描述 |
shared_ptr<T> sp unique_ptr<T> up |
空智能指针,可以指向类型为T的对象 |
p | 将p用作一个条件判断,若p指向一个对象,则为true |
*p | 解引用p,获得它指向的对象 |
p->mem | 等价于(*p).mem |
p.get() | 返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了。 |
swap(p,q) p.swap(q) |
交换p和q中的指针 |
1.shared_ptr类
智能指针也是模板,需要提供指向的类型,且默认初始化时,智能指针保存着一个空指针。
shared_ptr<string>sp;//可以指向string的空智能指针
相关操作如下:
操作 | 描述 |
make_shared<T>(args) | 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化对象。 |
make_shared<T>p(q) | p是shared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T* |
p = q | p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。 |
p.unique() | 若p.use_count()为1,返回true;否则返回false |
p.use_count() | 返回与p共享对象的智能指针数量;可能很慢,主要用于测试 |
最安全的分配和使用动态内存的方法是使用make_shared的函数,它和顺序容器的emplace成员类似,它的参数和指向的对象的某个构造函数匹配。
通常可以使用auto来推断类型,方便使用。
shared_ptr<string>sp = make_shared<string>(10,'9');//指向一个值为“9999999999”的string auto spInt = make_shared<int>(10);
每个shared_ptr都会记录有多少个其他的shared_ptr志向相同的对象。因此,可以认为每个shared_ptr都有一个与之关联的计数器,通常称为引用计数。当一个shared_ptr的引用计数为0,它就会自动释放自己所管理的对象。
auto r = make_shared<int>(10); r = q;//给r赋值,是它指向另一个对象;则q指向的对象的引用计数会递增,r原来指向的对象的引用计数会递减,如果减为0,则释放该对象。
如果你将shared_ptr存放在一个容器中,而后不再需要全部元素,只使用其中一部分,则要记得使用erase删除不再需要的那些元素。
下面的三种情况下使用动态内存:
- 程序不知道自己需要多少个对象;
- 程序不知道需要的对象准确类型;
- 程序需要在多个对象之间共享数据。
#include <iostream> #include <string> #include <memory> //智能指针和动态分配内存 #include <vector> #include <initializer_list> //初始值列表 #include <stdexcept> 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() { return data->empty(); } //添加删除元素 void push_back(const std::string &s){ data->push_back(s); } void pop_back(); //访问元素 std::string& front(); std::string& back(); const std::string& front()const; const std::string& back() const; private: std::shared_ptr<std::vector<std::string>> data; //private 检查函数。 void check(size_type i, const std::string &msg)const; }; //默认构造函数 StrBlob::StrBlob(): data(std::make_shared<std::vector<std::string>>()) { } //拷贝构造函数 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); } const std::string& StrBlob::back()const { check(0, "back on empty StrBlob"); return data->back(); } //避免代码重复和编译时间问题,用non-const版本调用const版本 //在函数中必须先调用const版本,然后去除const特性 //在调用const版本时,必须将this指针转换为const,注意转换的是this指针,所以<>里面是const StrBlob* 是const的类的指针。 //调用const版本时对象是const,所以this指针也是const,通过转换this指针才能调用const版本,否则调用的是non-const版本,non-const调用non-const会引起无限递归。 //return时,const_cast抛出去除const特性 std::string& StrBlob::back() { const auto &s = static_cast<const StrBlob*>(this)->back(); //<span style="color:#FF0000;">auto前面要加const,因为auto推倒不出来const。</span> return const_cast<std::string&>(s); } const std::string& StrBlob::front()const { check(0, "front on empty StrBlob"); return data->front(); } std::string& StrBlob::front() { const auto &s = static_cast<const StrBlob*>(this)->front(); return const_cast<std::string&>(s); } void StrBlob::pop_back() { check(0, "pop_back on empty StrBlob"); data->pop_back(); } int main() { std::shared_ptr<StrBlob>sp; StrBlob s({"wang","wei","hao"}); StrBlob s2(s);//共享s内的数据 std::string st = "asd"; s2.push_back(st); //s2.front(); std::cout << s2.front() << std::endl; std::cout << s2.back() << std::endl; }
还可以使用new返回指针来初始化智能指针,此时智能指针的构造函数是explicit。
shared_ptr<int>p1(new int(10));//正确:使用new初始化智能指针 shared_ptr<int>p2 = new int(10);//错误:必须显示的初始化
但是注意不要将普通的指针和智能指针混合使用,很容易出错。
get()操作
get可以讲指针的权限给代码,但是注意,必须保证代码不会delete指针,才能使用get。永远不要用get去初始化另一个智能指针或给它赋值。
reset()操作
reset会更新引用计数,所以在多个shared_ptr共享对象时,常与unique()一起使用。
异常
使用智能指针,即使程序块过早结束,智能指针类也能确保内存不再需要时将其释放;但是普通指针就不会。
void fun( ) { int *p = new int(42); //如果这时抛出一个异常且未捕获,内粗不会被释放,但是智能指针就可以释放。 delete p; }
标准很多都定义了析构函数,负责清理对象使用的资源,但是一些同时满足c和c++的设计的类,通常都要求我们自己来释放资源,可以使用智能指针来解决这个问题。
/*有问题*/ connection connect(*destination); void disconnect(connect); void f(destination &d) { connection c = connect(&d); disconnect(d);//如果没有调用disconnect,那么永远不会断开连接。 }
//使用智能指针优化,等于自己定义了delete代替本身的delete connection connect(*destination); void end_disconnect(connection*p) {disconnect(p);} void f(destination &d) { connection c = connect(&d); shared_ptr<connection>p(&d, end_connect);//定义自己的删除器 //f退出时,会自动调用end_connect。 }
总结:
智能指针陷阱
- 不使用相同的内置指针值初始化(或reset)多个智能指针;//多个智能指针还是单独的指向内置指针的内存,use_count分别为1
- 不delete get( )返回的指针;//两次delete释放,智能指针内部也会delete
- 不使用get( )初始化或reset另一个智能指针;//free( ): invalid pointer:也是多次释放
- 如果你使用get( )返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(删除函数向上面的disconnect( ))。
二、unique_ptr类
由于unique_ptr独占它所指向的对象,因此他不支持普通的拷贝和赋值。
但是,可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr。
操作 | 描述 |
unique_ptr<T> u1 unique_ptr<T,p> u2 |
空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针;u2会使用一个类型为D的可调用对象来释放它的指针 |
unique_ptr<T,p> u(d) | 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete |
u = nullptr | 释放u所指向的对象,将u置为空 |
u.release() | u放弃对指针的控制权,返回指针,并将u置为空 |
u.reset() | 释放u指向的对象 |
u.reset(q) u.reset(nullptr) |
如果提供了内置指针q,令u指向这个对象;否则将u置为空 |
但是有种特殊的拷贝可以支持:我们可以拷贝或赋值一个即将要被销毁的unique_ptr。
unique_ptr<int> clone(int p){ return unique_ptr<int>(new int(p));//返回一个unique_ptr }
在早的版本中提供了auto_ptr的类,它有unique_ptr 的部分特性,但是不能在容器中保存auto_ptr, 也不能在函数中返回 auto_ptr, 编写程序时应该使用unique_ptr。
向unique_ptr 传递删除器
#include <memory> #include <iostream> using namespace std; typedef int connection; connection* connect(connection *d) { cout << "正在连接..." << endl; d = new connection(40); return d; } void disconnect(connection *p) { cout << "断开连接..." << endl; } int main() { connection *p,*p2; p2 = connect(p); cout << p << endl; cout << *p2 << endl; unique_ptr<connection, decltype(disconnect)*>q(p2,disconnect); //在尖括号中提供类型,圆括号内提供尖括号中的类型的对象。 //使用decltype()关键字返回一个函数类型,所以必须添加一个*号来指出我们使用的是一个指针 }
三、weak_ptr
weak_ptr 是一种不控制对象生存期的智能指针,它指向由一个shared_ptr 管理的对象。将一个weak_ptr绑定到shared_ptr不会改变shared_ptr的引用计数,且最后一个指向对象的shared_ptr被销毁,对象就会被释放,不管有没有weak_ptr指向该对象。
weak_ptr操作
操作 | 描述 |
weak_ptr<T> w | 空weak_ptr可以指向类型为T的对象 |
weak_ptr<T> w(sp) | 与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp指向的类型。 |
w = p | p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象 |
w.reset() | 将w置为空 |
w.use_count() | 与w共享对象的shared_ptr的数量 |
w.expired() | 若w.use_count()为0,返回true,否则返回false |
w.lock() | 如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr |
当我们创建一个weak_ptr 必须用一个 shared_ptr 初始化。weak_ptr 不会更改shared_ptr 的引用计数。
不能直接使用weak_ptr访问对象,而必须调用lock();引入lock和expired是防止在weak_ptr 不知情的情况下,shared_ptr 被释放掉。
std::weak_ptr 是一种智能指针,它对被 std::shared_ptr 管理的对象存在非拥有性(“弱”)引用。在访问所引用的对象前必须先转换为 std::shared_ptr。
std::weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用std::weak_ptr 来跟踪该对象。需要获得临时所有权时,则将其转换为 std::shared_ptr,此时如果原来的 std::shared_ptr被销毁,则该对象的生命期将被延长至这个临时的 std::shared_ptr 同样被销毁为止。
此外,std::weak_ptr 还可以用来避免 std::shared_ptr 的循环引用。
四、动态数组
动态数组并不是数组类型,而是得到一个数组元素类型的指针。
大多数应用应该使用标准库容器而不是动态分配的数组,因为使用容器更简单、更安全。
int *p = new int[get_siae()];//方括号中的大小必须是整数,但不必是常量
int *p = new int[]{0,1,2,3,4,5,6,7,8,9};//C++11中可以使用列表初始化 string *sp = new string[10]{"ad","fd","xbv"};//初始化器中的元素较少时,剩下的进行值初始化;初始化器中的元素过多时,new失败,不会分配任何内存。
不能用auto分配数组。
动态分配一个空数组是合法的,但是不能解引用。
指向数组的unique_ptr
指向数组的unique_ptr不支持成员访问运算符
操作 | 描述 |
unique_ptr<T[]> u | u可以指向一个动态分配的数组,数组元素类型为T |
unique_ptr<T[]> u(p) | u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T* |
u[i] | 返回u拥有的数组中位置i处的对象,u必须指向一个数组 |
shared_ptr管理动态数组要定义自己的删除器,而且访问元素的时候要是用get()获取内置指针。
shared_ptr<int> sp(new int[10], [](int *p){delete[] p;}); sp.reset();//使用上面的lambda表达式释放数组 for(size_t i = 0;i != 10;++i) *(sp.get() + i) = i;
五、allocator类
new它将内存分配和对象构造结合到了一起,而allocator类允许我们将分配和初始化分离。
标准库allocator类定义在头文件 <memory>中。它帮助我们将内存分配和构造分离开来,它分配的内存是原始的、未构造的。
类似vector,allocator也是一个模板类,我们在定义一个allocator类类型的时候需要制定它要分配内存的类型,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:
allocator<string> alloc; auto const p = alloc.allocate(n); // 分配n个未初始化的string
allocator类的操作:
allocator<T> a | 定义了一个名为a的allocator对象,可以为类型为T的对象分配内存 |
a.allocate(n) | 分配一段原始的、未构造的内存,保存n个类型为T的对象 |
a.deallocate(p, n) | 释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate成员函数返回的指针,且n必须是创建时候的大小,在destroy之前,用户必须在这块内存上调用destroy函数 |
a.construct(p, args) | p必须是一个类型为T*的指针,只想一块原始内存,args被传递给类型为T的构造函数 |
a.destroy(p) | p为T*类型的指针,此算法对p执行析构函数 |
allocator分配的内存是未构造的,construct成员函数接受一个指针和零个或多个额外参数,在给定的位置构造一个元素,额外的参数用来初始化对象。这些额外参数必须和类型相匹配的合法的初始化器。 为了使用allocate返回的内存,我们必须用construct构造对象。
auto q = p; alloc.construct(q++); // *q为空字符串 alloc.construct(q++, 10, 'c'); // *q为cccccccccc alloc.construct(q++, "hi"); // *q为hi
还未构造对象的情况下或者是使用原始内存是错误的:
cout << *p << endl; // 正确,使用string的输出运算符 cout << *q << endl; // 错误,q指向未构造的内存
在这些对象使用结束后,我们使用destroy来销毁这些元素:
while (q != p) alloc.destroy(--q);
元素被销毁后,如果需要将内存归还给系统,就需要调用deallocate函数:
alloc.deallocate(p, n);
传递给deallocate的指针不能为空,必须指向由allocate分配的内存,而且,n必须为allocate分配时的大小。
uninitialized_copy(b, e, b2) | 从迭代器b和3 指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中,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个对象,b必须指向足够大的未构造的原始内存,能容纳给定数量的对象 |
vector<int> vec{0, 1, 2, 3, 4, 5}; auto p = alloc.allocate(vec.size() * 2); auto q = uninitialized_copy(vec.begin(), vec.end(), p); uninitialize_fill_n(q, vec.size(), 42);
uninitialized_copy在给定位置构造元素,函数返回递增后的目的位置迭代器。因此,一个uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。