当前位置: 首页 > news >正文

百度商桥网站代码去哪里添加山门做网站

百度商桥网站代码去哪里添加,山门做网站,微信小程序模板库,企业融资数据在哪查目录 1智能指针1.shared_ptr1.1 shared_ptr的基本用法使用shared_ptr要注意的问题运用 2.unique_ptr独占的智能指针示例#xff1a;管理动态内存 3.weak_ptr弱引用的智能指针weak_ptr的基本用法lock 的作用#xff1a;weak_ptr返回this指针weak_ptr解决循环引用问题weak_ptr使… 目录 1智能指针1.shared_ptr1.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匿名函数lambda3.1 匿名函数的基本语法为3.2 捕获列表3.3 匿名函数的简写3.4 Lambda捕获列表 4 C11标准库(STL)4.1 容器简介**4.2 迭代器简介**4.3 算法简介 5正则表达式 1智能指针 C程序设计中使用堆内存是非常频繁的操作堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率但是整体来说堆内存的管理是麻烦的C11中引入了智能指针的概念方便管理堆内存。使用普通指针容易造成堆内存泄露忘记释放二次释放程序发生异常时内存泄露等问题等使用智能指针能更好的管理堆内存。 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_ptrint p1(new int(1));std::shared_ptrint p2 p1;std::shared_ptrint p3;p3.reset(new int(1));if(p3) {cout p3 is not null;}我们应该优先使用make_shared来构造智能指针因为他更高效。 auto sp1 make_sharedint(100);//相当于 shared_ptrint sp1(new int(100));不能将一个原始指针直接赋值给一个智能指针例如下面这种方法是错误的 std::shared_ptrint p new int(1);shared_ptr不能通过“直接将原始这种赋值”来初始化需要通过构造函数和辅助方法来初始化。 对于一个未初始化的智能指针可以通过reset方法来初始化当智能指针有值的时候调用reset会引起引用计数减1。 另外智能指针可以通过重载的bool类型操作符来判断。 #include iostream #include memory using namespace std;int main() {// 创建一个空的 shared_ptrstd::shared_ptrint p1;// 使用 reset() 初始化 p1指向一个动态分配的 int 对象值为 1p1.reset(new int(1));// 创建另一个 shared_ptr p2指向与 p1 相同的对象std::shared_ptrint 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_ptrint 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 memoryusing namespace std;void DeleteIntPtr(int *p) {cout call DeleteIntPtr endl;delete p;}int main(){std::shared_ptrint p(new int(1), DeleteIntPtr);return 0;} 当p的引用计数为0时自动调用删除器DeleteIntPtr来释放对象的内存。删除器可以是一个lambda表达式上面的写法可以改为 std::shared_ptrint p(new int(1), [](int *p) {cout call lambda delete p endl;delete p;});当我们用shared_ptr管理动态数组时需要指定删除器因为shared_ptr的默认删除器不支持数组对象代码如下所示 std::shared_ptrint p3(new int[10], [](int *p) { delete [] p;});使用shared_ptr要注意的问题 不要用一个原始指针初始化多个shared_ptr例如下面错误范例 int *ptr new int; // 使用裸指针分配内存 shared_ptrint p1(ptr); // p1 管理裸指针 shared_ptrint p2(ptr); // p2 也尝试管理同一块内存在这段代码中ptr 是一个裸指针它指向通过 new 动态分配的内存。然后你用两个 shared_ptrp1 和 p2来管理这块内存。 问题所在 std::shared_ptr 会通过引用计数来管理资源的生命周期。当 shared_ptr 被销毁时会减少引用计数并在引用计数为零时自动释放资源调用 delete。如果你直接用裸指针ptr初始化两个 shared_ptr这会导致 双重删除 的问题因为 p1 和 p2 都会在销毁时调用 delete而裸指针已经在外部被 new 分配过一次这样会导致两次删除同一块内存从而发生未定义行为例如程序崩溃。 不要在函数实参中创建shared_ptr对于下面的写法 function(shared_ptrint(new int), g()); //有缺陷因为C的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的一般是从右到左但也可能从左到右所以可能的过程是先new int然后调用g()如果恰好g()发生异常而shared_ptr还没有创建 则int内存泄漏了正确的写法应该是先创建智能指针代码如下 shared_ptrint 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_ptrA GetSelf() {return shared_ptrA(this); // 不要这么做错误的做法}~A() {cout Deconstruction A endl;} };int main() {shared_ptrA sp1(new A); // 创建 shared_ptr sp1 来管理 A 的实例shared_ptrA 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_thisA // 继承 enable_shared_from_this { public:shared_ptrA GetSelf(){return shared_from_this(); // 返回指向当前对象的 shared_ptr}~A(){cout Deconstruction A endl;} };int main() {// 创建 shared_ptr sp1 来管理 A 类的实例shared_ptrA sp1 make_sharedA(); // 使用 make_shared 更加安全和高效// 使用 sp1 调用 GetSelf() 返回当前对象的 shared_ptrshared_ptrA 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_ptrB bptr; // A 持有 B 的 shared_ptr~A() {cout A is deleted endl;} };class B { public:shared_ptrA aptr; // B 持有 A 的 shared_ptr~B() {cout B is deleted endl;} };int main() {{shared_ptrA ap(new A); // 创建 shared_ptr Ashared_ptrB 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_ptrB bptr即 A 引用 B。B 类持有一个 shared_ptrA aptr即 B 引用 A。 引用计数增加 当你创建 shared_ptrA ap(new A) 时A 的引用计数是 1。当你创建 shared_ptrB 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 memoryclass MyClass { public:MyClass() {std::cout MyClass Constructor std::endl;}~MyClass() {std::cout MyClass Destructor std::endl;} };int main() {{std::shared_ptrMyClass ptr1 std::make_sharedMyClass();std::shared_ptrMyClass 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_ptrT my_ptr(new T);unique_ptrT my_other_ptr my_ptr; // 报错不能复制unique_ptr不允许复制但可以通过函数返回给其他的unique_ptr还可以通过std::move来转移到其他的unique_ptr这样它本身就不再拥有原来指针的所有权了。例如 unique_ptrT my_ptr(new T); // 正确 unique_ptrT my_other_ptr std::move(my_ptr); // 正确 unique_ptrT ptr my_ptr; // 报错不能复制std::make_shared是c11的一部分但std::make_unique不是。它是在c14里加入标准库的。 auto upw1(std::make_uniqueWidget()); // with make funcstd::unique_ptrWidget upw2(new Widget); // without make func使用new的版本重复了被创建对象的键入但是make_unique函数则没有。重复类型违背了软件工程的一个重要原则应该避免代码重复代码中的重复会引起编译次数增加导致目标代码膨胀。 除了unique_ptr的独占性 unique_ptr和shared_ptr还有一些区别比如 unique_ptr可以指向一个数组代码如下所示 std::unique_ptrint [] ptr(new int[10]);ptr[9] 9;std::shared_ptrint [] ptr2(new int[10]); // 这个是不合法的std::shared_ptr 是一个引用计数的智能指针它允许多个指针共享对同一资源的所有权并在最后一个指针超出作用域时释放资源。shared_ptr 通常用于单个对象但对于数组类型的资源它不直接支持。标准库中的 shared_ptr 是设计为管理单个对象的内存并不直接支持数组类型即 int[]即使你用 new 分配的是数组。 unique_ptr指定删除器和shared_ptr有区别 std::shared_ptrint ptr3(new int(1), [](int *p){delete p;}); // 正确 std::unique_ptrint ptr4(new int(1), [](int *p){delete p;}); // 错误unique_ptr需要确定删除器的类型所以不能像shared_ptr那样直接指定删除器可以这样写 std::unique_ptrint, void(*)(int*) ptr5(new int(1), [](int *p){delete p;}); // 正确关于shared_ptr和unique_ptr的使用场景是要根据实际应用需求来选择。如果希望只有一个智能指针管理资源或者管理数组就unique_ptr如果希望多个智能指针管理同一个资源就用shared_ptr。 示例管理动态内存 我们将创建一个 Person 类类的构造函数中分配一个动态字符串并且在析构函数中自动释放它。 代码实现 #include iostream #include memory #include stringclass Person { public:// 构造函数动态分配内存Person(const std::string name) {this-name std::make_uniquestd::string(name);}// 显示名字void show() const {std::cout Persons name is: *name std::endl;}private:// 使用 unique_ptr 来管理动态分配的内存std::unique_ptrstd::string name; };int main() {// 使用 unique_ptr 来管理 Person 对象std::unique_ptrPerson person std::make_uniquePerson(Alice);// 显示人的名字person-show();// 当 person 超出作用域时它会自动释放内存return 0; } 代码解析 Person 类 Person 类包含一个 std::unique_ptrstd::string用于管理动态分配的 std::string。在构造函数中我们使用 std::make_uniquestd::string来创建动态分配的字符串并将其赋值给 name 成员变量。在析构函数中我们不需要显式地调用 delete因为 unique_ptr会自动释放内存。 2.main 函数 使用 std::make_uniquePerson(Alice) 创建一个 Person 对象并将其分配给std::unique_ptr。 通过 person-show()调用成员函数来显示名字。 当 main 函数结束时person 会超出作用域自动释放 name 所指向的内存。 运行结果 Persons 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_ptrint sp(new int(10));weak_ptrint wp(sp);cout wp.use_count() endl; //结果讲输出1通过expired()方法判断所观察资源是否已经释放如下所示 shared_ptrint sp(new int(10));weak_ptrint wp(sp);if(wp.expired())cout weak_ptr无效,资源已释放;elsecout weak_ptr有效;通过lock方法获取监视的shared_ptr如下所示 lock有什么用处 #include iostream #include memoryusing namespace std;std::weak_ptrint gw;void f() {if (gw.expired()) {cout gw无效, 资源已释放 endl;} else {auto spt gw.lock();cout gw有效, *spt *spt endl;} }int main() {{auto sp make_sharedint(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 指向相同的对象并增加引用计数。如果对象已经被销毁即引用计数为 0lock() 返回一个空的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_thisA { public:shared_ptrA GetSelf() {return shared_from_this(); // 使用 shared_from_this 获取当前对象的 shared_ptr}~A() {cout Deconstruction A endl; // 析构时输出消息} };int main() {shared_ptrA sp1(new A); // 创建 shared_ptr sp1管理 A 类型对象shared_ptrA sp2 sp1-GetSelf(); // 使用 GetSelf 获取同一个对象的 shared_ptr sp2return 0; } 输出结果如下 Deconstruction A 在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的因为shared_from_this()是内部的weak_ptr调用lock()方法之后返回的智能指针在离开作用域之后spy的引用计数减为0A对象会被析构不会出现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_ptrB bptr; // 使用 weak_ptr 避免循环引用~A() {cout A is deleted endl;} };class B { public:std::shared_ptrA aptr;~B() {cout B is deleted endl;} };int main() {{// 创建 shared_ptr 管理 A 和 B 对象std::shared_ptrA ap(new A);std::shared_ptrB bp(new B);// 设置相互引用ap-bptr bp; // A 指向 B 的 weak_ptrbp-aptr ap; // B 指向 A 的 shared_ptr}cout main leave endl;return 0; } 这样在对B的成员赋值时即执行bp-aptrap;时由于aptr是weak_ptr它并不会增加引用计数所以ap的引用计数仍然会是1在离开作用域之后ap的引用计数为减为0A指针会被析构析构后其内部的bptr的引用计数会被减为1然后在离开作用域后bp引用计数又从1减为0B对象也被析构不会发生内存泄漏。 weak_ptr使用注意事项 weak_ptr在使用前需要检查合法性。 weak_ptrint wp;{shared_ptrint sp(new int(1)); //sp.use_count()1wp sp; //wp不会改变引用计数所以sp.use_count()1shared_ptrint sp_ok wp.lock(); //wp没有重载-操作符。只能这样取所指向的对象 }shared_ptrint 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_ptrint wp;shared_ptrint sp_ok;{shared_ptrint 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_ptrint wp声明了一个 weak_ptr它用于持有shared_ptrint 的弱引用。weak_ptr不会影响对象的引用计数它只是对对象的观察者。 shared_ptrint 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_ptrAsp) {...}..std::thread td(fn, sp1); 这时候必然不是线程安全的。 情况2多线程代码操作的不是同一个shared_ptr的对象 这里指的是管理的数据是同一份而shared_ptr不是同一个对象。比如多线程回调的lambda的是按值捕获的对象。 std::thread td([sp1]()){....});另个线程传递的shared_ptr是值传递而非引用 void fn(shared_ptrAsp) {...}..std::thread td(fn, sp1); 这时候每个线程内看到的sp他们所管理的是同一份数据用的是同一个引用计数。但是各自是不同的对象当发生多线程中修改sp指向的操作的时候是不会出现非预期的异常行为的。 也就是说如下操作是安全的。 void fn(shared_ptrAsp) {...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 threadvoid increment(shared_ptrint sp) {(*sp); }int main() {shared_ptrint sp std::make_sharedint(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_ptrint sp std::make_sharedint(0);这创建了一个shared_ptr并初始化一个整数 0。increment 函数会增加 sp 指向的整数。当多个线程同时操作同一个 shared_ptr 对象时引用计数和对象数据可能会发生竞争条件导致数据不一致或崩溃。 解决办法 需要确保在多线程访问同一个 shared_ptr 对象时进行同步。例如可以使用 std::mutex 来确保每个线程在修改对象时都能获取到锁从而避免竞争条件。 #include iostream #include memory #include thread #include mutexstd::mutex mtx; // 用于同步void increment(shared_ptrint sp) {std::lock_guardstd::mutex lock(mtx); // 确保每次只有一个线程可以修改(*sp); }int main() {shared_ptrint sp std::make_sharedint(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 threadvoid increment(shared_ptrint sp) {(*sp); }int main() {shared_ptrint sp1 std::make_sharedint(0); // 创建第一个 shared_ptrshared_ptrint sp2 std::make_sharedint(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右值引用和移动语义 作用C11中引用了右值引用和移动语义可以避免无谓的复制提高了程序性能。 左值是表达式结束后仍然存在的持久对象右值是指表达式结束时就不存在的临时对象。 区分左值和右值的便捷方法是看能不能对表达式取地址如果能则为左值否则为右值将亡值是C11新增的、与右值引用相关的表达式比如将要被移动的对象、T函数返回的值、std::move返回值和转换成T的类型的转换函数返回值。 C11中的所有的值必将属于左值、将亡值、纯右值三者之一将亡值和纯右值都属于右值。 区分表达式的左右值属性如果可对表达式用符取址则为左值否则为右值。 左值 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::vectorMyString vec; // 创建一个 MyString 类型的 vectorvec.push_back(MyString(World)); // 使用拷贝构造函数将 World 添加到 vector 中 }int main() {test(); // 调用测试函数return 0; } 实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。MyString(“Hello”) 和 MyString(“World”) 都是临时对象也就是右值。虽然它们是临时的但程序仍然调用了拷贝构造和拷贝赋值造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源既能节省资源有能节省资源申请和释放的时间。这正是定义转移语义的目的。 用c11的右值引用来定义这两个函数 // 用c11的右值引用来定义这两个函数 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 )语义 我们知道移动语义是通过右值引用来匹配临时值的那么普通的左值是否也能借组移动语义来优化性能呢C11为了解决这个问题提供了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;}// C11 移动构造函数右值引用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; // 清空源对象的数据指针}// C11 移动赋值操作符右值引用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 完美转发实现了参数在传递过程中保持其值属性的功能即若是左值则传递之后仍然是左值若是右值则传递之后仍然是右值。 现存在一个函数 Templateclass Tvoid 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::forwardint(a);解释 -int a 10;是合法的。这将右值 10 绑定到右值引用 a。 std::forwardint(a) 是在 左值引用 语境下使用 std::forward时的标准做法。它的目的是保持传递参数的值类别。如果 a 是右值引用std::forwardint(a) 会将 a作为右值进行转发如果 a 是左值引用std::forwardint(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::forwardT(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::forwardint(y)) endl;func(std::forwardint(y)); // std::forward 用于完美转发 y保持其值类别y 是左值return 0; }运行结果 -- func(1) R1 R1 R1 -- func(x) L10 R10 L10 -- func(std::forwardint(y)) L20 R20 L20解释输出 func(1): 第一个 Print(t) 会使用右值引用版本因为 1 是一个右值。第二个 Print(std::move(t)) 会将 t 转换为右值引用并调用右值版本的 Print。第三个 Print(std::forwardT(t)) 会保留 t 的右值性质因此也调用了右值版本的 Print。 func(x): 第一个 Print(t) 会使用左值引用版本因为 x 是左值。第二个 Print(std::move(t))会将 t 转换为右值引用调用右值版本的 Print。第三个 Print(std::forwardT(t))会使用左值引用版本因为 x 是左值。 func(std::forward(y)): 第一个 Print(t) 会使用左值引用版本因为 y 是左值并且 std::forward 保留了左值的特性。第二个 Print(std::move(t)) 会将 t 转换为右值引用调用右值版本的 Print。第三个 Print(std::forwardT(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::forwardA(a)); // 使用完美转发创建c右值引用构造cout m_ptr in c Addr: 0x c.m_ptr endl; // 输出c的指针地址c.deleteptr(); // 删除c中的指针资源// 使用vector示范右值引用和移动语义vectorint vect{1, 2, 3, 4, 5}; // 初始化一个整数vectorcout before move vect size: vect.size() endl; // 输出移动前的大小vectorint vect1 move(vect); // 将vect转移到vect1cout after move vect size: vect.size() endl; // 输出移动后的vect大小应为0cout new vect1 size: vect1.size() endl; // 输出新的vect1大小return 0; } 2.5 emplace_back 减少内存拷贝和移动 对于STL容器C11后引入了emplace_back接口。 emplace_back是就地构造不用构造后再次复制到容器中。因此效率更高。 考虑这样的语句 vectorstring testVec;testVec.push_back(string(16, a));上述语句足够简单易懂将一个string对象添加到testVec中。底层实现 首先string(16, ‘a’)会创建一个string类型的临时对象这涉及到一次string构造过程。其次vector内会创建一个新的string对象这是第二次构造。最后在push_back结束时最开始的临时对象会被析构。加在一起,这两行代码会涉及到两次string构造和一次析构。 c11可以用emplace_back代替push_backemplace_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_ptrTimeInterval time_interval_scope_begin std::make_sharedTimeInterval(d)#endif // TIME_INTERVAL_H #include vector #include string #include time_interval.h // 引入时间间隔头文件用于测量代码执行时间int main() {// 定义一个字符串的 vector 容器std::vectorstd::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::vectorFoo 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 无序容器 C11 增加了无序容器 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 小结 C11 在性能上做了很大的改进最大程度减少了内存移动和复制通过右值引用、 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 C11标准库(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 iostreamusing 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 23a 和 b 已被修改cout c c endl; // 输出 11a 已经被修改为 11cout d d endl; // 输出 20d 没有被修改 }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()展示了传递引用类型参数到 LambdaLambda 可以修改这些参数的值。 输出示例 d 20 23 c 11 d 20 说明 按值捕获Lambda 捕获外部变量时捕获的是变量的副本即复制品。因此如果在 Lambda 中修改捕获的值不会影响外部的原始变量。按引用捕获Lambda 捕获外部变量时捕获的是外部变量的引用。Lambda中对变量的修改会影响原始变量。传递引用类型参数如果传递的是引用类型参数Lambda 内部修改这些参数时会直接影响外部的变量。
http://www.ho-use.cn/article/10816623.html

