C++ Primer 第十二章 动态内存 重点解读

本文详细介绍了C++中的智能指针shared_ptr、unique_ptr和weak_ptr,包括它们的特性、方法和使用场景。同时涵盖了内存管理的new、delete、operatornew和operatordelete,以及如何与动态数组和allocator类配合使用,避免内存泄漏和悬空指针问题。
摘要由CSDN通过智能技术生成

1 动态内存与智能指针

为了更安全的使用动态内存,标准库两种智能指针

  • shared_ptr允许多个指针指向同一个对象
  • unique_ptr“独占”所指向的对象

标准库定义了一个伴随类

  • weak_ptr,它是一种弱引用,指向shared_ptr所管理的对象

​ 三者都定义在memory头文件中

1.1 shared_ptr

1.1.1 shared_ptr的方法

  • shared_ptrunique_ptr都支持的操作
shared_ptr<T> p空智能指针,可以指向类型为T的对象
p将p用作一个条件判断,若p指向一个对象,则为true
*p解引用p,获得它指向的对象
p->等价于(*p).member
p.get()返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也消失了,从而造成悬空指针
swap(p, q)p.swap(q)交换p和q中的指针
  • shared_ptr独有的操作
make_shared<T>(args)返回一个shared_ptr,指向一个动态分配的类型T的对象,使用args初始化对象
shared_ptr<T>p(q)p是shared_ptr的拷贝;此操作会递增q中的引用计数。q中的指针必须能转换为T*
p = qp和q都是shared_ptr,所保存的指针必须能互相转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放
p.use_count()返回与p共享对象的智能指针数量(返回引用计数)
p.unique()p.use_count()为1,返回true;否则返回false;表示p是否为唯一指向这一对象的智能指针

1.1.2 make_shared函数

#include <memory>   //std::shared_ptr; std::make_shared
#include <iostream>  
#include <string>
#include <vector>
using namespace std;

int main() {
	shared_ptr<string> p = make_shared<string>(5, 'x');
	cout << *p << endl;    //output: xxxxx

	vector<string> v;
	v.emplace_back(5, 'x');
	cout << v[0] << endl;    //output: xxxxx
}

image-20231023005331794

类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象。例如,调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配

1.2 智能指针和异常

1.3 unique_ptr

shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当我们定义一个unique_ptr时,需要将其绑定一个new返回的指针上。由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝和赋值操作支持移动赋值或移动构造

  • unique_ptr操作(其余相同操作参见shared_ptr表一)
unique_ptr<T> u1unique_ptr<T, D> u2unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针;u2会使用类型为D的的可调用对对象来释放它的指针
unique_ptr<T, D> u(d)unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u = nullptr释放u指向的对象,将u置为空

image-20231023224017234

  • 允许赋值一个右值unique_ptr(见1.3.1)和nullptr
u.release()u放弃对指针的控制权,返回指针,并将u置为空
u.reset()u.reset(q)u.reset(nullptr)释放u指向的对象。如果提供了内置指针q,则令u指向这个对象;否则将u置为空

重点:

unique_ptr<int> p1 = new int(1024);
unique_ptr<int> p2(p1.release());    //release将p1置为空
unique_ptr<int> p3 = new int(4201);
p2.reset(p3.release());    //reset释放了p2原来指向的内存

p2.release();   //错误:p2不会释放内存,并且我们丢失了p2原来绑定的指针
p2.reset();     //正确,p2指向的对象直接被释放
auto p = p2.release();  //正确,要记得delete(p)
  • release成员返回unique_ptr当前保存的指针并将其置空。因此p2别初始化为p1原来保存的指针,而p1被置为空
  • reset成员接收一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,他原来指向的对象将被释放。

1.3.1 传递unique_ptr参数和返回unique_ptr

image-20231024223349117

image-20231024223536147

以上两图分别表示了C++11中为类提供的移动语义,也就是说虽然unique_ptr不允许普通的拷贝,但是我们可以通过移动构造或移动赋值,转移一个将要被销毁的unqiue_ptr的所有权。

unique_ptr<int> clone(int p) {
    return unique_ptr<int>(new int(p));    //纯右值?
}

unique_ptr<int> clone(int p) {
    unique_ptr<int> ret = new int(p);
    return ret;    //将亡值?
}

1.3.2 向unique_ptr传递删除器

  • 重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型函数对象
  • 第十六章:1.9

1.3.3 习题补充

  1. 下面的unique_ptr声明中,哪些时合法的,哪些可能导致后续的程序错误?—— P420
  1. shared_ptr为什么没有release成员?

releaseunique_ptr的成员函数,用于将指针的所有权转移给另一个unique_ptr。 因为shared_ptr是多对一的关系,一个shared_ptr交出控制权,其它shared_ptr依旧可以控制这个对象。因此这个方法对shared_ptr无意义。

1.4 weak_ptr

  • weak_ptr指向一个shard_ptr管理的对象,且不会改变引用计数
