• 列表初始化与右值引用


    目录

    C++11(列表初始化与右值引用)

    列表初始化

    initilaizer_list

    auto

    decltype

    nullptr

    智能指针

    stl新增容器

    右值引用

    左值:

    右值:

    右值引用与左值引用的比较

    左值引用的作用

    右值引用的作用

    万能引用

    完美转发

    完美转发的价值


    C++11(初始化列表与右值引用)

    • C++11 是C++跟新了很多有用内容的一个版本,其中包括了列表初始化,initialiazer_list,右值引用等等。

    • 下面一起看一下C++11跟新的内容。

    列表初始化

    • 在C语言中,数组、结构体都可以用花括号来初始化,也就是这个{}。

    • 但是C++的类却不支持那样初始化,C++的类只能是单参数的类才能隐式类型转化,而列表初始化,也可以叫做多参数的隐式类型转换。

    • 有了花括号初始化后,花括号不仅可以初始化数组,还可以初始化类。

    花括号初始化内置类型

    1. int a = { 10 };
    2. int b{ 20 };

    花括号初始化还可以省略掉中间的赋值符号,但是我认为内置类型没有必要这样初始化。

    数组初始化

    1. int ptr1[] = { 0, 9 };
    2. int ptr2[]{ 8, 7 };

    类初始化

    1. struct A
    2. {
    3. A(int a)
    4. :_a(a)
    5. {}
    6. int _a;
    7. };
    8. // 单参数的类支持隐式类型转换,所以这样是可以的
    9. A aa = 10;
    10. // 如果不想隐式类型转换,那么可以加 explicit

    1. struct A
    2. {
    3. explicit A(int a)
    4. :_a(a)
    5. {}
    6.    
    7. int _a;
    8. };
    9. void test1()
    10. {
    11. A aa = 10;
    12. }
    13. // 可以看一下报错
    14. /*
    15. “初始化”: 无法从“int”转换为“A” test_2023_09_24
    16. */

    多参数隐式类型转化

    多参数隐式类型转化,也就是前面说的花括号初始化类。

    1. struct B
    2. {
    3. B(int b1, int b2)
    4. :_b1(b1), _b2(b2)
    5. {}
    6. int _b1;
    7. int _b2;
    8. };
     
    
    1. // 显然多参数的自定义类型是不支持下面这样书写的
    2. // B b = 1, 2;
    3. // 但是是C++11更新后,就支持多参数的隐式类型转化了
    4. B b = { 1, 2 };
    5. B b1 { 1, 2 };
    6. // 不过实质上,都是调用了构造函数
    7. struct B
    8. {
    9. B(int b1, int b2)
    10. :_b1(b1), _b2(b2)
    11. {
    12. cout << "B(int b1, int b2)" << endl;
    13. }
    14. int _b1;
    15. int _b2;
    16. };
    17. void test1()
    18. {
    19. B b = { 1, 2 };
    20. B b1{ 1, 2 };
    21. }
    22. // 下面调用该函数
    23. // 下面是输出结果
    24. B(int b1, int b2)
    25. B(int b1, int b2)

    initilaizer_list

    • 前面说了一个列表初始化,还有一个是 initializer_list ,这两个很容易混淆

    • initializer_list 是一个类,而它的类型就是 一个 {},里面可以有任意个数的单一类型的元素

    • 有了它,那么就可以用它来构造对象

    initializer_list 对象

    1. auto il = { 1, 2, 3, 4 };
    2. // 它的实际类型就是 initializer_list
    3. // 打印看一下类型
    4. cout << typeid(il).name() << endl;
    5. // 下面是输出结果
    6. class std::initializer_list<int>

    初始化自定义类型

    1. // initializer_list 初始化自定义类型必须要有对应的构造函数,如果没有写对应的构造函数,那么还是不能初始化
    2. vector<int> nums = {1,2,3,4,5};
    3. // 上面这样是可以的,vector 有 initializer_list 的构造函数
    4. // vector (initializer_list il,
    5. //       const allocator_type& alloc = allocator_type());
    6. // 如果没有对应的构造函数是不可以的
    7. // 在 stl 库中,所有的容器都有 initializer_list 的构造函数
    8. // 所以不仅是 vector 可以使用它来初始化,其他的容器也是可以的
    9. map<int, int> hash = {1, 2};// 可以这样初始化吗? 不可以,因为 map 里面存储的是一个 pair
    10. map<int, int> hash = { {1, 2} };// 这样才是正确的,同时也不是只能初始化一个值,可以加任意个数的值
    11. // 也可以不加赋值符号
    12. map<int, string> hash1{ {1, "a"}, {2, "b"}, {3, "c"} };
    13. // 其实这里有两层初始化,第一层是构造pair,而二层是使用pair构造map

    没有实现 initializer_list 的是不可以使用它来构造对象的

    1. // A 类型是前面用过的,并没有写对应的构造函数
    2. A a = { 1, 2, 3, 4 };// 所以这样是无法初始化的
    3. // 看一下报错
    4. /*
    5. 初始化”: 无法从“initializer list”转换为“A” test_2023_09_24
    6. */
    7. A a{1}// 这个是单参数/多参数的隐式类型转换
    8. vector<int> nums{1,2,3};// 这个是 initializer_list的构造函数

    auto

    • 其实这个之前介绍过

    • auto 可以由后面的值自动推导类型

    • 而且有了 auto 后还有了范围for

    auto自动推导类型

    1. int b = 10;
    2. auto a = b;
    3. cout << typeid(a).name() << endl;
    4. // 打印类型查看
    5. int
    6. // 也不仅可以这样推,还可以推导表达式
    7.    auto c = a + b;// 实际上这样推导也是正确的
    8. // 不仅可以推导内置类型,还可以推导自定义类型
    9. // 当我们在写迭代器的时候
    10. std::unordered_mapint>>::iterator it;// 当我们要写这么一个类型的时候,太长了,但是我们也可以是用 auto 来自动推导

    语法糖

    1. // 实际上我认为 auto 最好用的就是范围for,也叫做语法糖
    2. // 它可以遍历很多容器,只要有迭代器就可以使用语法糖
    3. int a[] = { 1,2,3,4,5,6,7,8,9 };
    4. for (auto nu : a)
    5. {
    6. cout << nu << " ";
    7. }
    8. // 可以这样遍历数组 a,因为这里的数组 a 的下标就是原生的迭代器
    9. // 既然原生的迭代器可以,那么自己写的迭代器也是可以的
    10. vector<int> nums = { 0,9,8,7,6,5,4,3,2,1 };
    11. for (auto nu : nums)
    12. {
    13. cout << nu << " ";
    14. }
    15. // 这样也是可以的
    16. // 也可以遍历 map,但是 map 的迭代器的解引用是一个 pair
    17. map<int, int> hash{ {1,10}, {2, 20}, {3, 30} };
    18. for (auto kv : hash)
    19. {
    20. cout << kv.first << " : " << kv.second << endl;
    21. }
    22. // 其实范围for,也就是利用 auto 自动推导类型,其实范围for的底层也就是替换成了迭代器,所以只要有迭代器就可以泡范围for
    23. // 范围for的类型那里不一定要写 auto,这里主要是不明确里面是什么类型,如果知道具体类型,也可以写具体类型
    24. vector vs{ "a", "b", "c", "d", "e" };
    25. for (string& str : vs)
    26. {
    27. cout << str << endl;
    28. }

    decltype

    • decltype可以将变量的类型声明为表达式的类型

    • auto 可以自动推导类型,但是必须要是后面的值推导,如果后面没有值,则不可以

    • 而 typeid().name() 是可以将变量的类型打印出来,并不能用来声明

    decltype声明类型

    1. int a = 10;
    2. decltype(a) b;
    3. cout << typeid(b).name() << endl;
    4. // 输出结果
    5. int
    6.    
    7. // decltype 不仅可以用变量来声明,还可以使用表达式
    8. decltype(10 + 20) c;

    nullptr

    • 在前面的 NULL 实际上是宏定义的结果,他们将 0 定义为了 NULL,所以在有些场景下是有问题的。

    • 而 nullptr 就是为了表示空指针

    智能指针

    • 这个会在后面说,这个内容比较多

    stl新增容器

    • 在C++11中 stl 新增了一些容器

    • 在新增的容器中,有两个是我们非常好用的 unordered_map,unordered_set

    • 还新增了一个 array 和 forward_list ,但是这两个几乎不常用,所以不多解释

    右值引用

    • 右值引用,我们很容易想起之前说的引用,而之前说的引用都是左值引用

    • 那么左值和右值是什么?

    • 左值就是可以被取地址,大概率可以被修改值的值,叫做左值

    • 右值就是不能被取地址的值

    左值:

    1. // 左值
    2. int a = 10; // a 就是一个左值
    3. int* Pa = &a;
    4. a = 20;// 可以被取地址,也可以被修改
    5. const int c = 100;// const int c 也是一个左值,可以被取地址
    6. const int* Pc = &c;// 但是不能被修改
    7. cout << &("hello world");// 常量字符串也是左值,可以被取地址

    右值:

    1. // 右值
    2. int a = 10, b = 20;
    3. a + b;             // a + b 就是常见的右值,因为 a + b 会有一个返回值,该返回值不能被取地址
    4. int func()
    5. {
    6. int a = 99;
    7. return a;
    8. }
    9. func();// 函数的传值范围也是右值,不能被取地址

    右值引用与左值引用的比较

    1. // 左值引用
    2. int a = 10;
    3. int& b = a;// 左值引用引用左值
    4. // 左值引用引用右值
    5. int& c = 1 + 2; // 这样是不可以的
    6. const int& c = 1 + 2; // 但是左值引用加 const 就可以引用右值
    7. // 右值引用
    8. int&& x = 10;// 右值引用引用右值
    9. // 右值引用引用左值
    10. int&& y = a;// 这样是不可以的
    11. int&& y = move(a);// 但是可以引用 move 后的左值

    总结:

    1. 左值引用只能引用左值,不能引用右值,但是左值引用加 const 就可以引用右值

    2. 右值引用只能引用右值,不能引用左值,但是可以引用 move 后的左值

    左值引用的作用

    • 可以用作传参

    • 可以用作返回值

    • 意义:减少拷贝

    右值引用的作用

    • 右值其实可以分为两种:纯右值、将亡值

    • 纯右值:内置类型的右值就是纯右值

    • 将亡值:自定义类型的右值就是将亡值

    1. void func1(string str)
    2. {
    3. string s(str);
    4. cout << s << endl;
    5. }
    6. // 在这种情况下,如果传值的话,怎么办?
    7. func1("hello");// 首先是值传递,而 string 在值传递的过程中会发生拷贝构造,就浪费了资源,但是在C++11 中,就可以将,将亡值的资源换给自己,然后在将自己没用的资源给将亡值,让将亡值销毁的时候带走
    8. void swap(string& str)
    9. {
    10. char* tmp = _a;
    11. _a = str._a;
    12. str._a = tmp;
    13. _size = str._size;
    14. _capacity = str._capacity;
    15. }
    16. string(const string& str)
    17. {
    18. _a = new char[str._size + 1];
    19. for (int i = 0; i < str._size; ++i)
    20. {
    21. _a[i] = str._a[i];
    22. }
    23. _size = str._size;
    24. _capacity = str._capacity;
    25. _a[_size] = '\0';
    26. cout << "string(const string& str) 深拷贝" << endl;
    27. }
    28. string(string&& str)
    29. {
    30. swap(str);
    31. cout << "string(string&& str) 浅拷贝" << endl;
    32. }
    33. string& operator=(string str)
    34. {
    35. swap(str);
    36. return *this;
    37. cout << "string& operator=(string str) 深拷贝" << endl;
    38. }
    39. string& operator=(string&& str)
    40. {
    41. swap(str);
    42. return *this;
    43. cout << "string& operator=(string&& str) 浅拷贝" << endl;
    44. }

    上面是自己实现的一个string,可以测试一下深浅拷贝

    1. void func(lxy::string str) // 传值深拷贝
    2. {
    3. }
    4. int main()
    5. {
    6. lxy::string s("hello");
    7. func(s);
    8. }
    9. // 查看输出结果
    10. string(const string& str) 深拷贝
    11. lxy::string func()
    12. {
    13. lxy::string str("hello");
    14. return str;
    15. }
    16. int main()
    17. {
    18. lxy::string ret = func();
    19. }
    20. // 如果在没有写移动构造和移动赋值的情况下会发生几次拷贝构造呢?
    21. // 这里来看一下
    22. // 1. 首先在 func 栈帧里面会有一个临时变量 str,然后返回该 str ,str 出了栈帧就会销毁,所以返回的是 str 的拷贝,返回之后,又会通过返回值构造一个对象,所以是两次深拷贝,但是由于连续的两次深拷贝实在是效率低下,所以编译器也做了优化,就是两次连续的深拷贝会被优化为一次,所以看一下结果
    23. string(const string& str) 深拷贝
    24. // 但是这也是针对两次连续的,如果不连续呢?
    25. // 那么就只能是两次深拷贝了
    26. int main()
    27. {
    28. lxy::string ret;
    29. ret = func();
    30. }
    31. // 查看结果
    32. string(const string& str) 深拷贝
    33. string& operator=(string str) 深拷贝;
    34. // 所以通过编译器的优化可以减少两次连续的深拷贝
    35. // 将移动构造和移动赋值放出来
    36. // 那么就只会有浅拷贝
    37. int main()
    38. {
    39. lxy::string ret = func();
    40. }
    41. // 查看结果
    42. string(string&& str) 浅拷贝;// 浅拷贝里面,我们只是 swap 了两个对象里面的几个内置类型,所以代价很低
    43. // 在看一下下面的这种写法
    44. int main()
    45. {
    46. lxy::string ret;
    47. ret = func();
    48. }
    49. // 这样写也会调用两次浅拷贝
    50. string(string&& str) 浅拷贝
    51. string& operator=(string&& str) 浅拷贝

    万能引用

    下面先看一段代码

    1. template<class T>
    2. void fun(T&& a)
    3. {
    4. }
    5. void test9()
    6. {
    7. int a = 10;
    8. const int b = 10;
    9. fun(a);
    10. fun(b);
    11. fun(10);
    12. fun(move(b));
    13. }

    这段代码 fun 能被调用成功吗?

    这里的 fun 的参数是 T&& ,下面其中有 左值、 const 左值、右值、 const 右值 , 那么电泳会怎么样?是编译报错还是调用出现问题,或者是正常调用?

    其实是调用正常的,因为如果在模板这里的话,函数模板会自己推导类型,如果是左值的话,就会推成左值,也叫作引用折叠,如果是右值那么就推的是右值。

    既然可以调用,那么看下面一段代码:

    1. void fun1(int& a)
    2. {
    3. cout << "fun1(int& a)" << endl;
    4. }
    5. void fun1(const int& a)
    6. {
    7. cout << "fun1(const int& a)" << endl;
    8. }
    9. void fun1(int&& a)
    10. {
    11. cout << "fun1(int&& a)" << endl;
    12. }
    13. void fun1(const int&& a)
    14. {
    15. cout << "fun1(const int&& a)" << endl;
    16. }
    17. template<class T>
    18. void fun(T&& a)
    19. {
    20. fun1(a);
    21. }
    22. void test9()
    23. {
    24. int a = 10;
    25. const int b = 10;
    26. fun(a);
    27. fun(b);
    28. fun(10);
    29. fun(move(b));
    30. }

    这段代码里面 fun 函数调用 fun1 函数,会正确调用到吗?

    看一下结果:

    1. fun1(int& a)
    2. fun1(const int& a)
    3. fun1(int& a)
    4. fun1(const int& a)

    全都调用到左值引用了?为什么?

    其实右值引用的变量是左值,下面看一下:

    1. int&& a = 10;
    2. cout << &a << endl;
    3. a = 100;
    4. cout << a << endl;

    结果:

    1. 0077F8D4
    2. 100

    其实右值不仅不可以被取地址,还不能被修改,但是这里看到右值引用的变量不仅可以被修改还可以被取地址,所以右值引用的变量是左值,所以上面的调用都会调用到左值,但是如果想要传过去依旧是右值呢?

    可以通过完美转发来实现:

    完美转发

    1. void fun1(int& a)
    2. {
    3. cout << "fun1(int& a)" << endl;
    4. }
    5. void fun1(const int& a)
    6. {
    7. cout << "fun1(const int& a)" << endl;
    8. }
    9. void fun1(int&& a)
    10. {
    11. cout << "fun1(int&& a)" << endl;
    12. }
    13. void fun1(const int&& a)
    14. {
    15. cout << "fun1(const int&& a)" << endl;
    16. }
    17. template<class T>
    18. void fun(T&& a)
    19. {
    20. //完美转发
    21. fun1(forward(a));
    22. }
    23. void test9()
    24. {
    25. int a = 10;
    26. const int b = 10;
    27. fun(a);
    28. fun(b);
    29. fun(10);
    30. fun(move(b));
    31. }

    还是上面的那一段代码,但是在 fun 函数调用 fun1 的时候,传值的时候我们对a进行了 forward (完美转发),完美转发过的值,本来是什么类型,那么就是什么类型。

    下面继续看一下结果:

    1. fun1(int& a)
    2. fun1(const int& a)
    3. fun1(int&& a)
    4. fun1(const int&& a)

    完美转发的价值

    上面我们知道了右值引用的价值,实际上完美转发就是可以将右值引用的价值发挥到极致:

    1. list ls;
    2. ls.push_back("hello world");

    上面有这么一段代码,其中我们的 string 是有一定构造的,而 list 也有移动构造的版本:

    • 在插入的时候 hello world 会隐式类型转换变成 string,但是这个 string 是一个将亡值。

    • 由于 list 的 push_back 也有自己的右值引用版本,所以此时 push_back 就会调用到右值版本。

      1. void push_back(string&& str)
      2. {
      3. list_node* newnode = new list_node(forward(str));
      4. ....
      5. }

    • push_back 里面会调用一个 new list_node 然后将 string 给 list_node。

    • list_node 在 new 的时候调用了构造函数, list_node 也写了右值版本。

      1. list_node(string&& str)
      2. :_prev(nullptr)
      3. ,_next(nullptr)
      4. ,_val(forward(str))
      5. {}

    • 但是此时传到 push_back 里面的变量此时虽然是右值引用的变量,但是实际上是左值,如果直接调用 list_node 的拷贝构造,那么一定会调用到左值版本的,所以传过去的时候还需要对其进行完美转发。

    • 而传过去之后, list_node 在进行 val 的构造的时候,会调用拷贝构造,所以在传给 value 的时候也需要进行完美转发

  • 相关阅读:
    STM32CubeMX 下载和安装 详细教程
    三款数据可视化工具深度解析:Tableau、ECharts与山海鲸可视化
    《uni-app》表单组件-Label组件
    C++中使用R“()“标记符书写多行字符串
    PyTorch构建训练集
    1.8 工程相关解析(各种文件,资源访问
    如何压缩视频?视频压缩变小方法汇总
    un7.30:linux——如何在docker容器中安装MySQL?
    6.二叉树.题目3
    postgresql|数据库|centos7下基于postgresql-12的主从复制的pgpool-4.4的部署和使用
  • 原文地址:https://blog.csdn.net/m0_73455775/article/details/133315062