【C++】右值引用和移动语义 | 新的类功能 | 可变参数模板(下)

简介: 【C++】右值引用和移动语义 | 新的类功能 | 可变参数模板(下)

👉新的类功能👈


原来C++类中,有 6 个默认成员函数:构造函数、析构函数、拷贝构造函数,赋值运算符重载、取地址重载和 const 取地址重载。重要的是前 4 个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。拷贝构造函数和赋值运算符重载是针对左值的拷贝,而移动构造和移动赋值时针对右值的拷贝。不需要深拷贝的类,也就不需要自己写移动构造和移动赋值。拷贝对象需要深拷贝是,自己写移动构造和移动赋值。比如:string、vector 和 list 等。



针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:


如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行浅拷贝,对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行浅拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用赋值运算符重载。

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。如果没有移动构造和移动赋值,才会去调用拷贝构造和赋值运算符重载。


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()
  {}*/
private:
  Joy::string _name;
  int _age;
};
int main()
{
  Person s1("张三", 18);  
  Person s2 = s1; // 拷贝构造
  Person s3 = std::move(s1);  // 移动构造
  Person s4;
  s4 = std::move(s2); // 移动赋值
  return 0;
}


38096762dfbd411bbc420640967d0853.png


注释掉 string 的移动构造和移动赋值


771fd2f26f0d4233b68851320ae68d23.png


写了 Person 类的析构函数


61ddf4ef441845d1bf1765ed2e3be913.png


类成员变量初始化


C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化。这个在类和对象就讲了,这里就不再细讲了。


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


C++11 可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成。


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


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


要求使用 delete 关键字实现一个类只能在堆上创建对象


// 只能在堆上创建对象
class HeapOnly
{
public:
  void Destroy()
  {
    delete[] _str;
    // delete this 也要调用析构函数
    operator delete(this);
  }
  HeapOnly()
  {
    _str = new char[10];
  }
  ~HeapOnly() = delete;
private:
  char* _str;
};
int main()
{
  //HeapOnly hp1;
  //static HeapOnly hp2;
  HeapOnly* ptr = new HeapOnly;
  ptr->Destroy();
  //delete ptr; // 编译报错
  return 0;
}


注:delete 主要做两件事:1. 调用对象的析构函数 2. 调用 operator delete 回收对象的内存。operator delete 底层也是调用 free 来释放空间的。


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


final 可以修饰一个类,表示这个类不能被继承;也能修饰一个虚函数,表示这个虚函数不能被重写。override 修饰子类的虚函数,如果子类的虚函数没有完成重写,就会编译报错。


👉可变参数模板👈

aef86e876e05487faf3919dc3c7f1e5a.png


在 C 语言中,我们就已经接触过可变参数了,只是没有深入了解过。其实可变参数底层是用一个数组来接收这些参数的,要用时将这些参数从数组取出来。


C++11 将可变参数扩展到模板,C++11 的新特性可变参数模板能够让你创建可以接受可变参数的函数模板和类模板。相比 C++98 / 03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,以后大家如果有需要,可以再深入学习。


下面就是一个基本可变参数的函数模板


// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}


上面的参数 args 前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为参数包,它里面包含了 0 到 N(N>=0)个模版参数。我们无法直接获取参数包 args 中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用 args[i] 这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。


计算可变参数的个数


// 可变参数的模板
template <class ...Args>
void ShowList(Args... args)
{
  cout << sizeof...(args) << endl;
}
int main()
{
  string str("hello");
  ShowList();
  ShowList(1);
  ShowList(1, 'A');
  ShowList(1, 'A', str);
  return 0;
}

692d456416bf4b68861997bd27515474.png


// 模板参数包不支持以下玩法
for (size_t i = 0; i < sizeof...(args); ++i)
{
  cout << args[i] << " ";
}
cout << endl;


递归函数方式展开参数包


// 递归终止函数
void ShowList()
{
  cout << "ShowList()" << endl;
}
// 展开函数,参数包args包含N个参数(N>=0)
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{
  cout << "ShowList(" << val << ", 参数包args有 " << sizeof...(args) << " 个参数)" << endl;
  ShowList(args...);  // 递归调用
}
int main()
{
  string str("hello");
  ShowList(1, 'A', str);
  return 0;
}

dde09046733b4cfab14ac470159cf14c.png


注:如果没有递归终止函数,就编译报错。

9b53944111124e7bba3722c2b671dbe1.png


