C++11新特性—nullptr,bind,lambda函数,智能指针,左值右值,移动语义与完美转发

news/2024/10/18 9:17:39/

nullptr

NULL一般来自C语言,是宏定义,在C++语言中,NULL被定义为整数0,但是这样会无法与整数0区分,所以C++11引入新特性nullptr,可以区分整数与指针特性,但是仍然存在问题-不能区分指针类型,这种情况下,必须显示的指明参数类型。

forward_list

只能单向遍历。

  • 内存占用小
  • 插入删除效率高
  • 无法随机访问
  • 插入删除会改变链表结构,所以迭代器可能会失效
  • 不支持逆向遍历

bind

可以通过引用来直接操作变量的值,而bind函数可以用来将函数对象和参数列表绑定成一个可调用对象。如果想要在bind函数中传递引用,可以使用std::ref()函数来包装引用对象,例如:

#include <functional>
#include <iostream>void add(int& a, int& b)
{a += b;
}int main()
{int a = 10;int b = 5;auto func = std::bind(add, std::ref(a), std::ref(b));func();std::cout << "a = " << a << std::endl; //a=15std::cout << "b = " << b << std::endl; //b=5return 0;
}

auto与decltype

auto可以通过初始值来进行类型推演,所以auto类型必须要有初始值。
decltype的作用是选择并返回操作数的数据类型,在这个过程中,编译器只是得到表达式的类型,而不用实际计算出表达式的值。

lambda

可以在代码中直接定义匿名函数,lambda函数可以作为参数传递给函数或者算法也可以作为返回值。编译器会生成一个匿名类,被称为闭包类型。
capture list用于捕获外部变量,可以是空或包含一个或多个变量。parameter list和return type(如果函数体是一个表达式则可以自动推导返回类型;如果是一个语句块,则需要显式指定返回类型)与普通函数的参数列表和返回类型相同。function body是函数体,可以包含一系列语句。

[capture list] (parameter list) -> return type { function body }

例子

auto add = [](int a, int b) -> int {return a + b;
};int sum = add(1, 2); // sum = 3

lambda函数还可以捕获外部变量

int x = 10;auto print = [x](int n) {std::cout << "x = " << x << ", n = " << n << std::endl;
};print(5); // x = 10, n = 5

还支持函数对象,可以用于实现比较、排序等算法

std::vector<int> v{5, 1, 3, 2, 4};std::sort(v.begin(), v.end(), [](int a, int b) {return a > b;
});

lambda函数可以忽略参数列表和返回值,但是必须包括捕获列表和函数体。

智能指针

防止堆内存泄露。智能指针是一个模板类,用来存储指向动态分配的对象的指针,负责自动释放动态分配的对象。符合RAII机制

shared_ptr

用于管理动态分配的对象,可以实现多个智能指针共享一个对象,并在所有智能指针都不再引用该对象时自动释放该对象。使用引用计数,每使用一次,计数加一,析构一次,计数减一,对象读取需要加锁,内部引用计数是线程安全的。

shared_ptr会有循环引用的问题,也就是两个对象互相使用一个shared_ptr成员变量指向对方。

weak_ptr

所以引入weak_ptr,弱引用一般是辅助shared_ptr使用的,并不对对象的内存进行管理,类似于普通指针,但是弱引用可以检测到所管理的对象是否已经被释放。弱引用只引用,不计数,弱引用并不保证所指向的内存是一定有效的。
std::weak_ptr可以通过std::shared_ptr进行初始化,当std::shared_ptr对象被销毁时,std::weak_ptr并不会影响该对象的引用计数,也不会自动释放该对象。相反,当需要使用该对象时,可以通过std::weak_ptr的lock()方法获取一个可用的std::shared_ptr对象,如果该对象已经被释放,则返回一个空的std::shared_ptr对象。

为什么可以解决循环引用的问题?

  • 可以将循环引用中的某个shared_ptr指针换成weak_ptr指针,这样不会增加引用计数。
