动态内存
C/C++程序的数据内存分区包括:
- 全局内存区:包括全局变量和静态变量;
- 堆内存区:通过maloc和new动态申请的内存,都是堆内存;
- 栈内存区:函数调用时传递的参数和函数中的局部变量。
1. malloc和free
在C语言中通过malloc申请内存,在内存使用完毕之后调用free将堆内存释放。
//调用maloc申请内存
char* buffer = malloc(100);
//........//使用内存
//调用free将动态内存释放
if(buffer == NULL)
free(buffer);
使用动态内存之前需要判断malloc函数的返回值,如果内存申请成功,则函数返回申请到的堆内存首地址;如果内存申请失败,则函数返回NULL。
2. realloc和calloc函数
calloc可以给分配的内存空间赋初值。
int* p = calloc(10, sizeof(int));
而realloc是重新在堆区分配内存,如果分配的内存比原来的大,会有两种情况:
- 原有空间后面有足够大的空闲空间,那么直接在原有空间后继续开辟内存,返回原有空间的首地址;
- 原有空间后续没有足够大的空闲内存,则重新分配一个足够大的空间,并且将原有空间的内容拷贝到新空间下,释放原来的空间,将新的内存单元的首地址返回。
p = realloc(p, sizeof(int)*20);
3. new和malloc的区别
- new操作符是从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。new能否在堆上动态分配内存,取决于operater new的实现细节。
- new操作符内存分配成时,返回的是对象类型的指针,类型严格与对象匹配,无需进行类型转换,故new是符和类型安全性的操作符;而malloc内存分配成功则是返回void*,需要通过强制类型转换将void*指针转换成需要的类型
- new内存分配失败时,会抛出bac_alloc异常,不会返回NULL;malloc分配内存失败时返回NULL。
- 使用new操作符申请内存分配无需指定内存块的大小,编译器会根据类型信息自行计算;而malloc需要显示地指出所需内存的大小。
- malloc函数会分配一块原始的内存空间;而new操作符会分为三步: 1)调用operater new函数分配原始的、未命名的内存空间;2)调用构造函数以构造对象,并为其传入初值;3)对象构造完成后,返回一个指向该对象的指针。
string* ps1 = new string; //默认初始化为空string
string* ps2 = new string(); //值初始化为空string
int* pi1 = new int; //默认初始化;*pi1的值未定义
int* pi2 = new int(); //值初始化;*pi2为0
对于内置类型这种没有默认构造函数的类型来说,使用值初始化有着良好定义的值;而默认初始化的对象的值则是未定义的。
4. 智能指针
智能指针包括shared_ptr、unique_ptr、weak_ptr这三种,它们定义在头文件中。
4.1 shared_ptr
智能指针shared_ptr会保存一个引用计数,当引用计数变为0时,会自动销毁对象,并自动释放相关联的内存。
类似vector,智能智能也是模板。创建的时候需要提供额外的信息来指明类型。例如:
shared_ptr<string> p1;
shared_ptr<list<int>> p2;
make_shared函数:它会在动态内存中分配一个对象并初始化它,返回指向该对象的shared_ptr。它的定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型。
shared_ptr<string> p1 = make_shared<string>(10, 'c');
shared_ptr<int> p2 = make_shared<int>(999);
或者我们也可以使用new来直接初始化一个智能指针。
shared_ptr<int> p1(new int(1024));
两种创建方式的区别:默认的构造函数会申请两次内存,而make_shared只会申请一次内存。因为shared_ptr内部有一个引用计数以及存放数据的内存,等于说有两部分内存,默认构造函数为数据内存和引用计数每个分别申请一次内存,而make_shared将数据和引用计数的内存申请放到一起。
关于make_shared的缺点 make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 弱引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题
shared_ptr常用的方法:
| 操作 | 说明 | 是否为shared_ptr所独有 |
|---|---|---|
| p.get() | 返回p中保存的指针,谨慎使用。 | 否 |
| p.unique() | 若p.use_count()为1,返回true;否则返回false | 是 |
| p.use_count() | 返回与p共享对象的智能指针数量,可能很慢。 | 是 |
注意以下几点:
- 不要混合使用普通指针和智能指针;
- 不要使用get初始化另一个智能指针或为智能指针赋值;
- 在改变底层对象之前,记得检查自己是否是当前对象的仅有用户。
if(!p.unique())
p.reset(new string(*p)); //不是唯一用户;分配新的拷贝
shared_ptr的大致实现原理:
//
// Created by ruan'ruan on 2023/6/27.
//
#ifndef OOP_BASE_MY_SHARED_PTR_H
#define OOP_BASE_MY_SHARED_PTR_H
#include <atomic>
class RefCounter{
public:
RefCounter(){
ref_cnt_ = 0;
}
void addRef(){
++ref_cnt_;
}
void decRef(){
--ref_cnt_;
}
int getRef(){
return ref_cnt_;
}
private:
std::atomic<int>ref_cnt_;
};
template <typename T>
class my_shared_ptr{
public:
//构造函数,初始化一个裸指针和引用计数指针;
my_shared_ptr(T* ptr = nullptr): ptr_(ptr), ctrl_blk_(new RefCounter()){
ctrl_blk_->addRef();
}
~my_shared_ptr(){
ctrl_blk_->decRef();
if(ctrl_blk_->getRef() == 0){
delete ptr_;
delete ctrl_blk_;
}
}
//拷贝构造函数;
my_shared_ptr(const my_shared_ptr& rhs): ctrl_blk_(rhs.ctrl_blk_), ptr_(rhs.ptr_){
ctrl_blk_->addRef();
}
//先增加右边的,再减去左边的,防止自我赋值;
my_shared_ptr& operator=(const my_shared_ptr& rhs){
rhs.ctrl_blk_->addRef();
ctrl_blk_->decRef();
if(ctrl_blk_->getRef() == 0){
delete ctrl_blk_;
delete ptr_;
}
ctrl_blk_ = rhs.ctrl_blk_;
ptr_ = rhs.ptr_;
return *this;
}
//方便测试;
RefCounter* ctrl_blk_;
private:
T* ptr_;
};
#endif //OOP_BASE_MY_SHARED_PTR_H
4.2 unique_ptr
一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。
unique_ptr不支持拷贝和赋值。
unique_ptr置为nullptr,会释放其所指向的对象。
std::unique_ptr<string> p1(new string("abcdef"));
p1 = nullptr;
| 操作 | 含义 |
|---|---|
| u.release() | 释放所有权,但不是释放资源,返回普通指针类型 |
| u.reset(q) | 释放所有权和资源;如提供内置指针q,令u指向这个对象,否则置u为空 |
虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另外一个uniqeu_ptr。
unique_ptr<string> p1(new string("abcdef"));
unique_ptr<string> p2(p1.release()); //所有权从p1->p2,且release将p1置为空
unique_ptr<string> p3(new string("trex"));
p2.reset(p3.release()); //reset释放了p2原来指向的内存
4.3 weak_ptr
weak_ptr时一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有wek_ptr指向对象,对象也还是会被释放。
**当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它。
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);
| 操作 | 含义 |
|---|---|
| w = p | p可以是一个shared_ptr或一个weak_ptr |
| w.use_count() | 与w共享对象的shared_ptr的数量 |
| w.expired() | 若w.use_count()为0,则返回true |
| w.lock() | 如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr |
当我们使用weak_ptr访问对象时,必须调用lock,不能直接访问。
if(shared_ptr<int> np = wp.lock()) { //
//在if中,np与p共享对象
}
4.4 weak_ptr是为了解决shared_ptr带来的循环引用的问题
4.4.1 weak_ptr解决shared_ptr循环引用的问题
定义两个类,每个类中又包含一个指向对方类型的智能指针作为成员变量,然后创建对象,设置完成后查看引用计数后退出,看一下测试结果:
class CB;
class CA
{
public:
CA() { cout << "CA() called! " << endl; }
~CA() { cout << "~CA() called! " << endl; }
void set_ptr(shared_ptr<CB>& ptr) { m_ptr_b = ptr; }
void b_use_count() { cout << "b use count : " << m_ptr_b.use_count() << endl; }
void show() { cout << "this is class CA!" << endl; }
private:
shared_ptr<CB> m_ptr_b;
};
class CB
{
public:
CB() { cout << "CB() called! " << endl; }
~CB() { cout << "~CB() called! " << endl; }
void set_ptr(shared_ptr<CA>& ptr) { m_ptr_a = ptr; }
void a_use_count() { cout << "a use count : " << m_ptr_a.use_count() << endl; }
void show() { cout << "this is class CB!" << endl; }
private:
shared_ptr<CA> m_ptr_a;
};
void test_refer_to_each_other()
{
shared_ptr<CA> ptr_a(new CA());
shared_ptr<CB> ptr_b(new CB());
cout << "a use count : " << ptr_a.use_count() << endl;
cout << "b use count : " << ptr_b.use_count() << endl;
ptr_a->set_ptr(ptr_b);
ptr_b->set_ptr(ptr_a);
cout << "a use count : " << ptr_a.use_count() << endl;
cout << "b use count : " << ptr_b.use_count() << endl;
}
测试结果如下:
CA() called!
CB() called!
a use count : 1
b use count : 1
a use count : 2
b use count : 2
通过结果可以看到,最后CA和CB的对象并没有被析构,其中的引用效果如下图所示,起初定义完ptr_a和ptr_b时,只有①③两条引用,然后调用函数set_ptr后又增加了②④两条引用,当test_refer_to_each_other这个函数返回时,对象ptr_a和ptr_b被销毁,也就是①③两条引用会被断开,但是②④两条引用依然存在,每一个的引用计数都不为0,结果就导致其指向的内部对象无法析构,造成内存泄漏。
解决这种状况的办法就是将两个类中的一个成员变量改为weak_ptr对象,因为weak_ptr不会增加引用计数,使得引用形不成环,最后就可以正常的释放内部的对象,不会造成内存泄漏,比如将CB中的成员变量改为weak_ptr对象,代码如下:
class CB
{
public:
CB() { cout << "CB() called! " << endl; }
~CB() { cout << "~CB() called! " << endl; }
void set_ptr(shared_ptr<CA>& ptr) { m_ptr_a = ptr; }
void a_use_count() { cout << "a use count : " << m_ptr_a.use_count() << endl; }
void show() { cout << "this is class CB!" << endl; }
private:
weak_ptr<CA> m_ptr_a;
};
测试结果如下:
CA() called!
CB() called!
a use count : 1
b use count : 1
a use count : 1
b use count : 2
~CA() called!
~CB() called!
通过这次结果可以看到,CA和CB的对象都被正常的析构了,引用关系如下图所示,流程与上一例子相似,但是不同的是④这条引用是通过weak_ptr建立的,并不会增加引用计数,也就是说CA的对象只有一个引用计数,而CB的对象只有2个引用计数,当test_refer_to_each_other这个函数返回时,对象ptr_a和ptr_b被销毁,也就是①③两条引用会被断开,此时CA对象的引用计数会减为0,对象被销毁,其内部的m_ptr_b成员变量也会被析构,导致CB对象的引用计数会减为0,对象被销毁,进而解决了引用成环的问题
5. 动态数组
5.1 new和数组
用new分配一个对象数组,我们要在类型名之后跟一对方括号。如下
int *pia = new int[100];
方括号中必须是整型,但不必是常量。
默认情况下,用new分配的对象,都是默认初始化的。可以对数组中的元素进行值初始化,在方括号后面加上一对括号即可。
int *pia = new int[100]();
string* psa = new string[10]();
//或者可以提供一个元素初始化器的花括号列表;
int *pia = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
//可以指定部分元素,剩余的进行默认初始化;
string* psa = new string[10]{"a", "an", "the", string(3, 'x')};
5.2 智能指针和动态数组
标准库提供了一个可以管理new分配的数组unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:
unique_ptr<int []> up(new int[10]());
up.release(); //自动用delete []销毁指针;
shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。
6. allocator类
new有一些灵活上局限,它将内存分配和对象构造组合在了一起。当我们分配一大块内存的时候,使用allocator将内存分配和对象构造分类,只在真正需要的时候才真正执行对象创建的操作。
allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来,它分配的内存是原始的、未构造的。
类似vector,allocator是一个模板,为了定义一个allocator对象,我们必须指明这个allocator可以分配得对象类型。当一个allocator对象分配内存时,他会根据给定得对象类型来确定恰当的内存大小和对齐位置:
allocator<string> alloc;
auto const p = alloc.allocate(n); //分配n个未初始化的string
| 函数 | 说明 |
|---|---|
| allocator a | 定义了一个名为a的allocator对象,它可以为类型T的对象分配内存 |
| a.allocate(n) | 分配一段原始的、未构造的内存,保存n个类型为T的对象 |
| a.deallocate(p, n) | 释放从T*指针p开始的内存,p必须是一个先前用allocate返回的指针,且n必须是p创建时所要求的大小。 |
| a.construct(p, args) | p同上,arg被传递给类型为T的构造函数,用来构造对象。 |
| a.destroy(p) | 对p所指向的对象执行析构函数。 |
标准库还为allocator类定义了两个伴随算法,用来拷贝和填充未初始化对象的算法。
| 函数 | 说明 |
|---|---|
| uninitialized_copy(b, e, b2) | 从迭代器b和e指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。 |
7. new的重载和placement new
placement new(std::size_t size, void* men),它的作用就是在申请好的内存空间上使用构造函数构造对象。例如在STL的allocater类中,它通过operate new和placement new实现了把内存申请和对象构造分开。当然operater new实际调用了malloc。置位new使用案例如下: