C++进阶重点知识(一)|智能指针|右值|lambda|STL|正则表达式
目录
- 1智能指针
- 1.`shared_ptr`
- 1.1 `shared_ptr`的基本用法
- 使用`shared_ptr`要注意的问题
- 运用
- 2.`unique_ptr`独占的智能指针
- 示例:管理动态内存
- 3.`weak_ptr`弱引用的智能指针
- `weak_ptr`的基本用法
- lock 的作用:
- weak_ptr返回this指针
- weak_ptr解决循环引用问题
- `weak_ptr`使用注意事项
- 4.智能指针安全性问题
- 情况 1:多线程代码操作的是同一个 `shared_ptr` 的对象
- 情况 2:多线程代码操作的不是同一个 shared_ptr 的对象
- 2右值引用和移动语义
- 2.0&&的特性
- 2.1 右值引用优化性能,避免深拷贝
- 2.2 移动(move )语义
- 2.4 `forward` 完美转发
- 2.5 emplace_back 减少内存拷贝和移动
- 2.6 `unordered container` 无序容器
- 2.6.1 `map`和`unordered_map`的差别
- 2.7 小结
- 3匿名函数`lambda`
- 3.1 匿名函数的基本语法为:
- 3.2 捕获列表
- 3.3 匿名函数的简写
- 3.4 Lambda捕获列表
- 4 C++11标准库(STL)
- 4.1 容器简介
- **4.2 迭代器简介**
- 4.3 算法简介
- 5正则表达式
1智能指针
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
1.shared_ptr
std::shared_ptr
使用引用计数,每一个shared_ptr
的拷贝都指向相同的内存。再最后一个shared_ptr
析构的时候,内存才会被释放。shared_ptr
共享被管理对象,同一时刻可以有多个shared_ptr
拥有对象的所有权,当最后一个shared_ptr
对象销毁时,被管理对象自动销毁。
简单来说,shared_ptr
实现包含了两部分,
- 一个指向堆上创建的对象的裸指针,
raw_ptr
裸指针(Raw Pointer)是指常规的指针,它直接指向内存中的某个地址,不带有任何智能或管理机制。与智能指针(如std::shared_ptr
和std::unique_ptr
)不同,裸指针没有自动管理资源的能力,因此它需要程序员手动管理内存的分配和释放。裸指针通常是 C++ 中最基本的指针类型,用于指向对象或数据。 - 一个指向内部隐藏的、共享的管理对象。
share_count_object
第一部分没什么好说的,第二部分是需要关注的重点:
use_count
,当前这个堆上对象被多少对象引用了,简单来说就是引用计数。
1.1 shared_ptr
的基本用法
通过构造函数、std::shared_ptr
辅助函数和reset
方法来初始化shared_ptr
,代码如下:
// 智能指针初始化
std::shared_ptr<int> p1(new int(1));std::shared_ptr<int> p2 = p1;std::shared_ptr<int> p3;p3.reset(new int(1));if(p3) {cout << "p3 is not null";}
我们应该优先使用make_shared
来构造智能指针,因为他更高效。
auto sp1 = make_shared<int>(100);//相当于
shared_ptr<int> sp1(new int(100));
不能将一个原始指针直接赋值给一个智能指针,例如,下面这种方法是错误的:
std::shared_ptr<int> p = new int(1);
shared_ptr
不能通过“直接将原始这种赋值”来初始化,需要通过构造函数和辅助方法来初始化。 对于一个未初始化的智能指针,可以通过reset
方法来初始化,当智能指针有值的时候调用reset
会引起引用计数减1。
另外智能指针可以通过重载的bool
类型操作符来判断。
#include <iostream>
#include <memory>
using namespace std;int main() {// 创建一个空的 shared_ptrstd::shared_ptr<int> p1;// 使用 reset() 初始化 p1,指向一个动态分配的 int 对象,值为 1p1.reset(new int(1));// 创建另一个 shared_ptr p2,指向与 p1 相同的对象std::shared_ptr<int> p2 = p1;// 此时引用计数应该是 2cout << "p2.use_count() = " << p2.use_count() << endl;// 调用 p1 的 reset(),释放对原对象的所有权,p1 不再指向任何对象p1.reset();cout << "p1.reset()\n";// 引用计数此时应该是 1,因为 p2 仍然指向原对象cout << "p2.use_count() = " << p2.use_count() << endl;// 判断 p1 是否为空if (!p1) {cout << "p1 is empty\n";}// 判断 p2 是否为空if (!p2) {cout << "p2 is empty\n";}// 调用 p2 的 reset(),释放 p2 对对象的所有权,p2 不再指向任何对象p2.reset();cout << "p2.reset()\n";// 此时 p2 不再指向对象,所以引用计数是 0cout << "p2.use_count() = " << p2.use_count() << endl;// 判断 p2 是否为空if (!p2) {cout << "p2 is empty\n";}return 0;
}
- 获取原始指针
当需要获取原始指针时,可以通过get
方法来返回原始指针,代码如下所示:
std::shared_ptr<int> ptr(new int(1));int *p = ptr.get(); //
不小心 delete p;
谨慎使用p.get()
的返回值,如果你不知道其危险性则永远不要调用get()函数。
p.get()
的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生,遵守以下几个约定:
- 不要保存
p.get()
的返回值 ,无论是保存为裸指针还是shared_ptr
都是错误的 - 保存为裸指针不知什么时候就会变成空悬指针,保存为
shared_ptr
则产生了独立指针 - 不要
delete p.get()
的返回值 ,会导致对一块内存delete
两次的错误
指定删除器
- 如果用
shared_ptr
管理非new
对象或是没有析构函数的类时,应当为其传递合适的删除器。
示例代码如下:
//1-1-delete#include <iostream>#include <memory>using namespace std;void DeleteIntPtr(int *p) {cout << "call DeleteIntPtr" << endl;delete p;}int main(){std::shared_ptr<int> p(new int(1), DeleteIntPtr);return 0;}
当p
的引用计数为0时,自动调用删除器DeleteIntPtr
来释放对象的内存。删除器可以是一个lambda
表达式,上面的写法可以改为:
std::shared_ptr<int> p(new int(1), [](int *p) {cout << "call lambda delete p" << endl;delete p;});
当我们用shared_ptr
管理动态数组时,需要指定删除器,因为shared_ptr
的默认删除器不支持数组对象,代码如下所示:
std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});
使用shared_ptr
要注意的问题
- 不要用一个原始指针初始化多个
shared_ptr
,例如下面错误范例:
int *ptr = new int; // 使用裸指针分配内存
shared_ptr<int> p1(ptr); // p1 管理裸指针
shared_ptr<int> p2(ptr); // p2 也尝试管理同一块内存
在这段代码中,ptr
是一个裸指针,它指向通过 new
动态分配的内存。然后你用两个 shared_ptr(p1 和 p2)
来管理这块内存。
问题所在:
std::shared_ptr
会通过引用计数来管理资源的生命周期。当shared_ptr
被销毁时,会减少引用计数,并在引用计数为零时自动释放资源(调用 delete)。- 如果你直接用裸指针(ptr)初始化两个
shared_ptr
,这会导致 双重删除 的问题,因为 p1 和 p2 都会在销毁时调用
delete,而裸指针已经在外部被 new 分配过一次,这样会导致两次删除同一块内存,从而发生未定义行为(例如程序崩溃)。
- 不要在函数实参中创建
shared_ptr
,对于下面的写法:
function(shared_ptr<int>(new int), g()); //有缺陷
因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int
,然后调用g()
,如果恰好g()
发生异常,而shared_ptr
还没有创建, 则int
内存泄漏了,正确的写法应该是先创建智能指针,代码如下:
shared_ptr<int> p(new int);function(p, g());
- 通过
shared_from_this()
返回this
指针。不要将this
指针作为shared_ptr
返回出来,因为this
指针本质上是一个裸指针,因此,这样可能会导致重复析构,看下面的例子。
#include <iostream>
#include <memory>
using namespace std;class A {
public:shared_ptr<A> GetSelf() {return shared_ptr<A>(this); // 不要这么做:错误的做法}~A() {cout << "Deconstruction A" << endl;}
};int main() {shared_ptr<A> sp1(new A); // 创建 shared_ptr sp1 来管理 A 的实例shared_ptr<A> sp2 = sp1->GetSelf(); // 错误:使用 this 指针创建一个新的 shared_ptrreturn 0;
}
运行后调用了两次析构函数。
在这个例子中,由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this
将会被构造的两个智能指针各自析构,导致重复析构的错误。
正确返回this的shared_ptr
的做法是:让目标类通过std::enable_shared_from_this
类,然后使用基类的成员函数shared_from_this()
来返回this
的shared_ptr
,如下所示。
#include <iostream>
#include <memory>
using namespace std;class A : public std::enable_shared_from_this<A> // 继承 enable_shared_from_this
{
public:shared_ptr<A> GetSelf(){return shared_from_this(); // 返回指向当前对象的 shared_ptr}~A(){cout << "Deconstruction A" << endl;}
};int main()
{// 创建 shared_ptr sp1 来管理 A 类的实例shared_ptr<A> sp1 = make_shared<A>(); // 使用 make_shared 更加安全和高效// 使用 sp1 调用 GetSelf() 返回当前对象的 shared_ptrshared_ptr<A> sp2 = sp1->GetSelf(); // sp2 和 sp1 共享同一个对象的所有权// 程序结束时,sp1 和 sp2 会自动销毁并释放内存return 0;
}
在weak_ptr
章节我们继续讲解使用shared_from_this()
的原因。
- 避免循环引用。循环引用会导致内存泄漏,比如:
#include <iostream>
#include <memory>
using namespace std;class B; // 提前声明 B 类
class A {
public:shared_ptr<B> bptr; // A 持有 B 的 shared_ptr~A() {cout << "A is deleted" << endl;}
};class B {
public:shared_ptr<A> aptr; // B 持有 A 的 shared_ptr~B() {cout << "B is deleted" << endl;}
};int main() {{shared_ptr<A> ap(new A); // 创建 shared_ptr Ashared_ptr<B> bp(new B); // 创建 shared_ptr Bap->bptr = bp; // A 指向 Bbp->aptr = ap; // B 指向 A}cout << "main leave" << endl; // 由于循环引用,A 和 B 并没有析构return 0;
}
运行结果:
main leave
循环引用 是指对象 A 和对象 B 通过shared_ptr
相互持有对方,形成一个环。这样,A 和 B 的引用计数都会增加,但由于它们相互引用,它们的引用计数永远不会归零。这会导致它们的析构函数永远不会被调用,内存永远不会释放,从而产生内存泄漏。
循环引用导致ap
和bp
的引用计数为2,在离开作用域之后,ap
和bp
的引用计数减为1,并不回减为0,导致两个指针都不会被析构,产生内存泄漏。
解决的办法是把A和B任何一个成员变量改为weak_ptr
,具体方法见weak_ptr
章节。
解释:
A 和 B 的相互引用:
- A 类持有一个
shared_ptr<B> bptr
,即 A 引用 B。 - B 类持有一个
shared_ptr<A> aptr
,即 B 引用 A。
引用计数增加:
- 当你创建
shared_ptr<A> ap(new A)
时,A 的引用计数是 1。 - 当你创建
shared_ptr<B> bp(new B)
时,B 的引用计数是 1。
然后,通过以下代码:
ap->bptr = bp; // A 持有指向 B 的 shared_ptr
bp->aptr = ap; // B 持有指向 A 的 shared_ptr
- A 对象的 bptr 持有 B 对象的
shared_ptr
,这意味着 B 对象的引用计数增加到 2。 - B 对象的 aptr 持有 A 对象的
shared_ptr
,这意味着 A 对象的引用计数也增加到 2。
循环引用的结果:
- 在
main()
函数结束时,ap
和bp
会超出作用域并应该被销毁。 - 然而,由于 A 和 B 互相持有
shared_ptr
,它们的引用计数永远不会减少到 0。 - A 对象的引用计数是 2(由 ap 和 bptr 引用),而 B 对象的引用计数也是 2(由
bp
和aptr
引用)。 - 由于循环引用,A 和 B 的引用计数永远不会归零,因此它们的析构函数不会被调用,对象的内存也永远无法被释放,导致内存泄漏。
运用
资源管理(自动释放内存)
shared_ptr
可以有效管理资源(如动态分配的内存、文件句柄、数据库连接等),确保资源在不再需要时被自动释放,避免内存泄漏。
示例:动态内存管理
#include <iostream>
#include <memory>class MyClass {
public:MyClass() {std::cout << "MyClass Constructor" << std::endl;}~MyClass() {std::cout << "MyClass Destructor" << std::endl;}
};int main() {{std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();std::shared_ptr<MyClass> ptr2 = ptr1; // 引用计数变为 2std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl;} // ptr1 和 ptr2 在作用域结束时被销毁,MyClass 对象被自动释放std::cout << "Out of scope." << std::endl;return 0;
}
输出:
MyClass Constructor
ptr1 use count: 2
MyClass Destructor
Out of scope.
在这个例子中,当 ptr1
和 ptr2
超出作用域时,它们的引用计数减少到零,MyClass 对象会被自动销毁,析构函数被调用,释放了资源。
2.unique_ptr
独占的智能指针
unique_ptr
是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr
赋值给另一个unique_ptr
。下面的错误示例。
unique_ptr<T> my_ptr(new T);unique_ptr<T> my_other_ptr = my_ptr; // 报错,不能复制
unique_ptr
不允许复制,但可以通过函数返回给其他的unique_ptr
,还可以通过std::move
来转移到其他的unique_ptr
,这样它本身就不再拥有原来指针的所有权了。例如
unique_ptr<T> my_ptr(new T); // 正确
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正确
unique_ptr<T> ptr = my_ptr; // 报错,不能复制
std::make_shared是c++11的一部分,但std::make_unique
不是。它是在c++14里加入标准库的。
auto upw1(std::make_unique<Widget>()); // with make funcstd::unique_ptr<Widget> upw2(new Widget); // without make func
使用new的版本重复了被创建对象的键入,但是make_unique
函数则没有。重复类型违背了软件工程的一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀。
除了unique_ptr
的独占性, unique_ptr
和shared_ptr
还有一些区别,比如
unique_ptr
可以指向一个数组,代码如下所示
std::unique_ptr<int []> ptr(new int[10]);ptr[9] = 9;std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的
std::shared_ptr
是一个引用计数的智能指针,它允许多个指针共享对同一资源的所有权,并在最后一个指针超出作用域时释放资源。shared_ptr
通常用于单个对象,但对于数组类型的资源,它不直接支持。标准库中的 shared_ptr
是设计为管理单个对象的内存,并不直接支持数组类型(即 int[]
),即使你用 new
分配的是数组。
unique_ptr
指定删除器和shared_ptr
有区别
std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;}); // 正确
std::unique_ptr<int> ptr4(new int(1), [](int *p){delete p;}); // 错误
unique_ptr
需要确定删除器的类型,所以不能像shared_ptr
那样直接指定删除器,可以这样写:
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); // 正确
关于shared_ptr
和unique_ptr
的使用场景是要根据实际应用需求来选择。如果希望只有一个智能指针管理资源或者管理数组就unique_ptr
,如果希望多个智能指针管理同一个资源就用shared_ptr
。
示例:管理动态内存
我们将创建一个 Person
类,类的构造函数中分配一个动态字符串并且在析构函数中自动释放它。
代码实现
#include <iostream>
#include <memory>
#include <string>class Person {
public:// 构造函数动态分配内存Person(const std::string& name) {this->name = std::make_unique<std::string>(name);}// 显示名字void show() const {std::cout << "Person's name is: " << *name << std::endl;}private:// 使用 unique_ptr 来管理动态分配的内存std::unique_ptr<std::string> name;
};int main() {// 使用 unique_ptr 来管理 Person 对象std::unique_ptr<Person> person = std::make_unique<Person>("Alice");// 显示人的名字person->show();// 当 person 超出作用域时,它会自动释放内存return 0;
}
代码解析
Person
类:
Person
类包含一个std::unique_ptr<std::string>
,用于管理动态分配的std::string
。- 在构造函数中,我们使用
std::make_unique<std::string>
来创建动态分配的字符串,并将其赋值给name
成员变量。 - 在析构函数中,我们不需要显式地调用
delete
,因为unique_ptr
会自动释放内存。
2.main 函数:
-
使用
std::make_unique<Person>("Alice")
创建一个 Person 对象,并将其分配给std::unique_ptr
。 -
通过
person->show()
调用成员函数来显示名字。 -
当 main 函数结束时,
person
会超出作用域,自动释放name
所指向的内存。
运行结果
Person's name is: Alice
总结
这个简化的例子展示了如何使用 std::unique_ptr
来管理动态分配的内存,确保在不再需要时自动释放资源。std::unique_ptr
不仅简化了内存管理,还避免了内存泄漏的风险,适合用于管理具有明确所有权的动态资源。
3.weak_ptr
弱引用的智能指针
share_ptr
虽然已经很好用了,但是有一点share_ptr
智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr
成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
weak_ptr
是一种***不控制对象生命周期***的智能指针, 它指向一个shared_ptr
管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr
, weak_ptr
只是提供了对管理对象的一个访问手段。weak_ptr
设计的目的是为配合shared_ptr
而引入的一种智能指针来协助 shared_ptr
工作, 它只可以从一个 shared_ptr
或另一个 weak_ptr
对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr
是用来解决shared_ptr
相互引用时的死锁问题,如果说两个shared_ptr
相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr
之间可以相互转化,shared_ptr
可以直接赋值给它,它可以通过调用lock
函数来获得shared_ptr
。
weak_ptr
没有重载操作符*
和->
,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr
获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr
中管理的资源是否存在。weak_ptr
还可以返回this
指针和解决循环引用的问题。
weak_ptr
的基本用法
- 通过
use_count()
方法获取当前观察资源的引用计数,如下所示:
shared_ptr<int> sp(new int(10));weak_ptr<int> wp(sp);cout << wp.use_count() << endl; //结果讲输出1
- 通过
expired()
方法判断所观察资源是否已经释放,如下所示:
shared_ptr<int> sp(new int(10));weak_ptr<int> wp(sp);if(wp.expired())cout << "weak_ptr无效,资源已释放";elsecout << "weak_ptr有效";
- 通过
lock
方法获取监视的shared_ptr
,如下所示:
lock
有什么用处?
#include <iostream>
#include <memory>using namespace std;std::weak_ptr<int> gw;void f() {if (gw.expired()) {cout << "gw无效, 资源已释放" << endl;} else {auto spt = gw.lock();cout << "gw有效, *spt = " << *spt << endl;}
}int main() {{auto sp = make_shared<int>(42);gw = sp;f(); // 这里gw有效,输出gw有效,*spt = 42}f(); // 这里gw无效,因为sp已经超出作用域,输出gw无效, 资源已释放return 0;
}
lock
是 std::weak_ptr
提供的一个成员函数,用于从一个 weak_ptr
获取一个 shared_ptr
,如果指向的对象还没有被销毁(即对象仍然有效),则返回一个有效的 shared_ptr
,否则返回一个空的 shared_ptr
。
lock 的作用:
-
返回一个有效的
shared_ptr
:weak_ptr
本身并不会增加对象的引用计数,只是一个“弱引用”。- 通过
lock()
,你可以获取一个shared_ptr
,这个shared_ptr
使得对象的生命周期得到延长,直到这个shared_ptr
被销毁。
-
防止悬挂指针:
- 如果对象已经被销毁,lock() 会返回一个空的 shared_ptr。这样你就可以通过检查空指针来避免访问已经被销毁的对象,防止出现悬挂指针(访问已经被释放的内存)的情况。
-
lock 的工作原理
- 当你调用
lock()
时,它首先检查weak_ptr
指向的对象是否仍然存在。 - 如果对象没有被销毁,它返回一个新的
shared_ptr
,该shared_ptr
指向相同的对象,并增加引用计数。 - 如果对象已经被销毁(即引用计数为 0),
lock()
返回一个空的shared_ptr
,表示对象已经被释放。
- 当你调用
weak_ptr返回this指针
shared_ptr
章节中提到不能直接将this指针返回shared_ptr
,需要通过派生std::enable_shared_from_this
类,并通过其方法shared_from_this
来返回指针,原因是std::enable_shared_from_this
类中有一个weak_ptr
,这个weak_ptr
用来观察this
智能指针,调用
shared_from_this()
方法是,会调用内部这个weak_ptr的lock()
方法,将所观察的shared_ptr
返回,再看前面的范例
#include <iostream>
#include <memory>
using namespace std;class A : public std::enable_shared_from_this<A> {
public:shared_ptr<A> GetSelf() {return shared_from_this(); // 使用 shared_from_this 获取当前对象的 shared_ptr}~A() {cout << "Deconstruction A" << endl; // 析构时输出消息}
};int main() {shared_ptr<A> sp1(new A); // 创建 shared_ptr sp1,管理 A 类型对象shared_ptr<A> sp2 = sp1->GetSelf(); // 使用 GetSelf 获取同一个对象的 shared_ptr sp2return 0;
}
输出结果如下:
Deconstruction A
在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()
是内部的weak_ptr
调用lock()
方法之后返回的智能指针,在离开作用域之后,spy的引用计数减为0,A对象会被析构,不会出现A对象被析构两次的问题。
需要注意的是,获取自身智能指针的函数尽在shared_ptr
的构造函数被调用之后才能使用,因为enable_shared_from_this
内部的weak_ptr只有通过shared_ptr才能构造。
weak_ptr解决循环引用问题
在shared_ptr
章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过weak_ptr
解决该问题,只要将A或B的任意一个成员变量改为weak_ptr
#include <iostream>
#include <memory>
using namespace std;class A;
class B;class A {
public:std::weak_ptr<B> bptr; // 使用 weak_ptr 避免循环引用~A() {cout << "A is deleted" << endl;}
};class B {
public:std::shared_ptr<A> aptr;~B() {cout << "B is deleted" << endl;}
};int main() {{// 创建 shared_ptr 管理 A 和 B 对象std::shared_ptr<A> ap(new A);std::shared_ptr<B> bp(new B);// 设置相互引用ap->bptr = bp; // A 指向 B 的 weak_ptrbp->aptr = ap; // B 指向 A 的 shared_ptr}cout << "main leave" << endl;return 0;
}
这样在对B的成员赋值时,即执行bp->aptr=ap;
时,由于aptr
是weak_ptr
,它并不会增加引用计数,所以ap
的引用计数仍然会是1,在离开作用域之后,ap的引用计数为减为0,A指针会被析构,析构后其内部的bptr
的引用计数会被减为1,然后在离开作用域后bp
引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。
weak_ptr
使用注意事项
weak_ptr
在使用前需要检查合法性。
weak_ptr<int> wp;{shared_ptr<int> sp(new int(1)); //sp.use_count()==1wp = sp; //wp不会改变引用计数,所以sp.use_count()==1shared_ptr<int> sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}shared_ptr<int> sp_null = wp.lock(); //sp_null .use_count()==0;
-wp.lock()
尝试从 weak_ptr
获取一个 shared_ptr
。如果 weak_ptr
指向的对象还存在(即引用计数大于零),lock()
会返回一个有效的 shared_ptr
,否则返回一个空的 shared_ptr
。
- 在 sp 的作用域内,当 sp 是有效的时,
wp.lock()
会成功,返回一个有效的shared_ptr
。此时,你可以通过sp_ok
访问指向的对象。
因为上述代码中sp
和sp_ok
离开了作用域,其容纳的K对象已经被释放了。 得到了一个容纳NULL
指针的sp_null
对象。在使用wp
前需要调用wp.expired()
函数判断一下。 因为wp
还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。直到最后一个weak_ptr
对象被析构,这块“堆”存储块才能被回收。否则weak_ptr
无法直到自己所容纳的那个指针资源的当前状态。
如果shared_ptr sp_ok
和weak_ptr wp;
属于同一个作用域呢?如下所示:
weak_ptr<int> wp;shared_ptr<int> sp_ok;{shared_ptr<int> sp(new int(1)); //sp.use_count()==1wp = sp; //wp不会改变引用计数,所以sp.use_count()==1sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}if(wp.expired()) {cout << "shared_ptr is destroy" << endl;} else {cout << "shared_ptr no destroy" << endl;}
代码解释:
-
weak_ptr<int> wp
:声明了一个weak_ptr
,它用于持有shared_ptr<int>
的弱引用。weak_ptr
不会影响对象的引用计数,它只是对对象的观察者。 -
shared_ptr<int> sp
:在sp
的作用域内,sp
被初始化为指向一个值为 1 的整数。此时 sp 的引用计数为 1。 -
wp = sp
:- 将
shared_ptr sp
赋给weak_ptr wp
,这只是让 wp 观察 sp 所管理的对象,并不会改变 sp 的引用计数。因此,sp.use_count()
仍然为 1。
- 将
-
sp_ok = wp.lock()
:- 调用
wp.lock()
尝试从weak_ptr
获取一个shared_ptr
。如果原对象依然存在,则返回一个有效的shared_ptr
;否则返回一个空的shared_ptr
。由于此时 sp 仍然有效,sp_ok
成功地获得了该对象的shared_ptr
。
- 调用
-
wp.expired()
:- 调用
wp.expired()
检查weak_ptr
是否指向一个已销毁的对象。如果原shared_ptr
管理的对象已经被销毁,wp.expired()
会返回 true,否则返回 false。 - 由于 sp 离开作用域后会被销毁,所以
wp.expired()
返回 true,输出 “shared_ptr is destroyed
”。
- 调用
可能的输出:
sp.use_count() == 1
sp.use_count() == 1
*sp_ok = 1
shared_ptr is destroyed
总结:
weak_ptr
不会增加引用计数,它仅仅是对shared_ptr
的一个观察者。lock()
方法用于从weak_ptr
获取一个shared_ptr
,如果对象仍然存在,返回一个有效的
shared_ptr
,否则返回一个空的shared_ptr
。- 使用
expired()
可以检查weak_ptr
所指向的对象是否已经被销毁。
4.智能指针安全性问题
引用计数本身是安全的,至于智能指针是否安全需要结合实际使用分情况讨论:
情况1:多线程代码操作的是同一个shared_ptr
的对象,此时是不安全的。
比如std::thread
的回调函数,是一个lambda
表达式,其中引用捕获了一个shared_ptr
std::thread td([&sp1]()){....});
又或者通过回调函数的参数传入的shared_ptr
对象,参数类型引用
void fn(shared_ptr<A>&sp) {...}..std::thread td(fn, sp1);
这时候必然不是线程安全的。
情况2:多线程代码操作的不是同一个shared_ptr
的对象
这里指的是管理的数据是同一份,而shared_ptr
不是同一个对象。比如多线程回调的lambda
的是按值捕获的对象。
std::thread td([sp1]()){....});
另个线程传递的shared_ptr
是值传递,而非引用:
void fn(shared_ptr<A>sp) {...}..std::thread td(fn, sp1);
这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的。
也就是说,如下操作是安全的。
void fn(shared_ptr<A>sp) {...if(..){sp = other_sp;} else {sp = other_sp2;}}
需要注意:所管理数据的线程安全性问题。显而易见,所管理的对象必然不是线程安全的,必然 sp1、sp2、sp3
智能指针实际都是指向对象A, 三个线程同时操作对象A,那对象的数据安全必然是需要对象
A自己去保证。
情况 1:多线程代码操作的是同一个 shared_ptr
的对象
这种情况下,如果多个线程同时操作相同的 shared_ptr
对象(指向同一个资源),而没有采取适当的同步机制(例如锁),就会发生数据竞争和不安全的行为。
问题:
std::shared_ptr
会维护引用计数,当shared_ptr
被复制或赋值时,它的引用计数会增加。当引用计数减少到零时,资源会被释放。- 如果多个线程同时访问和修改同一个
shared_ptr
,它的引用计数可能会出现竞争条件(race
condition)。这种竞争条件可能导致引用计数错误,从而导致对象提前释放或无法正确释放。 - 同样地,在不同线程中对
shared_ptr
对象进行修改(例如重置、赋值等)时,未加同步保护也会导致内存错误或崩溃。
示例代码(不安全):
#include <iostream>
#include <memory>
#include <thread>void increment(shared_ptr<int> sp) {(*sp)++;
}int main() {shared_ptr<int> sp = std::make_shared<int>(0); // 创建一个 shared_ptr// 启动多个线程操作同一个 shared_ptrstd::thread t1(increment, sp);std::thread t2(increment, sp);t1.join();t2.join();std::cout << "Value after threads: " << *sp << std::endl;return 0;
}
问题分析:
shared_ptr<int> sp = std::make_shared<int>(0);
:这创建了一个shared_ptr
,并初始化一个整数 0。increment
函数会增加 sp 指向的整数。- 当多个线程同时操作同一个
shared_ptr
对象时,引用计数和对象数据可能会发生竞争条件,导致数据不一致或崩溃。
解决办法:
- 需要确保在多线程访问同一个
shared_ptr
对象时进行同步。例如,可以使用std::mutex
来确保每个线程在修改对象时都能获取到锁,从而避免竞争条件。
#include <iostream>
#include <memory>
#include <thread>
#include <mutex>std::mutex mtx; // 用于同步void increment(shared_ptr<int> sp) {std::lock_guard<std::mutex> lock(mtx); // 确保每次只有一个线程可以修改(*sp)++;
}int main() {shared_ptr<int> sp = std::make_shared<int>(0); // 创建一个 shared_ptr// 启动多个线程操作同一个 shared_ptrstd::thread t1(increment, sp);std::thread t2(increment, sp);t1.join();t2.join();std::cout << "Value after threads: " << *sp << std::endl;return 0;
}
在这个修复版本中,std::mutex
和 std::lock_guard
确保了每次只有一个线程能够修改 shared_ptr
指向的资源,避免了并发修改时的竞争条件。
情况 2:多线程代码操作的不是同一个 shared_ptr 的对象
这种情况下,每个线程操作不同的 shared_ptr
对象,彼此之间没有共享资源。虽然此时不会发生引用计数竞争条件,但我们仍然需要注意资源的同步问题,特别是资源的释放顺序,避免提前释放或访问已经释放的资源。
示例代码(安全):
#include <iostream>
#include <memory>
#include <thread>void increment(shared_ptr<int> sp) {(*sp)++;
}int main() {shared_ptr<int> sp1 = std::make_shared<int>(0); // 创建第一个 shared_ptrshared_ptr<int> sp2 = std::make_shared<int>(10); // 创建第二个 shared_ptr// 启动多个线程操作不同的 shared_ptrstd::thread t1(increment, sp1);std::thread t2(increment, sp2);t1.join();t2.join();std::cout << "Value after threads: " << *sp1 << " and " << *sp2 << std::endl;return 0;
}
问题分析:
- 每个线程操作不同的
shared_ptr
对象(sp1 和 sp2),不会有共享资源,所以不存在竞争条件。 - 只要保证每个线程在处理完各自的
shared_ptr
后资源能够被正确释放,就没有问题。
总结与建议:
-
情况 1:同一个
shared_ptr
的对象在多线程中操作:- 如果多个线程共享同一个
shared_ptr
,则必须同步访问该对象,防止引用计数竞争条件和资源提前释放。使用std::mutex
或其他同步机制来确保线程安全。
- 如果多个线程共享同一个
-
情况 2:多个线程操作不同的
shared_ptr
对象:- 如果线程操作的是不同的
shared_ptr
,没有资源共享问题,不需要担心引用计数的竞争。但是需要注意,确保每个线程使用的对象在其生命周期内是有效的,避免访问已经释放的资源。
- 如果线程操作的是不同的
-
注意事项:
std::shared_ptr
在多个线程间共享时,必须保证同步,尤其是当多个线程在不同地方修改同一个shared_ptr
对象时。- 线程间的
shared_ptr
传递需要特别小心,确保没有提前销毁资源,避免访问已释放的内存。 - 在多线程环境下,尽量避免过度共享
shared_ptr
对象,尤其是当共享对象较复杂时,尽量将对象的所有权限制在一个线程中,其他线程通过传递const
引用或使用weak_ptr
来观察资源状态。
通过合理的同步机制和避免不必要的共享,可以安全地在多线程环境中使用 shared_ptr
。
2右值引用和移动语义
作用:C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。
左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象。
- 区分左值和右值的便捷方法是看能不能对表达式取地址,如果能则为左值,否则为右值;
- 将亡值是C++11新增的、与右值引用相关的表达式,比如:将要被移动的对象、
T&&
函数返回的值、std::move
返回值和转换成T&&
的类型的转换函数返回值。
C++11中的所有的值必将属于左值、将亡值、纯右值三者之一,将亡值和纯右值都属于右值。
区分表达式的左右值属性:如果可对表达式用&符取址,则为左值,否则为右值。
左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
- 变量、函数或数据成员的名字
- 返回左值引用的表达式,如 ++x、x = 1、cout << ’ ’
- 字符串字面量如 “hello world”
纯右值 prvalue
是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的情况有:
- 返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
- 除字符串字面量之外的字面量,如 42、true
2.0&&的特性
右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内存,只是该对象的一个别名。
通过右值引用的声明,该右值又“重获新生”,其生命周期其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
&& 的总结如下:
- 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值。
auto&&
或函数参数类型自动推导的T&&
是一个未定的引用类型,被称为universal references
,它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。- 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引 用。当
T&&
为模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右 值引用。 - 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。
2.1 右值引用优化性能,避免深拷贝
对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:
#include <iostream>
using namespace std;class A
{
public:A() : m_ptr(new int(0)) {cout << "constructor A" << endl;}~A() {cout << "destructor A, m_ptr: " << m_ptr << endl;delete m_ptr;m_ptr = nullptr;}private:int* m_ptr;
};// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{A a;A b;cout << "ready return" << endl;if (flag)return a;elsereturn b;
}int main()
{{A a = Get(false); // 运行报错}cout << "main finish" << endl;return 0;
}
打印
constructor A
constructor A
ready return
destructor A, m_ptr:0xf87af8
destructor A, m_ptr:0xf87ae8
destructor A, m_ptr:0xf87af8
main finish
在上面的代码中,默认构造函数是浅拷贝,main
函数的 a 和Get函数的 b 会指向同一个指针 m_ptr
,在析构的时候会导致重复删除该指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码(关闭返回值优化的情况下):
#include <iostream>
using namespace std;class A
{
public:A() : m_ptr(new int(0)) {// 构造函数,动态分配内存并初始化cout << "constructor A" << endl;}// 拷贝构造函数,深拷贝 m_ptr 指向的内存A(const A& a) : m_ptr(new int(*a.m_ptr)) {cout << "copy constructor A" << endl;}~A() {// 析构函数,释放动态分配的内存cout << "destructor A, m_ptr: " << m_ptr << endl;delete m_ptr;m_ptr = nullptr;}private:int* m_ptr; // 指向动态分配内存的指针
};// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{A a; // 创建局部对象 aA b; // 创建局部对象 bcout << "ready return" << endl;// 根据 flag 返回对象 a 或 bif (flag)return a;elsereturn b;
}int main()
{{A a = Get(false); // 调用 Get 函数并返回对象 b,触发拷贝构造函数}// main 函数结束后,程序会正常调用析构函数,释放内存cout << "main finish" << endl;return 0;
}
运行结果
constructor A
constructor A
ready return
copy constructor A
destructor A, m_ptr:0xea7af8
destructor A, m_ptr:0xea7ae8
destructor A, m_ptr:0xea7b08
main finish
返回 b 时:
- 调用拷贝构造函数,输出
copy constructor A
。 - b 被深拷贝到 main 中的 a。
因为当你在函数中返回一个局部对象时,C++ 编译器会需要创建一个新的对象来接收返回的值。因为函数的返回值通常是一个临时对象,它需要通过某种方式将返回值传递给调用者的变量。这个传递通常是通过拷贝构造来完成的。当 return b
; 被调用时,编译器会使用这个拷贝构造函数创建一个新对象,并将 b 中的内容(即 b.m_ptr
指向的值)复制到新的对象中。这就是为什么调用拷贝构造函数。
这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get
函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:
// 2-1-memory3
#include <iostream>
using namespace std;class A {
public:// 构造函数,初始化 m_ptrA() : m_ptr(new int(0)) {cout << "constructor A" << endl;}// 拷贝构造函数,复制 m_ptr 指向的值A(const A& a) : m_ptr(new int(*a.m_ptr)) {cout << "copy constructor A" << endl;}// 移动构造函数,移动 m_ptr 指向的资源A(A&& a) : m_ptr(a.m_ptr) {a.m_ptr = nullptr; // 将源对象的指针置为空,避免重复释放内存cout << "move constructor A" << endl;}// 析构函数,释放 m_ptr 指向的内存~A() {cout << "destructor A, m_ptr: " << m_ptr << endl;if (m_ptr) {delete m_ptr; // 释放内存m_ptr = nullptr;}}private:int* m_ptr; // 指向整数的指针
};// 为了避免返回值优化(RVO),此函数故意这样写
A Get(bool flag) {A a; // 创建对象 aA b; // 创建对象 bcout << "ready return" << endl;if (flag)return a; // 返回 aelsereturn b; // 返回 b
}int main() {{A a = Get(false); // 调用 Get,返回对象 b,触发拷贝构造或移动构造}cout << "main finish" << endl; // 打印主函数结束信息return 0;
}
运行结果
constructor A
constructor A
ready return
move constructor A
destructor A, m_ptr:0
destructor A, m_ptr:0xfa7ae8
destructor A, m_ptr:0xfa7af8
main finish
上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&
,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&&
用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move
语义),右值引用的一个重要目的是用来支持移动语义的。
移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。
以一个简单的 string
类为示例,实现拷贝构造函数和拷贝赋值操作符。
// 2-1-mystring
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string.h>
using namespace std;class MyString {
private:char* m_data; // 存储字符串的指针size_t m_len; // 字符串的长度// 将字符串拷贝到 m_datavoid copy_data(const char* s) {m_data = new char[m_len + 1]; // 分配内存,包含 '\0' 的空间memcpy(m_data, s, m_len); // 拷贝字符串内容m_data[m_len] = '\0'; // 确保字符串以 '\0' 结束}public:// 默认构造函数MyString() {m_data = NULL;m_len = 0;}// 构造函数,接受一个 C 风格字符串MyString(const char* p) {m_len = strlen(p); // 获取字符串的长度copy_data(p); // 拷贝字符串到 m_data}// 拷贝构造函数MyString(const MyString& str) {m_len = str.m_len; // 复制长度copy_data(str.m_data); // 拷贝数据std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;}// 拷贝赋值操作符MyString& operator=(const MyString& str) {if (this != &str) { // 防止自我赋值m_len = str.m_len;copy_data(str.m_data); // 拷贝数据}std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl;return *this;}// 析构函数,释放内存virtual ~MyString() {if (m_data) free(m_data); // 释放分配的内存}
};// 测试函数
void test() {MyString a; // 创建 MyString 对象 aa = MyString("Hello"); // 使用拷贝赋值操作符赋值std::vector<MyString> vec; // 创建一个 MyString 类型的 vectorvec.push_back(MyString("World")); // 使用拷贝构造函数将 "World" 添加到 vector 中
}int main() {test(); // 调用测试函数return 0;
}
实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。MyString(“Hello”)
和 MyString(“World”)
都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。
用c++11的右值引用来定义这两个函数
// 用c++11的右值引用来定义这两个函数
MyString(MyString&& str) {std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;m_len = str.m_len;m_data = str.m_data; // 避免了不必要的拷贝str.m_len = 0;str.m_data = NULL;
}MyString& operator=(MyString&& str) {std::cout << "Move Assignment is called! source: " << str.m_data << std::endl;if (this != &str) {m_len = str.m_len;m_data = str.m_data; // 避免了不必要的拷贝str.m_len = 0;str.m_data = NULL;}return *this;
}
有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。
2.2 移动(move )语义
我们知道移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借组移动语义来优化性能呢?C++11为了解决这个问题,提供了std::move()
方法来将左值转换为右值,从而方便应用移动语义。move
是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string.h>
using namespace std;class MyString {
private:char* m_data; // 数据指针,存储字符串size_t m_len; // 字符串长度// 用于复制字符串数据的辅助函数void copy_data(const char* s) {m_data = new char[m_len + 1]; // 为字符串数据分配内存memcpy(m_data, s, m_len); // 复制数据m_data[m_len] = '\0'; // 添加字符串结束符}public:// 默认构造函数MyString() {m_data = NULL;m_len = 0;}// 以 C 风格字符串为参数的构造函数MyString(const char* p) {m_len = strlen(p); // 计算字符串长度copy_data(p); // 复制数据}// 复制构造函数MyString(const MyString& str) {m_len = str.m_len; // 获取源对象长度copy_data(str.m_data); // 复制源对象的数据std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;}// 复制赋值操作符MyString& operator=(const MyString& str) {if (this != &str) { // 避免自赋值m_len = str.m_len; // 获取源对象长度copy_data(str.m_data); // 复制数据}std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl;return *this;}// C++11 移动构造函数(右值引用)MyString(MyString&& str) {std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;m_len = str.m_len; // 获取源对象的长度m_data = str.m_data; // 转移源对象的数据指针str.m_len = 0; // 清空源对象的长度str.m_data = NULL; // 清空源对象的数据指针}// C++11 移动赋值操作符(右值引用)MyString& operator=(MyString&& str) {std::cout << "Move Assignment is called! source: " << str.m_data << std::endl;if (this != &str) { // 避免自赋值m_len = str.m_len; // 获取源对象的长度m_data = str.m_data; // 转移源对象的数据指针str.m_len = 0; // 清空源对象的长度str.m_data = NULL; // 清空源对象的数据指针}return *this;}// 虚析构函数,确保正确析构对象virtual ~MyString() {if (m_data) free(m_data); // 释放内存}
};int main() {MyString a;// 使用移动赋值操作符a = MyString("Hello");// 使用复制构造函数MyString b = a;// 使用移动构造函数,将左值转换为右值MyString c = std::move(a); // move, 将左值转为右值return 0;
}
2.4 forward
完美转发
forward
完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
现存在一个函数
Template<class T>void func(T &&val);
根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。但要注意,引用以后,这个val值它本质上是一个左值!
看下面例子
int &&a = 10;int &&b = a; //错误
解释:
-
int &&a = 10;
:- 这行代码是合法的。它将一个右值 10 绑定到右值引用 a。这里 10 是一个临时的右值,可以绑定到右值引用 a。
-
int &&b = a;
:- 这行代码是错误的。a 是一个左值引用(即它引用的是 10 这个右值的临时对象),而右值引用不能绑定到左值引用。具体地说,右值引用只能绑定到右值,而不能绑定到另一个右值引用。
右值引用(int &&
)的本意是绑定到一个即将被销毁的临时对象或右值(如 10),而左值引用(int&
)则是绑定到一个存活的对象,表示它仍然存在。
因此我们有了std::forward()
完美转发,这种T &&val
中的val是左值,但如果我们用std::forward (val)
,就会按照参数原来的类型转发;
int &&a = 10;int &&b = std::forward<int>(a);
解释:
-int &&a = 10;
是合法的。这将右值 10 绑定到右值引用 a。
std::forward<int>(a)
是在 左值引用 语境下使用std::forward
时的标准做法。它的目的是保持传递参数的值类别。如果 a 是右值引用,std::forward<int>(a)
会将 a作为右值进行转发;如果 a 是左值引用,std::forward<int>(a)
会将 a 作为左值进行转发。
通过范例巩固下知识:
#include <iostream>
using namespace std;// 左值引用版本的 Print 函数
template <class T>
void Print(T &t) {cout << "L" << t << endl; // 打印 "L" 和 t 的值
}// 右值引用版本的 Print 函数
template <class T>
void Print(T &&t) {cout << "R" << t << endl; // 打印 "R" 和 t 的值
}// func 函数,接受右值引用 T&&,通过不同的方式传递 t 来调用 Print 函数
template <class T>
void func(T &&t) {Print(t); // 传递原始的 t(如果是左值,则会调用左值版本)Print(std::move(t)); // 使用 std::move 转换 t 为右值引用,调用右值版本Print(std::forward<T>(t)); // 使用 std::forward 完美转发 t,保持左值/右值特性
}int main() {cout << "-- func(1)" << endl;func(1); // 传递右值 1int x = 10; // x 是左值int y = 20; // y 是左值cout << "-- func(x)" << endl;func(x); // x 本身是左值,调用左值版本的 Printcout << "-- func(std::forward<int>(y))" << endl;func(std::forward<int>(y)); // std::forward 用于完美转发 y,保持其值类别(y 是左值)return 0;
}
运行结果:
-- func(1)
R1
R1
R1
-- func(x)
L10
R10
L10
-- func(std::forward<int>(y))
L20
R20
L20
解释输出:
-
func(1):
- 第一个
Print(t)
会使用右值引用版本,因为 1 是一个右值。 - 第二个
Print(std::move(t))
会将 t 转换为右值引用,并调用右值版本的 Print。 - 第三个
Print(std::forward<T>(t))
会保留 t 的右值性质,因此也调用了右值版本的 Print。
- 第一个
-
func(x):
- 第一个
Print(t)
会使用左值引用版本,因为 x 是左值。 - 第二个
Print(std::move(t))
会将 t 转换为右值引用,调用右值版本的 Print。 - 第三个
Print(std::forward<T>(t))
会使用左值引用版本,因为 x 是左值。
- 第一个
-
func(std::forward(y)):
- 第一个
Print(t)
会使用左值引用版本,因为 y 是左值,并且 std::forward 保留了左值的特性。 - 第二个
Print(std::move(t))
会将 t 转换为右值引用,调用右值版本的 Print。 - 第三个
Print(std::forward<T>(t))
会使用左值引用版本,保持 y 的左值性质。
- 第一个
-
总结:
std::move
将对象转换为右值引用,用于触发右值版本的函数。std::forward
用于完美转发,保留原始参数的值类别。- 在模板函数中,
std::move
和std::forward
是进行参数传递时确保值类别正确的重要工具。
综合示例
#include "stdio.h"
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;class A
{
public:// 默认构造函数,初始化为空指针和大小为0A() : m_ptr(NULL), m_nSize(0) {}// 带参构造函数,接收指针和大小并进行深拷贝A(int *ptr, int nSize){m_nSize = nSize;m_ptr = new int[nSize];printf("A(int *ptr, int nSize) m_ptr:%p\n", m_ptr);if (m_ptr){memcpy(m_ptr, ptr, sizeof(int) * nSize); // 复制数据}}// 拷贝构造函数,深拷贝m_ptr指向的数据A(const A &other){m_nSize = other.m_nSize; // 复制大小if (other.m_ptr){printf("A(const A &other) m_ptr:%p\n", m_ptr);if(m_ptr) // 如果原对象已经有内存,则先释放delete[] m_ptr;printf("delete[] m_ptr\n");m_ptr = new int[m_nSize]; // 分配新内存memcpy(m_ptr, other.m_ptr, sizeof(int) * m_nSize); // 复制数据}else{if(m_ptr) // 如果当前对象已有内存,释放delete[] m_ptr;m_ptr = NULL; // 设置为空}cout << "A(const A &other)" << endl; // 输出拷贝构造消息}// 右值引用构造函数,实现移动语义A(A &&other){m_ptr = NULL; // 初始化为空指针m_nSize = other.m_nSize; // 直接复制大小if (other.m_ptr){m_ptr = move(other.m_ptr); // 使用移动语义,避免不必要的拷贝other.m_ptr = NULL; // 清空原对象的指针}}// 析构函数,释放动态分配的内存~A(){if (m_ptr){delete[] m_ptr;m_ptr = NULL;}}// 删除指针的封装函数void deleteptr(){if (m_ptr){delete[] m_ptr;m_ptr = NULL;}}int *m_ptr = NULL; // 指向整数数组的指针int m_nSize = 0; // 数组的大小
};int main()
{int arr[] = {1, 2, 3}; // 初始化一个整数数组A a(arr, sizeof(arr) / sizeof(arr[0])); // 使用带参构造函数创建对象acout << "m_ptr in a Addr: 0x" << a.m_ptr << endl; // 输出a的指针地址A b(a); // 使用拷贝构造函数创建bcout << "m_ptr in b Addr: 0x" << b.m_ptr << endl; // 输出b的指针地址b.deleteptr(); // 删除b中的指针资源A c(std::forward<A>(a)); // 使用完美转发创建c(右值引用构造)cout << "m_ptr in c Addr: 0x" << c.m_ptr << endl; // 输出c的指针地址c.deleteptr(); // 删除c中的指针资源// 使用vector示范右值引用和移动语义vector<int> vect{1, 2, 3, 4, 5}; // 初始化一个整数vectorcout << "before move vect size: " << vect.size() << endl; // 输出移动前的大小vector<int> vect1 = move(vect); // 将vect转移到vect1cout << "after move vect size: " << vect.size() << endl; // 输出移动后的vect大小(应为0)cout << "new vect1 size: " << vect1.size() << endl; // 输出新的vect1大小return 0;
}
2.5 emplace_back 减少内存拷贝和移动
对于STL容器,C++11后引入了emplace_back
接口。
emplace_back
是就地构造,不用构造后再次复制到容器中。因此效率更高。
考虑这样的语句:
vector<string> testVec;testVec.push_back(string(16, 'a'));
上述语句足够简单易懂,将一个string
对象添加到testVec
中。底层实现:
- 首先,
string(16, ‘a’)
会创建一个string
类型的临时对象,这涉及到一次string
构造过程。 - 其次,
vector
内会创建一个新的string
对象,这是第二次构造。 - 最后在
push_back
结束时,最开始的临时对象会被析构。加在一起,这两行代码会涉及到两次string
构造和一次析构。
c++11可以用emplace_back
代替push_back
,emplace_back
可以直接在vector
中构建一个对象,而非创建一个临时对象,再放进vector
,再销毁。emplace_back
可以省略一次构建和一次析构,从而达到优化的目的。
测试范例
#ifndef TIME_INTERVAL_H
#define TIME_INTERVAL_H
#include <iostream>
#include <memory>
#include <string>
#ifdef GCC
#include <sys/time.h> // Linux 下获取时间
#else
#include <ctime> // Windows 下获取时间
#endif // GCCclass TimeInterval
{
public:TimeInterval(const std::string& d) : detail(d){init();}TimeInterval() // 默认构造函数{init();}~TimeInterval() // 析构函数,输出时间差{
#ifdef GCCgettimeofday(&end, NULL); // 获取结束时间std::cout << detail<< 1000 * (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1000 // 计算并输出时间差(单位:ms)<< " ms" << std::endl;
#elseend = clock(); // 获取结束时间std::cout << detail<< (double)(end - start) // 计算并输出时间差(单位:clock ticks)<< " ms" << std::endl;
#endif // GCC}protected:void init() {
#ifdef GCCgettimeofday(&start, NULL); // 获取当前时间(Linux下使用gettimeofday)
#elsestart = clock(); // 获取当前时间(Windows下使用clock)
#endif // GCC}
private:std::string detail; // 用于描述时间区间的字符串
#ifdef GCCtimeval start, end; // 用于存储开始和结束的时间点(Linux)
#elseclock_t start, end; // 用于存储开始和结束的时间点(Windows)
#endif // GCC
};// 宏定义:自动创建并管理 TimeInterval 对象的生命周期
#define TIME_INTERVAL_SCOPE(d) std::shared_ptr<TimeInterval> time_interval_scope_begin = std::make_shared<TimeInterval>(d)#endif // TIME_INTERVAL_H
#include <vector>
#include <string>
#include "time_interval.h" // 引入时间间隔头文件,用于测量代码执行时间int main() {// 定义一个字符串的 vector 容器std::vector<std::string> v;int count = 10000000; // 设置需要插入的元素数量为 1000 万v.reserve(count); // 预分配 1000 万大小的内存,避免 push_back 时动态扩容// 计时:测试使用 push_back 和左值引用(即普通拷贝)插入字符串{TIME_INTERVAL_SCOPE("push_back string:"); // 记录代码块执行时间for (int i = 0; i < count; i++) {std::string temp("ceshi"); // 创建一个临时字符串对象v.push_back(temp); // 将 temp 插入到 vector 中,使用的是左值引用}}v.clear(); // 清空 vector,准备进行下一次测试// 计时:测试使用 push_back 和右值引用(即移动语义)插入字符串{TIME_INTERVAL_SCOPE("push_back move(string):"); // 记录代码块执行时间for (int i = 0; i < count; i++) {std::string temp("ceshi"); // 创建一个临时字符串对象v.push_back(std::move(temp)); // 使用 std::move 转换 temp 为右值引用,避免不必要的拷贝}}v.clear(); // 清空 vector,准备进行下一次测试// 计时:测试直接创建字符串并使用 push_back 插入(右值引用){TIME_INTERVAL_SCOPE("push_back(string):"); // 记录代码块执行时间for (int i = 0; i < count; i++) {v.push_back(std::string("ceshi")); // 直接创建临时字符串,并插入到 vector 中}}v.clear(); // 清空 vector,准备进行下一次测试// 计时:测试使用 c 字符串直接插入(右值引用){TIME_INTERVAL_SCOPE("push_back(c string):"); // 记录代码块执行时间for (int i = 0; i < count; i++) {v.push_back("ceshi"); // 使用 c 字符串直接插入,C++ 自动将其转换为 std::string}}v.clear(); // 清空 vector,准备进行下一次测试// 计时:测试使用 emplace_back 插入字符串(只有一次构造,不会有拷贝){TIME_INTERVAL_SCOPE("emplace_back(c string):"); // 记录代码块执行时间for (int i = 0; i < count; i++) {v.emplace_back("ceshi"); // 使用 emplace_back 直接在 vector 中构造元素,避免拷贝}}
}
测试结果
push_back string:335 ms
push_back move(string):307 ms
push_back(string):285 ms
push_back(c string):295 ms
emplace_back(c string):234 ms
第1中方法耗时最长,原因显而易见,将调用左值引用的push_back
,且将会调用一次string
的拷贝构造函数,比较耗时,这里的string
还算很短的,如果很长的话,差异会更大
第2、3、4中方法耗时基本一样,参数为右值,将调用右值引用的push_back
,故调用string
的移动构造函数,移动构造函数耗时比拷贝构造函数少,因为不需要重新分配内存空间。
第5中方法耗时最少,因为emplace_back
只调用构造函数,没有移动构造函数,也没有拷贝构造函数。
为了证实上述论断,我们自定义一个类,并在普通构造函数、拷贝构造函数、移动构造函数中打印相应
描述:
#include <vector>
#include <string>
#include "time_interval.h"
using namespace std;class Foo {
public:Foo(std::string str) : name(str) {std::cout << "constructor" << std::endl;}Foo(const Foo& f) : name(f.name) {std::cout << "copy constructor" << std::endl;}Foo(Foo&& f) : name(std::move(f.name)){std::cout << "move constructor" << std::endl;}
private:std::string name;
};int main() {std::vector<Foo> v;int count = 10000000;v.reserve(count); //预分配十万大小,排除掉分配内存的时间{TIME_INTERVAL_SCOPE("push_back T:");Foo temp("test");v.push_back(temp);// push_back(const T&),参数是左值引用//打印结果://constructor//copy constructor}cout << " ---------------------\n" << endl;v.clear();{TIME_INTERVAL_SCOPE("push_back move(T):");Foo temp("test");v.push_back(std::move(temp));// push_back(T &&), 参数是右值引用//打印结果://constructor//move constructor}cout << " ---------------------\n" << endl;v.clear();{TIME_INTERVAL_SCOPE("push_back(T&&):");v.push_back(Foo("test"));// push_back(T &&), 参数是右值引用//打印结果://constructor//move constructor}cout << " ---------------------\n" << endl;v.clear();{std::string temp = "test";TIME_INTERVAL_SCOPE("push_back(string):");v.push_back(temp);// push_back(T &&), 参数是右值引用//打印结果://constructor//move constructor}cout << " ---------------------\n" << endl;v.clear();{std::string temp = "test";TIME_INTERVAL_SCOPE("emplace_back(string):");v.emplace_back(temp);// 只有一次构造函数,不调用拷贝构造函数,速度最快//打印结果://constructor}
}
2.6 unordered container
无序容器
C++11 增加了无序容器 unordered_map/unordered_multimap
和
unordered_set/unordered_multiset
,由于这些容器中的元素是不排序的,因此,比有序容器 map/multimap
和 set/multiset
效率更高。 map
和 set
内部是红黑树,在插入元素时会自动排序,而
无序容器内部是散列表( Hash Table
),通过哈希( Hash
),而不是排序来快速操作元素,使得效率更高。由于无序容器内部是散列表,因此无序容器的 key
需要提供 hash_value
函数,其他用法和 map/set
的用法是一样的。不过对于自定义的 key,需要提供 Hash 函数和比较函数。
2.6.1 map
和unordered_map
的差别
需要引入的头文件不同
map: #include < map >
unordered_map: #include < unordered_map >
内部实现机理不同
- map:
map
内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map
内部的所有元素都是有序的,红黑树的每一个节点都代表着map
的一个元素。因此,对于map
进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map
中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储 的,使用中序遍历可将键值按照从小到大遍历出来。 - unordered_map:
unordered_map
内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash
表中一个位置来访问记录,查找的时间复杂度可达到O(1)
,其在海量数据处理中有着广泛应 用)。因此,其元素的排列顺序是无序的。
优缺点以及适用处
map
:
- 优点:
- 有序性,这是
map
结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作 - 红黑树,内部实现一个红黑书使得
map
的很多操作在lgn
的时间复杂度下就可以实现,因此效率非常的高
- 有序性,这是
- 缺点:
- 空间占用率高,因为
map
内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间
- 空间占用率高,因为
- 适用处:
对于那些有顺序要求的问题,用map
会更高效一些
unordered_map
:
- 优点: 因为内部实现了哈希表,因此其查找速度非常的快
- 缺点: 哈希表的建立比较耗费时间
- 适用处:对于查找问题,
unordered_map
会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map
总结
- 内存占有率的问题就转化成红黑树 VS hash表 , 还是
unorder_map
占用的内存要高。 - 但是
unordered_map
执行效率要比map
高很多 - 对于
unordered_map
或unordered_set
容器,其遍历顺序与创建该容器时输入的顺序不一定相同,因为遍历是按照哈希表从前往后依次遍历的
2.7 小结
C++11 在性能上做了很大的改进,最大程度减少了内存移动和复制,通过右值引用、 forward
、emplace
和一些无序容器我们可以大幅度改进程序性能。
- 右值引用仅仅是通过改变资源的所有者来避免内存的拷贝,能大幅度提高性能。
forward
能根据参数的实际类型转发给正确的函数。emplace
系列函数通过直接构造对象的方式避免了内存的拷贝和移动。- 无序容器在插入元素时不排序,提高了插入效率,不过对于自定义 key 时需要提供
hash
函数和比较函数
3匿名函数lambda
3.1 匿名函数的基本语法为:
//[捕获列表](参数列表)->返回类型{函数体}int main(){auto Add = [](int a, int b)->int {return a + b;};std::cout << Add(1, 2) << std::endl; //输出3 return 0;}
一般情况下,编译器可以自动
推断出lambda
表达式的返回类型,所以我们可以不指定返回类型,即:
//[捕获列表](参数列表){函数体}int main(){auto Add = [](int a, int b) {return a + b;};std::cout << Add(1, 2) << std::endl; //输出3 return 0;}
但是如果函数体内有多个return
语句时,编译器无法自动推断出返回类型,此时必须
指定返回类型。
3.2 捕获列表
有时候,需要在匿名函数内使用外部变量,所以用捕获列表来传参,如
int main(){int c = 12;auto Add = [c](int a, int b)->int { //捕获列表加入使用的外部变量c,否则无法通过编译return c;
};std::cout << Add(1, 2) << std::endl;return 0;}
但是,如果Add中加入一句:c = a;
int main()
{int c = 12;auto Add = [&c](int a, int b)->int { //捕获列表改为了&c,表示按引用传递,就可以修改了;不加&表示按值传递,无法通过编译 c = a;return c;};std::cout << Add(1, 2) << std::endl;return 0;
}
补充知识:
- 如果捕获列表为
[&]
,则表示所有的外部变量都按引用
传递给lambda使用; - 如果捕获列表为
[=]
,则表示所有的外部变量都按值
传递给lambda使用; - 匿名函数构建的时候对于按值传递的捕获列表,会立即将当前可以取到的值拷贝一份作为常数,然
后将该常数作为参数传递。
3.3 匿名函数的简写
匿名函数由捕获列表、参数列表、返回类型和函数体组成;可以忽略参数列表和返回类型,但不可以忽略捕获列表和函数体,如:
auto f = []{ return 1 + 2; };
3.4 Lambda捕获列表
4 C++11标准库(STL)
STL定义了强大的、基于模板的、可复用的组件,实现了许多通用的数据结构及处理这些数据结构的算法。其中包含三个关键组件——容器(container,流行的模板数据结构)、迭代器(iterator)和算法(algorithm)。
| 迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。 |
| 算法 | 算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索
和转换等操作。 |
4.1 容器简介
STL容器,可将其分为四类:序列容器、有序关联容器、无序关联容器、容器适配器
序列容器:
有序关联容器(键按顺序保存):
无序关联容器:
容器适配器:
序列容器描述了线性的数据结构(也就是说,其中的元素在概念上” 排成一行"), 例如数组、向量和链表。
关联容器描述非线性的容器,它们通常可以快速锁定其中的元素。这种容器可以存储值的集合或 者键值对。
栈和队列都是在序列容器的基础上加以约束条件得到的,因此STL把stack
和queue
作为容器适配器来实现,这样就可以使程序以一种约束方式来处理线性容器。类型string
支持的功能跟线性容器一样, 但是它只能存储字符数据。
4.2 迭代器简介
迭代器在很多方面与指针类似,也是用于指向首类容器中的元素(还有一些其他用途,后面将会提到)。 迭代器存有它们所指的特定容器的状态信息,即迭代器对每种类型的容器都有一个实现。 有些迭代器的操作在不同容器间是统一的。 例如,*运算符间接引用一个迭代器,这样就可以使用它所指向的元素。++运算符使得迭代器指向容器中的下一个元素(和数组中指针递增后指向数组的下一个元素类似)。
STL 首类容器提供了成员函数 begin
和 end
。函数 begin
返回一个指向容器中第一个元素的迭代器,函数 end
返回一个指向容器中最后一个元素的下一个元素(这个元素并不存在,常用于判断是否到达了容器的结束位仅)的迭代器。 如果迭代器 i 指向一个特定的元素,那么 ++i
指向这个元素的下一个元素。* i
指代的是i
指向的元素。 从函数 end
中返回的迭代器只在相等或不等的比较中使用,来判断这个“移动的迭代器” (在这里指i)是否到达了容器的末端。
使用一个iterator
对象来指向一个可以修改的容器元素,使用一个 const_iterator
对象来指向一个不能修改 的容器元素。
每种容器所支持的迭代器类型决定了这种容器是否可以在指定的 STL 算 法中使用。 支持随机访问迭代器的容器可用于所有的 STL 算法(除了那些需要改变容器大小的算法,这样的算法不能在数组和 array
对象中使用)。 指向 数组的指针可以代替迭代器用于几乎所有的 STL 算法中,包括那些要求随机访问迭代器的算法。 下表显示了每种 STL 容器所支持的迭代器类型。 注意, vector 、 deque 、 list 、 set 、 multiset 、 map 、 multimap以及 string 和数组
都可以使用迭代器遍历
。
下表显示了在 STL容器的类定义中出现的几种预定义的迭代器typedef
。不是每种 typedef
都出现在每个容器中。 我们使用常量版本的迭代器来访问只读容器或不应该被更改的非只读容器,使用反向迭代器来以相反的方向访问容器。
下表显示了可作用在每种迭代器上的操作。 除了给出的对于所有迭代器都有的运算符,迭代器还必须提供默认构造函数、拷贝构造函数和拷贝赋值操作符。 前向迭代器支持++
和所有的输入和输出迭代器的功能
。 双向迭代器支持–操作和前向迭代器的功能。 随机访问迭代器支持所有在表中给出的操作。 另外, 对于输入迭代器和输出迭代器,不能在保存迭代器之后再使用保存的值。
4.3 算法简介
STL提供了可以用于多种容器的算法,其中很多算法都是常用的。插入、删除、搜索、排序
及其他一些对部分或全部序列容器和关联容器适用的算法。
STL包含了大约70个标准算法,表格中提供了这些算法的实例及概述。作用在容器元素上的算法只是间接地通过迭代器来实现。很多作用在序列元素上的算法通过一对迭代器定义:第一个迭代器指向这列元素的第一个,第二个迭代器指向最后一个元素之后的位置。 另外,还可以使用相似的方法创建自己的算法,这样它们就能和STL容器及迭代器一起使用了。
C++ 参考手册https://zh.cppreference.com/w/cpp
5正则表达式
https://zh.cppreference.com/w/cpp/regex
范例
#include <iostream>using namespace std;// 匿名函数的基本语法:
// [捕获列表](参数列表)->返回类型{函数体}// test1 函数展示了带有返回类型的 lambda 表达式
void test1()
{// 定义一个接受两个整数并返回其和的 Lambda 表达式auto Add = [](int a, int b) -> int {return a + b; // 返回 a 和 b 的和};std::cout << Add(1, 2) << std::endl; // 输出 3
}// test2 函数展示了编译器自动推导返回类型的 lambda 表达式
void test2()
{// 定义一个 Lambda 表达式,自动推导返回类型auto Add = [](int a, int b) {return a + b; // 返回 a 和 b 的和};std::cout << Add(1, 2) << std::endl; // 输出 3
}// test3 函数展示了按值捕获外部变量的 lambda 表达式
void test3()
{int c = 12; // 外部变量 cint d = 30; // 外部变量 d// Lambda 捕获外部变量 c 和 d(按值捕获)auto Add = [c, d](int a, int b) -> int {cout << "d = " << d << endl; // 打印 d 的值return c; // 返回捕获的 c 的值};d = 20; // 修改 d 的值,但 Lambda 仍然使用的是按值捕获的 dstd::cout << Add(1, 2) << std::endl; // 输出捕获的 c 的值,输出 12
}// test4 函数展示了按值捕获外部变量的 lambda 表达式,且无法修改捕获的变量
void test4()
{int c = 12; // 外部变量 c// 按值捕获 c,捕获的 c 是该变量的副本auto Add = [c](int a, int b) -> int {// c = a; // 编译报错,按值捕获的变量是不可修改的return c; // 返回捕获的 c 的值};std::cout << Add(1, 2) << std::endl; // 输出 12
}// test5 函数展示了按引用捕获外部变量的 lambda 表达式,并且可以修改捕获的变量
void test5()
{int c = 12; // 外部变量 cint d = 30; // 外部变量 d// 按引用捕获 c 和 d,捕获的 c 和 d 可以在 Lambda 内部被修改auto Add = [&c, &d](int a, int b) -> int {c = a; // 修改捕获的 ccout << "d = " << d << endl; // 打印 d 的值return c; // 返回修改后的 c};d = 20; // 修改 d 的值std::cout << Add(1, 2) << std::endl; // 输出修改后的 c 的值,即 1
}// test6 函数展示了传递引用类型参数给 lambda,并且修改了引用参数
void test6()
{int c = 12; // 外部变量 cint d = 30; // 外部变量 d// 按引用捕获 c 和 d,传递引用类型参数给 Lambdaauto Add = [&c, &d](int &a, int &b) -> int {a = 11; // 修改传入的参数 ab = 12; // 修改传入的参数 bcout << "d = " << d << endl; // 打印 d 的值return a + b; // 返回 a 和 b 的和};d = 20; // 修改 d 的值std::cout << Add(c, d) << std::endl; // 输出 11 + 12 = 23,a 和 b 已被修改cout << "c = " << c << endl; // 输出 11,a 已经被修改为 11cout << "d = " << d << endl; // 输出 20,d 没有被修改
}int main()
{test6(); // 调用 test6,展示捕获和修改引用变量的效果return 0;
}
代码详细注释:
test1()
和test2()
:展示了Lambda
表达式的基本用法,test1()
中明确指定了返回类型,test2()
使用了编译器自动推导返回类型。test3()
:展示了按值捕获外部变量。捕获的c
和d
变量在Lambda
内部不可修改(尤其是 c 是按值捕获的副本)。test4()
:按值捕获外部变量,并尝试修改c
,结果编译报错,因为捕获的c
是副本,无法修改。test5()
:展示了按引用捕获外部变量,c
和d
可以在Lambda
内部修改。特别地,c
的值被修改为1
。test6()
:展示了传递引用类型参数到Lambda
,Lambda
可以修改这些参数的值。
输出示例:
d = 20
23
c = 11
d = 20
说明:
- 按值捕获:
Lambda
捕获外部变量时,捕获的是变量的副本(即复制品)。因此,如果在Lambda
中修改捕获的值,不会影响外部的原始变量。 - 按引用捕获:
Lambda
捕获外部变量时,捕获的是外部变量的引用。Lambda
中对变量的修改会影响原始变量。 - 传递引用类型参数:如果传递的是引用类型参数,
Lambda
内部修改这些参数时,会直接影响外部的变量。