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

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

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

4. 完美转发

4.1 万能引用(引用折叠)

写多个重载函数,根据实参类型调用不同函数。

  • 形参类型分别是左值引用,const左值引用,右值引用,const右值引用:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
 
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
 
// 万能引用(引用折叠):t既能引用左值,也能引用右值
template<typename T>
void PerfectForward(T&& t)
{
  Fun(t); // 此时t变成了左值/const左值
}
 
int main()
{
  PerfectForward(10);           // 右值
 
  int a;
  PerfectForward(a);            // 左值
  PerfectForward(std::move(a)); // 右值
 
  const int b = 8;
  PerfectForward(b);          // const 左值
  PerfectForward(std::move(b)); // const 右值
 
  return 0;
}

       代码中的perfectForward函数模板被叫做万能引用模板,无论调用该函数时传的是什么类型,它都能推演出来:

       在函数模板推演的过程中会发生引用折叠:模板参数T&&中的两个&符号折叠成一个。当传入的实参是左值时,就会发生引用折叠,是右值时就不会发生引用折叠。


无论传的实参是什么,都不用改变模板参数T&&,编译器都能够自己推演。

这就是万能引用,只需要一个模板就可以搞定,不需要分类去写。

       上面万能模板中,虽然推演出来了各自实参类型,但是由于右值引用本身是左值属性,所以需要使用move改变属性后才能调用对应的重载函数。


       有没有办法不用move改变左值属性,让模板函数中的t保持它推演出来的类型。答案是有的,完美转发就能够保持形参的属性不变。

4.2 完美转发forward

完美转发同样是C++11提供的,它也是一个模板:

       完美转发:完美转发在传参的过程中保留对象原生类型属性。实参传递过来后,推演出的形参是什么类型就保持什么类型继续使用。

这里会语法就行:

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
 
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
 
 
// 万能引用(引用折叠):t既能引用左值,也能引用右值
template<typename T>
void PerfectForward(T&& t)
{
  Fun(std::forward<T>(t)); // 完美转发:保持t引用对象属性
}
 
int main()
{
  PerfectForward(10);           // 右值
 
  int a;
  PerfectForward(a);            // 左值
  PerfectForward(std::move(a)); // 右值
 
  const int b = 8;
  PerfectForward(b);          // const 左值
  PerfectForward(std::move(b)); // const 右值
 
  return 0;
}

       此时再使用万能引用的时候,在函数模板中调用重载函数时只需要使用完美转发就可以保持推演出来的属性不变,右值引用仍然是右值,const右值引用也仍然是右值。

需要注意的是:

       虽然右值不可以被修改,但是右值引用以后具有了左值属性,才能被转移,一旦被const修饰以后就无法转移了。所以在使用右值引用的时候,不要使用const来修饰。


5. 新的类功能

在原来的C++类中,有6大默认成员函数:


1. 构造函数 2. 析构函数 3. 拷贝构造函数 4. 拷贝赋值重载 5. 取地址重载 6. const 取地址重载


       重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的,而且完全符号我们使用的需求。

5.1 默认生成的移动构造/赋值

C++11中新增了两个:移动构造和移动赋值运算符重载,此时C++11一共有8个默认成员函数了。

这两个成员函数在前面已经介绍过了,这里站在默认成员函数的角度继续谈谈。

满足下列条件,编译器会自定生成移动构造函数:


没有自己显示定义移动构造函数,且没有实现析构函数,拷贝构造函数,拷贝赋值重载中的任何一个。

此时编译器会自定生成一个默认的移动构造函数。功能:


默认生成的移动构造函数,对于内置类型会逐字节进行拷贝。

对于自定义类型,如果实现了移动构造就调用移动构造,没有实现就调用拷贝构造。

满足下列条件,编译器会自动生成移动赋值重载函数


自己没有显示定义移动赋值重载函数。且没有实现析构函数,拷贝构造函数,拷贝赋值重载中的任何一个。

此时编译器会自动生成一个默认移动赋值函数。功能:


对于内置类型会按字节拷贝。

对于自定义类型,如果实现了移动赋值就调用移动赋值,如果没有实现就调用拷贝赋值。

       创建一个类,屏蔽掉拷贝构造,拷贝赋值,以及析构函数,成员变量有一个是我们自己实现的string,里面有移动构造和移动赋值。

namespace rtx
{
  class string
  {
  public:
    string(const char* str = "")
      :_size(strlen(str))
      , _capacity(_size)
    {
      _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)
      , _size(0)
      , _capacity(0)
    {
      cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
      string tmp(s._str);
      swap(tmp);
    }
 
