欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 明星 > C++11(1)

C++11(1)

2025/5/4 5:36:53 来源:https://blog.csdn.net/2402_83272216/article/details/147670609  浏览:    关键词:C++11(1)

文章目录

  • 列表初始化
    • C++98传统的{}
    • C++11中的{}初始化
    • C++11中的std::initializer_list
  • 右值引用和移动语义
    • 左值和右值
    • 左值引用和右值引用
    • 引用延长生命周期
    • 左值和右值的参数匹配
    • 右值引用和移动语义的使用场景
      • 左值引用主要使用场景回顾
      • 移动构造和移动赋值

简介:这篇文章主要写的是C++11版本相较于C++98所更新的重要内容一部分,不过重点当中亦有重点,那就是右值引用和移动语义,因为它能给C++带来实质性的提效,而列表初始化却只是让我们写代码方便简洁,但我觉得还是要了解了解,以便后续遇到新语法形式能看的懂。总的来说,这里的知识点比较琐碎与晦涩,我把我认为的重点和理解全部写到这篇文章上,如有不全和描述错误还请各位看官尽量包容包容,能力有限只求学会一点算一点,后续能力提升也会进行再次补充与说明

列表初始化

C++98传统的{}

C++98中⼀般数组和结构体可以⽤{}进⾏初始化

struct Point
{int _x;int _y;
};
int main()
{// C++98支持的int a1[] = { 1, 2, 3, 4, 5 };int a2[5] = { 0 };Point p = { 1, 2 };return 0;
}

C++11中的{}初始化

C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化

class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "Date(const Date& d)" << endl;}
private:int _year;int _month;int _day;
};
string str = "hello world";

上面是用常量字符串进行初始化构造出一个string对象,但它的语法逻辑却不是直接拿右值去初始化左值,它是先根据右值构造出一个string的临时对象,左值再拷贝构造这个临时对象,只不过编译器认为连续的构造 + 拷贝构造太浪费,所以就优化成直接构造。也就是不创建临时对象,直接根据右值进行构造,但它的语法逻辑上却是连续构造 + 拷贝构造优化成直接构造的结果。本质都是由构造函数支持的隐式类型转换

Date d = 2020; // 走的是单参数的隐式类型转换 -- C++98支持
Date d1 = {2020, 1, ,1} // 多参数则要用中括号括起来,走的也是隐式类型转换-- C++11支持
// 上面两个连续构造 + 拷贝构造 优化成直接构造,看下面运行结果,并没有去调用拷贝构造

在这里插入图片描述
因此就会出现这种写法 Date& r = 2020;但会出现编译错误,因为r引用的不是2020,它引用的是2020构造的临时对象,临时对象具有常性,所以需要加上constconst Date& r = 2020;那现在就会产生一个疑问为啥要有隐式类型转换呢?,不可以直接这样初始化吗?(Date d1(2020, 1, ,1))这不是更方便吗?那下面再看下隐式类型转换的作用

void push_back(const Date & val) 
{}// 没有隐式类型转换
Date d3(2022, 1, 1);
push_back(d3); // 有名对象初始化
push_back(Date(2022, 1, 1)); // 匿名对象初始化// 如果有隐式类型转换就很方便
push_back(2022);
push_back({ 2022, 1, 1 });

其次C++11还支持省略=号,即Date d1{2020, 1, ,1} 或者 int x{5}这种列表初始化写法(只有{}初始化才能省略),不过建议还是加上等号

C++11中的std::initializer_list

在这里插入图片描述

initializer_list它是一个容器,如果它用来给对象容器进行初始化的,那将会很方便。Date d1 = {2020, 1, ,1}这个{2020,1, 1} 与 {10, 20, 30} 不是一个东西,这个Date对象初始化走的是参数匹配,也就是{}中的参数个数得与构造函数的形参个数是一致的,那如果是vector对象初始化,那{}参数个数不确定,那岂不是就得写很多构造函数去完成初始化。所以就需要借助容器initializer_list去完成初始化

auto il = { 10, 20, 30, 40, 50 };
vector<int> ret = il;
// vector<int> ret = { 10, 20, 30, 40, 50 };
cout << typeid(il).name() << endl;
// 输出:class std::initializer_list<int>

那vector底层是如何借助容器initializer_list去完成初始化的呢?遍历访问 initializer_list,再尾插到vector中

vector(initializer_list<T> value)
{for (auto& e : value)push_back(e);
}

再来看看容器map是如何借助容器initializer_list完成初始化,{"sort", "排序"}这个是pair对象的{}初始化,也可以说是C++11支持的多参数的隐式类型转换,最外层的{}才是pair类型的initializer_list构造初始化map对象,这两个{}的含义是不相同的

//pair<string, string> kv1("sort", "排序");
//pair<string, string> kv2("string", "字符串");
//map<string, string> dict = { kv1, kv2 };map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };

注意上述的语法只是让我们用的更加的方便,但是没有实质性的提效,而接下来的右值引用和移动语义让C++在很多场景下带来了实质性的进步,是C++11中最最重要的语法!!!