#include <iostream>
#include <memory>struct Node;struct Edge {std::weak_ptr<Node> end;
};struct Node {std::vector<std::shared_ptr<Edge>> edges;
};int main() {std::shared_ptr<Node> node1(new Node);std::shared_ptr<Node> node2(new Node);node1->edges.push_back(std::make_shared<Edge>());node1->edges.back()->end = node2;// 在使用node2之前,先通过weak_ptr获取node2std::shared_ptr<Node> node2_copy = node1->edges.back()->end.lock();if (node2_copy) {std::cout << "node2 is alive" << std::endl;} else {std::cout << "node2 is dead" << std::endl;}// 手动销毁node2node2.reset();// 再次使用node2node2_copy = node1->edges.back()->end.lock();if (node2_copy) {std::cout << "node2 is alive" << std::endl;} else {std::cout << "node2 is dead" << std::endl;}return 0;
}
make_shared

make_shared是C++11引入的一个模板函数,它可以用于创建指向动态分配对象的std::shared_ptr智能指针,并将对象的内存分配和构造函数调用合并在一起,从而可以提高性能和内存使用效率。make_shared可以避免多次分配内存,减少内存碎片,并且可以减少动态分配对象的构造和析构次数,从而提高程序的性能。

实现一个shared_ptr类

1、shared_ptr需要计数指向同一个对象的指针的数量,保证在所有的指针全部释放后,对象才会被删除。
2、shared_ptr需要正确处理空指针。
3、shared_ptr需要能够正确地拷贝和赋值。

代码来源阿秀的学习笔记

template<typename T>
class SharedPtr
{
public:SharedPtr(T* ptr = NULL):_ptr(ptr), _pcount(new int(1)){}SharedPtr(const SharedPtr& s):_ptr(s._ptr), _pcount(s._pcount){(*_pcount)++;}SharedPtr<T>& operator=(const SharedPtr& s){if (this != &s){if (--(*(this->_pcount)) == 0){delete this->_ptr;delete this->_pcount;}_ptr = s._ptr;_pcount = s._pcount;*(_pcount)++;}return *this;}T& operator*(){return *(this->_ptr);}T* operator->(){return this->_ptr;}~SharedPtr(){--(*(this->_pcount));if (*(this->_pcount) == 0){delete _ptr;_ptr = NULL;delete _pcount;_pcount = NULL;}}
private:T* _ptr;int* _pcount;//指向引用计数的指针
};

make_shared是C++11之后的函数模板,头文件是,用于在动态内存中分配一个对象并初始化它,返回指向这个对象的shared_ptr。使用方法如下:

std::shared_ptr sp = std::make_shared(args…);
使用make_shared可以提高效率,避免使用shared_ptr(new T(args…))的方式。

unique_ptr

独享所有权,转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,原指针置空.。unique_ptr不支持普通的拷贝和赋值操作,拷贝一个unique_ptr,那么在拷贝结束之后,都会指向相同的资源,最后多次对同一块内存指针多次释放,造成程序崩溃。

实现一个unique_ptr

unique_ptr不能被复制,所以拷贝构造函数和拷贝赋值运算符都被禁用了。
移动构造函数和移动运算符则是将指针所有权转移给目标unique_ptr,原始指针成为nullptr。

