动态内存指的是在程序运行时阶段,由我们程序员自己负责分配和回收的内存。
C++用一对运算符管理动态内存:
- new
- 为对象分配空间,并返回指向该对象的指针,可选择初始化对象。
- 一次只能分配一个对象的内存块。
- delete
- 接受一个动态对象指针,销毁对象,并释放与之关联的内存。
- 一次只能释放一个对象。
一、new:分配动态内存
支持的操作
// 没圆括号,默认初始化:类执行默认构造,内置类型未定义。
T* p = new T; // 分配一个T类型对象的内存。
// T是类时,对象执行默认初始化
// T是内置类型时,值未定义。
// 有圆括号(),值初始化:类执行默认构造,内置类型有初始值,为0或者空。
T* P = new T(); // T是类时,对象执行默认初始化
// T是内置类型时,值初始化0。
T* P = new T(args); // args:逗号隔开的参数列表
// 调用T(args)构造函数初始化对象
T* p = new T[n]; // 分配一个长度为n,元素类型为T的数组空间,返回第一个元素指针。
// 元素执行默认初始化。
T* p = new T[n](); // 同上,但是元素执行值初始化。
T* p = new T[n]{...}; // 同上,{...}列表初始化数组元素
// 列表值数量不够,剩下的值初始化
// 列表值数量超标,分配失败,抛出异常:bad_array_new_length
const T* p = new const T(); // const版。
auto p = new auto(obj); // 与下等价
Type p = new Type(obj); // Type是obj的类型。
使用例子
int *pi = new int; // 值未定义
int *pi = new int(); // 值初始化为0
string *ps = new string; // 默认初始化,空string
string *ps = new string(); // 值初始化,空string
int *pia = new int[n]; // 方括号[]:表示动态分配一个数组。返回指向第一个元素的指针
int *pia = new int[10]; // 10个未初始化的int
int *pia2 = new int [10](); // 10个值初始化为0的int
string *psa = new string[10]; // 10个空string
string *psa2 = new string[10](); // 10个空string
int *pia3 = new int[10]{0,1,2,3}; // 数量不够,剩下的值初始化
// 数量超标,分配失败抛出异常
char *cp = new char[0]; // 正确:但cp不能解引用,new返回一个合法的非空指针
typedef int arrT[42];
int *p = new arrT;
int *pi = new int(1024) ;
string *ps = new string(10,' 9');
vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
Type obj{1,2,3}
auto p1 = new auto(obj); // 与下等价
Type p1 = new Type(obj);
auto p2 = new auto{a, b, c}; // 错误:括号中只能有单个初始化器
// 用new分配const对象是合法的:
const int *pci = new const int(1024); // 分配并初始化一个const int
const string *pcs = new const string; // 分配并默认初始化一个const的空string
new失败
new分配内存可能失败,比如内存耗尽,返回空指针,同时抛出一个bad_alloc异常。
可以用定位new的形式来阻止异常抛出。
int *p1 = new int; //如果分配失败,返回空指针,抛出异常std::bad_alloc
int *p2 = new (nothrow) int ; // 如果分配失败,new返回一个空指针,阻止抛出异常。
// 定位new,向new传递了额外的参数nothrow,告诉new不要抛异常
// nothrow、bad_alloc在new头文件中。
二、delete:释放动态内存
执行了两个动作:
- 销毁对象(析构)
- 释放内存
重复delete,行为未定义。
delete一个非动态分配对象(局部对象),这非常危险。
new出来的对象,一定要delete才会销毁,否则生命周期和程序一样长,如同全局变量。
delete p; // p必须指向一个动态分配的对象或是一个空指针,注意delete一个空指针是没有问题的代码。
delete[] parr; // 删除数组必须加上[],逆序销毁
// 忘记加[],行为未定义
const int *pci = new const int(1024);
delete pci; // 正确:释放一个 const 对象
pci = nullptr; // pci成了空悬指针(野指针),指向的地址非法了。手动赋值成空。
// 但这也仅仅是解决了pci的问题,其他相同指向的指针就爱莫能助了。
new、delete出错的三个常见原因:
1、shared_ptr
共享指针,共享一个动态对象。shared_ptr内部维护了一个动态对象的引用计数,表示这个动态对象被多少个共享指针所引用,当引用计数为0时,动态对象将被释放。
shared_ptr都有一个关联的引用计数器counter,记录着另外还有多少个shared_ptr指向所管理的动态对象。
- p.counter + 1
- shared_ptr p(q);
- shared_ptr p = q;
- fuck( p );
- return p;
- p.counter - 1
- p = q
- q的动态对象的引用计数+1,p的动态对象的引用计数-1,此时如果counter=0,则释放p的动态对象,最后p改成管理q的动态对象。
- p:“我不管以前的了,我要管q管的那个对象了”。
- p被销毁(如p作为局部变量被销毁),触发了析构函数。
- p = q
- counter == 0时
shared_ptr
shared_ptr
shared_ptr
shared_ptr
shared_ptr
make_shared
p; // 和普通指针一样操作。 p; // 解引用,获得指向的对象。 p->mem; // 等价于(p).mem p.get(); // 返回p中保存的内置指针 swap(p, q); // 交换指针 p.swap(q); // 同上
p = q; // p的引用计数-1,q的引用计数+1,计数为0,释放管理的内存。 // p、q是shared_ptr,所保存的指针必须能互相转换。
p.unique(); // p.use_count() == 1,当前p是否独占对象。 p.use_count(); // 与p共享对象的智能指针数量,可能很慢,用于调试。
// 如果p独占对象, p.reset()会释放对象 // p:“我不管手上的对象了,我要管q所管的对象” p.reset(); // 将p置为空,对象的智能指针引用计数-1 p.reset(q); // 等价于p = q,q是内置指针类型 p.reset(q, d); // 等价于p = q,调用d来释放q,而不是delete
<a name="eJBVA"></a>
### 使用例子
```cpp
shared_ptr<string> p1; // shared_ptr, 可以指向 string
shared_ptr<list<int>> p2; // shared_ptr, 可以指向 int 的 list
auto p3 = make_shared<int>(42); // 指向值为42的int的shared_ptr
auto p4 = make_shared<string>(2,'9'); // 指向值为“99”的string
auto p5 = make_shared<int>(); // 指向值初始化的int,即值为0
auto p6 = make_shared<vector<string>>(); // 指向动态分配的空vector<string>
p = new int(1024}; // 错误:不能将一个指针赋予shared_ptr
p.reset(new int(1024}}; // 正确:p指向一个新对象
if (!p.unique())
p.reset(new string(*p)); // 我们不是唯一用户,分配新的拷贝
*p += newVal; // 现在我们知道自己是唯一的用户,可以改变对象的值
shared_ptr<int> sp(new int[10], [](int *p){ delete[] p; }); // shared_ptr管理数组,必须提供删除器
sp.reset(); // 使用我们提供的lambda释放数组,它使用delete[]
// shared_ptr未定义下标运算符,并且不支持指针的算术运算
for(size_t i = 0; i != 10; ++i)
*(sp.get() + i) = i; //使用get获取一个内置指针
new、shared_ptr结合使用
shared_ptr<double> p1; // shared_ptr 可以指向一个double
shared_ptr<int> p2(new int(42)); // p2 指向一个值为 42 的 int
shared_ptr<int> p2(new int(1024)); // 正确:使用了直接初始化形式
shared_ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化形式,转换构造函数是explicit的。
注意,智能指针和内置指针不能混用,因为转换构造函数是explicit的。
void process(shared_ptr<int> ptr){}
int *x(new int(1024));
process(x); // 错误:不能将int*转换为一个shared_ptr<int>
process(shared_ptr<int> (x)); // 合法的,但process执行完之后,动态对象x的内存会被释放!
int j = *x; // 未定义的:x是一个空悬指针!
不要把get()值赋值给智能指针,因为get返回的是内置指针,赋值给智能指针的话,就对同一块内存形成了两套计数,也就是说会被释放2次!
shared_ptr<int> p(new int(42)); // 引用计数为 1
int* q = p.get(); // 正确:但使用 q 时要注意,不要让它管理的指针被释放
{
shared_ptr<int>(q);
}
// 程序块结束,q被销毁,它指向的内存new int(42)被释放,此时p将指向非法内存
int foo = *p ; // 未定义: p 指向的内存已经被释放了
使用规范
- 不使用相同的内置指针值初始化(或reset ) 多个智能指针。
- 不delete掉get()返回的指针。
- 不使用 get()初始化或reset另一个智能指针。
- 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
原理(简单代码实现)
```cpp
template
shared_ptr<T>& shared_ptr& operator=(const shared_ptr& sptr); // 拷贝赋值
T& operator*(); // (*p).fuck();
T* operator->(); // p->fuck();
T* getTarget() const; // 当前引用的动态对象
size_t useCount() const; // 当前引用计数
private: void release(); // 引用计数-1
private: size_t _count; // 动态对象_ptr的引用计数 T _ptr; // 引用的动态对象 }
template
template
template
template
auto tmpCount = sptr._count;
auto tmpPtr = sptr._ptr;
release();
_count = tmpCount;
_ptr = tmpPtr;
if(_ptr) ++(*_count);
return *this;
}
template
template
template
template
template
<a name="SgbEJ"></a>
### 循环引用
智能指针的目的是为了避免内存泄露,但智能指针本身也会产生内存泄露,比如循环引用,就是两个shared_ptr智能指针之间互相引用,导致这两个智能指针的引用计数一直不会归0,从而产生内存泄露,具体看下:
```cpp
class Parent; //
class Child; // Child和Parent结构完全相同。
class Parent {
public:
Parent() = default;
~Parent() = default;
void setChild(const shared_ptr<Child>& child){
_child = child;
}
public:
// 循环引用问题解决方法,只需要这里其中一个shared_ptr改为weak_ptr即可。
// 这样就打破了引用环。weak_ptr的介绍见下面。
shared_ptr<Child> _child;
};
int main(){
weak_ptr<Parent> parent;
weak_ptr<Child> child;
{
parent = shared_ptr<Parent>(new Parent);
child = shared_ptr<Parent>(new Child);
parent->setChild(child);
child->setParent(parent);
std::cout << "parent use count : " << parent.useCount() << std::endl; // 2
std::cout << "child use count : " << child.useCount() << std::endl; // 2
}
std::cout << "parent use count : " << parent.useCount() << std::endl; // 1,这里应该是0的
std::cout << "child use count : " << child.useCount() << std::endl; // 1
}
2、unique_ptr
独占指针,任意时刻最多只能有一个unique_ptr指针指向同一个对象,所以不支持拷贝控制,但是可以进行移动控制。
unique_ptr<int> ptr1(new int);
unique_ptr<int> ptr2;
ptr2 = ptr1; // 错误,这样发生了拷贝,就不是独占了,因此不会支持拷贝控制
ptr2 = std::move(ptr1); // 正确,ptr1将被置空。
支持的操作
unique_ptr<T> up;
p; // 和普通指针一样。
*p; // 解引用,获得指向的对象。管理的是new[]的数组时,不支持。
p->mem; // (*p).mem,管理的是new[]的数组时,不支持。
p.get(); // 返回p中保存的内置指针
swap(p, q); // 交换指针
p.swap(q); // 同上
p[i]; // 返回u拥有的数组中位置i处的对象,up必须指向一个数组
// 空unique_ptr,可以指向类型为T的对象
unique_ptr<T> u1; // 调用delete来释放对象
unique_ptr<T, D> u2; // D类型的可调用对象来释放对象。
unique_ptr<T, D> u(d); // 空unique_ptr, 指向类型为T的对象,
// 用类型为D的可调用对象d来代替delete
unique_ptr<T[]> u; // u可以指向一个动态分配的数组,数组元素类型为T
unique_ptr<T[]> u(p); // u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*
// up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up(new int[10]);
up.release(); // 自动用delete[]销毁其指针
u = nullptr; // 释放u指向的对象,将u置为空
u.release(); // u放弃对指针的控制权,返回内置指针,井将u置为空
// 并不会触发delete,仅仅是放弃。
// 管理的是new T[]的数组时,会触发delete[]销毁。
u.reset(); // 释放u指向的对象
u.reset(q); // 释放u指向的对象,令u指向内置指针q指向的对象
u.reset(nullptr); // 释放u指向的对象,u置位空。
使用例子
unique_ptr<string> p2(p1.release()); //将所有权从p1转移给p2,release将p1置为空
unique_ptr<string> p3(new string("Trex"));
p2.reset(p3.release()) ; // 1、p3.release()释放所有权
// 2、reset释放了p2原来指向的内存
// 3、p2拥有对p3管理对象的所有权。
p2.release(); // 错误:p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); // 正确,但我们必须记得delete(p)
unique_ptr<double> p1; // 可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42)); // p2指向一个值为42的int
unique_ptr<string> p1(new string( "Stegosaurus"));
unique_ptr<string> p2 (pl); // 错误:unique_ptr不支持拷贝构造
unique_ptr<string> p3;
p3 = p2; // 错误:unique_ptr 不支持拷贝赋值
unique_ptr<string> p4(new int(42));
unique_ptr<string> p5 = std::move(p4); // 移动构造,p4将被清空。
3、weak_ptr
“弱指针”,类似Lua的“weak table”(如果你知道的话)。
不控制所指向对象生存期的智能指针,始终指向一个由shared_ptr管理的对象,weak_ptr不影响shared_ptr的引用计数。可以解决shared_ptr带来的循环引用问题。
支持的操作
weak_ptr<T> w; // 空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp); // 与sp指向相同对象的weak_ptr。T必须能转换为sp指向的类型
// 记住,weak_ptr只和shared_ptr发生关系。
w = p; // p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象
w.reset(); // 将w置为空
w.use_count(); // 与w共享对象的shared_ptr的数量
w.expired(); // w.use_count() == 0
w.lock(); // 如果expired为true,返回一个空shared_ptr:
// 否则返回一个指向w的对象的shared_ptr
// 不能直接用weak指针来访问对象,应该使用lock返回shared_ptr来访问
// 这样的话才能确保对象不存在了也不会出错。
规范使用
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p,p的引用计数未改变
.....
// 使用前必须lock确保对象还在
if (auto np = wp.lock()){ // np不为空说明所指对象还在。
}
四、allocator(内存池)
解决需求:先分配一大块内存,后面按需初始化创建对象。
功能:内存分配与对象构造分离。
定义在memory头文件,是一个模板。
根据给定的对象类型来确定恰当的内存大小和对齐位置
支持的操作
allocator<T> allocator; // 定义一个可以为T类型对象分配内存的分配器。
auto p = a.allocate(n); // 分配(原始)内存
// 分配一段原始内存(为构造初始化),可以保存n个T类型对象。
// n=0,返回nullptr
a.deallocate(p, n); // 释放(原始)内存
// allocate的反过程,参数p、n必须是上面的p、n。
// 正确的逻辑应该是在此之前调用析构函数,
a.construct(p, args); // 在原始内存中构造对象
// 在p所指内存上构造一个T对象,构造参数args,
// p指向allocate分配的原始内存的某一个地址。
a.destroy(p); // 触发p所指对象的析构函数。
使用例子
auto q = p; // q指向最后构造的元素之后的位置
alloc.construct(q++); // *q为空字符串
alloc.construct(q++, 10, 'c'); // *q为cccccccccc
alloc.construct(q++, "hi") ; // *q为hi!
cout << *q << endl; // 灾难:q指向未构造的内存!
while(q != p) alloc.destroy(--q); // 析构前面构造的内存
allocator<string> alloc; // 可以分配string的allocator对象
auto const p = alloc.allocate(n); // 分配n个未初始化的string
allocator算法
拷贝和填充原始内存的算法。
// 这些函数在给定目的位置创建元素,而不是由系统分配内存给它们。
uninitialized_copy(b,e,b2); // 迭代器b、e范围拷贝到b2开始的内存。必须足够大。
uninitialized_copy_n(b,n,b2); // n个迭代器b的元素拷贝到b2开始的内存,必须足够大。
// 返回尾迭代器,后面可以接着分配。
uninitialized_fill(b,e,t); // 在迭代器b、e内存范围填充t的拷贝
uninitialized_fill_n(b,n,t); // 从迭代器b指向的内存地址开始,填充n个t的拷贝。
// 原始内存必须足够大。
vector<int> vi; // 假设填充了若干元素。
auto p = alloc.allocate(vi.size() * 2); // 分配2倍的动态内存空间。
auto q = uninitialized_copy(vi.begin(), vi.end(), p); // 范围拷贝
uninitialized_fill_n(q, vi.size(), 42); // 剩下的用42来初始化
注意
allocator的内容本身比较简单,但是非常容易出错,典型的问题就是deallocate之前忘记destroy。
五、C动态内存管理
分配内存
- malloc
- 分配(连续的)动态内存,返回内存地址指针,内存不足返回NULL。
- calloc
- 分配动态内存,会初始化为0,和malloc功能类似。
- realloc
- 重新分配动态内存。对原有的动态内存要么扩大要么缩小。扩大,新内存接到尾部,缩小释放尾部内存。
- alloca
- 在栈上申请内存。
- 程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。 ```cpp
void malloc(size_t size); void calloc(size_t elem_num, size_t elem_size); void realloc(void ptr, unsigned int new_size);
realloc(NULL, new_size); // 两者等价。 malloc(new_size); // 两者等价。
<a name="iCHfH"></a>
## 释放内存
free:把动态内存返回给内存池。
```cpp
void free(void* ptr); // ptr必须是malloc、calloc、realloc返回的指针。
一个安全的C动态内存分配器
malloc、calloc、realloc可能返回NULL导致程序崩溃,但很有可能是程序本身逻辑问题导致,我们可以隐藏这些库函数,代替以封装的函数。
// alloc.h
#include <stdio.h>
#include <stdlib.h>
#define malloc // 禁止使用malloc
#define MALLOC(elem_num, elem_size) // alloc((elem_num) * sizeof(elem_size))
void* alloc(size_t size);
// alloc.c
#include "alloc.h"
#include <iostream>
#undef malloc
using std::cout;
using std::endl;
void* alloc(size_t size){
if(size == 0) return nullptr;
void* pRet = nullptr;
pRet = malloc(size);
if(!pRet) {
cout<< "out of memory." << endl;
exit(1);
}
return pRet;
}
void function() {
int *new_memory;
new_memory = MALLOC(25, int);
}
六、内存泄露(Memory Leak)
由于疏忽或错误导致程序未能释放已经不再使用的内存。一般就是指堆内存泄露。
char *p = (char*) malloc(10);
char *p1= (char*) malloc(10);
p = p1; // p原本指向的动态内存泄露了。