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

C++11的一些特性

一、右值引用与移动构造

1.1右值引用

        左值与右值最大的区别就是,左值可以取地址,而右值不能取地址。例如:

1;//常量
10 + 2;//表达式
A();//匿名对象

还有将亡值,也就是超出作用域将要被销毁掉的值

        以上这些都是右值,不能取地址。那么什么是右值引用呢?

        与左值引用类似,右值引用用来引用右值。

int&& a = 1;//常量
int&& b = 10 + 2;//表达式
A&& c = A();//匿名对象

        区别于左值引用只使用一个“&”,右值引用需要使用“&&”与左值引用进行区分。右值引用不能绑定左值,但是如果使用move可以将左值转化为右值被右值引用,但是这个时候右值引用绑定的是左值的拷贝,并不是左值本身。

        另外,右值引用后的变量实际上是一个左值,也就是说例如:int&& a = 1; 这里的a实际上是左值,这是为了给移动构造做铺垫。

1.2移动构造

        当函数返回值是函数内创建的临时变量时,接受返回值的过程实际上需要经历两次拷贝构造,如果构造是深拷贝就会浪费资源,而移动构造就是为解决这个问题产生的:

class MyString {
private:char* data_;size_t size_;public:// 移动构造函数MyString(MyString&& other): data_(other.data_), // 直接"偷"指针size_(other.size_)  // 复制size{// 关键步骤:置空源对象,防止其析构时释放我们刚偷的内存other.data_ = nullptr;other.size_ = 0;}// 析构函数~MyString() {delete[] data_; // 安全:如果data_是nullptr,delete[]是安全的}// ... 拷贝构造函数、拷贝赋值、移动赋值、其他成员函数 ...
};

        上面是移动构造的示例,通过右值引用接收将亡值,再将将亡值的数据直接转移走,如果正常拷贝构造需要new空间,那么使用移动构造就可以省去这个过程,节省大量资源。而移动赋值也是一样的,直接将数据转移,而不需要new新空间然后再赋值,从而将两次拷贝构造消耗的资源节省出来。

// 移动赋值运算符MyString& operator=(MyString&& other) {// 1. 防止自赋值 (虽然移动自赋值少见,但安全第一)if (this != &other) {// 2. 释放当前对象持有的资源delete[] data_;// 3. "偷"资源data_ = other.data_;size_ = other.size_;// 4. 置空源对象other.data_ = nullptr;other.size_ = 0;}return *this; // 5. 返回 *this}

        而右值引用的变量是左值,也是因为移动构造需要修改这个变量,如果变量是右值不能修改,那么移动构造就失去了意义。

二、lambda表达式与std::function

2.1 lambda表达式

        lambda表达式的格式为 [capture-list] (parameters) mutable -> return-type {function-body}

2.1.1 capture-list

        capture-list就是捕捉列表,用来捕捉当前作用域中的变量:

  • []:不捕获任何变量

  • [x]:按值捕获变量 x

  • [&x]:按引用捕获变量 x

  • [=]:按值捕获所有外部变量

  • [&]:按引用捕获所有外部变量

  • [this]:捕获当前类的 this 指针

  • [x, &y]:混合捕获(x 按值,y 按引用)

  • [=, &x]:默认按值捕获,但 x 按引用

  • [&, x]:默认按引用捕获,但 x 按值

         捕捉列表不能省略,用来确定是lambda表达式。

2.1.2 parameters

        类似于函数的参数,用来接收输入的参数,这个部分可以省略。

2.1.3 mutable

        如果有这个关键字,那么就能修改捕获到的值,但是不会影响被捕获的值本身,因为如果不是引用捕捉,那么捕捉实际上是对外部变量进行了拷贝。这个关键字也可以省略。

2.1.4  -> return-type

        用来标明返回的类型,通常也可以省略,因为会自动识别返回类型。

2.1.5 {function-body}

        这个部分{}内部的内容可以不写,但是这个花括号必须写,是函数的主体。

        lambda表达式使用时,类似于伪函数,通常使用auto f1 = [capture-list] (parameters) mutable -> return-type {function-body};接收lambda表达式,然后f1(parameters)即可。

int main(void) {auto f1 = [](int a, int b)->bool { return a < b;};cout << f1(1, 2);return 0;
}

        例如这样,就是一个简单的lambda表达式。

2.2 std::function

        function是一个包装器,可以将函数指针,伪函数,lambda表达式包装成类,例如:

int main(void) {function<bool(int, int)> f1 = [](int a, int b)->bool { return a < b;};cout << f1(1, 2);return 0;
}

         此时,f1的类型就是class std::function<bool __cdecl(int,int)>,如果不进行包装,f1的类型就比较复杂:class `int __cdecl main(void)'::`2'::<lambda_1>,且不同编译器的命名方式也不同。因为lambda表达式的类型是隐藏的。

        那么function有什么用呢?可以作为函数指针,伪函数,lambda表达式的类型去初始化类模板:

int main(void) {map<string, function<int(int, int)>> mop;mop.insert(make_pair("+", [](int a, int b) {return a + b;}));mop.insert(make_pair("-", [](int a, int b) {return a - b;}));mop.insert(make_pair("*", [](int a, int b) {return a * b;}));mop.insert(make_pair("/", [](int a, int b) {return a / b;}));cout << mop["+"](1, 2);return 0;
}

        这样就可以很简单的完成识别字符串中的运算类型并进行运算,而不需要使用switch写一大串,增加了可读性,且修改方便。

2.3 std::bind

int Add(int a, int b) {return a + b;
}int main(void) {function<int(int)> f1 = bind(Add, placeholders::_1, 10);cout << f1(2);return 0;
}

         当函数参数固定时,重复输入就会比较麻烦,这个时候,使用bind就可以固定住想要固定的参数,就如上面代码所示,将b固定为10。

        placeholders::_1表示接收到的第一个参数_2则为第二个,例如:

int test(int a, int b, int c) {return a - b + c;
}int main(void) {function<int(int, int)> f1 = bind(test, 10, placeholders::_2, placeholders::_1);cout << f1(2, 3);return 0;
}

        这里就是将a固定为10,第一个参数给到c,第二个参数给到b,因此结果等于10-3+2=9。

        bind通常与function一起使用。

三、统一初始化与初始化列表

        统一初始化其实就是可以使用{}进行初始化,但是这样的初始化有不同的含义,例如:

int a{1};
double b{};//初始化为0.0class A{
public:A(int a):_a(a){}
private:int _a;
};
A c{10};

        在上面的例子中,初始化与直接使用()的方式类似,但是不能使用浮点数类型来初始化int类型,会报错因为涉及精度转换,类似的 float f1{1e40}; 这样也会报错。

        在这种情况下的初始化,本质上类似于类型转换,因此如果精度不正确就会报错。

list<int> l1  = {1, 2, 3, 4, 5};
map<string, string> s1 = {{"banana", "香蕉"},{"apple", "苹果"},{"orange", "橘子"}};

        而像上面这种情况下的初始化,则涉及到initializer_list这个在C++11中才有的容器。

        在list的例子中,{1, 2, 3, 4, 5}被初始化成initializer_list<int>类型,然后传入list的初始化函数:

        因此,如果要自定义的类型支持这种类似于C语言数组的初始化方式,就需要实现支持initializer_list的初始化方式。因为initializer_list支持迭代器,因此,只需要用范围for,然后调用insert或者push_back等即可。 

四、其他

  1. auto类型推导,当类型过长或复杂时,使用auto直接推导类型。
  2. nullptr 用来区别于NULL,代表空指针。
  3. decltype 可以进行类型推导,尽管typeid().name也可以推导类型,但是decltype可以用来初始化类模板。
  4. override 用于指示派生类中的成员函数应该重写基类中的虚函数,没有重写就会报错,类似于一种强制重写的机制。
  5. finish 用于限制类的继承或成员函数的重写。用在类则不能被继承,用在成员函数则不能被重写。

 对于智能指针和并发控制的介绍等后面再更新了。

 

相关文章:

  • JavaEE:使用JMeter进行接口并发测试
  • docker执行yum报错Could not resolve host: mirrorlist.centos.org
  • 旧物回收小程序开发:开启绿色生活新方式
  • 使用Vue3开发Electorn桌面应用
  • 车载电子电器架构 --- 电子电气架构设计方案
  • 容器通信技术与数据持久化
  • Solidity内部合约创建全解析:解锁Web3开发新姿势
  • 4.1 FFmpeg编译选项配置
  • 基于Attention机制的模型。这使得它摆脱了RNN模型顺序读取序列的缺点,可以实现高度的并行化的理解
  • 《Effective Python》第九章 并发与并行——使用 concurrent.futures 实现真正的并行化
  • HarmonyOS 5的分布式通信矩阵是如何工作的?
  • Docker 高级管理笔记
  • HTML5简介
  • 高云GW5AT-LV60 FPGA图像处理板
  • 升级到 .NET 9 分步指南
  • redis分布式锁 Redisson在电商平台开发中的实际应用
  • sqlsuger 获取表行数
  • 基于物联网的智能饮水机系统设计
  • 1.23Node.js 中操作 mongodb
  • Qt Library库系列----Serial串口
  • 备案需要网站吗/推广代理平台登录
  • 网站开发项目进度表/免费外链代发
  • 东莞营销型网站建设流程/自动点击关键词软件
  • 南阳网站建设价格/怎么让某个关键词排名上去
  • 商丘网络营销服务/沧州网站优化
  • 深圳做外贸网站多少钱/企业管理培训课程网课免费