template class unique_ptr { 
public: unique_ptr() : ptr(nullptr) {}explicit unique_ptr(T* p) : ptr(p) {}unique_ptr(const unique_ptr& other) = delete;unique_ptr& operator=(const unique_ptr& other) = delete;unique_ptr(unique_ptr&& other) : ptr(other.ptr) { other.ptr = nullptr; }unique_ptr& operator=(unique_ptr&& other) {if (&other == this) return *this;delete ptr;ptr = other.ptr;other.ptr = nullptr;return *this;}~unique_ptr() { if (ptr) delete ptr; }T& operator*() const { return *ptr; }T* operator->() const { return ptr; }//返回对象原始指针T* get() const { return ptr; }void reset() {delete ptr;ptr = nullptr;}void reset(T* p) {delete ptr;ptr = p;}
private: T* ptr;
};
 int main() { unique_ptr up(new int(42)); std::cout << up << std::endl; // 输出42 up.reset(new int(17));std::cout << up << std::endl; // 输出17 }

auto_ptr

为了解决“有异常抛出时发生内存泄漏”的问题,因为常规指针在抛出异常的时候有可能导致空间得不到释放而内存泄露。auto_ptr构造的时候获得某个对象的控制权,析构的时候释放这个对象。

  • auto_ptr对象析构的时候会删除它所拥有的指针,使用的时候避免多个auto_ptr对象管理同一个指针。
  • auto_ptr内部是使用“delete”所以不能管理数组。

左值和右值

  • 在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。

  • 左值是指既能出现在等号左边也能出现在等号右边的变量(或表达式),右值则只能出现在等号右边

  • 左值持久,右值短暂,左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象(将要被销毁的对象)。

  • 纯右值指的是临时变量和不跟对象关联的字面量值;将亡值就是在确保其他变量不再使用的时候,通过盗取的方式避免内存空间释放,延长声明周期。

  • 右值表示无法获取地址的对象,有常量值、函数返回值、lambda表达式等。无法获取地址,但不表示其不可改变,当定义了右值的右值引用时就可以更改右值。

左值引用和右值引用

  • 由于右值通常不具有名字,所以也只能通过引用的方式找到他。C++11中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置
  • 左值引用是具体变量值的别名,右值引用是匿名的变量的别名。常量左值引用可以接受非常量左值、常量左值、右值对其初始化;非常量左值只能接受非常量左值对其进行初始化。
  • 右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。
  • 右值引用独立于左值和右值,也就是说右值引用类型的变量有可能是左值或者右值。

深入理解(&)左值引用和(&&)右值引用

代码来源阿秀的学习笔记

#include <bits/stdc++.h>
using namespace std;template<typename T>
void fun(T&& t)
{cout << t << endl;
}int getInt()
{return 5;
}int main() {int a = 10;int& b = a;  //b是左值引用int& c = 10;  //错误,c是左值不能使用右值初始化int&& d = 10;  //正确,右值引用用右值初始化int&& e = a;  //错误,e是右值引用不能使用左值初始化const int& f = a; //正确,左值常引用相当于是万能型,可以用左值或者右值初始化const int& g = 10;//正确,左值常引用相当于是万能型,可以用左值或者右值初始化const int&& h = 10; //正确,右值常引用const int& aa = h;//正确int& i = getInt();  //错误,i是左值引用不能使用临时变量(右值)初始化int&& j = getInt();  //正确,函数返回值是右值fun(10); //此时fun函数的参数t是右值fun(a); //此时fun函数的参数t是左值return 0;
}

move

  • 移动语义:有些类的资源是独一无二的,比如IO,unique_ptr等,他们不可以复制,但是可以把资源交出所有权给新的对象,成为可移动的。
  • move语义在一些对象的构造时可以获取到已有的资源而不需要通过拷贝,申请新的内存。这样数据的拷贝就不是深拷贝了,而是浅拷贝。这样移动而非拷贝将会大幅度提升性能。例如一些即将消亡的右值,可以用移动语义接管

move的实现

实际上move就是一个类型转换器,将左值转换成右值

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{return static_case<typename remove_reference<T>::type&&>(t);
}
remove_reference

通过模板去除引用

通用引用

move的输入参数类型称为通用引用类型,通用引用就是既可以接收左值也可以接收右值
代码来源C++高阶知识:深入分析移动构造函数及其原理

#include<iostream>
template<typename T>
void f(T&& param){std::cout << "the value is "<< param << std::endl;
}int main(int argc, char *argv[]){int a = 123;auto && b = 5;   //通用引用,可以接收右值int && c = a;    //错误,右值引用,不能接收左值auto && d = a;   //通用引用,可以接收左值const auto && e = a; //错误,加了const就不再是通用引用了func(a);         //通用引用,可以接收左值func(10);        //通用引用,可以接收右值
}

在上述代码中有两种类型的通用引用,一种是auto,另一种是通过模板定义的T&&,实际上auto和T是等价的。

完美转发

forward

作用是保持原来值的属性不变也就是说,如果原来是左值,那么处理之后还是左值;如果原来是右值,那么处理之后还是右值。
代码来源聊聊C++中的完美转发

#include<iostream>
template<typename T>
void print(T & t){std::cout << "lvalue" << std::endl;
}template<typename T>
void print(T && t){std::cout << "rvalue" << std::endl;
}template<typename T>
void testForward(T && v){print(v);print(std::forward<T>(v));print(std::move(v));
}int main(int argc, char * argv[])
{testForward(1);std::cout << "======================" << std::endl;int x = 1;testFoward(x);
}

运行结果如下

lvalue
rvalue
rvalue
=========================
lvalue
lvalue
rvalue

传入的1是右值,但是经过函数传参之后变成了左值,第二行使用forward函数,所以不会改变它的右值属性,第三行move将传入的参数强制转换成左值。
传入的x是左值,forward之后也是左值,move之后成为右值。

forward实现原理

template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{return static_cast<T&&>(param);
}template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{return static_cast<T&&>(param);
}

完美转发失败

完美转发是指将一个函数的参数列表中的参数原封不动地转发到另一个函数中,但是完美转发有可能失败。

  • 函数模板和非函数模板参数类型不匹配
  • 模板参数推导失败
    会导致完美转发失败的实参种类有大括号初始化、以值0或者NULL表达空指针、仅有声明的整型static const成员变量、模板或重载的函数名字、以及位域。
    条款30 熟悉完美转发的失败情形

http://www.ppmy.cn/news/69787.html

相关文章

【手撕红黑树】

前言 相信很多人初学者听到了红黑树后心中不免有些心慌&#xff0c;那你看到了这篇文章后相信会有所收获&#xff0c;我其实刚开始也是对红黑树抱着一种害怕甚至是恐惧&#xff0c;但是在老师的帮助下也终于慢慢的不在恐惧了&#xff0c;你想知道为什么的话就继续往下看吧。&am…

[Python物联网]Python基础知识和语法--Python模块和包--Python快速上手开发物联网上位机程序

目录 一、前言 二、模块的导入 三、模块的定义 四、包的定义 五、包的相对导入 六、示例代码 七、总结 一、前言 在 Python 中&#xff0c;模块是指一个包含 Python 代码的文件。而包则是指一个包含多个模块的目录。模块和包是 Python 代码复用的基本组织方式。在本文中…

说说谷歌Chrome浏览器无痕浏览器窗口

当您启用无痕浏览后&#xff0c;设备的其他用户将不会看到您的历史记录。 Chrome 不会保存您的浏览记录或您在表单中填写的信息。当您浏览时&#xff0c;Chrome 会记住相应的 Cookie 和网站数据&#xff0c;但当您退出无痕模式时&#xff0c;Chrome 会删除这些数据。您可在打开…

Redis进阶

主要内容 Redis持久化Redis主从Redis哨兵Redis分片集群 Redis持久化 Redis有两种持久化的方案: RDB持久化AOF持久化 1. RDB持久化 RDB全称Redis Database Backup file&#xff08;Redis数据备份文件&#xff09;&#xff0c;也被叫做Redis数据快照。简单来说就是把内存中的所…

Android Framework——Binder 监控方案

作者&#xff1a;低性能JsonCodec 在 Android 应用开发中&#xff0c;Binder 可以说是使用最为普遍的 IPC 机制了。我们考虑监控 Binder 这一 IPC 机制&#xff0c;一般是出于以下两个目的&#xff1a; 卡顿优化&#xff1a;IPC 流程完整链路较长&#xff0c;且依赖于其他进程…

【Linux系统】Linux进程信号详解

Linux进程信号 0 引言1 认识信号1.1 什么是信号1.2 发送信号的本质1.3 信号的处理 2 信号的产生2.1 键盘产生2.2 调用系统函数向进程发送信号2.3 由软件条件产生信号2.4 硬件异常产生信号 3 信号的保存4 信号的处理5 总结 0 引言 本篇文章会从Linux信号的产生到信号的保存&…

信息熵、交叉熵、KL散度公式的简单理解

整理&#xff1a;我不爱机器学习 1 信息量 信息量是对信息的度量&#xff0c;就跟时间的度量是秒一样&#xff0c;考虑一个离散的随机变量 x 的时候&#xff0c;当观察到的这个变量的一个具体值的时候&#xff0c;我们接收到了多少信息呢&#xff1f; 例如听到太阳从东方升起…

免费优质的网页设计素材网站推荐

找到一个免费优质的网页设计素材网站并不容易。 有些网站要么需要网站的会员&#xff0c;要么设计素材质量差。本文整理总结了10个免费的网页设计材料网站&#xff0c;希望给大家带来一些帮助。 即时设计资源社区 即时设计资源社区是由在线协同设计软件即时设计推出的设计社…