从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+笔试题。

目录
相关文章
|
3月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
114 12
|
9月前
|
安全 编译器 C语言
C++入门1——从C语言到C++的过渡
C++入门1——从C语言到C++的过渡
156 2
|
4月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
236 6
|
5月前
|
存储 机器学习/深度学习 编译器
【C++终极篇】C++11:编程新纪元的神秘力量揭秘
【C++终极篇】C++11:编程新纪元的神秘力量揭秘
|
7月前
|
算法 编译器 C语言
【C语言】C++ 和 C 的优缺点是什么?
C 和 C++ 是两种强大的编程语言,各有其优缺点。C 语言以其高效性、底层控制和简洁性广泛应用于系统编程和嵌入式系统。C++ 在 C 语言的基础上引入了面向对象编程、模板编程和丰富的标准库,使其适合开发大型、复杂的软件系统。 在选择使用 C 还是 C++ 时,开发者需要根据项目的需求、语言的特性以及团队的技术栈来做出决策。无论是 C 语言还是 C++,了解其优缺点和适用场景能够帮助开发者在实际开发中做出更明智的选择,从而更好地应对挑战,实现项目目标。
281 0
|
9月前
|
C语言 C++
C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
在C语言中,`static`关键字主要用于变量声明,使得该变量的作用域被限制在其被声明的函数内部,且在整个程序运行期间保留其值。而在C++中,除了继承了C的特性外,`static`还可以用于类成员,使该成员被所有类实例共享,同时在类外进行初始化。这使得C++中的`static`具有更广泛的应用场景,不仅限于控制变量的作用域和生存期。
199 10
|
10月前
|
存储 编译器 C语言
【C语言基础考研向】07逻辑运算符与赋值运算符
本文介绍了C语言中的逻辑运算符与逻辑表达式、赋值运算符以及求字节运算符`sizeof`。逻辑运算符包括`!`(逻辑非)、`&&`(逻辑与)和`||`(逻辑或),其优先级规则与数学运算符类似。通过示例展示了如何用这些运算符判断闰年及逻辑非的运算方向。此外,文章还解释了左值与右值的概念及其在赋值运算中的应用,并介绍了复合赋值运算符的使用方法,如加后赋值`+=`和乘后赋值`*=`。最后,通过`sizeof`运算符示例展示了如何获取变量的字节大小。
240 8
|
10月前
|
算法 机器人 C语言
ROS仿真支持C++和C语言
ROS仿真支持C++和C语言
295 1
|
9月前
|
C语言 C++
实现两个变量值的互换[C语言和C++的区别]
实现两个变量值的互换[C语言和C++的区别]
99 0
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
1216 1