    string(string&& s) // 移动构造
      :_str(nullptr)
      , _size(0)
      , _capacity(0)
    {
      cout << "string(string&& s) -- 移动构造(资源转移)" << endl;
      swap(s);
    }
 
    string& operator=(const string& s) // 拷贝赋值
    {
      cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
      string tmp(s);
      swap(tmp);
 
      return *this;
    }
 
    string& operator=(string&& s) // 移动赋值
    {
      cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
      swap(s);
 
      return *this;
    }
 
    ~string()
    {
      delete[] _str;
      _str = nullptr;
    }
 
  protected:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
}
 
class Person
{
public:
  //Person(const char* name = "", int age = 0)
  //  :_name(name)
  //  , _age(age)
  //{}
  //Person(const Person& p) // 拷贝构造
  //  :_name(p._name)
  //  , _age(p._age)
  //{}
  //Person& operator=(const Person& p) // 拷贝赋值
  //{
  //  if (this != &p)
  //  {
  //    _name = p._name;
  //    _age = p._age;
  //  }
  //  return *this;
  //}
  //~Person()
  //{}
 
protected:
  rtx::string _name;
  int _age;
};
 
int main()
{
  Person s1;
  Person s2 = s1;
  Person s3 = std::move(s1);
  Person s4;
  s4 = std::move(s2);
  return 0;
}

       此时Person就自动生成了移动构造函数,并且调用了string中的移动构造和移动赋值函数来构造string对象。

       将Person中的拷贝构造,拷贝赋值,析构函数任意放出一个来。(这里只放出了析构)使用右值构建string对象时,都会调用string的拷贝构造和拷贝赋值函数。


  • 编译器默认生成的移动赋值和移动构造类型。
  • 如果符合条件就生成,内置类型按字节处理,自定义类型调用自定义类型的移动赋值或者移动构造,如果没有的化就调用它们的拷贝赋值或者拷贝构造。
  • 如果不符合条件,就直接调用自定义类型的拷贝复制或者拷贝构造。

5.2 类里新的关键字

强制生成默认函数的关键字default:

这个default并不是switch中的default,而是C++11的新用法。

  • 假设类中的某个默认成员函数没有自动生成,但是我们需要它,就可以用default,强制让编译器自动生成默认函数。

       5.1里的代码:将Person中的拷贝构造,拷贝复制,析构函数都显示定义,此时就破坏了自动生成移动构造的条件。把Person里的注释放开,使用default强制生成默认的移动构造函数


从结果中可以看到,仍然调用了string中的移动构造函数,而不是调用的拷贝构造(深拷贝)。

  • 说明Person中仍然生成了默认的移动构造函数。

禁止生成默认成员函数的关键字delete:

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁 已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即 可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

C++98不生成默认成员函数的方法:直接一个分号(要放到保护或者私有里,这里就不放了)

       在Person类中不显示定义拷贝构造函数,拷贝复制函数,析构函数,此时符合自动生成默认移动构造的条件。 声明移动构造函数,但是没有定义(要放到保护或者私有里,防止类外实现,这里就不放了)。此时在编译的时候就会报错,这是C++98中的方式,利用链接时找不到函数的定义报错。C++11就新增delete关键字使其在编译阶段就报错:

  • C++11中,使用delete同样可以实现不让自动生成默认成员函数。

       同样在编译时报错了。编译器会自动生成移动构造函数,但是此时使用了delete,编译器就会报错,告诉我们这里生成了移动构造。这是为了在编译阶段就报错,而不是运行时再报错。

以前提到的一道题:

// 要求delete关键字实现,一个类,只能在堆上创建对象
class HeapOnly
{
public:
    HeapOnly()
    {
        _str = new char[10];
    }
 
    ~HeapOnly() = delete;
 
    //void Destroy() // 如果要销毁只能这样
    //{
    //    delete[] _str;
 
    //    operator delete(this);
    //}
 
private:
    char* _str;
    //...
};

继承和多态中的final与override关键字

这两个关键字在继承和多态部分详细讲解过,这里不再详细讲解。

final

  • 在继承中,被final修饰的类叫做最终类,是无法继承的。
  • 在多态中,被final修饰的虚函数是无法进行重写的。

override

  • 在多态中,用来检查虚函数是否完成了重写。

本篇完。

       C++11中的很多东西虽然让C++越来越不像C++,比如列表初始化等内容,但是还是有一些非常有用的东西的:比如今天讲到的右值引用,和下一篇学的lambda表达式。


下一篇:从C语言到C++_34(C++11_下)可变参数+ lambda+function+bind+笔试题。

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