C++基础之动态内存
C++支持动态分配对象,它的生命周期与它们在哪里创建无关,只有当显示的被释放时,这些对象才会被销毁。分配在静态或栈内存中的对象由编译器自动创建和销毁。
new在动态内存中为对象分配空间并返回一个指向该对象的指针,并调用构造函数构造对象;delete接受一个动态对象指针,调用析构函数销毁该对象,并释放与之相关的内存。
那么new、delete和malloc、free有什么区别和联系呢?
1、new/delete是C++的操作符,而malloc与free是C++/C 语言的标准库函数,不在编译器控制权限之内。
2、new做两件事,一是分配内存,二是调用类的构造函数;同样,delete会调用类的析构函数和释放内存。而malloc和free只是分配和释放内存。
3、new建立的是一个对象,而malloc分配的是一块内存;new建立的对象可以用成员函数访问,不要直接访问它的地址空间;malloc分配的是一块内存区域,用指针访问,可以在里面移动指针;new出来的指针是带有类型信息的,而malloc返回的是void指针。
4、new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。
5、new自动计算需要分配的空间,而malloc需要手工计算字节数
6、new是类型安全的,而malloc不是,比如:
int* p = new float[]; // 编译时指出错误
int* p = malloc(*sizeof(float)); // 编译时无法指出错误
new operator 由两步构成,分别是 operator new 和 construct
7、operator new对应于malloc,但operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上。而malloc无能为力
8、new将调用constructor,而malloc不能;delete将调用destructor,而free不能。
9、malloc/free要库文件支持,new/delete则不要。
new动态分配内存
new无法为其分配的对象命名,而是返回一个指向该对象的指针;默认情况下,动态分配的对象是默认初始化,因此内置对象和组合对象是未初始化的。
int *p1 = new int;//动态分配一个未初始化的无名对象
string *p2 = new string;//默认初始化为空string
养成一个为动态分配的对象初始化的好习惯。
如果提供括号包围的初始化器,则可以通过初始化器来推断对象类型,就可以使用auto,但是这是当括号中仅有单一初始化器的时候才能使用auto。
动态分配一个const对象必须进行初始化。
auto p1 = new auto("haha");
const int *p2 = new const int();
默认情况下,new操作符时内存不足会抛出一个bad_alloc的异常。
但是还有定位new的形式,使得此时不抛出异常,而返回一个空指针。定位new允许我们想new中传递额外的参数。
int *p = new (nothrow) int;//如果失败,返回空指针
delete释放内存
通常,编译器是不能分辨一个指针是指向的静态对象还是动态对象,所以下面的操作是错的,但是编译期检查不出来。
int i = ;
int *p = &i;
delete p;//释放局部变量,错误
注意:new和delete经常出现的三种错误
- 忘记释放内存;
- 使用已经释放的内存;
- 同一块内存释放两次。
delete之后重置指针为空是一个好习惯,悬空指针很危险,通常上面的第二个错误就是悬空指针引起的;但是这样也只是提供有限的保护。
一、智能指针
动态分配内存容易出现问题,因为确保在正确的时间释放内存很困难,我们可能经常忘记释放的内存。于是C++11的标准库中提供了两种智能指针来管理动态对象,它们自己负责动态释放所指向的对象。
分别是:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。
shared_ptr和unique_ptr都支持的操作如下:
操作 | 描述 |
shared_ptr<T> sp unique_ptr<T> up |
空智能指针,可以指向类型为T的对象 |
p | 将p用作一个条件判断,若p指向一个对象,则为true |
*p | 解引用p,获得它指向的对象 |
p->mem | 等价于(*p).mem |
p.get() | 返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了。 |
swap(p,q) p.swap(q) |
交换p和q中的指针 |
1.shared_ptr类
智能指针也是模板,需要提供指向的类型,且默认初始化时,智能指针保存着一个空指针。
shared_ptr<string>sp;//可以指向string的空智能指针
相关操作如下:
操作 | 描述 |
make_shared<T>(args) | 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化对象。 |
make_shared<T>p(q) | p是shared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T* |
p = q | p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。 |
p.unique() | 若p.use_count()为1,返回true;否则返回false |
p.use_count() | 返回与p共享对象的智能指针数量;可能很慢,主要用于测试 |
最安全的分配和使用动态内存的方法是使用make_shared的函数,它和顺序容器的emplace成员类似,它的参数和指向的对象的某个构造函数匹配。
通常可以使用auto来推断类型,方便使用。
shared_ptr<string>sp = make_shared<string>(,'');//指向一个值为“9999999999”的string
auto spInt = make_shared<int>();
每个shared_ptr都会记录有多少个其他的shared_ptr志向相同的对象。因此,可以认为每个shared_ptr都有一个与之关联的计数器,通常称为引用计数。当一个shared_ptr的引用计数为0,它就会自动释放自己所管理的对象。
auto r = make_shared<int>();
r = q;//给r赋值,是它指向另一个对象;则q指向的对象的引用计数会递增,r原来指向的对象的引用计数会递减,如果减为0,则释放该对象。
如果你将shared_ptr存放在一个容器中,而后不再需要全部元素,只使用其中一部分,则要记得使用erase删除不再需要的那些元素。
下面的三种情况下使用动态内存:
- 程序不知道自己需要多少个对象;
- 程序不知道需要的对象准确类型;
- 程序需要在多个对象之间共享数据。
#include <iostream>
#include <string>
#include <memory> //智能指针和动态分配内存
#include <vector>
#include <initializer_list> //初始值列表
#include <stdexcept> class StrBlob
{
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string>il);
size_type size()const{ return data->size(); }
bool empty() { return data->empty(); }
//添加删除元素
void push_back(const std::string &s){ data->push_back(s); }
void pop_back();
//访问元素
std::string& front();
std::string& back();
const std::string& front()const;
const std::string& back() const; private:
std::shared_ptr<std::vector<std::string>> data;
//private 检查函数。
void check(size_type i, const std::string &msg)const;
}; //默认构造函数
StrBlob::StrBlob():
data(std::make_shared<std::vector<std::string>>()) { }
//拷贝构造函数
StrBlob::StrBlob(std::initializer_list<std::string>il):
data(std::make_shared<std::vector<std::string>>(il)) { } void StrBlob::check(size_type i, const std::string &msg)const
{
if(i >= data->size())
throw std::out_of_range(msg);
} const std::string& StrBlob::back()const
{
check(, "back on empty StrBlob");
return data->back();
} //避免代码重复和编译时间问题,用non-const版本调用const版本
//在函数中必须先调用const版本,然后去除const特性
//在调用const版本时,必须将this指针转换为const,注意转换的是this指针,所以<>里面是const StrBlob* 是const的类的指针。
//调用const版本时对象是const,所以this指针也是const,通过转换this指针才能调用const版本,否则调用的是non-const版本,non-const调用non-const会引起无限递归。
//return时,const_cast抛出去除const特性 std::string& StrBlob::back()
{
const auto &s = static_cast<const StrBlob*>(this)->back(); //<span style="color:#FF0000;">auto前面要加const,因为auto推倒不出来const。</span>
return const_cast<std::string&>(s);
} const std::string& StrBlob::front()const
{
check(, "front on empty StrBlob");
return data->front();
} std::string& StrBlob::front()
{
const auto &s = static_cast<const StrBlob*>(this)->front();
return const_cast<std::string&>(s);
} void StrBlob::pop_back()
{
check(, "pop_back on empty StrBlob");
data->pop_back();
} int main()
{
std::shared_ptr<StrBlob>sp;
StrBlob s({"wang","wei","hao"});
StrBlob s2(s);//共享s内的数据
std::string st = "asd";
s2.push_back(st);
//s2.front();
std::cout << s2.front() << std::endl;
std::cout << s2.back() << std::endl;
}
还可以使用new返回指针来初始化智能指针,此时智能指针的构造函数是explicit。
shared_ptr<int>p1(new int());//正确:使用new初始化智能指针
shared_ptr<int>p2 = new int();//错误:必须显示的初始化
但是注意不要将普通的指针和智能指针混合使用,很容易出错。
get()操作
get可以讲指针的权限给代码,但是注意,必须保证代码不会delete指针,才能使用get。永远不要用get去初始化另一个智能指针或给它赋值。
reset()操作
reset会更新引用计数,所以在多个shared_ptr共享对象时,常与unique()一起使用。
异常
使用智能指针,即使程序块过早结束,智能指针类也能确保内存不再需要时将其释放;但是普通指针就不会。
void fun( )
{
int *p = new int();
//如果这时抛出一个异常且未捕获,内粗不会被释放,但是智能指针就可以释放。
delete p;
}
标准很多都定义了析构函数,负责清理对象使用的资源,但是一些同时满足c和c++的设计的类,通常都要求我们自己来释放资源,可以使用智能指针来解决这个问题。
/*有问题*/
connection connect(*destination);
void disconnect(connect);
void f(destination &d)
{
connection c = connect(&d);
disconnect(d);//如果没有调用disconnect,那么永远不会断开连接。
}
//使用智能指针优化,等于自己定义了delete代替本身的delete
connection connect(*destination);
void end_disconnect(connection*p) {disconnect(p);}
void f(destination &d)
{
connection c = connect(&d);
shared_ptr<connection>p(&d, end_connect);//定义自己的删除器
//f退出时,会自动调用end_connect。
}
总结:
智能指针陷阱
- 不使用相同的内置指针值初始化(或reset)多个智能指针;//多个智能指针还是单独的指向内置指针的内存,use_count分别为1
- 不delete get( )返回的指针;//两次delete释放,智能指针内部也会delete
- 不使用get( )初始化或reset另一个智能指针;//free( ): invalid pointer:也是多次释放
- 如果你使用get( )返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(删除函数向上面的disconnect( ))。
二、unique_ptr类
由于unique_ptr独占它所指向的对象,因此他不支持普通的拷贝和赋值。
但是,可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr。
操作 | 描述 |
unique_ptr<T> u1 unique_ptr<T,p> u2 |
空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针;u2会使用一个类型为D的可调用对象来释放它的指针 |
unique_ptr<T,p> u(d) | 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete |
u = nullptr | 释放u所指向的对象,将u置为空 |
u.release() | u放弃对指针的控制权,返回指针,并将u置为空 |
u.reset() | 释放u指向的对象 |
u.reset(q) u.reset(nullptr) |
如果提供了内置指针q,令u指向这个对象;否则将u置为空 |
但是有种特殊的拷贝可以支持:我们可以拷贝或赋值一个即将要被销毁的unique_ptr。
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p));//返回一个unique_ptr
}
在早的版本中提供了auto_ptr的类,它有unique_ptr 的部分特性,但是不能在容器中保存auto_ptr, 也不能在函数中返回 auto_ptr, 编写程序时应该使用unique_ptr。
向unique_ptr 传递删除器
#include <memory>
#include <iostream> using namespace std; typedef int connection; connection* connect(connection *d)
{
cout << "正在连接..." << endl;
d = new connection();
return d;
} void disconnect(connection *p)
{
cout << "断开连接..." << endl;
} int main()
{
connection *p,*p2;
p2 = connect(p);
cout << p << endl;
cout << *p2 << endl;
unique_ptr<connection, decltype(disconnect)*>q(p2,disconnect);
//在尖括号中提供类型,圆括号内提供尖括号中的类型的对象。
//使用decltype()关键字返回一个函数类型,所以必须添加一个*号来指出我们使用的是一个指针
}
三、weak_ptr
weak_ptr 是一种不控制对象生存期的智能指针,它指向由一个shared_ptr 管理的对象。将一个weak_ptr绑定到shared_ptr不会改变shared_ptr的引用计数,且最后一个指向对象的shared_ptr被销毁,对象就会被释放,不管有没有weak_ptr指向该对象。
weak_ptr操作
操作 | 描述 |
weak_ptr<T> w | 空weak_ptr可以指向类型为T的对象 |
weak_ptr<T> w(sp) | 与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp指向的类型。 |
w = p | p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象 |
w.reset() | 将w置为空 |
w.use_count() | 与w共享对象的shared_ptr的数量 |
w.expired() | 若w.use_count()为0,返回true,否则返回false |
w.lock() | 如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr |
当我们创建一个weak_ptr 必须用一个 shared_ptr 初始化。weak_ptr 不会更改shared_ptr 的引用计数。
不能直接使用weak_ptr访问对象,而必须调用lock();引入lock和expired是防止在weak_ptr 不知情的情况下,shared_ptr 被释放掉。
std::weak_ptr 是一种智能指针,它对被 std::shared_ptr 管理的对象存在非拥有性(“弱”)引用。在访问所引用的对象前必须先转换为 std::shared_ptr。
std::weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用std::weak_ptr 来跟踪该对象。需要获得临时所有权时,则将其转换为 std::shared_ptr,此时如果原来的 std::shared_ptr被销毁,则该对象的生命期将被延长至这个临时的 std::shared_ptr 同样被销毁为止。
此外,std::weak_ptr 还可以用来避免 std::shared_ptr 的循环引用。
四、动态数组
动态数组并不是数组类型,而是得到一个数组元素类型的指针。
大多数应用应该使用标准库容器而不是动态分配的数组,因为使用容器更简单、更安全。
int *p = new int[get_siae()];//方括号中的大小必须是整数,但不必是常量
int *p = new int[]{,,,,,,,,,};//C++11中可以使用列表初始化
string *sp = new string[]{"ad","fd","xbv"};//初始化器中的元素较少时,剩下的进行值初始化;初始化器中的元素过多时,new失败,不会分配任何内存。
不能用auto分配数组。
动态分配一个空数组是合法的,但是不能解引用。
指向数组的unique_ptr
指向数组的unique_ptr不支持成员访问运算符
操作 | 描述 |
unique_ptr<T[]> u | u可以指向一个动态分配的数组,数组元素类型为T |
unique_ptr<T[]> u(p) | u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T* |
u[i] | 返回u拥有的数组中位置i处的对象,u必须指向一个数组 |
shared_ptr管理动态数组要定义自己的删除器,而且访问元素的时候要是用get()获取内置指针。
shared_ptr<int> sp(new int[], [](int *p){delete[] p;});
sp.reset();//使用上面的lambda表达式释放数组 for(size_t i = ;i != ;++i)
*(sp.get() + i) = i;
五、allocator类
new它将内存分配和对象构造结合到了一起,而allocator类允许我们将分配和初始化分离。
标准库allocator类定义在头文件 <memory>中。它帮助我们将内存分配和构造分离开来,它分配的内存是原始的、未构造的。
类似vector,allocator也是一个模板类,我们在定义一个allocator类类型的时候需要制定它要分配内存的类型,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:
allocator<string> alloc;
auto const p = alloc.allocate(n); // 分配n个未初始化的string
allocator类的操作:
allocator<T> a | 定义了一个名为a的allocator对象,可以为类型为T的对象分配内存 |
a.allocate(n) | 分配一段原始的、未构造的内存,保存n个类型为T的对象 |
a.deallocate(p, n) | 释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate成员函数返回的指针,且n必须是创建时候的大小,在destroy之前,用户必须在这块内存上调用destroy函数 |
a.construct(p, args) | p必须是一个类型为T*的指针,只想一块原始内存,args被传递给类型为T的构造函数 |
a.destroy(p) | p为T*类型的指针,此算法对p执行析构函数 |
allocator分配的内存是未构造的,construct成员函数接受一个指针和零个或多个额外参数,在给定的位置构造一个元素,额外的参数用来初始化对象。这些额外参数必须和类型相匹配的合法的初始化器。 为了使用allocate返回的内存,我们必须用construct构造对象。
auto q = p;
alloc.construct(q++); // *q为空字符串
alloc.construct(q++, , 'c'); // *q为cccccccccc
alloc.construct(q++, "hi"); // *q为hi
还未构造对象的情况下或者是使用原始内存是错误的:
cout << *p << endl; // 正确,使用string的输出运算符
cout << *q << endl; // 错误,q指向未构造的内存
在这些对象使用结束后,我们使用destroy来销毁这些元素:
while (q != p)
alloc.destroy(--q);
元素被销毁后,如果需要将内存归还给系统,就需要调用deallocate函数:
alloc.deallocate(p, n);
传递给deallocate的指针不能为空,必须指向由allocate分配的内存,而且,n必须为allocate分配时的大小。
uninitialized_copy(b, e, b2) | 从迭代器b和3 指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中,b2指向的内存必须足够大,能容下输入序列中的元素的拷贝 |
uninitialized_copy_n(b, n, b2) | 从迭代器b指向的元素开始,拷贝n个元素到b2开始的原始内存中 |
uninitialized_fill(b, e, t) | 在迭代器b和e指定的原始内存范围中创建对象,值均为t的拷贝 |
uninitialized_fill_n(b, n, t) | 从迭代器b指向的原始内存地址开始创建n个对象,b必须指向足够大的未构造的原始内存,能容纳给定数量的对象 |
vector<int> vec{, , , , , };
auto p = alloc.allocate(vec.size() * );
auto q = uninitialized_copy(vec.begin(), vec.end(), p);
uninitialize_fill_n(q, vec.size(), );
uninitialized_copy在给定位置构造元素,函数返回递增后的目的位置迭代器。因此,一个uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。
C++基础之动态内存的更多相关文章
- 数据结构基础(1)--数组C语言实现--动态内存分配
数据结构基础(1)--数组C语言实现--动态内存分配 基本思想:数组是最常用的数据结构,在内存中连续存储,可以静态初始化(int a[2]={1,2}),可以动态初始化 malloc(). 难点就是数 ...
- 数据结构基础——指针及动态内存分配(malloc)
一.指针 C语言中的指针是一种数据类型,比如说我们用int *a;就定义了一个指针a,它指向一个int类型的数.但是这个指针是未初始化的,所以,一般的,我们都在创建指针时初始化它,以免出错,在还不吃的 ...
- C/C++基础----动态内存
why 管理较难,忘记释放会内存泄漏,提早释放可能非法引用,重复释放. 为了更容易,更安全的使用动态内存,提供智能指针,其默认初始化保存一个空指针. what shared_ptr允许多个指针指向同一 ...
- c++基础(六)——动态内存
在我们的程序中,静态内存——用来保存局部 static 对象,类 static数据成员,以及定义在任何函数之外的变量.栈内存——用来保存定义在函数内的非 static 对象.分配在 静态内存 或 栈 ...
- 转: Linux C 动态内存分配 malloc及相关内容 .
一.malloc()和free()的基本概念以及基本用法: 1.函数原型及说明: void *malloc(long NumBytes):该函数分配了NumBytes个字节,并返回了指向这块内存的指针 ...
- c语言基础学习08_内存管理
=============================================================================涉及到的知识点有:一.内存管理.作用域.自动变 ...
- FreeRTOS 动态内存管理
以下转载自安富莱电子: http://forum.armfly.com/forum.php 本章节为大家讲解 FreeRTOS 动态内存管理,动态内存管理是 FreeRTOS 非常重要的一项功能,前面 ...
- Linux C 动态内存分配--malloc,new,free及相关内容
一.malloc()和free()的基本概念以及基本用法: 1.函数原型及说明: void *malloc(long NumBytes):该函数分配了NumBytes个字节,并返回了指向这块内存的指针 ...
- Android JNI编程(五)——C语言的静态内存分配、动态内存分配、动态创建数组
版权声明:本文出自阿钟的博客,转载请注明出处:http://blog.csdn.net/a_zhon/. 目录(?)[+] 一:什么是静态内存什么又是动态内存呢? 静态内存:是指在程序开始运行时由编译 ...
随机推荐
- 记录一则clear重做日志文件的案例
1.官方文档描述 2.故障报错信息 3.分析解决问题 1.官方文档描述 关于Clearing a Redo Log File的官方文档描述: A redo log file might become ...
- cogs 80. 石子归并 动态规划
80. 石子归并 ★★ 输入文件:shizi.in 输出文件:shizi.out 简单对比时间限制:1 s 内存限制:128 MB 设有N堆沙(shi)子排成一排,其编号为1,2,3, ...
- 随笔编号-16 MySQL查看表及索引大小方法
目标:阿里云OS数据库DMS,单个主库最大存储空间为2T.最近公司业务扩展很快,一天数据量达到7.9G左右.要求备份清理历史数据,备份到其他磁盘. 准备: 如果想知道MySQL数据库中每个表占用的空间 ...
- Javaweb MVC设计模式
Javaweb MVC设计模式 一.Java EE开发流程 二.MVC设计模式 什么是MVC? MVC是Model-View-Controller的简称,即模型-视图-控制器. MVC是一种设计模式, ...
- 关于工作流引擎ccflow待办分类 研究与技术实现
关于工作流引擎待办分类 研究与技术实现 关键字:工作流引擎 BPM系统 待办类型 名词:待办 概要介绍:待办就是当前的登录人员要处理的工作,在工作流程里面的节点类型不同,业务场景不同,我们把待办分为如 ...
- bzoj 1146 网络管理Network (CDQ 整体二分 + 树刨)
题目传送门 题意:求树上路径可修改的第k大值是多少. 题解:CDQ整体二分+树刨. 每一个位置上的数都会有一段持续区间 根据CDQ拆的思维,可以将这个数拆成出现的时间点和消失的时间点. 然后通过整体二 ...
- 51 nod 石子归并 + v2 + v3(区间dp,区间dp+平行四边形优化,GarsiaWachs算法)
题意:就是求石子归并. 题解:当范围在100左右是可以之间简单的区间dp,如果范围在1000左右就要考虑用平行四边形优化. 就是多加一个p[i][j]表示在i到j内的取最优解的位置k,注意能使用平行四 ...
- hdu 2050 折线分割平面 dp递推 *
折线分割平面 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Subm ...
- PAT L1-043. 阅览室
L1-043. 阅览室 时间限制 400 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 陈越 天梯图书阅览室请你编写一个简单的图书借阅统计程序.当读者 ...
- Android Activity启动耗时统计方案
作者:林基宗 Activity的启动速度是很多开发者关心的问题,当页面跳转耗时过长时,App就会给人一种非常笨重的感觉.在遇到某个页面启动过慢的时候,开发的第一直觉一般是onCreate执行速度太慢了 ...