右值引用和移动语义

C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名

左值和右值

  1. 左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址
// 以下的a,b,d,s都是左值,都可以取到地址
int a = 1;
int* b = new int(2);
const int c = 2;
string s = "hello world";cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &s << endl;
  1. 右值也是⼀个表⽰数据的表达式,要么是字⾯值常量(比如10)、要么是表达式求值过程中创建的临时对象(比如 a + b 或者 "hello world")等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址
int a = 1, b = 5;
// 下面都是右值,没有一个能取地址成功的a + b; // 表达式求值过程中创建的临时对象
"hello world"; // 常量字符串
string("hello world"); // 匿名对象
fmin(0.2, 0.1); // 函数传值返回 double fmin  (double x , double y);cout << &(a + b) << endl;
cout << &string("hello world") << endl;
cout << &("hello world") << endl;
cout << &fmin(0.2, 0.1) << endl;
  1. 反正一定要记住,左值是可以取到它的地址的,而右值是不能取地址的

左值引用和右值引用

  1. Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名
int a = 10;
int& b = a; // 给a取了个别名b —— 左值引用int&& c = 10 + 5; // 给10 + 5这个运算出来的临时对象取了别名c —— 右值引用
  1. 左值引用是不能直接引用右值的,但是const左值引用可以引用右值
//int& a = 10 + 5; 出现编译错误
const int& a = 10 + 5; // const左值引用可以引用右值
  1. 右值引用是不能直接引用左值的,但是右值引用可以引用move(左值)
int b = 10;
// int&& c = b; 无法将右值引用绑定到左值
int&& c = move(b);// 可以将move这个函数的底层理解为强转,相当于 (int&&)b强转的效果
  1. ⼀个右值被右值引⽤绑定后,那也就意味着其变量表达式是左值属性,右值引⽤变量和变量表达式的属性都是左值。比如下面的c是10 + 5的别名,而c它是个左值,那10 + 5这个变量表达式的属性也是左值啊
int a = 10;
int& b = a; // 左值引用,那这个b一定是左值,这是肯定的
int&& c = 10 + 5;// 这个是右值引用,那这个c它是左值还是右值呢?
cout << &c << endl; // 左值
  1. 语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看上⾯代码中r1和rr1的汇编层实现,底层都是⽤指针实现的(指针不也得需要开辟空间取储存,所以归根到底还是开了空间的)。那底层汇编等实现和上层语法表达的意义有时是背离的,所以不要并到⼀起去理解

引用延长生命周期

右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。

// 10 + 5 这个临时对象生命周期本来在这一行就结束了
const int& b = 10 + 5;// 现在这个 10 + 5 这个临时对象跟b的生命周期一样长
int&& c = 10 + 5; // b和c啥时候被销毁,那这个临时对象生命周期就结束
// 这里的b与c后续都是无法赋值修改的

如何取验证这一点呢?看下面的这段代码

class AA
{
public:AA(int a1, int a2):_a1(a1),_a2(a2){}~AA(){cout << "~AA()" << endl;}
private:int _a1 = 1;int _a2 = 1;
};

这个临时对象生命周期在这一行就结束了

在这里插入图片描述

通过const左值引用 与 右值引用去延长临时对象的生命周期这里先不去讨论const左值引用 与 右值引用的区别,先学好这里的语法

在这里插入图片描述

左值和右值的参数匹配

  1. C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配
  2. •C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)
void f(int& x)
{std::cout << "左值引用重载 f(" << x << ")\n";
}void f(const int& x)
{std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}void f(int&& x)
{std::cout << "右值引用重载 f(" << x << ")\n";
}

在这里插入图片描述

右值引用和移动语义的使用场景

左值引用主要使用场景回顾

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决⼤多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,比如下面的杨辉三角算法题

class Solution {
public:// 这⾥的传值返回拷⻉代价就太⼤了vector<vector<int>> generate(int numRows) {vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; ++i) {vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; ++i) {for (int j = 1; j < i; ++j) {vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}return vv;}
};
  1. 杨辉三角这道算法题,它定义了一个函数局部变量,将数据储存在这个局部变量中,然后再传值返回,那如果这个vv占用的空间很大很大,那它传值返回的代价是不是十分的大,C++98的解决方案只能是被迫让它作为输出型参数去解决,即 void generate(int numRows, vector<vector<int>>& vv)

  2. 那如果是C++11中可以使用右值引用作为返回值返回,是不是可以直接这么写vector<vector<int>> && generate(int numRows) ,还是不行。因为这个VV它是个函数局部变量,它开辟在函数栈帧上,它的生命周期跟函数栈帧一样长,出了这个作用域函数结束了,它也就随之销毁了。因为右值引用也是取别名,不就是给VV取别名吗,但VV随着函数结束也随之销毁,所以右值引用直接返回也无法起作用