weak_ptr<T> w
weak_ptr<T> w(sp)shared_ptr sp指向相同的对象,T必须能转换为sp指向的类型
w = p
w.reset()将w置为空
w.use_count()与w共享对象的shared_ptr的数量
w.expired()return w.use_count == 0 ? true : false
w.lock()如果expiredtrue,返回一个空shared_ptr,否则返回一个指向w的对象的shared_ptr

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock

std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string &msg) const {
    auto ret = wptr.lock();   //
    if (!ret) 
        throw std::runtime_error("unbound StrBlobPtr");
    if (i >= ret=>size())
        throw std::out_of_range(msg);
   return ret;
}

2 直接内存管理

《C++ Primer》P726-730 、P407-411

2.1 new

image-20231027000224816

image-20231027000120515

	/*列表初始化*/
	vector<int>* p = new vector<int>{ 0,1,2,3,4,5,6,7 };    

	/*值初始化*/
	string* ps1 = new string;    //默认初始化为空string
	string* ps2 = new string();  //值初始化为空string
	int* pi1 = new int;          //默认初始化; *pi1的值未定义
	int* pi2 = new int();        //值初始化为0;*pi2的为0

	/*动态分配const对象*/
	const string* ps = new const string;
  • 单一初始化器才能使用auto
	auto p1 = new auto("xxx");   //p1指向一个与"xxx"类型相同的对象,该对象用"xxx"初始化
	auto p2 = new auto{ "x", "xx" };   //error:括号中智能有单个初始化器

2.2 delete

image-20231027000025715

image-20231027000056125

2.4 shared_ptr 和 new 结合使用

2.4.1 不能将一个内置指针隐式类型转换成一个智能指针

如前所述,如果我们不初始化一个智能指针,他就会被初始化为一个空指针。我们还可以用new返回的指针来初始化智能指针

	shared_ptr<int> p(new int(1024));

	shared_ptr<int> p2 = new int(1024); //errror:不存在从int*到shared_ptr<int>的适当构造函数

接收指针传参的智能指针构造函数时explicit的。因此我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化方式来初始化一个智能指针

