
目录
一、lambda表达式
1.lambda表达式的定义
Lambda表达式是C++11提供的一个新语法,使用起来非常方便。
在C++11之前,如果我们要使用sort函数进行排序,默认是排升序,如果我们想要排降序,就需要传递进一个库里面写好的仿函数。
#include <algorithm>
#include <functional>
int main()
{
int array[] = {4,1,8,5,3,7,0,9,2,6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array+sizeof(array)/sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
对于内置类型的排序,这种方法不算特别麻烦。但实际上我们使用的排序都不会是单一元素的排序,一般排序都是多种元素结合的排序,这就需要我们对自定义类型进行排序。比如结构体,我们要控制结构体内不同类型数据的比较规则,就需要自己写一个仿函数来控制排序。
比如我们要写一个学生信息的排序,按照身高高的学生优先,如果身高相同则年龄小的学生优先这样的规则进行排序。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
struct Student
{
string _name; // 学生名字
int _age; // 学生年龄
int _height; // 学生身高
Student(const string &name, const int &age, const int &height)
: _name(name), _age(age), _height(height)
{
}
};
// 按照身高高的优先,如果身高相同则按年龄小的优先
struct CmpStudent
{
bool operator()(const Student& s1, const Student& s2)
{
if(s1._height == s2._height)
{
return s1._age < s2._age;
}
else
{
return s1._height > s2._height;
}
}
};
int main()
{
vector<Student> v = {{"张三", 18, 180}, {"李四", 15, 200}, {"王五", 13, 200}};
sort(v.begin(), v.end(), CmpStudent());
for(auto e : v)
{
cout << e._name << " - " << e._age << " - " << e._height << endl;
}
return 0;
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名, 这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
例如上面的例子,我们可以利用Lambda表达式来简化:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
struct Student
{
string _name; // 学生名字
int _age; // 学生年龄
int _height; // 学生身高
Student(const string &name, const int &age, const int &height)
: _name(name), _age(age), _height(height)
{
}
};
int main()
{
vector<Student> v = {{"张三", 18, 180}, {"李四", 15, 200}, {"王五", 13, 200}};
// Lambda表达式
sort(v.begin(), v.end(), [](const Student& s1, const Student& s2){
if(s1._height == s2._height)
{
return s1._age < s2._age;
}
else
{
return s1._height > s2._height;
}
});
for(auto e : v)
{
cout << e._name << " - " << e._age << " - " << e._height << endl;
}
return 0;
}
可以看出lambda表达式实际是一个匿名函数。
2.lambda表达式书写格式
书写格式:[capture-list] (parameters) mutable -> return-type { statement }
lambda表达式各部分说明:
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda 函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以 连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量 性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回 值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获 到的变量。
- 注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
// 所以这是最简单的Lambda表达式,该Lambda表达式没有任何意义
[]{};
我们再拿个示例来演示一下:
#include <iostream> #include <algorithm> #include <vector> using namespace std; int main() { int a = 10, b = 100; auto Add = [](int x, int y)->int{ return x + y; }; cout << Add(a, b) << endl; return 0; }
这里我们就可以有点感觉看出lambda通俗一些应该是这样:
[捕获外部变量] (参数) { 代码逻辑 }[] 捕获框:要不要用外面的变量?不用就空着,用的话里面写规则(后面讲)。
() 参数框:要不要传参数?不用就空着,用的话像普通函数一样写参数。
{} 代码框:要执行的逻辑写这里,一行代码可以省略大括号。
3.捕捉列表的规则
通过了上面的讲述,你可能觉得lambda是有一定价值,但是还没懂为什么好用,到底怎么用,我们就来看看。
普通函数想用到外面的变量,得传参数;但 Lambda 可以直接 “抓” 外面的变量用,这就是 [] 的作用。
分两种常用 “抓法”:
- [=] 把外面所有变量,复制一份到 Lambda 里用(改副本不影响原变量)
- [&] 把外面所有变量,直接用引用(改 Lambda 里的变量,原变量也会变)
当然,如果想要具体的某个值也可以直接抓,如[a, b, &ret]
所以,我们再回归我们开篇的sort的降序排序,这里我们就可以直接引用lambda表达式:
int main() {
vector<int> nums = {3, 1, 4, 2};
// 用sort排序,第三个参数用Lambda写“降序”规则
sort(nums.begin(), nums.end(), [] (int a, int b) {
return a > b; // a比b大就放前面 = 降序
});
// 打印排序后的结果:4 3 2 1
for (int n : nums) {
cout << n << " ";
}
return 0;
}
4.lambda表达式的底层原理
函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的 类对象。
class Rate
{
public:
Rate(double rate): _rate(rate)
{}
double operator()(double money, int year)
{ return money * _rate * year;}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double{return monty*rate*year;
};
r2(10000, 2);
return 0;
i.这里的Rate类就是一个典型的函数对象,定义Rate r1(rate)时,编译器会:
① 分配一块内存给r1对象,里面存着成员变量_rate(值是 0.49);
② 调用构造函数,把传入的rate=0.49赋值给_rate。ii.当你写r1(10000, 2)时,看起来像调用函数,但本质是:
编译器把()调用转换成「调用重载的operator()方法」,执行逻辑:
return 10000 * 0.49(对象里存的_rate) * 2;所以你写的auto r2 = [=](double money, int year)->double{return money*rate*year;};
看起来是一行代码,但编译器在编译时,会帮你做这些事(这使他完全等价于手写 Rate 类)
- 编译器自动生成一个 “匿名类”(你看不到,但真实存在)
- 编译器自动创建这个匿名类的对象r2
- 调用r2(10000, 2)的过程
我们在通过反汇编验证一下我们的推测。
可以发现,是对的。
回到开题,lambda为什么好用,简单说:Lambda 就是 C++ 编译器为了 “偷懒” 设计的语法糖 —— 用一行代码代替你手写函数对象的所有工作,底层逻辑完全一致。
二、智能指针
1.为什么需要智能指针
C++11提供了智能指针,它可以解决普通指针使用过程中内存泄漏的问题。
C++相比较Java这种面向对象语言,最危险的地方就是C++没有像Java那样有垃圾处理机制,也就是我们定义的指针必须自己释放,否则就会造成内存泄漏。所以说,C++其实是一门容错非常低的语言。
除了我们在写代码的时候会忘记释放指针以外,加入异常处理机制以后有一些场景的内存泄漏问题会更加难以处理,比如下面这份代码示例:在main函数体内调用Func函数并且捕获异常,在Func函数中,首先是new了p1和p2指针变量,然后调用div函数。由于div函数可能会抛出异常,所以在Func函数内需要拦截这个异常,先将p1和p2的指针资源释放,再重新抛出这个异常。
但这并不代表着没有内存泄漏的风险了,在Func函数中如果new一个p2指针变量时new内部抛异常了,这时候p1指针变量已经申请内存有空间了,p2在申请内存的时候抛异常就会直接跳到main函数体的catch语句块处理异常,导致p1指针变量无法释放,造成内存泄漏。
#include <iostream>
#include <exception>
#include <stdexcept>
using namespace std;
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
int *p1 = new int;
int *p2 = new int;
// 如果div抛异常了,先释放p1和p2,再将异常重新抛出
try
{
cout << div() << endl;
}
catch (...)
{
delete p1;
delete p2;
throw;
}
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception &e)
{
cout << e.what() << endl;
}
return 0;
}
2.什么是内存泄露
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内 存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
- 堆内存泄漏(Heap leak)
- 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一 块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分 内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
- 系统资源泄漏
- 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放 掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何解决:1、事前预防型。如智能指针等。2、事后查错型。如泄 漏检测工具。
3.RAII思想
RAII是Resource Acquisition Is Initialization的简称,字面意思就是资源的获取就是对象的初始化,它是一种利用对象生命周期来控制程序资源的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
总结起来就一句话:把资源绑定到对象的生命周期 —— 对象创建时自动拿资源,对象销毁时自动还资源,绝不漏还、绝不多还。
4.智能指针的原理
上述的RAII思想并不能完全称作是智能指针,因为它只提供了自动释放资源的功能,却不能像原生指针一样使用,指针应该要可以解引用,可以通过->去访问空间里的内容,所以我们还需要重载*和->才能让它像指针一样去使用。
template <class T>
class SmartPtr
{
public:
SmartPtr(T *ptr)
: _ptr(ptr)
{
}
~SmartPtr()
{
delete _ptr;
cout << "释放资源成功" << endl;
}
T &operator*() { return *_ptr; }
T *operator->() { return _ptr; }
private:
T *_ptr;
};
所以智能指针的原理是:
- RAII思想
- 重载operator*和operator->,具有像指针一样的行为。
5.智能指针的类型
1.auto_ptr
为了解决智能指针的拷贝问题,C++98提出了auto_ptr智能指针,它实现的原理是管理权转移,它在实现拷贝构造函数时,将原来对象的资源转移到新对象中,再将原来对象的指针置空。
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
但其实auto_ptr是存在很大缺陷的,所以auto_ptr使用并不多,甚至很多地方禁止使用auto_ptr,我们举个例子看一下它存在的问题:
int main()
{
std::auto_ptr<int> sp1(new int);
std::auto_ptr<int> sp2(sp1); // 管理权转移
// sp1悬空
*sp2 = 10;
cout << *sp2 << endl;
cout << *sp1 << endl;
return 0;
}
上面这份代码将sp1拷贝给sp2以后,sp1就悬空了,这个对象就不能再被使用,否则就会报错。
2.unique_ptr
C++11中开始提供更靠谱的unique_ptr

unique_ptr是C++11借鉴boost库引入进来的一种智能指针方案,它解决智能指针拷贝问题的方法非常简单粗暴:直接禁止拷贝,如果出现拷贝就会发生编译错误。
在boost库中unique_ptr的实现方式是只声明拷贝构造函数但不给定义,并且为了防止外界对拷贝构造进行定义,所以将它设置成了私有。
在C++11中unique_ptr的实现方式是在拷贝构造函数后面加上=delete,这是一种新语法,能让该函数变成删除函数,外界不能调用删除函数,否则就会编译报错。
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
3.shared_ptr
hared_ptr和unique_ptr的区别在于,shared_ptr可以支持拷贝,并且能够解决智能指针拷贝带来的问题。而unique_ptr不支持拷贝,拷贝会直接报错。所以如果我们确实要使用智能指针之间的拷贝,就可以使用shared_ptr。
shared_ptr的解决拷贝问题方案是采用了引用计数器,引用计数器会记录有多少个对象管理这一块资源, 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源; 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对 象就成野指针了。
shared_ptr在使用引用计数器的时候,并不是每个对象都有一个引用计数器,也不是将引用计数器设置为全局的,因为这样做都不合适。正确的方案应该是让每一个资源都有一个自己的引用计数器,智能指针对象指向哪一个资源,也就同时指向了这个资源的引用计数器。
这里我们手撕一下shared_ptr(有注释版):
// 模板类:支持任意类型的指针管理(比如int、自定义类等)
template<class T>
class share_ptr
{
public:
// 构造函数:接管裸指针,初始化引用计数
// 参数ptr:外部传入的需要管理的堆内存裸指针(比如new出来的指针)
share_ptr(T* ptr)
:_ptr(ptr) // 让当前对象的_ptr指向外部传入的堆内存
,_count(new int(1)) // 引用计数初始化为1(表示当前只有1个share_ptr管理这块内存)
{ }
// 拷贝构造函数:让新对象和原有对象共享同一块堆内存
// 参数date:要拷贝的share_ptr对象(注意:标准库这里应该用const引用,你漏了)
share_ptr(share_ptr<T>& date)
{
// 1. 共享堆内存资源:新对象的_ptr指向原有对象的堆内存
_ptr = date._ptr;
// 2. 共享引用计数:新对象的_count指向原有对象的计数指针(所有共享对象共用一个计数)
_count = date._count;
// 3. 引用计数+1(多了一个对象管理这块内存)
(*_count)++;
}
// 析构函数:RAII核心——对象销毁时自动释放资源(遵循引用计数规则)
~share_ptr()
{
// 如果引用计数为1:说明当前对象是最后一个管理该内存的对象
if (*_count == 1)
{
delete _ptr; // 释放堆内存(归还资源)
_ptr = nullptr; // 置空指针,避免野指针
delete _count; // 释放引用计数的堆内存(计数本身也是new出来的)
_count = nullptr; // 置空计数指针
}
else
{
(*_count)--; // 不是最后一个对象,仅将引用计数-1
}
}
// 赋值运算符重载:让当前对象接管新对象的堆内存,同时释放原有内存
// 参数date:要赋值的share_ptr对象
void operator=(share_ptr<T>& date)
{
// 【注意1】自赋值检查:如果自己赋值给自己,直接返回(避免重复操作)
if (_ptr == date._ptr)
{
return;
}
// 【注意2】你原代码的致命错误:
// 1. 赋值前没有释放当前对象原有资源(如果当前对象是最后一个使用者,会内存泄漏)
// 2. *_count++ 顺序错误(先取值再++,且应该先绑定新count再++)
// 下面是修正后的逻辑(保留你的代码结构,标注错误)
// 第一步:释放当前对象原有资源(你原代码缺失这一步!)
// 如果当前对象是原有内存的最后使用者,释放内存和计数
if (*_count == 1)
{
delete _ptr;
delete _count;
}
else
{
(*_count)--; // 不是最后一个,计数-1
}
// 第二步:共享新对象的资源
_ptr = date._ptr; // 指向新的堆内存
_count = date._count;// 指向新的引用计数
(*_count)++; // 新计数+1(你原代码把这行写在了_count赋值前面,顺序错了)
}
// 重载->运算符:支持像普通指针一样使用->访问对象成员
// 比如share_ptr<Person> p(new Person()); p->show();
T* operator->()
{
return _ptr; // 返回管理的堆内存指针
}
// 重载*运算符:支持像普通指针一样解引用
// 比如share_ptr<int> p(new int(10)); *p = 20;
T& operator*()
{
return *_ptr; // 返回堆内存对象的引用(避免拷贝,支持修改)
}
private:
T* _ptr = nullptr; // 管理的堆内存裸指针(核心资源)
int* _count = nullptr; // 引用计数指针(所有共享对象共用同一个计数,所以用指针)
};
shared_ptr的线程安全问题
shared_ptr的线程安全分 为两方面:
- 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时 ++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2。这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁 的,也就是说引用计数的操作是线程安全的。
- 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。就像你雇了个管家(智能指针)帮你看房子(堆内存),管家只保证 房子没人住时锁门(释放内存),但不管 屋里的东西被人同时乱翻(多线程访问对象)。
- 怎么解决?
- 给堆对象加 “访问锁”,让同一时间只有一个线程能操作对象。1.访问对象的时候加互斥锁,访问就要先拿锁,就像进无人的图书馆自习室必须刷身份卡.2.把 “对象 + 锁” 封装成线程安全类,外部只用调用安全的接口,不用手动加锁。
所以总的来说:智能指针只管 “内存释放的安全”,不管 “对象本身的访问安全”。
shared_ptr的循环引用(weak_ptr)
shared_ptr虽然很好用,但循环引用是它的致命缺陷,光靠shared_ptr无法解决这个问题,我们可以写一份代码模拟一下循环引用的场景:我们定义一个双向链表,用shared_ptr创建两个对象分别管理两个节点空间,然后再将节点的_next和_prev指针改成shared_ptr类型,最后连接两个节点。
#include <iostream>
#include <memory>
using namespace std;
struct ListNode
{
int _val = 0;
// ListNode* _next = nullptr;
// ListNode* _prev = nullptr;
shared_ptr<ListNode> _next = nullptr;
shared_ptr<ListNode> _prev = nullptr;
// 析构函数仅用来判断是否被调用
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> p1(new ListNode());
shared_ptr<ListNode> p2(new ListNode());
// 这样连接节点会报错,因为_next的类型是List Node*,p2的类型是shared_prt
// p1->_next = p2;
// p2->_prev = p1;
// 将_next的类型改成shared_prt,才可以这样连接节点
p1->_next = p2;
p2->_prev = p1;
return 0;
}
当创建shared_ptr的对象p1和p2分别管理两个节点的时候,它们还没有连接起来,所以这两块空间的引用计数器此时都是1。这时还是可以正常释放资源的。

但当我们让空间一的_next指向p2,也就是指向空间二,让空间二的_prev指向p1,也就是空间一时,由于_next和_prev也是shared_ptr类型的对象,所以空间一有p1和_prev两个shared_ptr对象管理,空间二有p2和_next两个shared_ptr对象管理,两个空间的引用计数器此时都是2。
这就出现循环引用的问题了,因为一旦出了main函数的作用域,p1和p2对象就会销毁,两个空间的引用计数器各自减一,但由于还没有减到0,所以两个空间还不会销毁。
- 空间一资源想要被释放,需要让空间二的_prev对象先析构,不再管理空间一;但空间二的_prev要想析构,需要空间二的资源被释放;
- 空间二的资源想要被释放,需要让空间一的_next对象先析构,不再管理空间二;但空间一的_next要想析构,需要空间一的资源被释放。
这就是一个无解的死循环,所以最终将导致空间一和空间二在堆上申请了空间无法释放,造成内存泄漏的问题。
所以,这里我们就要引入一个新的指针,weak_ptr。
weak_ptr是 C++ 专门用来解决shared_ptr循环引用的 “弱引用指针”,特点:✅ 指向
shared_ptr管理的对象,但不增加引用计数;✅ 可以随时转换成
shared_ptr(判断对象是否还存活)
weak_ptr 的核心用法(怎么用它访问对象?)
我们可以直接将上述例子中的_next和_prev设置成为weak_ptr类型,既可以实现节点之间的连接,也可以解决循环引用的问题。
struct ListNode
{
int _val = 0;
// ListNode* _next = nullptr;
// ListNode* _prev = nullptr;
// 会出现循环引用问题
// shared_ptr<ListNode> _next = nullptr;
// shared_ptr<ListNode> _prev = nullptr;
weak_ptr<ListNode> _next;
weak_ptr<ListNode> _prev;
// 析构函数仅用来判断是否被调用
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
也就是说
外部持有对象 → 用shared_ptr(保证对象活着)
内部互相引用 → 用weak_ptr(打破循环计数)
4.定制删除器
在C++的标准库中提供的智能指针,它默认只对new出来的对象进行处理,也就是说在析构函数的时候只会使用delete析构,但如果我们是用new[]来申请的一块连续空间,我们需要用delete[]来释放这一块空间,就需要我们自己定制删除器。
我们可以自定义仿函数实现我们想要的删除功能,这就叫定制删除器,在定义智能指针的同时传递进我们写的仿函数即可。
删除器是一个可调用对象(函数指针、lambda、std::function等)!
传仿函数:
template<class T>
struct Function
{
void operator()(T* ptr)
{
delete[] ptr;
ptr = nullptr;
}
};
传Lambda:
shared_ptr<string> V2(new string[10], [](string* ptr) { delete[] ptr; ptr = nullptr; });

C++11 lambda与智能指针详解

950

被折叠的 条评论
为什么被折叠?