逗号表达式展开参数包


这种展开参数包的方式,不需要通过递归终止函数,是直接在ShowList函数体中展开的, Printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式,逗号表达式会按顺序执行逗号前面的表达式。


ShowList函数中的逗号表达式(printarg(args), 0)是先执行printarg(args),再得到逗号表达式的结果 0。同时还用到了C++11 的另外一个特性始化列表,通过初始化列表来初始化一个变长数组,{(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为 0 的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分Printarg(args)打印出参数,也就是说在构造数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。


template <class T>
void PrintArg(const T& val)
{
  cout << val << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
  int arr[] = { (PrintArg(args), 0)... };
  cout << endl;
  for (size_t i = 0; i < sizeof...(args); ++i)
  {
    cout << arr[i] << ' ';
  }
  cout << endl;
}
int main()
{
  string str("hello");
  ShowList(1, 'A', str);
  return 0;
}


函数调用展开参数包


template <class T>
int PrintArg(const T& val)
{
  cout << val << " ";
  return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
  int arr[] = { PrintArg(args)... };
  cout << endl;
  for (size_t i = 0; i < sizeof...(args); ++i)
  {
    cout << arr[i] << ' ';
  }
  cout << endl;
}
int main()
{
  string str("hello");
  ShowList(1, 'A', str);
  return 0;
}

7d4c5d325dd04a78be9546ee227179f3.png


当一个函数需要传多个参数,但你并不知道要传多少个参数,就可以将这个函数弄成可变参数函数模板。


STL 容器中的 empalce 相关接口函数


首先我们看到的 emplace 系列的接口,支持模板的可变参数,并且万能引用。那么相对 insert 和 emplace 系列接口的优势到底在哪里呢?


b5bf771c20c84106b0831d10b3634e37.png8f291c3d9ca64f26a7c40d0d64138bbd.png


push_back 是先构造一个对象,再用这个对象拷贝构造到对应空间中去;而 emplace_back 是将参数包往下传,然后直接调用构造函数将对象构造出来。所以,在某些情况下 emplace 的接口更加高效。


class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
    :_year(year)
    , _month(month)
    , _day(day)
  {
    cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;
  }
  Date(const Date& d)
    :_year(d._year)
    , _month(d._month)
    , _day(d._day)
  {
    cout << "Date(const Date& d)" << endl;
  }
  Date& operator=(const Date& d)
  {
    cout << "Date& operator=(const Date& d))" << endl;
    return *this;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  // 没有区别
  vector<int> v1;
  v1.push_back(1);
  v1.emplace_back(2);
  // 有区别
  list<Date> lt1;
  lt1.push_back(Date(2022, 11, 16));
  cout << "---------------------------------" << endl;
  lt1.emplace_back(2022, 11, 16);
  return 0;
}


3c464e982957419899d87e36aa3acdba.png


👉总结👈


本篇博客主要讲解了什么是左值引用和右值引用、左值引用和右值引用的区别、完美转发、类的新功能和可变参数模板等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

相关文章
|
18小时前
|
存储 编译器 C语言
从C语言到C++_11(string类的常用函数)力扣58和415(中)
从C语言到C++_11(string类的常用函数)力扣58和415
5 0
|
18小时前
|
存储 C语言 C++
从C语言到C++_11(string类的常用函数)力扣58和415(上)
从C语言到C++_11(string类的常用函数)力扣58和415
5 0
|
18小时前
|
存储 编译器 程序员
从C语言到C++④(第二章_类和对象_上篇)->类->封装->this指针(下)
从C语言到C++④(第二章_类和对象_上篇)->类->封装->this指针
4 0
|
1天前
|
C++
【C++基础】类class
【C++基础】类class
9 1
|
1天前
|
安全 程序员 编译器
C++程序中的基类与派生类转换
C++程序中的基类与派生类转换
8 1
|
1天前
|
C++
C++程序中的类成员函数
C++程序中的类成员函数
7 1
|
1天前
|
C++
C++程序中的类封装性与信息隐蔽
C++程序中的类封装性与信息隐蔽
8 1
|
1天前
|
C++
C++程序中的类声明与对象定义
C++程序中的类声明与对象定义
9 1
|
1天前
|
数据安全/隐私保护 C++
C++程序中的派生类
C++程序中的派生类
7 1
|
1天前
|
C++
C++程序中的派生类成员访问属性
C++程序中的派生类成员访问属性
9 1