文章目录
- 1 动态内存与智能指针
- 1.1 `shared_ptr`类
- 1.1.1 `shared_ptr`的方法
- 1.1.2 make_shared函数
- 1.2 智能指针和异常
- 1.3 `unique_ptr`
- 1.3.1 传递unique_ptr参数和返回unique_ptr
- 1.3.2 向unique_ptr传递删除器
- 1.3.3 习题补充
- 1.4 `weak_ptr`
- 2 直接内存管理
- 2.1 `new`
- 2.2 `delete`
- 2.4 shared_ptr 和 new 结合使用
- 2.4.1 不能将一个内置指针隐式类型转换成一个智能指针
- 2.4.2 不要混合使用普通指针和智能指针
- 2.4.3 不要使用get()初始化另一个智能指针或为智能指针赋值
- ※3 控制内存分配——第十九章 P726
- 3.1 重载new和delete
- 3.1.1 **`new`表达式调用的详细步骤**——三步
- 3.1.2 **`delete`表达式调用的详细步骤**——两步
- 3.1.3[`operator new`接口和`operator delete`接口](https://en.cppreference.com/w/cpp/memory/new/operator_new)
- 3.2 `operator new` 内使用`malloc`
- 3.3 `placement new`——定位new表达式
- 4 动态数组
- 4.1 new 和数组
- 4.2智能指针和动态数组
- 4.2.1 通过`unique_ptr`管理 —— `unique_ptr`通过默认模板实参调用`delete[]`
- 4.2.2 通过`shared_ptr`管理
- 4.2.2 详细的智能指针删除器区别看第十六章—1.9
- 4.2 `allocator`类
- 4.2.1 allocator分配未构造的内存
- 4.2.2 `construct_at`与`destroy_at`——since C++20
- 4.2.3 拷贝和填充未初始化内存的算法
- 4.2.3 拷贝和填充未初始化内存的算法
1 动态内存与智能指针
为了更安全的使用动态内存,标准库两种智能指针
shared_ptr
允许多个指针指向同一个对象unique_ptr
“独占”所指向的对象
标准库定义了一个伴随类
weak_ptr
,它是一种弱引用,指向shared_ptr
所管理的对象
三者都定义在memory
头文件中
1.1 shared_ptr
类
1.1.1 shared_ptr
的方法
shared_ptr
和unique_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 = q | p和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
}
类似顺序容器的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> u1 、unique_ptr<T, D> u2 | 空unique_ptr ,可以指向类型为T的对象。u1 会使用delete 来释放它的指针;u2 会使用类型为D 的的可调用对对象来释放它的指针 |
---|---|
unique_ptr<T, D> u(d) | 空unique_ptr ,指向类型为T的对象,用类型为D的对象d代替delete |
u = nullptr | 释放u指向的对象,将u置为空 |
- 允许赋值一个右值
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
以上两图分别表示了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 习题补充
- 下面的
unique_ptr
声明中,哪些时合法的,哪些可能导致后续的程序错误?—— P420
shared_ptr
为什么没有release
成员?
release
是unique_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() | 如果expired 为true ,返回一个空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
/*列表初始化*/
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
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_ptr ,reset 会释放此对象。若传递了可选的参数内置指针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
表达式调用的详细步骤——三步
调用一个名为
operator new
(或者operator new[]
)的标准库函数,该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象数组)。编译器运行相应的构造函数以构造这些对象,并为其传入初始值。
对象被分配了空间并构造完成,返回一个指向该对象的指针
3.1.2 delete
表达式调用的详细步骤——两步
- 对sp所指的对象或者arr所指的数组中的元素执行对应的析构函数
- 编译器调用名为
operator delete
(或者operator delete[]
)的标准库函数释放内存空间
- 应用程序可以在全局作用域中定义
operator new
函数和operator delete
函数,也可以将他们定义为成员函数,如果被分配(释放)对象是类类型,则编译器首先在类以及基类的作用域中查找 - 并且我们可以通过作用域限定符令
new
或delete
表达式直接执行全局作用域中的版本
//删除类中的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个重载版本。
- 如果将operator new 或 operator delete 定义成类的成员函数时,他们是隐式静态的,因为
operator new
发生在对象构造之前,也就是说此时只能由类去调用,同样operator delete
发生在对象析构之后 - 调用
operator new
或operator new[]
时,第一个参数类型必须是size_t
,且不能有默认实参,它表名存储该对象所需的字节数或存储数组中所有元素所需要的总空间 - 不允许重载
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 和数组
-
分配一个数组会得到一个元素类型的指针, 当用new分配一个数组时,我们并未得到一个数组类型的指针,而是的到一个数组元素类型的指针,因此不能对动态数组调用begin或end
-
使用初始化器初始化
string *psa = new string[3]{"123", "456", "789"};
string *psa1 = new string[10](); //10个空string
int* pia = new int[10](); //10个值为0的int
- 动态分配一个空数组是合法的:当我们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)
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分配未构造的内存
deallocate
释放内存,在调用deallocate
之前,用户必须对每个在这块内存中创建的对象调用destory
;destroy
只是单纯的调用在改空间上对象的析构
#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_at
与destroy_at
——since C++20
C++20之后弃用了construct
和 destroy
改用construct_at
与destroy_at
#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 拷贝和填充未初始化内存的算法
uninitialized_copy
uninitialized_copy_n
uninitialized_fill
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 拷贝和填充未初始化内存的算法
uninitialized_copy
uninitialized_copy_n
uninitialized_fill
uninitialized_fill_n