【C++】—— c++11新的类功能

简介: 【C++】—— c++11新的类功能



(一)默认成员函数

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

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

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。

 

💨 C++11 新增了两个:移动构造函数和移动赋值运算符重载

1、 移动构造函数

移动构造函数是C++中的特殊成员函数之一,用于以移动语义的方式构造新对象。它是在C++11中引入的,旨在提高对象的性能和效率。

移动构造函数通常用于在不进行资源拷贝的情况下将临时对象或者右值引用的对象的内容转移到新创建的对象中。通过移动构造函数,可以避免不必要的拷贝操作,提高代码的性能。


2、代码辅助理解

接下来我们通过代码来尝试着学习相关的知识:

  • 首先,我先给出手动实现的string类,代码如下:
namespace zp
{
  class string
  {
  public:
    typedef char* iterator;
    iterator begin()
    {
      return _str;
    }
    iterator end()
    {
      return _str + _size;
    }
    string(const char* str = "")
      :_size(strlen(str))
      , _capacity(_size)
    {
      cout << "string(char* str)" << endl;
      _str = new char[_capacity + 1];
      strcpy(_str, str);
    }
    // s1.swap(s2)
    void swap(string& s)
    {
      ::swap(_str, s._str);
      ::swap(_size, s._size);
      ::swap(_capacity, s._capacity);
    }
    // 拷贝构造
    string(const string& s)
      :_str(nullptr)
    {
      cout << "string(const string& s) -- 深拷贝" << endl;
      string tmp(s._str);
      swap(tmp);
    }
    // 移动构造
    string(string&& s)
      :_str(nullptr)
    {
      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;
    }
    // s1 = 将亡值
    string& operator=(string&& s)
    {
      cout << "string& operator=(string&& s) -- 移动赋值" << endl;
      swap(s);
      return *this;
    }
    ~string()
    {
      //cout << "~string()" << endl;
      delete[] _str;
      _str = nullptr;
    }
    char& operator[](size_t pos)
    {
      assert(pos < _size);
      return _str[pos];
    }
    void reserve(size_t n)
    {
      if (n > _capacity)
      {
        char* tmp = new char[n + 1];
        strcpy(tmp, _str);
        delete[] _str;
        _str = tmp;
        _capacity = n;
      }
    }
    void push_back(char ch)
    {
      if (_size >= _capacity)
      {
        size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
        reserve(newcapacity);
      }
      _str[_size] = ch;
      ++_size;
      _str[_size] = '\0';
    }
    //string operator+=(char ch)
    string& operator+=(char ch)
    {
      push_back(ch);
      return *this;
    }
    string operator+(char ch)
    {
      string tmp(*this);
      tmp += ch;
      return tmp;
    }
    const char* c_str() const
    {
      return _str;
    }
  private:
    char* _str;
    size_t _size;
    size_t _capacity; // 不包含最后做标识的\0
  };
  //const zp::string& to_string(int value)
  zp::string to_string(int value)
  {
    bool flag = true;
    if (value < 0)
    {
      flag = false;
      value = 0 - value;
    }
    zp::string str;
    while (value > 0)
    {
      int x = value % 10;
      value /= 10;
      str += ('0' + x);
    }
    if (flag == false)
    {
      str += '-';
    }
    std::reverse(str.begin(), str.end());
    return str;
  }
}

接下来,有这样的一段代码,我们对它进行运行分析:

接下来,我们给出相关示例,把代码运行起来看最终我们的样例是调用的什么:

【解释说明】

  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。

此时,我们把其中的析构函数进行相关的实现,在看最终的打印结果是什么:

【解释说明】

  1. 使用 move 操作可以显式地将 s1转换为右值引用,并尝试将 s1 通过移动语义移动到 s3 中;
  2. 此时,我们可以发现,当我们手动的实现了一个析构函数之后,编译器就会对识别进行深拷贝操作;

【小结】

针对移动构造函数有一些需要注意的点如下:

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

这里解释一下为什么生成默认移动构造条件这么苛刻的问题:

  1. 编译器之所以把这个条件设计这么苛刻,因为它认为说你是你自己要实现的拷构构造和拷贝赋值还是析构,按理来说你这个类呢,就是一个深拷贝的类,那你是一个深拷贝的类呢,那这个时候移动赋值,它不知道咋处理比较好;
  2. 就比如说这有个指针,那这个指针我要把你的资源转移,就是你可以认为它自己把控不住,它不知道该咋编译做资源移动。其次这个指针指向的资源就一定要移动吗?我们认为这是不一定的,要看我们实际当中的需求是什么,它自己把控不住,这个时候他就不再给你自动生成了;
  3. 那什么时候他觉得他可以把控住呢?就是像刚才这样的,你没有实现析构,你不是深拷贝的类,它就可以把控住。

3、移动赋值运算符重载

移动赋值运算符重载的原理跟上述移动构造函数一样的。大家只需记住一个,另一个类似的就可以记住!!!


(二)default关键字

