从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(上)

简介: 从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值

       在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98 / 03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。


       相比于C++98 / 03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98 / 03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。C++11增加的语法特性非常篇幅非常多,这里没办法壹壹讲解,所以本博客主要讲解实际中比较实用的语法。

1. 列表初始化initializer_list

  • 列表:花括号:{ }就被叫做列表。

之前可以使用列表来初始化数组,初始化结构体变量,初始化元素类型为结构体变量的数组等等。

  • C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加
#include <iostream>
using namespace std;
 
class Date
{
public:
  Date(int year, int month, int day)
    :_year(year)
    , _month(month)
    , _day(day)
  {
    cout << "Date(int year, int month, int day)" << endl;
  }
 
protected:
  int _year = 1;
  int _month = 1;
  int _day = 1;
};
 
int main()
{
  int x1 = 1;
 
  int x2 = { 2 }; // 要能看懂,但是不建议使用
  int x3{ 2 };
 
  Date d1(2023, 1, 1); // 都是在调用构造函数
 
  Date d2 = { 2023, 2, 2 }; // 要能看懂,但是不建议使用
  Date d3{ 2023, 3, 3 };
 
  return 0;
}

可以不加等号进行初始化,如上图代码所示,但是强烈不建议使用

       这其实很鸡肋,没有什么价值,继续使用C++98中的方式就挺好的,而且容易理解,C++11中的方式反而不太好理解了。C++中这种鸡肋的语法被很多人吐槽,理性看待。

可以不加等号进行初始化,如上图代码所示,但是强烈不建议使用

       这其实很鸡肋,没有什么价值,继续使用C++98中的方式就挺好的,而且容易理解,C++11中的方式反而不太好理解了。C++中这种鸡肋的语法被很多人吐槽,理性看待。

列表初始化真正有意义的地方是用于初始化STL中的容器:

       之前提到:vector和list以及map等STL中的容器也可以像普通数组一样使用初始化列表来初始化了。这是因为列表初始化本身就是一个类模板:

       如上图所示,这是C++11才有的一个类型,该类型叫做列表初始化,而且还有自己的成员函数,包括构造函数,计算列表大小的接口,获取列表迭代器位置。(但几乎都不用)


       C++11为这些容器提供了新的构造函数,该构造函数是使用列表来初始化对象的,它的形参就是initializer_list,所以列表初始化才可以初始化STL中的容器。


赋值运算符重载函数也有一个列表的重载版本:

#include <iostream>
#include <vector>
#include <list>
#include <map>
using namespace std;
 
class Date
{
public:
  Date(int year, int month, int day)
    :_year(year)
    , _month(month)
    , _day(day)
  {
    cout << "Date(int year, int month, int day)" << endl;
  }
 
protected:
  int _year = 1;
  int _month = 1;
  int _day = 1;
};
 
int main()
{
  int x1 = 1;
 
  int x2 = { 2 }; // 要能看懂,但是不建议使用
  int x3{ 2 };
 
  Date d1(2023, 1, 1); // 都是在调用构造函数
 
  Date d2 = { 2023, 2, 2 }; // 要能看懂,但是不建议使用
  Date d3{ 2023, 3, 3 };
 
  // 调用支持list (initializer_list<value_type> il)类似这样的构造函数
  vector<int> v1 = { 1, 2, 3, 4, 5, 6 };
  vector<int> v2 { 1, 2, 3, 4, 5, 6 };
 
  list<int> lt1 = { 1, 2, 3, 4, 5, 6 };
  list<int> lt2{ 1, 2, 3, 4, 5, 6 };
 
  auto x = { 1, 2, 3, 4, 5, 6 };
  cout << typeid(x).name() << endl; // 打印初始化列表的类型
 
  vector<Date> v3 = {d1, d2, d3};
  vector<Date> v4 = { { 2022, 1, 1 }, {2022, 11, 11} };
 
  string s1 = "11111";
 
  map<string, string> dict = { { "sort", "排序" }, { "insert", "插入" } }; // 构造
 
  initializer_list<pair<const string, string>> kvil = { { "left", "左边" }, { "right", "右边" } }; // 赋值重载
  dict = kvil; // 上面的类型就不能用auto推导,编译器不知道那里是一个pair
 
  return 0;
}


2. 前面提到的一些知识点

2.1 小语法

C++11提供了一些新的小语法,很多我们都接触过甚至是使用过,这里系统讲讲。


c++11提供了多种简化声明的方式,尤其是在使用模板时。这里讲auto和decltype


auto:这个关键字我们已经使用过很多了


       在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

#define _CRT_SECURE_NO_WARNINGS 1
 
#include <iostream>
#include <map>
using namespace std;
 
