当前位置:   article > 正文

C++之内存管理_args... 溢出 c++

args... 溢出 c++


我们承担ROS,FastDDS等通信中间件,C++,cmake等技术的项目开发和专业指导和培训,有10年+相关工作经验,质量有保证,如有需要请私信联系。

本文主要介绍C++11及之后的内存管理,包括只能指针,内存分配器等内容。头文件:#include<memory>

智能指针

智能指针功能独有操作
std::share_ptr共享式:允许多个指针指向同一对象1.make_shared<T>(args) 使用args初始化此对象,返回一个shared_ptr,指向一个动态分配的类型为T的对象; 2.shared_ptr<T>p(q):p是q的拷贝:会递增q中的计数器; 3.p=q:会递减p所指内存的引用计数,递增q所指内存的引用计数; 4.p.unique(): 若p.use_count()为1返回true,否则返回false; 5.p.use_count():返回与p共享对象的智能指针数量,主要用于调试;
std::unique_ptr独占式:独占所指向的对象,它所指向的对象只有一个拥有者1.为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少; 2.可以移动但不能拷贝(仅通过move来转移),一旦转移成功,原来的unique_ptr就失去了对象内存的所有权
std::weak_ptr弱引用;共享但不引用,不控制对象的声明周期shared_ptr和weak_ptr在计数上是原子操作,性能较好; 只有一个接受一个shared_ptr 的构造函数,指向shared_ptr指针指向的对象内存但不拥有该内存;线程安全级别上与stl容器相同

shared_ptr

允许多个指针指向同一对象, 允许多个对象之间的内存共享,解决多个对象直接共享的内存的管理。

初始化

  • 使用std::weak_ptr初始化: 构造函数的实参可以是std::weak_ptr, 但如果是空的,会抛出异常:bad_weak_ptr