相关文章:

  • 贵阳网站设计与开发怎么做百度云服务器建设网站
  • 专业建站公司的业务内容有哪些公司网站怎么建立
  • 曲阜住房城乡建设局网站网页公司制作
  • 免费做祝福网站公司网站建设方案建议
  • 免费推广平台排行聊城seo整站优化报价
  • 国家商标查询官方网站北京网站建设价
  • 烟台网站建设推荐企汇互联见效付款公司logo设计理念说明
  • 网站建设 自查表网站建设费用明细表
  • 仿站是什么意思经三路专业做网站
  • 海口网站建设平台wordpress 数据交互
  • 武进网站建设多少钱攀枝花三线建设网站
  • 营销型网站和普通网站的区别iis7 wordpress伪静态
  • 网站站内优化个人博客大全
  • 做网站互联网公司排名网络美工是干啥的
  • 中国代理网官方网站义乌门户网站建设
  • 网站版面布局结构怎么做微课网站
  • 做网站需要宽带销售管理软件排行
  • 音乐网站答辩可以做蛋白三位结构图的网站
  • 网站建设后怎么手机网页制作html
  • 电商网站开发的主流技术wordpress 修改小部件
  • 手机端网站建设郑州网站开发工具 哪个好
  • 下面有关网络营销特点的论述正确的有宁波自适应网站建设优化建站
  • 网站技术报务费如何做会计分录网站开发技术服务费合同范本
  • 网站收录下降的原因做网站合同
  • 门户网站是什么jsp网站 值班功能
  • 陕西省城乡住房建设部网站网站推广方式百度云
  • e龙岩网站建设网站有哪些
  • 微信手机网站搭建网站建设的一些知识
  • 珠海酒店网站建设公司建个网站需要多少钱
  • asp评价网站开发文档开发app需要多少资金