【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++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
12月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
466 12
|
10月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
297 0
|
10月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
250 0
|
编译器 C++
模板(C++)
本内容主要讲解了C++中的函数模板与类模板。函数模板是一个与类型无关的函数家族,使用时根据实参类型生成特定版本,其定义可用`typename`或`class`作为关键字。函数模板实例化分为隐式和显式,前者由编译器推导类型,后者手动指定类型。同时,非模板函数优先于同名模板函数调用,且模板函数不支持自动类型转换。类模板则通过在类名后加`&lt;&gt;`指定类型实例化,生成具体类。最后,语录鼓励大家继续努力,技术不断进步!
|
编译器 C++
㉿㉿㉿c++模板的初阶(通俗易懂简化版)㉿㉿㉿
㉿㉿㉿c++模板的初阶(通俗易懂简化版)㉿㉿㉿
|
安全 C++
【c++】模板详解(2)
本文深入探讨了C++模板的高级特性,包括非类型模板参数、模板特化和模板分离编译。通过具体代码示例,详细讲解了非类型参数的应用场景及其限制,函数模板和类模板的特化方式,以及分离编译时可能出现的链接错误及解决方案。最后总结了模板的优点如提高代码复用性和类型安全,以及缺点如增加编译时间和代码复杂度。通过本文的学习,读者可以进一步加深对C++模板的理解并灵活应用于实际编程中。
214 0
|
存储 安全 算法
深入理解C++模板编程:从基础到进阶
在C++编程中,模板是实现泛型编程的关键工具。模板使得代码能够适用于不同的数据类型,极大地提升了代码复用性、灵活性和可维护性。本文将深入探讨模板编程的基础知识,包括函数模板和类模板的定义、使用、以及它们的实例化和匹配规则。
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
10月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
394 0