int main()
{
  int i = 10;
  auto p = &i;
  auto pf = strcpy;
  cout << typeid(p).name() << endl;
  cout << typeid(pf).name() << endl;
  map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
  //map<string, string>::iterator it = dict.begin();
  auto it = dict.begin();
 
  return 0;
}


decltype:

关键字decltype将变量的类型声明为表达式指定的类型。

使用typeid().name()只能打印出类型的名称,并不能用这个名称继续创建变量,而decltype可以:

template<class T1, class T2>
void F(T1 t1, T2 t2)
{
  decltype(t1 * t2) ret;
  cout << typeid(ret).name() << endl;
}
 
int main()
{
  const int x = 1;
  double y = 2.2;
  decltype(x * y) ret; // ret的类型是double
  decltype(&x) p; // p的类型是int*
  cout << typeid(ret).name() << endl;
  cout << typeid(p).name() << endl;
  F(1, 'a');
  return 0;
}

       使用decltype可以自动推演类型,并且可以用推演出的结果继续创建变量,如上图所示,对于一些不同类型直接的运算结果,decltype有奇效。


nullptr:

       由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

在C++中存在条件编译:(以后用nullptr就行了)这算是修复了一个bug

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL  ((void*)0)
#endif
#endif

范围for循环

       范围for我们也一直都在使用,这是C++11提供的语法糖,使用起来非常方便,它的底层就是迭代器,只是编译器给自动替换了,这里就不再详细讲解了。一般是这么用的:

#include<iostream>
using namespace std;
int main()
{
    int arr[] = { 1, 2, 3, 4, 5 };
    for (auto& e : arr)
    {
        cout << e << " ";
    }
    cout << endl;
    return 0;
}


2.2 STL中的一些变化

新容器:

       红色框中的是C++11增加的新容器,基本只有unordered_map和unordered_set有用,其他很鸡肋。容器array对标的是静态数组,array也是一个静态的,也就是在栈区上的,大小是通过一个非类型模板参数确定的。容器forward_list是一个单链表,也很鸡肋,因为绝大部分场景双链表都可以满足要求,而且更加方便,唯一使用到单链表的地方就是哈希桶中。前面都提到过。


       至于unordered_map和unordered_set,这两个容器的底层是哈希桶,虽然不能实现排序,但是可以降重。而且在查找时具有其他容器无法比拟的效率。这两个容器是非常实用的,而且也是我们经常使用的。


容器中的使用新方法:


1. 使用列表构造

在前面就讲解过了,几乎每个容器都增加了新的接口,使用std::initializer_list类型来构造。


2. 移动构造和移动赋值

在下面讲解了右值引用就可以明白了。


3. emplace_xxx插入接口或者右值引用版本的插入接口。

同样在后面才能学习到。

3. 右值和右值引用

       传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性, 之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

3.1 右值和右值引用概念

什么是左值?什么是右值?

  • 左值:一个表示数据的表达式,如变量名或者指针解引用。
  • 特点:可以对左值取地址 + 可以对左值赋值。

上图代码中所示的变量都属于左值,要牢记左值可以取地址这一个特性。

  • 定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址。
  • 左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。

  • 右值:也是一个表示数据的表达式。如:字面常量,表达式返回值,函数返回值,类型转换时的临时变量等等。
  • 特点:右值不可以取地址,不可以赋值。

要牢记右值特性:不能取地址不能赋值。

  • 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边。

什么是右值引用?

左值引用是给左值取别名,右值引用显而易见就是给右值取别名。

  • 右值引用使用两个&符号。

上图代码中的rr1,rr2,rr3就是三个右值的别名,也就是右值引用。

3.2 右值引用类型的左值属性

  • 右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。

       对于内置类型的右值,如字面常量,一旦右值引用以后,就会被存储到特定的位置,并且可以取到该地址,而且还可以修改。

int main()
{
  int&& rr1 = 10;
  cout << rr1 << endl;
 
  rr1 = 5;
  cout << rr1 << endl;
 
  const double&& rr2 = (1.1 + 2.2);
  //rr2 = 5.5; // 不能修改
 
  return 0;
}

       字面常量10原本是不可以被修改的,但是右值引用以后,在特定的位置开辟了变量来存放10,所以就可以被修改了。


       表达式或者函数的返回值,会有一个临时变量来存放返回值,我们知道这样的临时变量具有常性,也是右值。对于这种右值引用,编译器会修改它的属性,将常性修改,并且存储在特定位置。注意const类型的右值,即便开辟了变量存放该右值也是不可以被修改的,因为被const修饰了。


内置类型的右值被称为纯右值。


自定义类型的右值被称为将亡值。


       对于自定义类型的右值,如容器的临时变量,它确确实实会被销毁,而不会被存放。自定义类型的右值才能体现出右值存在的意义,后面会详细讲解。