std::weak_ptr<int> w;
try {
      std::shared_ptr<int> s(w);
} catch (const std::exception& e) {
      std::cout << e.what() << std::endl;
}
// 输出:bad_weak_ptr
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 使用std::unique_ptr初始化
// 这段代码使用std::unique_ptr初始化std::shared_ptr,完成初始化后q失去了对象管理权,输出:q is null
// 注意:只能move一个unique_ptr初始化shared_ptr
std::unique_ptr<int> q(new int(3));
std::shared_ptr<int> s1(std::move(q));  // 如 std::shared_ptr<int> s1(q);这种是不允许的
if (!q) {
    std::cout << "q is null\n";
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 使用std::shared_ptr初始化
std::shared_ptr<int> s1(new int(2));  // 使用原始指针初始化
std::shared_ptr<int> s2(s1);          // 使用std::shared_ptr拷贝初始化
std::cout << s2.use_count() << std::endl;
std::shared_ptr<int> s3(std::move(s2));  // 使用std::shared_ptr移动初始化
if (!s2) {
	std::cout << "s2 is null\n";
}
std::cout << s3.use_count() << std::endl;
// 执行结果:2; s2 is null; 2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 使用std::make_shared初始化
std::shared_ptr<int> s4 = std::make_shared<int>(5);
  • 1
  • 使用原始指针初始化。接受指针参数的智能指针构造函数是explicit的,不能将一个内置指针转换为一个智能指针,必须使用直接初始化的形式:
shared_ptr<int>p1 = new int(1024);     // 错误,必须使用直接初始化的形式,这样会被视为需要一个隐式转换
shared_ptr<int>p2(new int(1024));     // 正确
  • 1
  • 2
  • 先声明一个shared_ptr, 然后使用reset对它赋值

析构

  • 定义自己的deleter:shared_ptr提供的default deleter调用的是delete,而不是delete[],所以如果用new[]建立一个array of object,必须定义自己的deleter,也可以使用为unique_ptr而提供的辅助函数作为deleter,其内调用delete[]:
std::shared_ptr<int> p(new int[10], std::default_delete<int[]>());
  • 1

方法

  • sp.swap(sp1) / swap(sp1, sp2):置换sp1和sp2的pointer和deleter
  • sp.reset()/sp.reset(ptr):放弃拥有权,并重新初始化,拥有empty/ptr
  • sp.get():返回原始指针
  • sp.use_count():返回共享对象拥有的数量
  • sp.unique():返回sp是否是唯一拥有者
  • 支持比较操作符
  • 转换操作:
    • static_pointer_cast(sp):对sp执行static_cast<>语义
    • dynamic_pointer_cast(sp):对sp执行dynamic_cast<>语义
    • const_pointer_cast(sp): 对sp执行const_cast<>语义
  • 注意:
    • shared_ptr引用计数是线程安全且无锁的(原子操作),但指向的对象不是线程安全的,读写需要加锁
    • 循环依赖会出问题,通常的做法是owner持有指向child的shared_ptr, child持有指向owner的weak_ptr——具体实现

unique_ptr

独占式拥有,可确保一个对象和其相应资源同一时间只被一个pointer拥有,类定义签名:

// 第一种形式:
template<class T, class Deleter = std::default_delete<T>> 
class unique_ptr;

// 第二种形式:
template < class T, class Deleter> 
class unique_ptr<T[], Deleter>;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

初始化

  • 必须直接初始化而不是赋值:unique_ptr<int> up(new int);
  • 移动构造初始化
std::unique_ptr<int> q0(new int(2));
std::unique_ptr<int> q1(std::move(q0));
  • 1
  • 2
  • std::make_unique<T> (C++14开始支持)初始化
std::unique_ptr<int> q2 = std::make_unique<int>(5);
  • 1

方法

  • 不再提供指针运算符如++等,这被视为优点
  • up.reset()/up.reset(ptr):对智能指针调用deleter,并令为0/重新初始化为ptr,之前的先析构掉了
  • up.get():返回原始指针
  • up.release():获得unique_ptr拥有的对象并放弃up的拥有权
  • bool():检查是否unique pointer是否拥有对象
  • up1.swap(up2):置换up1和up2的pointer和deleter
  • unique_ptr的特化版本:支持数组,但接口稍有不同,不再提供*和->,而是提供[],用以访问其所指向的array中的某一个对象

问题

  • 为什么一个函数返回unique_ptr不报错?示例如下:
// 定义类A
class A {
public:
	explicit A(int a) : x_(a) {
		std::cout << "A(int a)\n";
	}
	A(const A& a) : x_(a.x_) {
		std::cout << "A(const A& a)\n";
	}
	A& operator=(const A& a) {
		x_ = a.x_;
		std::cout << "operator=(const A& a)\n";
		return *this;
	}
	A(A&& a) : x_(a.x_) {
		std::cout << "A(A&& a)" << std::endl;
	}
	A& operator=(A&& a) {
		x_ = a.x_;
		std::cout << "operator=(A&& a)" << std::endl;
		return *this;
	}

private:
	int x_;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

定义函数

std::unique_ptr<A> unique_test0() {
	std::unique_ptr<A> q = std::make_unique<A>(6);
	return q;
}

void unique_test1() {
	std::unique_ptr<A> q0 = unique_test0();
}

int main() {
	unique_test1();
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 返回:A(int a) (Linux和Win结果一致),说明调用unique_test0函数返回处编译器直接优化为构造,避免了拷贝和移动。

shared_ptr和unique_ptr共有的特性

  1. *p:解引用p,获得他指向的对象;
  2. p->mem: 等价于*p
  3. p.get():返回p中保存的指针
  4. swap(p, q)/p.swap(q):交换p和q中的指针
void processWidght(shared_ptr<Widget> pw, int priority) { ... }
// 现在调用processWidget:
processWidget(shared_ptr<Widget>(new Widget), priority());
// 可能会造成资源泄露,因为c++编译器不确定以何种次序完成参数调用,如果先调用new widget,再priority过程中发生异常,new Widget返回的指针就会遗失,所以使用分离语句
shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

weak_ptr

std::weak_ptr是一种不控制所指向对象生存期间的智能指针,指向一个由shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

应用场景

  1. 解决循环引用:如果你有两个对象互相持有对方的std::shared_ptr,那么这两个对象都不会被销毁,因为他们的引用计数永远不会降到0。在这种情况下,你可以使用std::weak_ptr来打破这个循环。例如,如果你有一个树或图数据结构,你可以使用std::shared_ptr来管理节点之间的父-子关系,然后使用std::weak_ptr来管理节点之间的子-父关系。

  2. 缓存:如果你有一个需要大量内存的对象,你可能想要在不需要它时释放它,但是在稍后需要它时又能快速地获取它。在这种情况下,你可以使用std::shared_ptr来管理这个对象,然后在需要缓存这个对象的地方使用std::weak_ptr。当std::weak_ptr被转换为std::shared_ptr时,如果原始对象已经被销毁,那么这个转换操作将返回一个空的std::shared_ptr

  3. 观察者模式:在观察者模式中,一个或多个观察者对象“观察”一个主题对象。当主题对象改变时,它会通知所有的观察者对象。在这种情况下,主题对象可以持有观察者对象的std::weak_ptr,这样即使观察者对象被销毁,主题对象也不会阻止它们被销毁。

循环引用

两个或多个对象相互引用,形成一个闭环,导致无法正确收回内存。示例如下:
定义两个类

struct B0;
struct A0 {
	std::shared_ptr<B0> b;
	~A0() {
		std::cout << "~A0\n";
	}
};

struct B0 {
	std::shared_ptr<A0> a;
	~B0() {
		std::cout << "~B0\n";
	}
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

循环引用:

// 导致析构不了:
void cirular_reference1() {
	std::shared_ptr<A0> a0 = std::make_shared<A0>();
	std::shared_ptr<B0> b0 = std::make_shared<B0>();
	a0->b = b0;
	b0->a = a0;
}
// 注意,以下代码不是循环引用:
void cirular_reference2() {
	A0 a0;
	B0 b0;
	a0.b = std::make_shared<B0>(b0);
	b0.a = std::make_shared<A0>(a0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

使用std::weak_ptr代替std::shared_ptr(只替换B0中的变量)解决循环引用无法释放内存的问题:

// 调用cirular_reference1()函数可以正常析构
struct B0
{
	std::weak_ptr<A0> a;
	~B0() {
		std::cout << "~B0\n";
	}
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

方法

  • weak_ptr<T> w: 空weak_ptr类型
  • weak_ptr<T> w(sp):使用shared_ptr初始化weak_ptr,指向与shared_ptr相同的对象,T必需要能转换为sp指向的类型
  • w=p:p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象
  • wp.swap(wp2)
  • wp.reset():将wp置为空
  • wp.use_count():返回与wp共享对象的shared_ptr的数量
  • wp.expired():如果use.count()为0返回true,否则为false
  • wp.lock():如果expired()为true,返回一个空shared_ptr,否则返回一个指向w的对象的shared_ptr。
  • wp.owner_before()

如果不确定隐身于weak_ptr背后的对象是否仍旧存活,有以下几个选择:

  • expired():会在weak_ptr不再共享对象时返回true,等同于检查use_count()是否为0,但速度更快
  • 可以使用相应的shared_ptr的构造函数明确将weak_ptr转换为一个shared_ptr,如果被指对象不存在了该构造会抛出一个bad_weak_ptr异常(派生自std::exception),其what()函数返回字符串"bad_weak_ptr"
  • 调用use_count(),询问相应对象的个数;通常只是为调试而使用use_count(),但这个函数效率低

enable_shared_from_this

这是一个以派生类为模板类型实参的j基类模板,继承他,this指针就能变身为shared_ptr, 也就是能让一个对象(假设其名为t,被一个shared_ptr对象pt管理)安全的生成其他额外的std::shared_ptr实例,它们与pt共享对象t的所有权。std::enable_shared_from_this类给派生类添加了两个方法:

  • shared_from_this():返回一个shared_ptr,它共享对象的所有权。
  • weak_from_this():返回一个weak_ptr, 它跟踪对象的所有权
class Foo : public std::enable_shared_from_this<Foo> {
};
  • 1
  • 2

new

  • 新标准中可以用花括号列表初始化new的元素
int *p = new int[10]{0,1,2,3,4,5,6,7,8,9};
  • 1
  • 动态分配一个空数组是合法的

allocator模板

allocator模板,将内存分配和对象构造分离开来。它分配的内存是原始的,未构造的。

  1. 定义并分配原始内存:
allocator<T> a;     //定义一个名为a的allocator对象
a.allocate(n);     //分配一段原始的,未构造的内存,保存n个类型为T的对象
  • 1
  • 2
  1. 构造元素:construct函数接受一个指针和零个或多个额外参数,args被传给类型为T的构造函数,用来在p指向的内存中构造一个对象;为使用allocate返回的内存,必须使用construct构造对象 a.construct(p,args);
  2. 销毁:destroy,使用完对象后,必须用每个构造的元素调用destroy销毁之,即析构
while( q != p )
     alloc.destroy(--q);     //q指向最后一个构造的元素
  • 1
  • 2
  1. 元素被销毁后可以用这部分内存保存其他元素,也可以归还系统,释放内存通过调用deallocate
alloc.deallocate(p,n);     //释放从p开始的内存,这段内存保存了n个类型为T的对象
  • 1

拷贝和填充未初始化内存的算法:allocator算法

  • uninitialized_copy(b, e, b2):从迭代器b和e指定的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2的构造必须足够大
  • uninitialized_copy_n(b, n, b2):从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中
  • uninitialized_fill(b, e, t):从迭代器b和e指定的原始内存中创建对象,对象的值均为t的拷贝
  • uninitialize_fill_n(b, n, t):从迭代器b指向的内存地址开始创建n个对象,对象的值都是t的拷贝

小型对象分配

  • 为什么要用小型对象分配而不是C++标准通用的
    • new/delete通常用来分配中大型对象(数百个或数千个bytes)
    • 除了速度慢,C++缺省分配器的通用性也造成了小型对象空间分配的低效。缺省分配器管理一个记忆池,一般通过new分配管理的个数达到了4-32个bytes,所以如果分配大内存,开销微不足道,但如果分配8个bytes,开销达到50%-400%
  • new和malloc的区别:malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
    • 申请的内存所在位置不同:
      • mollloc函数从自由存储区(free store)上为对象动态分配内存空间
      • new操作符从堆上动态分配内存。new甚至可以不为对象分配内存!定位new的功能可以办到这一点:new (place_address) type,operator new不分配任何的内存,它只是简单地返回指针实参。
    • 返回类型安全性
      • new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。
      • malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
    • 内存分配失败时的返回值
      • new内存分配失败时,会抛出bad_alloc异常,它不会返回NULL;
      • malloc分配内存失败时返回NULL。
    • 是否需要指定内存大小
      • 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,
      • malloc则需要显式地指出所需内存的尺寸。
    • 是否调用构造函数/析构函数
      • new
        □ 调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
        □ 编译器运行相应的构造函数以构造对象,并为其传入初值。
        □ 对象构造完成后,返回一个指向该对象的指针。
      • 使用delete操作符来释放对象内存时会经历两个步骤:
        □ 第一步:调用对象的析构函数。
        □ 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。
  • 对数组的处理:C++提供了new[]与delete[]来专门处理数组类型:。
  • 是否相互调用:new底层其实还是用malloc实现动态内存分配的,所以new调用malloc,malloc不会调用new。
  • 函数重载:new可以重载;malloc不行

常见内存问题及对应工具

问题对应工具
缓冲区溢出可以使用vector或string
空悬指针/野指针shared_ptr/weak_ptr配合使用
重复释放unique_ptr
内存泄漏unique_ptr
不配对的new[]/deletevector或者boost中的数组相关的
内存碎片内存池
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/在线问答5/article/detail/1015234
推荐阅读
  

闽ICP备14008679号