【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


👉总结👈


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

相关文章
|
6天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
21 2
|
12天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
40 5
|
19天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
49 4
|
20天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
46 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
22 1
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
2月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
54 1