移动构造和移动赋值

  1. 移动构造函数是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
  2. 移动赋值是⼀个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤
  3. 对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“掠夺”引⽤的右值对象的资源,⽽不是像拷贝构造和拷贝赋值那样去拷贝资源,从而提⾼效率

下面是一段string的模拟实现代码(包含移动构造和移动赋值

#define _CRT_SECURE_NO_WARNINGS 1
#include<assert.h>
#include<iostream>
#include<string>
#include<string.h>
#include<algorithm>
using namespace std;namespace xiao
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(char* str)-构造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}string(const string& s):_str(nullptr){cout << "string(const string& s) -- 拷贝构造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}string& operator=(const string& s){cout << "string& operator=(const string& s) -- 拷贝赋值" <<endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}~string(){cout << "~string() -- 析构" << endl;delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];if (_str){strcpy(tmp, _str);delete[] _str;}_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity *2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}size_t size() const{return _size;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;};
}

先关注移动构造和拷贝构造

// 拷贝构造
string(const string& s):_str(nullptr)
{cout << "string(const string& s) -- 拷贝构造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}
}
// 移动构造
string(string&& s)
{cout << "string(string&& s) -- 移动构造" << endl;swap(s);
}

xiao::string s3 = xiao::string("yyyyy");咱们在上面说过,const左值引用它是能引用右值的,右值引用也是能引用右值得,但在左值和右值的参数匹配中讲到,如果有更匹配的它会去走更匹配的,也就是上面这个构造会去走移动构造它在语法逻辑上走的是构造 + 移动构造 去初始化 s3,但很可能会被编译器优化成直接构造,也就是下面图片右边的结果
在这里插入图片描述
那现在讨论一下,这个拷贝构造与移动构造的区别到底在什么地方,移动构造又是如何进行实质性的提效的通过下面图片可以看到,这个拷贝构造进行了两次开辟空间去构造,关键是有一个刚刚开了空间不一会就析构了,那万一这个对象非常大那还得了。而移动构造直奔主题,反正你还是要析构的,那你的资源给我吧,直接移动你的资源
在这里插入图片描述
在这里插入图片描述

可以通过move(s1)给它强转成右值,就会去走移动构造,这样调试就能看s1的资源到底有没有被掠夺,不过s1它是个左值,在实践应用中不要轻易掠夺左值的资源,这里只是验证资源掠夺的情形

在这里插入图片描述

接下来咱们来看看编译器的合二为一与合三为一的优化先看看编译器不优化的结果,这里是不借助C++11的移动构造,只讨论拷贝构造与编译器的优化

在这里插入图片描述

再看看编译器合二为一的优化下面是vs 2019 debug环境下编译器对拷贝构造的优化结果

在这里插入图片描述

vs2019的release 和 vs2020的debug 和 release下能做到合三为一优化,啥意思呢?就是在main这个函数栈帧中,只构造一次初始化对象ret,而generate这个函数中的vv就是ret对象的别名,这样三次构造就被优化成一次构造

在这里插入图片描述

但是合二为一,合三为一都得看编译器是否优化(看编译器实现者本身)。也就是说它非常吃环境配置,那有一些项目它是用老版本的编译器,那这个环境它没有优化,那消耗不是非常的大吗?所以这时候只能从语法出发,回归到代码本身,因此C++11的移动构造遍发挥了作用,只需要不断的去转移资源

在这里插入图片描述

当我们弄清楚移动构造后,那移动赋值其实也是一样的,照样去转移资源即可

// 移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;
}

在这里插入图片描述

这里如果我们想去看看关闭编译器优化的情况,其拷贝构造,拷贝赋值,移动构造,移动赋值去验证上面所说的。可以在linux下可以将下⾯代码与 string的模拟实现代码片段拷贝到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elide-constructors 的⽅式关闭构造优化分别讨论有拷贝构造但没有移动构造的场景,有拷贝构造也有移动构造的场景,只有拷贝构造和拷贝赋值但没有移动构造和移动赋值的场景,有拷贝构造和拷贝赋值也有移动构造和移动赋值的场景

namespace xiao
{string addStrings(string num1, string num2) {// 先做好准备工作string sum;sum.reserve(max(num1.size(), num2.size()) + 1);int next = 0; //考虑到进制的问题,定义个nextint cur1 = num1.size() - 1, cur2 = num2.size() - 1;while (cur1 >= 0 || cur2 >= 0){//先把cur1 和 cur2 的值给提取出来int val1 = cur1 >= 0 ? num1[cur1--] - '0' : 0;int val2 = cur2 >= 0 ? num2[cur2--] - '0' : 0;//开始尾插到s中int count = val1 + val2 + next;int ret = count % 10;next = count / 10;sum.push_back(ret + '0');// 插入的是字符,别忘记了}// 处理边界情况,next = 1;if (next == 1) sum.push_back(1 + '0');// 将s直接逆置reverse(sum.begin(), sum.end());return sum;}
}int main()
{xiao::string ret = xiao::addStrings("1111", "2222");cout << ret.c_str() << endl;return 0;
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词