- 1.智能指针
- 1.1.智能指针简介
- 1.2.shared_ptr——基于引用计数的共享内存解决方案
- 1.2.1.基本用法
- 1.2.2.reset/get方法
- 1.2.3.指定内存回收逻辑
- 1.2.4.std::make_shared
- 1.2.5.shared_ptr支持数组
- 1.2.6.shared_ptr不要用delete删除
- 1.3.unique_ptr——独占内存的解决方案
- 1.3.1.基本用法
- 1.3.2.unique_ptr不支持复制,但可以移动
- 1.3.3.指定内存回收逻辑
- 1.4.weak_ptr——防止循环引用而引入的智能指针
- 2.C++动态内存中的一些问题
- 智能指针引入目的:使用new/delete内存的所有权不清晰。如下代码所示,可以在函数外销毁指针,也可以在函数内销毁指针,从而造成了内存所有权不清晰的情况。本质上是因为new/delete是非常紧密的两个操作,但是被我们认为分开了。
// 1.在函数外销毁指针
int* fun()
{
int* res = new int(3);
return res;
}
int main()
{
int* y = fun(); // 这个指针只能在fun函数外销毁了
}
// 2.在函数内部销毁,如下静态内存,是在fun析构的时候被自动释放
int* fun()
{
static int res(3);
return &res;
}
int main()
{
int* y = fun(); // 这个指针不能在fun函数外手动delete
}
- 智能指针:它本质上是一个对象,在其析构函数里进行delete操作。auto_ptr在C++11之前提出,性能不好,所以不建议使用。现在更多的使用shared_ptr/uniuqe_ptr/weak_ptr。
- 引用计数的基本用法:
#include#include // 智能指针头文件 int main() { std::shared_ptr x(new int(3)); // 引用计数为1 std::shared_ptr y = x; // 引用计数为2,x和y指向相同的内存地址 } // main结束的时候,调用shared_ptr的析构函数:先对引用计数-1, // 如果引用计数为0,那么释放对应的内存。比如上述先析构y,引用计数变成1,不释放内存;再析构x,引用计数变成0,释放new int(3)的内存
- 使用shared_ptr解决之前的问题:
疑问:不确定下面的代码智能指针的引用计数到底是1还是2?
std::shared_ptr1.2.2.reset/get方法fun() { std::shared_ptr res(new int(3)); return res; } int main() { // 这里不用担心谁来管理这个指针的问题,因为shared_ptr会自动处理。 std::shared_ptr y = fun(); } // 简单分析一下上述逻辑,析构时应该是这样的(不确定?): // 1.析构y,因为fun函数里有一个shared_ptr,所以y的引用计数为2,-1=1 // 2.fun函数被析构,函数内部有一个shared_ptr,析构时引用计数-1变成0,释放内存
- get返回这个智能指针的地址,比如int*。这个主要目的是为了和没有使用智能指针的代码兼容。如下所示:
void fun(int* ptr)
{}
int main()
{
std::shared_ptr x = new int(3);
fun(x.get());
}
- reset重新关联内存,如下所示,x.reset会先判断x是否关联了内存,如果关联了内存那么先释放,然后重新关联新输入的内存。如果reset不传入任何参数,那么默认会释放原先关联的内存,然后把现在的智能指针绑定到nullptr上。
// 1.reset传入参数,先析构原对象,再绑定到新的对象上 std::shared_ptr1.2.3.指定内存回收逻辑x = fun(); x.reset(new int(3)); // 2.reset不传入任何参数,先析构原对象,再绑定到nullptr上 x.reset(); // 上述等价于: x.reset((int*)nullptr); // 注意这里必须显示把nullptr转化成int*,因为nullptr是nullptr_t类型,不是int*类型
- 可能有的时候由于特殊需要,认为智能指针的delete太简单了,需要附加一些功能,就可以添加一个删除逻辑。感觉和类的析构函数很像?但是感觉这个好处是可以通过这种方式来测试智能指针什么时候析构。
void fun(int* ptr)
{
std::cout << "Call deleter funn";
delete ptr;
}
int main()
{
// 析构的时候不会直接删除,而是把new int(3)的指针传给fun函数,由fun函数执行删除操作
std::shared_ptr x(new int(3), fun);
}
- 另外一种用法是针对静态变量的指针的析构,不应该使用delete来销毁内存,因为静态变量的内存应该是整个程序结束之后自动释放的。所以此时可以使用重定义deletor来避免默认的delete释放内存。
void dummy(int*) {}
std::shared_ptr fun()
{
static int res = 3;
return std::shared_ptr(&res, dummy);
}
int main()
{
{
std::shared_ptr x = fun();
}
// 出了作用域x被销毁,此时不会调用默认的delete函数错误的释放静态内存,
// 而是进入dummy函数什么都不干,从而保证程序的正确性
}
1.2.4.std::make_shared
建议使用std::make_shared来构造智能指针。这个原因和shared_ptr内部工作原理相关:其包含原始内容的指针和引用计数的指针,注意引用计数一定是一个指针,因为指向同一块内存的智能指针是共享引用计数的,要想共享只能使用指针了。
- 如果使用普通写法,那么可能构造的对象的指针和引用计数的指针相距很远,这样每次访问引用计数之后再访问对象指针的时候就会产生cache miss降低性能。
- 而使用make_shared写法,传入的不是new的一个指针,而是直接把对象的值传给他,这样它内部会引入优化机制,让对象的指针和引用计数的指针尽量靠近,这样访问性能更高。
// 1.普通写法:不推荐 std::shared_ptr1.2.5.shared_ptr支持数组x = new int(3); // 2.std::make_shared写法:推荐 std::shared_ptr x = std::make_shared (3); // 上面的写起来很麻烦,但是可以使用auto来简化 auto x = std::make_shared (3); // 和上面的完全等价
- 如下的代码所示,指针指向的是数组,但是释放的时候默认的仍然是释放对象的形式,所以是不合法的。
std::shared_ptrx(new int[5]); // 析构的时候默认的调用是delete (x.get()); 显然是不对的
- 在C++17之前,这种问题没有什么好的解决方法,只能像2.3一样引入特定的内存回收逻辑,然后在里面调用delete[]释放数组内存。但是从C++17开始,可以使用新的方式来解决这个问题:
std::shared_ptr1.2.6.shared_ptr不要用delete删除x(new int[5]); // 释放使用delete[], 不会有问题
因为手动删除的时候,shared_ptr并不知道,也就是它的引用计数并不会变化,所以shared_ptr析构的时候会产生重复释放的错误。还有一些其他类似的问题:
// 1.手动删除shared_ptr关联的指针
{
std::shared_ptr x(new int(3));
delete x.get();
} // 问题:在析构x的时候,重复删除它绑定的指针
// 2.手动删除shared_ptr关联的指针
{
std::shared_ptr x (new int(3));
std::shared_ptr y (x.get());
} // 问题:两个智能指针没有通过复制的方式绑定,而是直接绑定原始指针,
// 导致引用计数不同步,析构y的时候就释放ptr的内存了,析构x的时候再次 释放ptr内存导致错误
// 3.手动删除shared_ptr关联的指针
{
int* ptr = new int(3);
std::shared_ptr x(ptr);
std::shared_ptr y(ptr);
delete x.get();
} // 问题:两个智能指针没有通过复制的方式绑定,而是直接绑定原始指针,
// 导致引用计数不同步,析构y的时候就释放ptr的内存了,析构x的时候再次释放ptr内存导致错误
1.3.unique_ptr——独占内存的解决方案
1.3.1.基本用法
#include1.3.2.unique_ptr不支持复制,但可以移动#include int main() { std::unique_ptr x(new int(3)); // 独占内存 std::unique_ptr y = x; // 错误
注意移动是什么意思,就是把一个左值变成将亡值,也就是之后不会再调用这个变量。所以使用move的时候会把这个指针的内容交给另一个指针,接着这个指针变成将亡值,然后就被析构掉了。
1.3.3.指定内存回收逻辑这个和shared_ptr不太一样,C++标准里规定要类模板里要加入deletor的类型,如下所示:
- 引入原因:出现了循环引用。如下代码所示,在程序执行结束后,x和y对应的内存都不会被释放。原因是在构造xy的时候他们的引用计数都是1,但是在下面赋值他们的隐居的时候,x的邻居赋值y,导致y的引用计数变成2;同理y的邻居赋值x,导致x的引用计数变成2。这样在析构xy的时候,他们引用计数变成1,从而不会被释放。根本原因是xy这两个智能指针构成了一个环,从而造成循环引用。
struct Str
{
std::shared_ptr nei; //neighbor,邻居
~Str()
{
std::cout << "~Str is calledn";
}
}
int main()
{
std::shared_ptr x(new Str());
std::shared_ptr y(new Str());
x->nei = y;
y->nei = x;
}
- 解决:打破环,但是实际当中有时候我们必须使用这个环,所以这个方法不行。为了解决这个问题,C++引入了weak_ptr,weak_ptr其内部本质就是一个shared_ptr,但是当复制的时候,并不会增加引用计数,所以这样就解决了循环引用导致引用计数增加的问题。比如上述问题可以如下解决:
struct Str
{
std::weak_ptr nei; //neighbor,邻居
~Str()
{
std::cout << "~Str is calledn";
}
}
int main()
{
std::shared_ptr x(new Str());
std::shared_ptr y(new Str());
x->nei = y; // 这里shared_ptr可以赋值给weak_ptr
y->nei = x;
}
- lock方法:没有太听懂。。。。
- sizeof不会返回动态分配的内存大小:这个是因为new返回的都是一个指针,所以sizeof得到的都是指针的大小,对于一个特定平台其大小是确定的,比如64位机就是8个字节。类似的,使用std::vector也不能用sizeof得到动态分配的内存大小。其实本质上这个是因为sizeof是编译期获得的,而动态内存是在运行期获得的。
- 使用分配器allocator来分配内存:注意分配器只负责分配内存,并不会对对象进行初始化。分配器是一个模板,可以传入不同的对象,来分配不同类型的内存。
std::allocatoral; int* ptr = al.allocator(3); // 分配3个int内存
- 使用malloc/free管理内存:继承自C语言的内存分配/释放,但是它只能分配分配内存,并不能在内存上构造对象。这是因为在C语言中没有对象的概念,所以对构造对象也不重视。使用的时候需要包含头文件#include
。 - 注意allocator和malloc的区别:allocator是类模板,必须分配的内存和存储的对象类型相同,而malloc只关注分配的内存大小,不管存储的对象是什么。
- 使用aligned_alloc分配对齐内存:C语言引入的,并不是C++引入的。仍然建议使用allocator分配内存,不建议使用malloc和aligned_alloc分配内存。



