从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

目录
相关文章
|
1天前
|
算法 编译器 C语言
【C语言】C++ 和 C 的优缺点是什么?
C 和 C++ 是两种强大的编程语言,各有其优缺点。C 语言以其高效性、底层控制和简洁性广泛应用于系统编程和嵌入式系统。C++ 在 C 语言的基础上引入了面向对象编程、模板编程和丰富的标准库,使其适合开发大型、复杂的软件系统。 在选择使用 C 还是 C++ 时,开发者需要根据项目的需求、语言的特性以及团队的技术栈来做出决策。无论是 C 语言还是 C++,了解其优缺点和适用场景能够帮助开发者在实际开发中做出更明智的选择,从而更好地应对挑战,实现项目目标。
14 0
|
2月前
|
C语言 C++
C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
在C语言中,`static`关键字主要用于变量声明,使得该变量的作用域被限制在其被声明的函数内部,且在整个程序运行期间保留其值。而在C++中,除了继承了C的特性外,`static`还可以用于类成员,使该成员被所有类实例共享,同时在类外进行初始化。这使得C++中的`static`具有更广泛的应用场景,不仅限于控制变量的作用域和生存期。
59 10
|
3月前
|
编译器 C++
C++ 11新特性之右值引用
C++ 11新特性之右值引用
50 1
|
3月前
|
存储 编译器 C语言
【C语言基础考研向】07逻辑运算符与赋值运算符
本文介绍了C语言中的逻辑运算符与逻辑表达式、赋值运算符以及求字节运算符`sizeof`。逻辑运算符包括`!`(逻辑非)、`&&`(逻辑与)和`||`(逻辑或),其优先级规则与数学运算符类似。通过示例展示了如何用这些运算符判断闰年及逻辑非的运算方向。此外,文章还解释了左值与右值的概念及其在赋值运算中的应用,并介绍了复合赋值运算符的使用方法,如加后赋值`+=`和乘后赋值`*=`。最后,通过`sizeof`运算符示例展示了如何获取变量的字节大小。
|
2月前
|
C语言 C++
实现两个变量值的互换[C语言和C++的区别]
实现两个变量值的互换[C语言和C++的区别]
23 0
|
3月前
|
存储 算法 C语言
C语言手撕实战代码_二叉树_构造二叉树_层序遍历二叉树_二叉树深度的超详细代码实现
这段代码和文本介绍了一系列二叉树相关的问题及其解决方案。其中包括根据前序和中序序列构建二叉树、通过层次遍历序列和中序序列创建二叉树、计算二叉树节点数量、叶子节点数量、度为1的节点数量、二叉树高度、特定节点子树深度、判断两棵树是否相似、将叶子节点链接成双向链表、计算算术表达式的值、判断是否为完全二叉树以及求二叉树的最大宽度等。每道题目均提供了详细的算法思路及相应的C/C++代码实现,帮助读者理解和掌握二叉树的基本操作与应用。
|
4月前
|
C语言
C语言结构体赋值的四种方式
本文总结了C语言结构体的四种赋值方式,并通过示例代码和编译运行结果展示了每种方式的特点和效果。
272 6
|
3月前
|
编译器 C语言 C++
从C语言到C++
本文档详细介绍了C++相较于C语言的一些改进和新特性,包括类型检查、逻辑类型 `bool`、枚举类型、可赋值的表达式等。同时,文档还讲解了C++中的标准输入输出流 `cin` 和 `cout` 的使用方法及格式化输出技巧。此外,还介绍了函数重载、运算符重载、默认参数等高级特性,并探讨了引用的概念及其应用,包括常引用和引用的本质分析。以下是简要概述: 本文档适合有一定C语言基础的学习者深入了解C++的新特性及其应用。
|
5月前
|
程序员 编译器 C语言
云原生部署问题之C++中的nullptr相比C语言中的NULL优势如何解决
云原生部署问题之C++中的nullptr相比C语言中的NULL优势如何解决
55 10
|
6月前
|
搜索推荐 程序员 C语言
指针赋值与引用传递:C语言的基础知识与实践技巧
指针赋值与引用传递:C语言的基础知识与实践技巧