右值引用是右值的别名,它所指向的右值是不可以被修改的。

但是右值引用本身也是一种类型,并且它的属性是左值,可以取地址,可以赋值。

从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中):https://developer.aliyun.com/article/1522391

目录
相关文章
|
3天前
|
存储 Java C++
【c++】list详细讲解
【c++】list详细讲解
14 5
|
3天前
|
Java C++ Python
【c++】list 模拟
【c++】list 模拟
7 1
|
11天前
|
存储 编译器 C语言
【C++】list模拟实现
本文档介绍了C++ STL中`list`容器的模拟实现,包括`ListNode`节点类、迭代器类和`list`类的详细设计。`ListNode`模板类存储数据并维护前后指针;`ListIterator`是一个复杂的模板类,提供解引用、自增/自减以及比较操作。`list`类包含了链表的各种操作,如插入、删除、访问元素等,并使用迭代器作为访问接口。实现中,迭代器不再是简单的指针,而是拥有完整功能的对象。此外,文档还提到了迭代器的实现对C++语法的特殊处理,使得`it-&gt;_val`的写法成为可能。文章通过分步骤展示`list`的各个组件的实现,帮助读者深入理解STL容器的内部工作原理。
|
11天前
|
算法 搜索推荐 C++
【C++】list的使用(下)
`C++` 中 `std::list` 的 `merge()`、`sort()` 和 `reverse()` 操作: - `merge(x)` 和 `merge(x, comp)`: 合并两个已排序的`list`,将`x`的元素按顺序插入当前`list`,`x`清空。比较可自定义。 - `sort()` 和 `sort(comp)`: 对`list`元素排序,保持等价元素相对顺序。内置排序基于稳定排序算法,速度较慢。 -reverse(): 反转`list`中元素的顺序。 这些操作不涉及元素构造/销毁,直接移动元素。注意,`sort()`不适合`std::list`,因链表结构不利于快速排序
|
11天前
|
C++ 容器
【C++】list的使用(下)
这篇博客探讨了C++ STL中`list`容器的几个关键操作,包括`splice()`、`remove()`、`remove_if()`和`unique()`。`splice()`允许高效地合并或移动`list`中的元素,无需构造或销毁。`remove()`根据值删除元素,而`remove_if()`则基于谓词移除元素。`unique()`则去除连续重复的元素,可选地使用自定义比较函数。每个操作都附带了代码示例以说明其用法。
|
11天前
|
编译器 C++ 容器
【C++】list的使用(上)
迭代器在STL中统一了访问接口,如`list`的`begin()`和`end()`。示例展示了如何使用正向和反向迭代器遍历`list`。注意`list`的迭代器不支持加减操作,只能用`++`和`--`。容器的`empty()`和`size()`用于检查状态和获取元素数。`front()`和`back()`访问首尾元素,`assign()`重载函数用于替换内容,`push_*/pop_*`管理两端元素,`insert()`插入元素,`erase()`删除元素,`resize()`调整大小,`clear()`清空容器。这些接口与`vector`和`string`类似,方便使用。
|
12天前
|
存储 C++
C++的list-map链表与映射表
```markdown C++ 中的`list`和`map`提供链表和映射表功能。`list`是双向链表,支持头尾插入删除(`push_front/push_back/pop_front/pop_back`),迭代器遍历及任意位置插入删除。`map`是键值对集合,自动按键排序,支持直接通过键来添加、修改和删除元素。两者均能使用范围for循环遍历,`map`的`count`函数用于统计键值出现次数。 ```
15 1
|
14天前
|
搜索推荐 程序员 C语言
指针赋值与引用传递:C语言的基础知识与实践技巧
指针赋值与引用传递:C语言的基础知识与实践技巧
|
11天前
|
存储 编译器 C语言
【C++】list的使用(上)
**C++ STL的list是一个基于双向循环链表的容器,支持常数时间内插入和删除,但不支持随机访问。默认构造函数、填充构造、迭代器范围构造和拷贝构造提供多种初始化方式。析构函数自动释放内存,赋值运算符重载用于内容替换。示例代码展示了构造和赋值操作。**
|
11天前
|
存储 算法 程序员
C++基础知识(八:STL标准库(Vectors和list))
C++ STL (Standard Template Library标准模板库) 是通用类模板和算法的集合,它提供给程序员一些标准的数据结构的实现如 queues(队列), lists(链表), 和 stacks(栈)等. STL容器的提供是为了让开发者可以更高效率的去开发,同时我们应该也需要知道他们的底层实现,这样在出现错误的时候我们才知道一些原因,才可以更好的去解决问题。