​ `` `

我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete

  • 定义和改变shared_ptr的其他方法
shared_ptr<T> p(q)p管理内置指针q所指向的对象;q必须是指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u)p从unique_ptr u那里接管了对象的所有权;将u置为空
shared_ptr<T> p(q, d)p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d(删除器)来替代delete
shared_ptr<T> p(p2, d)p是shared_ptr p2的拷贝,唯一的区别是p将用可调用对象d来替代delete
p.reset()p.reset(q)p.reset(q, d)若p是为一直想其对象的shared_ptrreset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空。若还传递了参数d,将会调用d而不是delete来释放q

2.4.2 不要混合使用普通指针和智能指针

#include <memory>   //std::shared_ptr; std::make_shared
#include <iostream>  
using namespace std;

void process(shared_ptr<int> ptr) {}

int main() {
	int* x = new int(1024);
	process(x);   //error
	process(shared_ptr<int>(x));   //合法的,但内存会被释放
	int j = *x;  //未定义的:x是一个悬空指针!!!
	cout << j << endl;    //打印随机值,并不是1024
}

我们将一个临时的shared_ptr传递给process,当调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数变为0了。因此,当临时对象被销毁时,它所指向的内存会被销毁

2.4.3 不要使用get()初始化另一个智能指针或为智能指针赋值

	shared_ptr<int> p(new int(1024));
	int* q = p.get();
	process(shared_ptr<int>(q));
	int foo = *p;
	cout << foo << endl;

结果与上述类似

※3 控制内存分配——第十九章 P726

3.1 重载new和delete

string *sp = new string("xxxx");
string *arr = new string[10];    //分配十个默认初始化的对象

delete sp;
delete[] arr;

3.1.1 new表达式调用的详细步骤——三步

  1. 调用一个名为operator new(或者operator new[])的标准库函数,该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象数组)。

  2. 编译器运行相应的构造函数以构造这些对象,并为其传入初始值。

  3. 对象被分配了空间并构造完成,返回一个指向该对象的指针

3.1.2 delete表达式调用的详细步骤——两步

  1. 对sp所指的对象或者arr所指的数组中的元素执行对应的析构函数
  2. 编译器调用名为operator delete(或者operator delete[])的标准库函数释放内存空间
  • 应用程序可以在全局作用域中定义operator new函数和operator delete函数,也可以将他们定义为成员函数,如果被分配(释放)对象是类类型,则编译器首先在类以及基类的作用域中查找
  • 并且我们可以通过作用域限定符令newdelete表达式直接执行全局作用域中的版本
//删除类中的new和delete,并使用全局的new和delete
class Test {
public:
	Test() = default;
	Test(int x) : ele(x) {}
	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;
private:
	int ele;
};
int main() { 
	Test* t = new Test();    //error:无法调用已删除的函数
	Test* t = ::new Test();   //使用全局作用域的new

	delete t;     //error
	::delete t;    //使用全局作用域的delete
}

3.1.3operator new接口和operator delete接口

标准库定义了operator new函数和operator delete函数重载的8个重载版本。

  1. 如果将operator new 或 operator delete 定义成类的成员函数时,他们是隐式静态的,因为operator new发生在对象构造之前,也就是说此时只能由类去调用,同样operator delete发生在对象析构之后
  2. 调用operator newoperator new[]时,第一个参数类型必须是size_t,且不能有默认实参,它表名存储该对象所需的字节数或存储数组中所有元素所需要的总空间
  3. 不允许重载void* operator new(size_t, void*);

3.2 operator new 内使用malloc

  • 头文件 —— cstdlib

  • 当定义了全局的operator new 和 operator delete 后,这两个函数必须以某种方式执行分配内存与释放内存的操作

void *operator new(size_t size) {
    cout << "operator new" << endl;
    if (void *mem = malloc(size)) {
        reutrn mem;
    } else {
        throw bad_alloc();
    }
}

void operator delete(void *mem) noexcept {
    free(mem);
}

int main() {
	int* a = ::new int(1);  //打印:operator new
}

3.3 placement new——定位new表达式

4 动态数组

4.1 new 和数组

  1. 分配一个数组会得到一个元素类型的指针, 当用new分配一个数组时,我们并未得到一个数组类型的指针,而是的到一个数组元素类型的指针,因此不能对动态数组调用begin或end

  2. 使用初始化器初始化

string *psa = new string[3]{"123", "456", "789"};

string *psa1 = new string[10]();  	//10个空string
int* pia = new int[10]();		   //10个值为0的int
  1. 动态分配一个空数组是合法的:当我们new分配一个大小为0的数组时,new返回一个合法的非空指针。就像尾后指针一样

4.2智能指针和动态数组

4.2.1 通过unique_ptr管理 —— unique_ptr通过默认模板实参调用delete[]

标准库提供了一个可以管理new分配的数组的unique_ptr版本

unique_ptr<int[]> up(new int[10]);
up.release(); 	//自动调用delete[]销毁其指针
//主体模板
template <class T, class D = default_delete<T>> class unique_ptr;
//数组特化模板
template <class T, class D> class unique_ptr<T[],D>;

//当传入的是数组类型时,D的类型为default_delete<T[]>
template <class T> class default_delete;

template <class T> class default_delete<T[]>;
  • 当调用特化的default_delete类创建出的对象时(operator()),如下图,函数体为::delete[](ptr)

image-20240122235640482

4.2.2 通过shared_ptr管理

  • unique_ptr不同,shared_ptr不直接管理动态数组。如果希望使用shared_ptr管理动态数组,必须提供自己定义的删除器
shared_ptr<int> sp(new int[10], [](int *p) {delete[] p;} );
sp.reset();		//使用我们提供的lambda释放数组,它使用delete[]

4.2.2 详细的智能指针删除器区别看第十六章—1.9

4.2 allocator

  • 标准库allocator类定义在头文件memory中,他帮助我们将内存分配和对象构造分离开来。他提供一种类型感知的内存分配方法,他分配的内存是原始的、未构造的

4.2.1 allocator分配未构造的内存

image-20240123112400856

  • deallocate释放内存,在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destorydestroy只是单纯的调用在改空间上对象的析构

image-20240123113508305

#include <memory>
#include <string>

int main() {
	int n = 3;
	std::allocator<std::string> a;
	auto const p = a.allocate(n);
	auto q = p;

	a.construct(q++);
	a.construct(q++, 10, 'c');
	a.construct(q++, "abc");
	
	while (q != p) {
		a.destroy(--q);
	}
	
	a.deallocate(p, n);
	
}

4.2.2 construct_atdestroy_at——since C++20

C++20之后弃用了constructdestroy 改用construct_atdestroy_at

image-20240123115121988

#include <memory>
#include <string>
#include <iostream>
int main() {
	int n = 1;
	std::allocator<std::string> a;
	auto const p = a.allocate(n);

	std::construct_at(p, 10, 'c');
	std::cout << *p;

	std::destroy_at(p);
	std::cout << *p;   //未定义行为

	a.deallocate(p, n);
}

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

  1. uninitialized_copy
  2. uninitialized_copy_n
  3. uninitialized_fill
  4. uninitialized_fill_n

片转存中…(img-l5DrCSyF-1705985226761)]

#include <memory>
#include <string>
#include <iostream>
int main() {
	int n = 1;
	std::allocator<std::string> a;
	auto const p = a.allocate(n);

	std::construct_at(p, 10, 'c');
	std::cout << *p;

	std::destroy_at(p);
	std::cout << *p;   //未定义行为

	a.deallocate(p, n);
}

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

  1. uninitialized_copy
  2. uninitialized_copy_n
  3. uninitialized_fill
  4. uninitialized_fill_n
  • 20
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dusong_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值