c++中的动态内存管理
在C++中, 动态内存管理涉及 new 和 delete 操作符, 前者负责在堆内存分配并初始化一个对象并返回其指针, 而后者销毁指针所指向的对象, 并回收相关内存.
malloc 与 free 是包含在 <cstdlib> 中的标准库函数, 前者用于分配 size bytes 大小的内存并返回指向该内存块起始地址的 void* 类型指针 (当内存不足时返回一个空指针), 而后者用于释放内存. 当使用 free 释放不经由 malloc (或 calloc, realloc) 分配的内存时, 会产生未定义行为.
/* malloc example: random string generator*/
#include <stdio.h> /* printf, scanf, NULL */
#include <stdlib.h> /* malloc, free, rand */
int main ()
{
int buffer_size;
char * buffer = nullptr;
buffer = (char*) malloc (buffer_size);
if (buffer) {
//... do something
}
free (buffer);
return 0;
}
malloc
free
内存泄漏(memory leak): 指程序未能释放已经不再使用的内存的情况.
内存泄漏的情形
- 指针变量超出作用范围而其所指的内存没有被释放:
void foo() {
int *p=new int(10);
... // 没有 delete p
return;
}
// 此时已经超出 p 的作用范围, 而 p 指向的内存没有被释放.
2. 指针变量指向其他对象的地址, 同时既没有其他指针指向其所指的内存, 而该内存区域也没有被释放:
SomeClass *foo=new SomeClass(...);
foo=nullptr;
3. 数组指针没有使用 delete[] 释放内存
SomeType* p = new SomeType[1000];
...
delete p; // 使用 delete [] p 删除
李健的回答
注: 当需要销毁的是带有析构函数的对象时, 使用 delete 将会调用被销毁对象的析构函数, 而使用 delete [] 则会逐一调用数组内对象的虚构函数. 如果是销毁基本数据类型数组, 则 delete 与 delete [] 实际效果相同, 因为分配基本类型内存时,内存大小已经确定, 系统可以通过指针直接获取实际分配内存空间的大小.
4. 基类析构函数没有声明为虚析构函数, 而在子类中有新分配的堆内存.
轮子哥的例子
注: 基类中声明为虚(virtual)的函数, 在派生类中的同名函数(函数名, 返回类型, 参数个数与类型均相同)都自动成为虚函数.
5. 使用 shared_ptr 有循环引用.
6. 使用指针时抛出异常导致跳过 delete 语句.
void foo() {
int *p = new int(40);
...
if(something_happend) {
throw exception;
}
delete p;
}
以上两条参见 「已注销」同学的回答
悬垂指针 (dangling pointer): 这种情况是指针所指向的对象已经被销毁而指针没有做出相应修改使其仍然指向已经回收的内存地址.
产生悬垂指针的一种情况是使用 delete 或 free 将指针指向的内存释放后, 指针就成为悬垂指针, 这时需将指针重置为 nullptr (自C++ 11) 或 NULL.
// delete 之后, 未将指针置为空
int *ptr = new int(5);
...
delete ptr; //此时ptr为悬垂指针
ptr = nullptr; // 自 c++11 可使用 nullptr 关键字, 或置为 NULL
另一种情况指针保存了一个超出变量作用域的变量的地址 (如函数), 一旦超出变量作用域 (如函数返回), 则分配给这些变量的空间被回收.
int *func(void)
{
int num = 1234;
...
return #
} // 超出 num 作用域, num 占用的内存被系统回收.
野指针(wild pointer) 是指指针未经初始化 (甚至不是 NULL 或 nullptr), 其结果不可预料.
Text onlyint *p; // 此时指针 p 未初始化, 是野指针
int x = new int(10);
p = &x; // 将 x 的地址赋予p 此时 p 不再是野指针
NULL 与 nullptr 关键字
宏 (macro) NULL 是一个依赖于具体实现的空指针常量(null pointer constant), 可能是一个值为 0 的常亮表达式, 或者是一个转换为 void* 类型的 0值常亮表达式. 下面是 C中可能的定义:
// C++ compatible:
#define NULL 0
// C++ incompatible:
#define NULL (10*2 - 20)
#define NULL ((void*)0)
在 C 中, NULL
可能被定义为void*
类型, 但在 C++ 中不被允许.
在 C++11 之前, NULL 是一个 整数型常量表达式右值, 其值为 0; 而自 C++11 之后被定义为一个0值 整数型字面量(literal), 或 纯右值 (prvalue) 类型 std::nullptr_t. 可能的定义如下:
#define NULL 0
//since C++11
#define NULL nullptr
一个空指针常量可能被显示转换为任何指针类型, 如果一个空指针常量是整数型, 它可能被转换为 std::nullptr_t 类型的纯右值.
参考:
NULL in C
NULL in C++
附:
为什么建议你用nullptr而不是NULL - 守望的文章 - 知乎
智能指针
c++中的智能指针在头文件<memory>中定义.
auto_ptr (自c++11弃用), shared_ptr , weak_ptr, unique_ptr (自c++11)
将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。所有的智能指针类都有一个explicit构造函数,以指针作为参数。比如auto_ptr的类模板原型为:
templet<class T>
class shared_ptr {
explicit auto_ptr(X* p = 0) ;
...
};
因此不能自动将指针转换为智能指针对象,必须显式调用
# include <memory>
std::shared_ptr<std::string> ps (new std::string(str));
double *p_dou = new double;
shared_ptr<double> sp_d;
sp_d = shared_ptr<double>(p_dou);
// 或者 shared_ptr<double> sharedptr_d(p_dou);
应避免将智能指针指向非堆内存, 以避免将delete运算符用于非堆内存而产生错误.
auto_ptr (自 C++11 弃用)
智能指针auto_ptr在被赋值操作的时候,被赋值的取得其所有权,去赋值的丢失其所有权。在下的代码示例中: 赋值后, pstr 丢失字符串的所有权, 变成空指针, 而nptr获取该字符串对象的所有权.
auto_ptr< string> pstr (new string ("Hello World.");
auto_ptr<string> nptr;
nptr = ps;
若采用智能指针数组, 在将数组内的auto_ptr进行赋值后, 数组内的元素丢失对象所有权
auto_ptr<string> strs[3] =
{
auto_ptr<string> (new string("Dynamic Memory")),
auto_ptr<string> (new string("Smart Pointers")),
auto_ptr<string> (new string("Memory Management")),
};
auto_ptr<string> pstr;
pstr = strs[2]; // strs[2]失去所有权变成空指针
...
for(int i = 0; i < 3; ++i)
cout << *strs[i] << endl; // 当i==2时,出现问题
unique_ptr 提供独享所有权, 即当unique_ptr超出生存周期时, 对象被销毁. unique_ptr 不能被直接拷贝或赋值. 简而言之 unique_ptr 具有下列特点:
1、拥有它指向的对象,
2、无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作,
3、保存指向某个对象的指针,当它本身的生命周期结束的时候,会使用给定的删除器释放它指向的对象.
注: unique_ptr 不能用于赋值操作, 但可以std::move 转移对象的所有权.
...
unique_ptr<string> ptest(new string("Hello world."));
foo(std::move(ptest));
自 C++ 14 开始 可以在标准库中使用 make_unique 函数
unique_ptr<int> pi = make_unique<int>(10);
一个简单的 make_unique 实现,
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
参考: 在 C++11 中编写 make_unique 函数
(TODO 移动语义与完美转发 )
shared_ptr 提供共享所有权, 只有当最后一个指向同一对象的shared_ptr超出生存周期时, 对象才被销毁. shared_ptr 使用引用计数 (reference count) 决定它所指向的对象是否应被销毁. 下面使用共享智能指针的示例取自 C++ Primer, Fifth Edition.
shared_ptr<string> p1; // 指向 string 的共享智能指针 shared_ptr
shared_ptr<list<int>> p2; // 指向 整数型数组 的共享智能指针 shared_ptr
使用智能指针的方式类似于普通指针, 解引用智能指针返回其所指对象; 如果在条件语句中使用智能指针, 则需要测试其是否为空(null):
// 如果 p1 不为 null, 检查它是否为一个空字符串
if (p1 && p1->empty())
*p1 = "hi"; // 如果是, 解引用 p1 并赋一个新的字符串值
使用 make_shared 函数是一种安全的分配动态内存的方式 (C++ Primer, Fifth Edition, 12.1.1. The shared_ptr
Class):
// shared_ptr 指针 p3 指向一个值为 42 的整数型变量
shared_ptr<int> p3 = make_shared<int>(42);
// p4 指向一个值为 "9999999999" 的字符串变量
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5 指向一个整数型变量并将其初始化为 0 (§ 3.3.1 (p. 98))
shared_ptr<int> p5 = make_shared<int>();
// 使用关键字 auto 自动推断 p6 的类型.
auto p6 = make_shared<vector<string>>();
注: 如果从同一个指针构造两个独立的共享指针, 则该指针指向的内存将会被释放两次从而产生错误.
int *p = new int(1);
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p); // 如果sp1 已经释放 p 所指向的内存, sp2 将再次释放从而产生错误.
注: 如果将 shared_ptr 放入容器, 需记得 erase 不再需要的元素.
weak_ptr 指向由 shared_ptr 管理的对象, 并且不增加 shared_ptr 的计数.
weak_ptr 是 shared_ptr 的补充, 它并不拥有对对象的所有权, 即不增加 shared_ptr 中的引用计数; 同样的它也不保有对象的指针, 因此我们不能通过 weak_ptr 访问对象. 必须将 weak_ptr 转换为 shared_ptr 才能访问其所引用的对象. 换句话说, weak_ptr 建模了临时所有权, 只有当要访问对象存在时才起作用. 下面的例子取自 std::weak_ptr/cppreference.com 显示了如何使用 lock() 函数获取它所关联的对象的 shared_ptr 指针.
#include <iostream>
#include <memory>
std::weak_ptr<int> gw;
void observe()
{
std::cout << "use_count == " << gw.use_count() << ": ";
if (auto spt = gw.lock()) { // Has to be copied into a shared_ptr before usage
std::cout << *spt << "n";
}
else {
std::cout << "gw is expiredn";
}
}
int main()
{
{
auto sp = std::make_shared<int>(42);
gw = sp;
observe();
}
observe();
}
使用 weak_ptr 可以防止在循环引用中无法通过引用计数判断对象是否应被销毁, 只需要将循环引用中的一个指针设为 weak_ptr.
将weak_ptr传递给shared_ptr的构造函数,要是对象已被析构,则抛出std::exception异常
share_ptr 的结构
shared_ptr 内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块中包含一个引用计数(reference count), 一个弱计数(weak count)和其它一些数据。控制块数据保存在堆(heap)中以便在多个shared_ptr之间共享。
借助移动语义(std::move()) 可以通过 unique_ptr 来构造 shared_ptr :
unique_ptr<string> p1{ new string("senlin") };
shared_ptr<string> p2{ std::move(p1) };
参考:
谈谈 shared_ptr 的那些坑
Pros and cons of make_shared vs. normal shared_ptr construction
附1: C++智能指针与析构函数
C++智能指针2:(虚?)析构函数(标准与实现的差异) - ParseJW的文章 - 知乎
ParseJW:C++智能指针2:(虚?)析构函数(标准与实现的差异)
附2: JAVA 的垃圾回收机制
JVM中使用垃圾回收机制回收不再被引用的实例对象并释放其所占用的内存. 判断内存单元是否还被对象使用以确定是否回收该内存区域. 判断方法包括引用计数器和对象引用遍历.
引用计数器的原理是设置一整数型变量记录某对象是否被引用以及引用次数. 当对象被创建时, 该对象将计数器的值初始化为1, 每当有新引用指向对象时, 计数器 +1; 每当有引用结束其生命周期时, 计数器 -1; 当计数器的值变为0时, 就将该对象回收. 引用计数有一个问题, 即当出现循环引用时, 计数将永远不会为0, 处于循环中的对象永远不会被回收.
对象遍历是指从一组根对象(被称为GC Roots)出发, 沿对象关系图上的每一条引用链(Reference Chain)递归确定当前可达对象. 如果某对象不能从这些根对象到达, 则将其回收.