【C++语法进阶】C++动态内存管理
1. 动态内存
使用动态分配的内存空间。(这部分内存是除了静态内存和栈以外的内存池,又被称为自由空间,也可以称为堆(heap))
什么时候需要动态内存
- 程序有时不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要再多个对象间共享数据
动态内存的主要运用
- 动态分配内存空间,使程序可以自由根据需求创建对象,而不用在编译时就确定大小
- 管理复杂数据结构,根据需要动态添加或删除元素
- 减少内存浪费
- 处理大规模数据,动态内存分配可以避免栈空间溢出问题,因为堆空间相对较大
2. 直接管理内存
2.1 分配内存运算符:new
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表达式就会失败。
默认情况下,new不能分配所要求的空间,就会抛出一个bad_alloc
异常。
可改变new
的使用方式来阻止抛出异常
// 如果分配失败,new返回空指针,不会抛出异常
int *p1 = new (nothrow) int;
这种形式的new被称为定位new
,允许我们向new
传递额外参数。
2.2 释放内存运算符:delete
由内置指针管理的动态内存在被显式释放前会一直存在,所以我们在对象的生命周期结束时,一定要手动释放
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;
内存盲区:我们动态分配的内存,只使用了某指针来指向它时,若该指针被销毁前,未释放对应内存,那么这块内存会被一直占用,且无法定位并释放。
空悬指针:指向一块曾经保存数据对象,但现在已经无效的内存的指针。
- 未初始化的指针所有缺点空悬指针都具备
- 一般这种情况是因为,我们delete指针后,指针值变为无效,但很多机器上指针仍保存(已经释放的)动态内存地址
- 避免方式:在指针即将离开其作用域之前释放它所关联的内存
- 若我们需要保留该指针,可以delete之后,将nullptr赋予指针
2.3 动态内存易错点
1、忘记delete内存。—> 会导致“内存泄漏”,这种内存将永远不可能归还给自由空间。
2、使用已经释放掉的对象。
3、同样一块内存释放两次。
3. 智能指针【C++ 11】
3.1 概念与共性
头文件:<memory>
概念:
- 用于管理动态内存的指针
- 负责自动释放所指向的对象
- 只能指针也是模板
C++11提供的智能指针:
shared_ptr
:允许多个指针指向同一个对象,计数器为0时,自动释放动态内存。unique_ptr
:“独占”所指向的对象weak_ptr
:是一个伴随类,是一种弱引用,指向shared_ptr所管理的对象。
引用计数
我们在使用智能指针时,智指指向的这块已分配的动态内存会有一个关联的计数器,通常称为引用计数。当该动态内存块
引用计数归零时,它会自动释放所管理的对象。
也就是说:计数器绑定的是内存块,而非某智能指针。多个智能指针指向同一块动态内存时,会共享同一个引用计数
1、引用计数递增情况:
- 拷贝一个智能指针
- 用一个智能指针初始化另一个智能指针
- 智能指针作为参数传递函数(限
值传递
)
2、引用计数递减情况:
- 智能指针被赋予一个新值
- 智能指针被销毁
- 智能指针离开其作用域
3.2 shared_ptr
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()
调用make_shard()是最安全的分配和使用动态内存的方式,推荐使用这种方式创建sp
此函数在动态内存中分配一个对象并初始化它,返回指向该对象的shared_ptr
- 类似顺序容器的emplace对象,make_shared用参数来构造给定类型的对象
- 也就是一个变量如何使用构造方式来初始化,与之用法相同
注意事项:
- 如果将shared_ptr存放于一个容器,而后不再需要全部元素,而只使用其中一部分,记得用erase删除不再需要的元素
练习:StrBlob类
《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;
}
3.3 unique_ptr
某个时刻,只能有一个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()
调用release会切断unique_ptr和它原管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针,或给另一个智能指针赋值。
也就是,我们只调用up.release(),只是切断up与一块动态内存的联系,而这块动态内存没有被释放,并且我们会丢失指针。
所以,即使我们不需要另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放。
up.release(); // 错误,p2不会释放内存,且会丢失指针。
auto p = up.release(); // 正确,但别忘了delete(p)
无拷贝的unique_ptr的例外
不能拷贝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;
}
3.4 weak_ptr
weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向一个shared_ptr
管理的对象。
- wp的绑定,不会改变sp的引用计数(sp.count == 0,不管是否还存在wp绑定,对象都会被释放)
- 创建一个wp时,需要用一个sp来初始化它
- wp可能指向一个已经不存在的对象
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。 |
- 由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock
if(shared_ptr<int> np = wp.lock()){ // 如果np不为空则条件成立
// 在if中,np与p共享对象
}
练习:StrBlob类的伴随指针类StrBlobPtr
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;
}
3.5 内置指针与智能指针之间的转化
1、 智能指针的构造参数是explicit
的,意味着用构造方式创建一个智指时不支持隐式转换
内->智: 一个用来初始化智能指针的普通指针必须指向动态内存,且不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式
// 错误,必须使用直接初始化,因为这样是隐式转换
shared_ptr<int> p1 = new int(1024);
// 正确
shared_ptr<int> p2(new int(1024));
2、shared_ptr
可以协调对象的析构,但仅限于自身(也是shared_ptr)的拷贝。sp之间可以互相感知,但sp感知不到内置指针。
- 如果混用內指和智指指向动态内存,当sp.count == 0, sp自动释放,会导致普通指针悬空。
- 故,当我们从內指传递地址给智指后,就不应该在使用內指来操作这块内存。
- 总结:使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。
3、不要用get()
初始化另一个智能指针或为智能指针赋值
- 智能指针的
get()
函数返回一个内置指针,指向智能指针管理的对象 get()
用来将指针的访问权限传递给代码,只有确定代码不会delete指针的情况下,才能用get。
3.6 智能指针使用规范一览
- 不使用相同的内置指针初始化(or reset)多个智能指针
- 不
delete
- >get()
返回的指针 - 不使用
get()
初始化或reset
另一个智能指针 - 如果使用
get()
返回的指针,记住当最后一个对应的智能指针销毁后,这个指针就无效了 - 如果使用智能指针管理的资源不是
new
分配的内存,记住传递给它一个删除器。【这种情况通常是因为对应程序类没有析构函数,所以需要手动定义或使用它的删除器,详见p416(C++ Primer 第五版)】
附录
问题1:智能指针如何避免空悬指针问题?
智能指针通过其设计和实现来避免空悬指针(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++ 编程中推荐优先使用智能指针来管理动态分配的内存。
问题2:默认初始化为什么是未定义的?
对于内置类型(如 int
、double
、指针等)或组合类型(例如结构体或类)的对象,默认初始化意味着它们的值将是未定义的。
C++标准规定了三种初始化方式:
- 默认初始化:对象被定义时没有显式初始化,其值将是未定义的(内置类型的值是未定义的)。
- 值初始化:对象被初始化时没有显式提供初始值,但根据类型不同有不同的行为:
- 内置类型:值初始化将对象初始化为0(对于数值类型)、nullptr(对于指针类型)或者调用默认构造函数(对于类类型)。
- 类类型:如果类没有默认构造函数,则无法进行值初始化。
- 直接初始化:使用括号进行初始化,如
int x(5);
或int y = int(10);
。
对于动态分配的对象来说,它们是通过默认初始化得到的,因此其值是未定义的。这意味着在使用动态分配的对象之前,必须确保对其进行正确的初始化。否则,它们的值可能是随机的,可能导致程序行为不确定或错误。
参考:
[1] 《C++ Primer(第五版)》 by: Stanly B.Lippman, Josee Lajoie, Barbara E.Moo