在C++11之前,如果在类的定义中没有显式声明默认构造函数、复制构造函数、移动构造函数、复制赋值运算符或移动赋值运算符时,编译器会自动生成这些函数的默认版本。然而,在C++11及以后的标准中,如果我们显式地定义了一个带有参数的构造函数、复制构造函数、移动构造函数、复制赋值运算符或移动赋值运算符,编译器将不再自动生成默认版本。

为了强制生成默认版本的函数,我们可以使用关键字 default。在类的定义内部,用 = default形式指定函数。这将告诉编译器生成该函数的默认版本。

  • 代码展示:


(三)delete关键字

当我们在类中手动定义了自定义构造函数、复制构造函数、移动构造函数、赋值运算符或移动赋值运算符时,编译器就不会再自动生成默认版本的这些函数。然而,在某些情况下,我们可能希望保留编译器自动生成的默认版本。

使用 delete 关键字可以告诉编译器生成该函数的默认版本,即恢复被手动定义函数覆盖的默认行为。

  • 代码展示:


(四)委托构造函数

委托构造函数是C++11引入的特性之一,它允许一个构造函数调用同一类的其他构造函数来完成对象的初始化。通过委托构造函数,可以减少代码的冗余、提高可维护性,并且确保初始化逻辑的一致性。

以下是委托构造函数的简要说明和示例:

1、优势

  • 委托构造函数的语法:

委托构造函数使用特殊的语法来调用同一类的其他构造函数。它在成员初始化列表中使用冒号(:)后面的成员初始化器列表来调用其他构造函数。

class Test 
{
public:
  Test(int x, int y) :  // 构造函数1,委托给构造函数2
    Test(x, y, 0) {   // 委托构造函数
  }
  Test(int x, int y, int z) :
    // 构造函数2,实际完成对象初始化的构造函数
  {
    // 具体的初始化逻辑
  }
};

【解释说明】

  • 这样,当我们使用委托构造函数创建对象时,只需调用适合的构造函数,并由该构造函数负责完成所有初始化工作;
  • 这样做可以避免在多个构造函数中复制相同的初始化代码,提高了代码的可维护性。

  • 委托构造函数的特点:
  • 委托构造函数的声明和定义位于同一类中,并且在其他构造函数的前面。
  • 委托构造函数不能有初始值列表,因为它的作用是将初始化任务委托给其他构造函数。
  • 委托构造函数可以有自己的成员初始化列表,用于初始化委托所使用的其他构造函数中未初始化的成员变量。

2、缺点

委托构造函数在使用时存在一种潜在的问题,称为"委托环"(delegation cycle)。委托环是指构造函数之间形成了循环的委托调用关系,导致无限递归或编译错误的情况。

下面是一个示例,展示了如何在不小心的情况下创建委托环的代码:

class Test 
{
public:
  Test(int x) {
    // 执行一些初始化操作
  }
  Test() : Test(0) { // 委托构造函数,调用了另一个构造函数
    // 其他逻辑
  }
};

【解释说明】

  1. 在上面的代码中,Test 类有一个带有参数的构造函数和一个不带参数的构造函数,而不带参数的构造函数使用委托构造函数调用了带有参数的构造函数。这看起来没有问题,但实际上却形成了委托环;
  2. 当创建一个不带参数的 Test 对象时,会调用不带参数的构造函数。然后,由于委托构造函数的存在,它又会调用带有参数的构造函数。然后,带有参数的构造函数又会调用不带参数的构造函数。这样就形成了循环,导致无限递归。

委托构造函数自身不应该包含其他的初始化语句,否则会导致重复初始化的问题

class Test 
{
public:
  Test(int value) 
    : Test(value, 0.0) 
  {
    // 错误!委托构造函数体中存在其他初始化语句
    tmp_ = 42;
  }
  Test(int value, double rate)
    : value_(value)
    , rate_(rate)
  {
    // 构造函数体...
  }
private:
  int value_;
  double rate_;
  int tmp_;
};

【解释说明】

  1. 在上述示例中,委托构造函数的体内存在对 tmp_ 的初始化,这将导致该变量在构造过程中被初始化两次,可能会产生不可预料的结果。
  2. 正确的做法是,在委托构造函数内部只进行调用,不要插入其他的初始化语句。

总结

以上便是关于本期 c++11新的类功能全部知识。感谢大家的观看与支持!!!

相关文章
|
1月前
|
存储 C++ UED
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展
本文介绍了如何通过四步实现C++插件化编程,实现功能定制与扩展。主要内容包括引言、概述、需求分析、设计方案、详细设计、验证和总结。通过动态加载功能模块,实现软件的高度灵活性和可扩展性,支持快速定制和市场变化响应。具体步骤涉及配置文件构建、模块编译、动态库入口实现和主程序加载。验证部分展示了模块加载成功的日志和配置信息。总结中强调了插件化编程的优势及其在多个方面的应用。
231 64
|
7天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
33 4
|
8天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
27 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
16 0
|
1月前
|
存储 编译器 C语言
深入计算机语言之C++:类与对象(上)
深入计算机语言之C++:类与对象(上)