赞
踩
使用动态分配的内存空间。(这部分内存是除了静态内存和栈以外的内存池,又被称为自由空间,也可以称为堆(heap))
什么时候需要动态内存
动态内存的主要运用
new
运算符可以帮我们分配一块动态内存。
由于在自由空间分配的内存是无名的,因此new
无法为其分配的对象命名,而是返回一个指向该对象的指针。
默认情况下,动态分配的对象是默认初始化的(如果我们没有显式进行初始化),可能造成新对象未初始化。
// pi指向一个未初始化的int
int *pi = new int;
// 直接初始化,pi1指向的对象值为1024
int *pi1 = new int(1024);
// 使用传统构造方式初始化,*ps为"9999999999"
string *ps = new string(10, '9');
// C++ 11 列表初始化
vector<int> *pv = new vector<int>{1,2,3};
// C++ 11 括号中仅有单一初始化器时可用auto
auto p1 = new auto(obj);
自由空间存在被耗尽的情况,此时new表达式就会失败。
默认情况下,new不能分配所要求的空间,就会抛出一个bad_alloc
异常。
可改变new
的使用方式来阻止抛出异常
// 如果分配失败,new返回空指针,不会抛出异常
int *p1 = new (nothrow) int;
这种形式的new被称为定位new
,允许我们向new
传递额外参数。
由内置指针管理的动态内存在被显式释放前会一直存在,所以我们在对象的生命周期结束时,一定要手动释放
delete表达式
用来将动态内存归还系统,其可接受一个指针,释放指针指向的对象空间。
!注意!:delete只能释放【动态】空间。并且delete p
后,指针p
仍然存在,只是指向内存空间已被释放。这种情况下p
是野指针
int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
// 以下错误,释放的是非指针、局部变量
delete i;
delete pi1;
// 以下正确,释放一个空指针总是无错的
delete pi2;
delete pd;
// 以下错误,pd2指向的对象已经被释放
delete pd2;
内存盲区:我们动态分配的内存,只使用了某指针来指向它时,若该指针被销毁前,未释放对应内存,那么这块内存会被一直占用,且无法定位并释放。
空悬指针:指向一块曾经保存数据对象,但现在已经无效的内存的指针。
1、忘记delete内存。—> 会导致“内存泄漏”,这种内存将永远不可能归还给自由空间。
2、使用已经释放掉的对象。
3、同样一块内存释放两次。
头文件:<memory>
概念:
C++11提供的智能指针:
shared_ptr
:允许多个指针指向同一个对象,计数器为0时,自动释放动态内存。unique_ptr
:“独占”所指向的对象weak_ptr
:是一个伴随类,是一种弱引用,指向shared_ptr所管理的对象。我们在使用智能指针时,智指指向的这块已分配的动态内存会有一个关联的计数器,通常称为引用计数。当该动态内存块
引用计数归零时,它会自动释放所管理的对象。
也就是说:计数器绑定的是内存块,而非某智能指针。多个智能指针指向同一块动态内存时,会共享同一个引用计数
1、引用计数递增情况:
值传递
)2、引用计数递减情况:
shared_ptr支持的操作一览:
语法 | 作用 |
---|---|
shared_ptr<T> sp | 创建智能指针,可以指向类型为T的对象 |
sp | 用作判断条件,若sp指向一个对象,则为true |
*sp | 解引用,获取指向对象 |
sp->mem | 等价(*sp).mem |
sp.get() | 返回sp中保存的指针(是内置指针!) |
swap(sp,sq) sp.swap(sq) | 交换两个指针 |
make_shared<T> (args) | 返回一个指向T类型动态对象的shared_ptr,args 用于初始化此对象 |
shared_ptr<T> sp(sq) | sp是sq的拷贝 |
sp = sq | 同上 |
sp.unique() | 若sp.use_count() == 1,返回true,否则false |
sp.use_count() | 返回与sp共享对象的智指数量,可能很慢 |
sp.reset(q, d) | q,d 可选。若sp为唯一指向其对象的shared_ptr,reset会释放此对象。若传递了q,会令sp指向q,否则将sp置空。 若传递了参数d,会调用d而不是delete来释放sp。 |
基础方式:
shared_ptr<string> p1;
shared_ptr<int> p2 = make_shared<int>(42);
shared_ptr<string> p3 = make_shared<string>(10,'9');
使用别的指针初始化shared_ptr:
// (1) q可为內指,但必须指向new分配的内存,且能转为T*类型
shared_ptr<T> p(q);
// (2) p从unique_ptr u那里接管对象的所有权,将u置空
shared_ptr<T> p(u);
// (3) 在(1)的基础上,p将使用可调用对象d代替delete,这种格式也可以用在sp上,非內指独有
shared_ptr<T> p(q, d);
调用make_shard()是最安全的分配和使用动态内存的方式,推荐使用这种方式创建sp
此函数在动态内存中分配一个对象并初始化它,返回指向该对象的shared_ptr
注意事项:
《C++ Primer》(第五版)p405
/* 使用strBlob来实现vector<string>的数据共享 避免访问已经销毁的对象 */ class StrBlob { public: // 类型别名用法 typedef vector<string>::size_type size_type; // 默认构造参数 StrBlob(); StrBlob(std::initializer_list<string> il); // 常成员函数,只能调用成员变量与常成员函数 // 不可改变非静态成员变量 // 增删查 size_type size() const { return data->size(); } bool empty() const { return data->empty(); } void push_back(const string &t) { data->push_back(t); } void pop_back(); // 元素访问 string &front(); string &back(); // 实现元素访问的const重载,便于类对象为const类型时,也可以进行首尾访问 // 前一个const代表返回值为const引用,该引用只能用来读取值,不能用于修改 // 后一个const代表函数自身是一个 const成员函数,在函数体内不可以修改对象的成员变量 const string &front() const; const string &back() const; private: /* data */ std::shared_ptr<vector<string>> data; // 检查一个给定索引是否在合法范围内 void check(size_type i, const string &msg) const; }; // 默认构造参数,创建一个默认的vector<string>对象 StrBlob::StrBlob() : data(std::make_shared<vector<string>>()) {} // 使用构造方式初始化有参对象 StrBlob::StrBlob(std::initializer_list<string> il) : data(std::make_shared<vector<string>>(il)) {} void StrBlob::check(size_type i, const string &msg) const { // 检查一个给定索引是否在合法范围内 if (i >= data->size()) throw std::out_of_range(msg); } string &StrBlob::front() { check(0, "front on empty StrBlob"); return data->front(); } const string &StrBlob::front() const { check(0, "front on empty StrBlob"); return data->front(); } string &StrBlob::back() { check(0, "back on empty StrBlob"); return data->back(); } const string &StrBlob::back() const { 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() { StrBlob b1; { StrBlob b2 = {"a", "an", "the"}; b1 = b2; b2.push_back("about"); // 以下结果都为4 std::cout << "b1's size: " << b1.size() << std::endl; std::cout << "b2's size: " << b2.size() << std::endl; } return 0; }
某个时刻,只能有一个unique_ptr指向一个给定对象,up完全拥有它所指向的对象,因此up不支持普通拷贝或赋值操作。
unique_ptr 支持操作一览:
语法 | 作用 |
---|---|
unique_ptr<T> up | 创建智能指针,可以指向类型为T的对象,使用delete来释放 |
unique_ptr<T, D> up | 在上条基础上,使用一个类型为D的可调用对象来释放它的指针 |
unique_ptr<T, D> up(d) | 在上条基础上,用类型为D的对象d带代替delete |
up | 用作判断条件,若up指向一个对象,则为true |
*up | 解引用,获取指向对象 |
up->mem | 等价(*up).mem |
up.get() | 返回up中保存的指针(是内置指针) |
swap(up, uq)/up.swap(uq) | 交换两个指针 |
up.release() | up放弃对指针的控制权,返回指针,并将up置空 |
up.reset(q) | q可选为內指,可为nullptr。如果提供了q,令up指向q所指向的对象,否则up置空 |
调用release会切断unique_ptr和它原管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针,或给另一个智能指针赋值。
也就是,我们只调用up.release(),只是切断up与一块动态内存的联系,而这块动态内存没有被释放,并且我们会丢失指针。
所以,即使我们不需要另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放。
up.release(); // 错误,p2不会释放内存,且会丢失指针。
auto p = up.release(); // 正确,但别忘了delete(p)
不能拷贝uq的规则有一个例外:我们可以拷贝或赋值一个将要销毁的uq。
// (1) 从函数中返回一个unique_ptr是允许的
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p));
}
// (2) 还可以返回一个局部对象的拷贝
unique_ptr<int> clone(int p){
unique_ptr<int> ret(new int(p));
//...
return ret;
}
weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向一个shared_ptr
管理的对象。
weak_ptr用法一览:
语法 | 作用 |
---|---|
weak_ptr<T> w(sp) | 创建一个wp,与sp指向相同对象,T必须能转换为sp指向的类型 |
weak_ptr<T> w | 空wp |
w = p | 这里p可为sp,也可为wp,赋值后w与p共享对象 |
w.reset() | 将w置空 |
w.use_count() | 与w共享对象的sp数量 |
w.expired() | 若w.use_count ==0 ,返回true, 否则返回false |
w.lock() | 若expired为true, 返回一个空的sp,否则返回一个指向w的对象的sp。 |
if(shared_ptr<int> np = wp.lock()){ // 如果np不为空则条件成立
// 在if中,np与p共享对象
}
class StrBlob; // 前置声明 // StrBlob的伴随类,防止访问非法。 class StrBlobPtr { public: StrBlobPtr() : curr(0) {} StrBlobPtr(StrBlob& a, std::size_t sz = 0); std::string& deref() const; StrBlobPtr& incr(); private: std::shared_ptr<std::vector<std::string>> check(std::size_t i, const std::string& msg) const; std::weak_ptr<std::vector<std::string>> wptr; std::size_t curr; }; StrBlobPtr::StrBlobPtr(StrBlob& a, size_t sz) :wptr(a.data), curr(sz) {} // 检查合法性 std::shared_ptr<std::vector<string>> StrBlobPtr::check(size_t i, const string &msg) const { auto ret = wptr.lock(); // 若vec的sp存在,则会返回这个sp,否则返回空的sp // 假设该vec已经不存在 if (!ret) throw std::runtime_error("unbound StrBlobPtr"); // 检查访问下标是否合法 if (i >= ret->size()) throw std::out_of_range(msg); return ret; // 否则,返回指向vector的sp } string &StrBlobPtr::deref() const { // 确认访问合法性 auto p = check(curr, "dereference past end"); return (*p)[curr]; } // 前缀递增:返回递增后的对象的引用 StrBlobPtr &StrBlobPtr::incr() { check(curr, "increment past end of StrBlobPtr"); ++curr; return *this; }
1、 智能指针的构造参数是explicit
的,意味着用构造方式创建一个智指时不支持隐式转换
内->智: 一个用来初始化智能指针的普通指针必须指向动态内存,且不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式
// 错误,必须使用直接初始化,因为这样是隐式转换
shared_ptr<int> p1 = new int(1024);
// 正确
shared_ptr<int> p2(new int(1024));
2、shared_ptr
可以协调对象的析构,但仅限于自身(也是shared_ptr)的拷贝。sp之间可以互相感知,但sp感知不到内置指针。
3、不要用get()
初始化另一个智能指针或为智能指针赋值
get()
函数返回一个内置指针,指向智能指针管理的对象get()
用来将指针的访问权限传递给代码,只有确定代码不会delete指针的情况下,才能用get。delete
- > get()
返回的指针get()
初始化或reset
另一个智能指针get()
返回的指针,记住当最后一个对应的智能指针销毁后,这个指针就无效了new
分配的内存,记住传递给它一个删除器。【这种情况通常是因为对应程序类没有析构函数,所以需要手动定义或使用它的删除器,详见p416(C++ Primer 第五版)】智能指针通过其设计和实现来避免空悬指针(dangling pointer)的问题。以下是智能指针如何避免空悬指针的主要原因:
std::unique_ptr
,它禁止多个指针共享同一块内存,只允许一个指针拥有所有权。当 std::unique_ptr
超出其作用域时,会自动释放其所管理的内存,并将指针设置为 nullptr
,避免了指针悬空的问题。std::shared_ptr
,它使用引用计数来管理所有权。多个 std::shared_ptr
可以共享同一块内存,并且内部维护了一个引用计数,记录有多少个 shared_ptr
指向该内存块。只有当所有 shared_ptr
都释放了对内存的引用,引用计数为零时,才会释放内存。这样可以避免早期的释放导致空悬指针的问题。std::weak_ptr
通常配合 std::shared_ptr
使用,它不增加引用计数,只是对 shared_ptr
的引用进行跟踪。当需要访问共享的内存时,可以通过 lock()
方法将 weak_ptr
转换为 shared_ptr
,如果 shared_ptr
已经释放了内存,则返回空指针。这样可以避免因为循环引用导致的内存泄漏和空悬指针的问题。总体来说,智能指针通过其设计和实现机制,提供了更加安全和方便的内存管理方式,有效地避免了空悬指针的问题。因此,在 C++ 编程中推荐优先使用智能指针来管理动态分配的内存。
对于内置类型(如 int
、double
、指针等)或组合类型(例如结构体或类)的对象,默认初始化意味着它们的值将是未定义的。
C++标准规定了三种初始化方式:
int x(5);
或 int y = int(10);
。对于动态分配的对象来说,它们是通过默认初始化得到的,因此其值是未定义的。这意味着在使用动态分配的对象之前,必须确保对其进行正确的初始化。否则,它们的值可能是随机的,可能导致程序行为不确定或错误。
参考:
[1] 《C++ Primer(第五版)》 by: Stanly B.Lippman, Josee Lajoie, Barbara E.Moo
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。