文章参考<零声教育>的C/C++linux服务期高级架构系统教程,学习笔记。
0 内容
1 智能指针
是什么 / 定义
参考:C++中智能指针的模板类
- 在一个类中,存在指向另一个类对象的指针
- 重载指针运算符(比如:->,*),利用当前类的对象(通过指针运算符)操纵另一个类的成员
- 在析构函数中定义了delete操作,借助于变量的作用域,实现类对象空间的自动释放
- #include
对指针做了封装,让它更好用;实质是一个对象,行为表现的却像一个指针。
用来干什么的 / 主要解决什么问题?
- 避免内存泄漏:new了,忘记delete,智能指针会自动释放 。(类的析构函数中delete了指针成员)
🔺 内存泄漏?
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。——百度百科
🔺 栈内存,堆内存
栈: 连续存储;
堆: 非连续的树形存储;
栈内存: 程序自动向操作系统申请分配及回收,速度快使用方便(如函数参数、局部变量、临时变量等,const局部变量也存储在栈区中)。
堆内存: 需要手动申请和释放(new delete),速度较慢、地址不连续。
- 共享所有权指针的传播和释放,比如多线程使用同一个对象时析构的问题。(避免重复释放)
深拷贝: 增加了一个指针并申请了一个新内存,新指针指向新内存(获得了一个对象的复制实体)
前拷贝: 增加一个指针指向已存在的内存地址(引用?),应用在只读场景下性能更好?
怎么用——四个智能指针
auto_ptr- unique_ptr:独占资源所有权的指针,没有引用计数,性能较好
- shared_ptr:共享资源所有权的指针,有引用计数,性能略差
- weak_ptr:共享资源的观察者,配合shared_ptr,不影响资源的生命周期,解决循环引用的问题
1.1 share_ptr
- 可以有多个shared_ptr拥有对象的所有权,只在 refCount=0 时析构 (此时才delete Ptr to T 释放内存)
- 裸指针不能直接赋给智能指针 (需要通过构造方法)
裸指针 = 原始指针
悬空指针 = 被释放内存的指针
(指向某一内存,然后内存被回收了,但它还指着那里……不知道会发生什么了!)
野指针 = 不知道指向哪里的指针(定义的时候未初始化)
基本用法和常用函数
函数 | 用法 |
---|---|
s.get() | 返回 shared_ptr 中保存的裸指针谨慎使用,不要保存也不要delete! |
s.reset(…) | 重置 shared_ptr 带参数时,减少引用计数并指向新对象 |
s.use_count() | 返回 shared_ptr 的强引用计数 |
s.unique() | 若 use_count() 为1,返回true,否则false |
make_shared(value) | 优先用这个构造智能指针,更高效 |
std::shared_ptr p(new int(1), DeleteIntPtr) | 指定删除器(就是写个删除函数)管理非new对象、没有析构函数的类、动态数组时(share_ptr的默认删除器不支持数组对象) |
s.shared_from_this() | 返回this指针 |
使用shared_ptr需要注意的
- 不要用一个原始指针初始化多个shared_ptr对象(可能多次释放)
- 不要在函数实参中创建shared_ptr:计算顺序不一定
- 不要将this指针作为shared_ptr返回(this本质上也是裸指针,可能多次释放)
function(shared_ptr<int>(new int), g()); //如果左->右:new int -> g() -> shared_ptr, g()异常则可能导致内存泄漏
//先创建智能指针,再定义函数
std::shared_ptr<int> p(new int);
function(p, g());
return shared_ptr<A>(this); //no!
return shared_from_this(); //yes!大概就是让两个指向同一个原始指针的shared_ptr的引用计数都正确……更多的学无余力看源码了
- 避免循环引用(循环引用->引用计数不为0,不析构->内存泄漏)
1.2 weak_ptr(弱引用的智能指针)
是什么,为什么
- shared_ptr依旧存在内存泄漏的风险(如循环引用场景),为协助shared_ptr而使用
- 从一个shared_ptr或者weak_ptr对象构造,不同表述对比揣摩:
- 不共享资源(即不改变引用计数),只获得资源的观测权。
- 不控制对象生命周期,只提供对管理对象的一个访问手段。
基本用法和常用函数
use_count() | 获取当前观察资源的引用计数 |
---|---|
expired() | 判断所观察资源是否已经释放(true=已经释放) |
lock() | |
(先lock再ex) | 从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源; |
当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。 |
- weak_ptr和this指针
- 在外面创建A对象的智能指针,和通过对象返回this的智能指针都是安全的
- shared__from_this()是内部weak_ptr调用lock()后返回的智能指针
- 构造后才能使用获取自身智能指针的函数(enable_shared_from_this内部的weak_ptr智能通过shared_ptr构造)
- weak_ptr解决循环引用问题
使用weak_ptr需要注意的(细节见讲义)
- weak_ptr在使用前需要检查合法性(调用expired(),避免指向空指针)
(配合之前的图食用)
1.3 unique_ptr(细节见讲义)
- 独占型智能指针,不与其他智能指针共享资源,不允许将其赋值给另一个unique
- 可以指向一个数组
std::unique_ptr<int []> ptr(new int[10]);
- 需要确定删除器的类型
std::unique_ptr<int, void(*)(int*)> ptr(new int(1), [](int *p){delete p;});
1.4 智能指针安全性问题
参考:https://blog.csdn.net/weixin_42142630/article/details/121165649
- 共享引用计数的不同的share_ptr被多个线程写,是线程安全的。
- 同一个shared_ptr被多个线程写,不是线程安全的。
- 同一个shared_ptr被多个线程读,是线程安全的。
线程安全:多线程操作一个共享数据的时候,保证所有线程的行为是符合预期的则称为线程安全。
2 右值引用
- 引用的本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝。
- 左值引用无法指向右值 (右值没有地址)
int &ref_a = a;
。 const
左值引用可以指向右值,不会修改指向值。- 右值引用有办法指向左值
int &&ref_a_right = std::move(a);
std::move(),强制转换左右值,无性能提升。
- 被声明出来的左值引用、右值引用本身,都是左值。
- 右值引用既可以是左值也可以是右值,有名称为左值,否则为右值。
如
int &&ref = std::move(a);
直接声明的ref
是左值,move
返回的int &&
是右值。
2.1 std::move() 移动语义
右值引用的主要目的:避免深拷贝,优化性能
- 浅拷贝重复释放问题
class A
{
public:
A() :m_ptr(new int(0)){
std::cout << "constructor A" <<std::endl;
} //默认构造函数是浅拷贝,导致main中的a和get返回的b指向同一个指针地,析构时重复释放
// A(const A& a) :m_ptr(new int(*a.m_ptr)){
// std::cout << "copy constructor A" << std::endl;
// } //提供深拷贝的拷贝构造函数,来避免重复删除同一个指针
// A(A&& a) :m_ptr(a.m_ptr){
// a.m_ptr = nullptr; //防止a析构时delete data,提前置空其m_ptr
// std::cout << "move constructor A" << std::endl;
// } //浅拷贝的移动构造函数
~A(){
std::cout << "destructor A, m_ptr: " << m_ptr << std::endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr; //A是含有堆内存的类
};
A Get(bool flag)
{
A a;
A b;
std::cout << "ready return" <<std::endl;
if(flag) return a;
else return b;
}
int main()
{
{
A a = Get(false);
}
std::cout << "main finish" << std::endl;
return 0;
}
/*
int main(){
MyString a;
a = MyString("Hello Lizzy!"); //move assignment
Mystring c = std::move(a); //move constructor
std::vector<MyString> vec;
vec.push_back(MyString("World")); //move constructor
}
- 移动语义: 将对象的状态或所有权从一个对象转移到另一个对象(浅拷贝),只是转义、没有内存拷贝。(减少不必要临时对象的创建)
(std::move()
例程见讲义)
2.2 forward 完美转发
使得参数在传递过程中能够保持其值属性(左值变右值)
int &&a = 10;
int &&b = a; //错误
int &&b = std::forward<int>(a); //正确
2.3 emplace_back 减少内存拷贝和移动
2.4 小结
3 匿名函数
3.1 基本语法
[捕获列表](参数列表)->返回类型{函数体}
,返回类型可以不指定(一个return情况下)
3.2 捕获列表
参考:基础篇:Lambda 表达式和函数对象
(让匿名函数能够使用外部变量=捕获)
- 值捕获: 被捕获的变量在lambda表达式被创建时拷贝,而非调用时才拷贝。
- 引用捕获: 值会变化
- 隐式捕获: &,=
- 空捕获: []
- 表达式捕获: 捕获列表内放的是表达式,能够捕获右值
3.3 其他
泛型lambda
lambda函数的形式参数可以使用auto关键字来产生意义上的泛型:auto add = [](auto x, auto y) { return x+y; };
可变lambda
使用mutable
修饰,使得lambda可以修改值捕获时的变量值:auto ff = [v]() mutable { return ++v; };