从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

目录
相关文章
|
19小时前
|
存储 算法 C++
C++一分钟之-容器概览:vector, list, deque
【6月更文挑战第21天】STL中的`vector`是动态数组,适合随机访问,但插入删除非末尾元素较慢;`list`是双向链表,插入删除快但随机访问效率低;`deque`结合两者优点,支持快速双端操作。选择容器要考虑操作频率、内存占用和性能需求。注意预分配容量以减少`vector`的内存重分配,使用迭代器而非索引操作`list`,并利用`deque`的两端优势。理解容器内部机制和应用场景是优化C++程序的关键。
16 5
|
7天前
|
存储 缓存 编译器
【C++进阶】深入STL之list:模拟实现深入理解List与迭代器
【C++进阶】深入STL之list:模拟实现深入理解List与迭代器
8 0
|
7天前
|
C++ 容器
【C++进阶】深入STL之list:高效双向链表的使用技巧
【C++进阶】深入STL之list:高效双向链表的使用技巧
11 0
|
7天前
|
编译器 C++
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
|
8天前
|
C语言 C++ 编译器
【C++语言】冲突-C语言:输入输出、缺省参数、引用、内联函数
【C++语言】冲突-C语言:输入输出、缺省参数、引用、内联函数
【C++语言】冲突-C语言:输入输出、缺省参数、引用、内联函数
|
8天前
|
C语言 C++
【C++语言】冲突-C语言:命名空间
【C++语言】冲突-C语言:命名空间
|
10天前
|
存储 C++
C++的list-map链表与映射表
这篇教程介绍了C++中`list`链表和`map`映射表的基本使用。`list`链表可通过`push_front()`、`push_back()`、`pop_front()`和`pop_back()`进行元素的添加和删除,使用迭代器遍历并支持在任意位置插入或删除元素。`map`是一个键值对的集合,元素自动按键值排序,可使用下标操作符或`insert()`函数插入元素,通过迭代器遍历并修改键值对,同时提供`count()`方法统计键值出现次数。教程中包含多个示例代码以帮助理解和学习。
14 2
|
15天前
|
程序员 C语言 C++
C语言学习记录——动态内存习题(经典的笔试题)、C/C++中程序内存区域划分
C语言学习记录——动态内存习题(经典的笔试题)、C/C++中程序内存区域划分
19 0
|
16天前
|
存储 C++
C++初阶学习第十一弹——探索STL奥秘(六)——深度刨析list的用法和核心点
C++初阶学习第十一弹——探索STL奥秘(六)——深度刨析list的用法和核心点
21 7
|
16天前
|
编译器 C++
C++进阶之路:何为运算符重载、赋值运算符重载与前后置++重载(类与对象_中篇)
C++进阶之路:何为运算符重载、赋值运算符重载与前后置++重载(类与对象_中篇)
24 1