今天我们来学习两个C++中的高级特性,匿名函数Lambda表达式和便于对象生命周期管理的智能指针。
Lambda表达式
C++ 中的 Lambda 表达式是一种函数对象的匿名函数,它可以在需要函数的任何地方定义和使用,而无需显式地声明一个函数。Lambda 表达式的语法简洁灵活,使得代码更加清晰和易读。
Lambda表达式的基本语法:
[capture](parameters) -> return_type { body }
① [capture] 是可选的捕获列表,用于捕获外部变量,在捕获列表中的变量可以在lambda表达式中被使用,具体方式有值捕获、引用捕获或混合捕获;(可为空)
② (parameters) 是参数列表,类似于函数声明时的入参;(可为空)
③ mutable规范 mutable规范的使用,允许Lambda表达式的主体可以修改通过值捕获的变量;(可选)
④ 异常规范 在这里可以使用noexcept来指示Lambda表达式不会引发任何异常;(可选)
⑤ -> return_type 返回类型类似于函数声明时的返回类型,如果没有规定,Lambda表达式的返回类型将被自动推导;(可选)
⑥ { body } Lambda体,定义函数。(必选)
capture
捕获主要有两种形式,值捕获和引用捕获,我们可以指定某个变量被捕获,也可以默认捕获所有变量:
[&] 通过引用捕获所有变量
[=] 通过值捕获所有变量
如果我们要用引用捕获a,值捕获b,可以写作:
[&a, b]
[b, &a]
[=, &a] // 默认值捕获所有变量,但是引用捕获a
[&, b] // 默认引用捕获所有变量,但是值捕获b
对于使用引用捕获的变量,在Lambda表达式中发生的值改变,会作用于原变量,注意以下例子:
#include <iostream>
int main() {
int x = 5;
// 使用引用捕获外部变量 x
auto lambda = [&]() {
x += 10;
std::cout << "Inside lambda: x = " << x << std::endl;
};
lambda(); // 调用 Lambda 表达式
std::cout << "Outside lambda: x = " << x << std::endl;
return 0;
}
output:
Inside lambda: x = 15
Outside lambda: x = 15
对于使用值捕获的变量,它只能被视作常量,任何对于值捕获的变量的修改都会造成编译无法通过。
test.cpp: In lambda function:
test.cpp:8:14: error: assignment of read-only variable 'x'
x += 10;
^~
但是,如果允许mutable规范,则使用值捕获的变量将被视作“拷贝”。
#include <iostream>
int main() {
int x = 5;
// 使用引用捕获外部变量 x
auto lambda = [=]() mutable {
x += 10;
std::cout << "Inside lambda: x = " << x << std::endl;
};
lambda(); // 调用 Lambda 表达式
std::cout << "Outside lambda: x = " << x << std::endl;
return 0;
}
output:
Inside lambda: x = 15
Outside lambda: x = 5
参数列表
参数列表用于接受输入参数,在大多数方面与正常函数声明时的参数列表类似。
auto y = [] (int first, int second)
{
return first + second;
};
甚至可以:(C++14)
auto y = [] (auto first, auto second)
{
return first + second;
};
返回类型
返回类型大部分时间不需要明确指明,可以靠自动推导得到。比较神奇的是,Lambda表达式的返回值也可以是Lambda表达式:
#include <iostream>
int main() {
// 外部的 Lambda 表达式返回一个内部 Lambda 表达式
auto outerLambda = []() {
// 内部的 Lambda 表达式
return []() {
std::cout << "Inner lambda" << std::endl;
};
};
// 调用外部 Lambda 表达式,获得内部 Lambda 表达式
auto innerLambda = outerLambda();
// 调用内部 Lambda 表达式
innerLambda();
return 0;
}
output:
Inner lambda
Lambda 体
Lambda 表达式的 Lambda 体是一个复合语句。它可以包含普通函数或成员函数体中允许的任何内容。普通函数和 lambda 表达式的主体均可访问以下变量类型:
从封闭范围捕获变量。
参数。
本地声明变量。
类数据成员(在类内部声明并且捕获
this
时)。具有静态存储持续时间的任何变量(例如,全局变量)。
智能指针
在现代 C++ 编程中,标准库包含智能指针,该指针用于确保程序不存在内存和资源泄漏且是异常安全的。
智能指针的使用
使用智能指针需要引用<memory>头文件,它们对于对象生存期及资源管理或“获取资源即初始化”编程惯用法至关重要。此习惯用法的主要目的是确保资源获取与对象初始化同时发生,从而能够创建该对象的所有资源并在某行代码中准备就绪。
智能指针是被推荐使用的。大多数情况下,当初始化原始指针或资源句柄以指向实际资源时,会立即将指针传递给智能指针。在现代 C++ 中,原始指针仅用于范围有限的小代码块、循环或者性能至关重要且不会混淆所有权的 Helper 函数中。
下面的示例将原始指针与智能指针的声明使用及释放过程进行了比较:
void UseRawPointer()
{
// Using a raw pointer -- not recommended.
Song* pSong = new Song(L"Nothing on You", L"Bruno Mars");
// Use pSong...
// Don't forget to delete!
delete pSong;
}
void UseSmartPointer()
{
// Declare a smart pointer on stack and pass it the raw pointer.
unique_ptr<Song> song2(new Song(L"Nothing on You", L"Bruno Mars"));
// Use song2...
wstring s = song2->duration_;
//...
} // song2 is deleted automatically here.
在上述代码第9行,对于原始指针,在堆上声明后,必须进行显式内存释放delete,否则,造成内存泄漏,而对于智能指针,当智能指针被回收时,若已经不再有智能指针引用,会自动回收指针指向的对象。
如上例所示,智能指针是你在堆栈上声明的类模板,并可通过使用指向某个堆分配的对象的原始指针进行初始化。在初始化智能指针后,它将拥有原始的指针,智能指针负责删除原始指针指定的内存。
下面的例子以之后仍要继续分析的unique_ptr演示了智能指针的使用:
class LargeObject
{
public:
void DoSomething(){}
};
void ProcessLargeObject(const LargeObject& lo){}
void SmartPointerDemo()
{
// Create the object and pass it to a smart pointer
std::unique_ptr<LargeObject> pLarge(new LargeObject());
//Call a method on the object
pLarge->DoSomething();
// Pass a reference to a method.
ProcessLargeObject(*pLarge);
} //pLarge is deleted automatically when function block goes out of scope.
在以上例子中,第11行用一个原始指针声明了一个智能指针,名为pLarge。注意,不要将智能指针放置在堆空间上(不要使用new和malloc去申请一个智能指针!),在11行中,指定了智能指针的指针类型(LargeObject),因为智能指针是一个模板类,第14、17行,就像使用原始指针一样,使用智能指针,因为智能指针重载了->和*运算符,在该智能指针作用域结束后,将会自动回收对应的LargeObject类。
智能指针作为一个模板类,也具有使用“点”访问的成员函数。例如,一些 C++ 标准库智能指针具有释放指针所有权的重置成员函数。如果你想要在智能指针失效之前释放其内存将很有用,这会很有用,如以下示例所示:
void SmartPointerDemo2()
{
// Create the object and pass it to a smart pointer
std::unique_ptr<LargeObject> pLarge(new LargeObject());
//Call a method on the object
pLarge->DoSomething();
// Free the memory before we exit function block.
pLarge.reset();
// Do some other work...
}
智能指针通常提供直接访问其原始指针的方法。C++ 标准库智能指针拥有一个用于此目的的 get 成员函数,CComPtr 拥有一个公共的 p 类成员。通过提供对基础指针的直接访问,你可以使用智能指针管理你自己的代码中的内存,还能将原始指针传递给不支持智能指针的代码。
void SmartPointerDemo4()
{
// Create the object and pass it to a smart pointer
std::unique_ptr<LargeObject> pLarge(new LargeObject());
//Call a method on the object
pLarge->DoSomething();
// Pass raw pointer to a legacy API
LegacyLargeObjectFunction(pLarge.get());
}
接下来,我们将介绍三种不同的智能指针:
unique_ptr
顾名思义,unique_ptr仅允许基础指针的一个所有者,它的大小等于一个指针且支持右值引用,当它失效时回收内存。unique_ptr 不共享它的指针。它无法复制到其他 unique_ptr,无法通过值传递到函数(还是那句话,不能拷贝),也无法用于需要副本的任何 C++ 标准库算法,但是可以进行所有权转换,将内存空间的所有权转换到另一个unique_ptr。
特性:可移动,但不可复制
unique_ptr<Song> SongFactory(const std::wstring& artist, const std::wstring& title)
{
// Implicit move operation into the variable that stores the result.
return make_unique<Song>(artist, title);
}
void MakeSongs()
{
// Create a new unique_ptr with a new object.
auto song = make_unique<Song>(L"Mr. Children", L"Namonaki Uta");
// Use the unique_ptr.
vector<wstring> titles = { song->title };
// Move raw pointer from one unique_ptr to another.
unique_ptr<Song> song2 = std::move(song);
// Obtain unique_ptr from function that returns by value.
auto song3 = SongFactory(L"Michael Jackson", L"Beat It");
}
声明时,可以借助make_unique模板函数进行。
可以声明数组:
// Create a unique_ptr to an array of 5 integers.
auto p = make_unique<int[]>(5);
// Initialize the array.
for (int i = 0; i < 5; ++i)
{
p[i] = i;
wcout << p[i] << endl;
}
shared_ptr
shared_ptr的原始指针可以被多个所有者共享,当所有所有者都失效时,才会回收内存。在初始化一个 shared_ptr 之后,我们可复制它,按值将其传入函数参数,然后将其分配给其他 shared_ptr 实例。所有实例均指向同一个对象,并共享同一个“控制块”(每当新的 shared_ptr 添加、失效或重置时增加和减少引用计数)的访问权限。当引用计数达到零时,控制块将删除内存资源和自身。
可以看出,shared_ptr需要占用更多的空间,在作者的测试中,占用大小为16B,两倍于unique_ptr。
我们以以下例子来演示shared_ptr的生成、复制、重置以及计数为0后回收:
#include <iostream>
#include <memory>
struct Resource {
Resource() {
std::cout << "Resource acquired." << std::endl;
}
~Resource() {
std::cout << "Resource released." << std::endl;
}
};
int main() {
std::shared_ptr<Resource> sharedPtr1(new Resource()); // 生成 shared_ptr,计数为1
{
std::shared_ptr<Resource> sharedPtr2 = sharedPtr1; // 复制 shared_ptr,计数增加为2
std::cout << "Current reference count: " << sharedPtr1.use_count() << std::endl;
sharedPtr2.reset(); // shared_ptr2 重置,计数减少为1
std::cout << "Current reference count: " << sharedPtr1.use_count() << std::endl;
}
sharedPtr1.reset(); // sharedPtr1 重置,计数减少为0,资源回收
return 0;
}
output:
Resource acquired.
Current reference count: 2
Current reference count: 1
Resource released.
可以通过下列方式将 shared_ptr
传递给其他函数:
按值传递
shared_ptr
。这将调用复制构造函数,增加引用计数,并使被调用方成为所有者。此操作的开销很小,但此操作的开销可能很大,具体取决于要传递多少shared_ptr
对象。当调用方和被调用方之间的(隐式或显式)代码协定要求被调用方是所有者时,请使用此选项。按引用或常量引用传递
shared_ptr
。在这种情况下,引用计数不会增加,并且只要调用方不超出范围,被调用方就可以访问指针。或者,被调用方可以决定基于引用创建一个shared_ptr
,并成为一个共享所有者。当调用方并不知道被调用方,或当您由于性能原因必须传递一个shared_ptr
且希望避免复制操作时,请使用此选项。传递基础指针或对基础对象的引用。这使被调用方能够使用对象,但不会使其能够共享所有权或延长生存期。如果被调用方通过原始指针创建一个
shared_ptr
,则新的shared_ptr
独立于原始的,并且不会控制基础资源。当调用方和被调用方之间的协定明确指定调用方保留shared_ptr
生存期的所有权时,请使用此选项。在确定如何传递
shared_ptr
时,确定被调用方是否必须共享基础资源的所有权。“所有者”是只要它需要就可以使基础资源一直有效的对象或函数。如果调用方必须保证被调用方可以将指针的生命延长到其(函数)生存期以外,则请使用第一个选项。如果您不关心被调用方是否延长生存期,则按引用传递并让被调用方复制或不复制它。如果必须为帮助器函数提供对基础指针的访问权限,并且你知道帮助器函数将使用该指针并且将在调用函数返回前返回,则该函数不必共享基础指针的所有权。它只需在调用方的
shared_ptr
的生存期内访问指针即可。在这种情况下,按引用传递shared_ptr
或传递原始指针或对基础对象的引用是安全的。通过此方式传递将提供一个小的性能好处,并且还有助于您表达编程意图。有时,例如,在一个
std::vector<shared_ptr<T>>
中,您可能必须将每个shared_ptr
传递给 lambda 表达式主体或命名函数对象。如果 lambda 或函数没有存储指针,则将按引用传递shared_ptr
以避免调用每个元素的复制构造函数。
void use_shared_ptr_by_value(shared_ptr<int> sp);
void use_shared_ptr_by_reference(shared_ptr<int>& sp);
void use_shared_ptr_by_const_reference(const shared_ptr<int>& sp);
void use_raw_pointer(int* p);
void use_reference(int& r);
void test() {
auto sp = make_shared<int>(5);
// Pass the shared_ptr by value.
// This invokes the copy constructor, increments the reference count, and makes the callee an owner.
use_shared_ptr_by_value(sp);
// Pass the shared_ptr by reference or const reference.
// In this case, the reference count isn't incremented.
use_shared_ptr_by_reference(sp);
use_shared_ptr_by_const_reference(sp);
// Pass the underlying pointer or a reference to the underlying object.
use_raw_pointer(sp.get());
use_reference(*sp);
// Pass the shared_ptr by value.
// This invokes the move constructor, which doesn't increment the reference count
// but in fact transfers ownership to the callee.
use_shared_ptr_by_value(move(sp));
}
weak_ptr
有时,对象必须存储一种方法来访问shared_ptr
的基础对象,而不会导致引用计数递增。通常,在 shared_ptr
实例之间有循环引用时,会出现这种情况。这时就会用到weak_ptr
。
如果循环引用不可避免,应当使用weak_ptr
为一个或多个所有者提供对另一个shared_ptr
所有者的弱引用。使用weak_ptr
,可以创建一个联接到现有相关实例集的shared_ptr
(前提是基础内存资源仍然有效)。weak_ptr
本身不参与引用计数,因此,它无法阻止引用计数变为零。但是,可以使用 weak_ptr
尝试获取初始化该副本的 shared_ptr
的新副本。若已删除内存,则 weak_ptr
的 bool 运算符返回 false
。若内存仍然有效,则新的共享指针会递增引用计数,并保证只要 shared_ptr
变量保留在作用域内,内存就会有效。
std::weak_ptr 类型具有以下常用的成员函数:
lock(): 用于获取一个指向共享资源的 std::shared_ptr 对象。如果资源仍然可用,则返回一个有效的 std::shared_ptr,否则返回一个空的 std::shared_ptr。
expired(): 检查 std::weak_ptr 是否已经过期,即所指向的资源是否已经被释放。如果资源已经释放,则返回 true,否则返回 false。
use_count(): 返回与 std::weak_ptr 共享资源的 std::shared_ptr 对象的引用计数。注意,由于 std::weak_ptr 是弱引用,它并不会增加引用计数。
reset(): 将 std::weak_ptr 重置为空,不再指向任何资源。
owner_before(): 比较两个 std::weak_ptr 对象的所有权关系。它接受另一个 std::weak_ptr 对象作为参数,并返回一个布尔值,指示当前 std::weak_ptr 对象的所有权是否在参数对象之前。
以如下示例演示weak_ptr的用法:
#include <iostream>
#include <memory>
struct Resource {
int value;
Resource(int val) : value(val) {}
~Resource() {
std::cout << "Resource released: " << value << std::endl;
}
};
int main() {
std::shared_ptr<Resource> sharedPtr = std::make_shared<Resource>(10);
std::weak_ptr<Resource> weakPtr(sharedPtr);
std::cout << "Shared pointer use count: " << sharedPtr.use_count() << std::endl;
// 使用 weakPtr 获取 sharedPtr
if (auto ptr = weakPtr.lock()) {
std::cout << "Resource value: " << ptr->value << std::endl;
std::cout << "Shared pointer use count: " << sharedPtr.use_count() << std::endl;
// 此处存在weakPtr.lock() 临时生成的一个shared_ptr<Resource>,导致引用计数增加,lock函数的返回值是shared_ptr<Resource>。
} else {
std::cout << "Resource is no longer available." << std::endl;
}
// 释放 sharedPtr 的所有权
sharedPtr.reset();
// 再次尝试使用 weakPtr 获取 sharedPtr
if (weakPtr.expired()) {
std::cout << "Resource is no longer available." << std::endl;
}
return 0;
}
output:
Shared pointer use count: 1
Resource value: 10
Shared pointer use count: 2
Resource released: 10
Resource is no longer available.
主要参考:https://learn.microsoft.com/zh-cn/cpp/cpp