C++运算符重载-下篇 (Boolan)

不同于隐式转换,显式转换运算符必须通过转换的方式来调用。
如果转换操作会导致异常或丢失信息,则应将其标记为 explicit
这可阻止编译器静默调用可能产生意外后果的转换操作。
省略转换将导致编译时错误 CS0266。

implicit 关键字用于声明隐式的用户定义类型转换运算符。
如果可以确保转换过程不会造成数据丢失,则可使用该关键字在用户定义类型和其他类型之间进行隐式转换。

C++运算符重载-下篇 (Boolan)

本章内容:

该引用摘自:explicit(C#
参考)

引用摘自:implicit(C#
参考)

5. 重载下标运算符

  • 本节假设你没有听说过STL中的vector或array的模板,我们来自己实现一个动态分配的数组类。这个类允许设置和获取指定索引位置的元素,并自动完成所有的内存分配操作。一个动态分配数组的定义类如下所示:

      template <typename T>
      class Array
      {
      public:
          // 创建一个可以按需要增长的设置了初始化大小的数组
          Array();
          virtual ~Array();
    
          // 不允许分配和按值传递
          Array<T>& operator=(const Array<T>& rhs) = delete;      // C++11 禁用赋值函数重载
          Array(const Array<T>& src) = delete;                    // C++11 禁用拷贝构造函数
    
          // 返回下标x对应的值,如果下标x不存在,则抛出超出范围的异常。
          T getElementAt(size_t x) const;
    
          // 设置下标x的值为val。如果下标x超出范围,则分配空间使下标在范围内。
          void setElementAt(size_t x, const T& val);
      private:
          static const size_t kAllocSize = 4;
          void resize(size_t newSize);
          // 初始化所有元素为0
          void initializeElement();
          T *mElems;
          size_t mSize;
      };
    
  • 这个接口支持设置和访问元素。它提供了随机访问的保证:客户可以创建数组,并设置元素1、100和1000,而不必考虑内存管理的问题。

  • 下面是这些方法的实现:

      template <typename T> Array<T>::Array()
      {
          mSize = kAllocSize;
          mElems = new T[mSize];
          initializeElements();
      }
    
      template <typename T> Array<T>::~Array()
      {
          delete[] mElems;
          mElems = nullptr;
      }
    
      template <typename T> void Array<T>::initializeElements()
      {
          for (size_t i=0; i<mSize; i++)
          {
              mElems[i] = T();
          }
      }
    
      template <typename T> void Array<T>::resize(size_t newSize)
      {
          // 拷贝一份当前数组的指针和大小
          T *oldElems = mElems;
          size_t oldSize = mSize;
          // 创建一个更大的数组
          mSize = newSize;            // 存储新的大小
          mElems = new T[newSize];    // 给数组分配新的newSize大小空间
          initializeElements();       // 初始化元素为0
          // 新的size肯定大于原来的size大小
          for (size_t i=0; i < oldSize; i++)
          {
              // 从老的数组中拷贝oldSize个元素到新的数组中
              mElems[i] = oldElems[i];
          }
          delete[] oldElems;          // 释放oldElems的内存空间
          oldElems = nullptr;
      }
    
      template <typename T> T Array<T>::getElementAt(size_t x) const
      {
          if (x >= mSize)
          {
              throw std::out_of_range("");
          }
          return mElems[x];
      }
    
      template <typename T> void Array<T>::setElementAt(size_t x, const T& val)
      {
          if (x >= mSize)
          {
              // 在kAllocSize的基础上给数组重新分配客户需要的空间大小
              resize(x + kAllocSize);
          }
          mElems[x] = val;
      }
    
  • 下面是使用这个类的例子:

      Array<int> myArray;
      for (size_t i=0; i<10; i++)
      {
          myArray.setElementAt(i, 100);
      }
      for (size_t j=0; i< 10; j++)
      {
          cout << myArray.getElementAt(j) << " ";
      }
    
  • 从中可以看出,我们不需要告诉数组需要多少空间。数组会分配保存给定元素所需要的足够空间,但是总是使用setElementAt()getElementAt()方法不是太方便。于是我们想像下面的代码一样,使用数组的索引来表示:

      Array<int> myArray;
      for (size_t i=0; i<100; i++)
      {
          myArray[i] = 100;
      }
      for (size_t j=0; j<10; j++)
      {
          cout << myArray[j] << " ";
      }
    
  • 要使用下标方法,则需要使用重载的下标运算符。通过以下方式给类添加operator[]

      template <typename T> T& Array<T>::operator[] (size_t x)
      {
          if (x >= mSize)
          {
              // 在kAllocSize的基础上给数组重新分配客户需要的空间大小
              resize(x + kAllocSize);
          }
          return mElems[x];
      }
    
  • 现在,上面使用数组索引表示法的代码可以正常使用了。operator[]可以设置和获取元素,因为它返回的是位置x处的元素的索引。可以通过这个引用对这个元素赋值。当operator[]用在赋值语句的左侧时,赋值操作实际上修改了mElems数组中位置x处的值。

显示转换关键字explicit能向阅读代码的每个人清楚地指示您要转换类型。

仍以Student求和举例

5.1 通过operator[]提供只读访问

  • 尽管有时operator[]返回可以作为左值的元素会很方便,但并非总是需要这种行为。最好还能返回const值或const引用,提供对数组中元素的只读访问。理想情况下,可以提供两个operator[]:一个返回引用,另一个返回const引用。示例代码如下:

      T& operator[] (size_t x);
      const T& operator[] (size_t x);     // 错误,不能基于返回类型来重载(overload)该方法。
    
  • 然而,这里存在一个问题:不能仅基于返回类型来重载方法或运算符。因此,上述代码无法编译。C++提供了一种绕过这个限制的方法:如果给第二个operator[]标记特性const,编译器就能区别这两个版本。如果对const对象调用operator[],编译器就会使用const operator[];如果对非const对象调用operator[],编译器会使用非constoperator[]。下面是这两个运算符的正确原型:

      T& operator[] (size_t x);
      const T& operator[] (size_t x) const;
    
  • 下面是const operator[]的实现:如果索引超出了范围,这个运算符不会分配新的内存空间,而是抛出异常。如果只是读取元素值,那么分配新的空间就没有意义了:

      template <typename T> const T& Array<T>::operator[] (size_t x) const
      {
          if (x >= mSize)
          {
              throw std::out_of_range("");
          }
          return mElems[x];
      }
    
  • 下面的代码演示了这两种形式的operator[]

      void printArray(const Array<int>& arr, size_t size);
      int main()
      {
          Array<int> myArray;
          for (size_t i=0; i<10; i++)
          {
              myArray[i] = 100;           // 调用non-const operator[],因为myArray是一个non-const对象
          }
          printArray(myArray, 10);
          return 0;
      }
    
      void printArray(const Array<int>& arr, size_t size)
      {
          for (size_t i=0; i<size; i++)
          {
              cout << arr[i] << "";       //调用const operator[],因为arr是一个const对象
          }
          count << endl;
      }
    
  • 注意,仅仅是因为arr是const,所以printArray()中调用的是const operator[]。如果arr不是const,则调用的是非const operator[],尽管事实上并没有修改结果值。

该引用摘自:使用转换运算符(C#
编程指南)

    class Student
    {
        /// <summary>
        /// 语文成绩
        /// </summary>
        public double Chinese { get; set; }

        /// <summary>
        /// 数学成绩
        /// </summary>
        public double Math { get; set; }
    }

5.2 非整数数组索引

  • 这个是通过提供某种类型的键,对一个集合进行“索引”的范例的自然延伸;vector(或更广义的任何线性数组)是一种特例,其中的“键”只是数组中的位置。将operator[]的参数看成提供两个域之间的映射:键域到值域的映射。因此,可编写一个将任意类型作为索引的operator[]。这个类型未必是整数类型。STL的关联容器就是这么做的,例如:std::map

  • 例如,可以创建一个关联数组,其中使用string而不是整数作为键。下面是关联数组的定义:

      template <typename T>
      class AssociativeArray
      {
      public:
          AssociativeArray();
          virtual ~AssociativeArray();
          T& operator[] (const std::string& key) const;
          const T& operator[] (const std::string& key) const;
      private:
          // 具体实现部分省略……
      }
    
  • 注意:不能重载下标运算符以便接受多个参数,如果要提供接受多个索引下标的访问,可以使用函数调用运算符。

仍以Student为例,取语文和数学成绩的和,不使用explicit

不使用implicit 求和

6. 重载函数调用运算符

  • C++允许重载函数调用运算符,写作operator()。如果自定义类中编写一个operator(),那么这个类的对象就可以当做函数指针使用。只能将这个运算符重载为类中的非static方法。下面的例子是一个简单的类,它带有一个重载的operator()以及一个具有相同行为的方法:

      class FunctionObject
      {
      public:
          int operator() (int inParam);   // 函数调用运算符
          int doSquare(int inParam);      // 普通方法函数
      };
    
      // 实现重载的函数调用运算符
      int FunctionObject::operator() (int inParam);
      {
          return inParam * inParam;
      }
    
  • 下面是使用函数调用运算符的代码示例,注意和类的普通方法调用进行比较:

      int x = 3, xSquared, xSquaredAgain;
      FunctionObject square;
      xSquared = square(x);                   // 调用函数调用运算符
      xSquaredAgain = square.doSquare(x);     // 调用普通方法函数
    
  • 带有函数调用运算符的类的对象称为函数对象,或简称为仿函数(functor)。

  • 函数调用运算符看上去有点奇怪,为什么要为类编写一个特殊方法,使这个类的对象看上去像函数指针?为什么不直接编写一个函数或标准的类的方法?相比标准的对象方法,函数函数对象的好处如下:这些对象有时可以伪装为函数指针。只要函数指针类型是模板化的,就可以把这些函数对象当成回调函数传入需要接受的函数指针的例程。

  • 相比全局函数,函数对象的好处更加复杂,主要有两个好处:

  • (1)对象可以在函数对象运算符的重复调用之间,在数据数据成员中保存信息。例如,函数对象可以用于记录每次通过函数调用运算符调用采集到的数字的连续总和。

  • (2)可以通过设置数据成员来自定义函数对象的行为。例如,可以编写一个函数对象,来比较函数参数和数据成员的值。这个数据成员是可配置的,因此这个对象可以自定义为执行任何比较操作。

  • 当然,通过全局变量或静态变量都可以实现上述任何好处。然而,函数对象提供了一种更简洁的方式,而使用全局变量或静态变量在多线程应用程序中可能会产生问题。

  • 通过遵循一般的方法重载规则,可为类编写任意数量的operator()。确切的讲,不同的operator()必须有不同数目的参数或不同类型的参数。例如,可以向FunctionObject类添加一个带string引用参数的operator()

      int operator() (int inParam);
      void operator() (string& str);
    
  • 函数调用运算符还可以用于提供数组的多重索引的下标。只要编写一个行为类似于operator[],但接受多个参数的operator()即可。这项技术的唯一问题是需要使用()而不是[]进行索引,例如myArray(3, 4) = 6

    class Student
    {
        /// <summary>
        /// 语文成绩
        /// </summary>
        public double Chinese { get; set; }

        /// <summary>
        /// 数学成绩
        /// </summary>
        public double Math { get; set; }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var a = new Student
            {
                Chinese = 90.5d,
                Math = 88.5d
            };

            //a的总成绩 语文和数据的总分数
            Console.WriteLine(a.Chinese + a.Math);          
        }
    }

7. 重载解除引用运算符

  • 可以重载3个解除引用运算符:*、->、->*。目前不考虑->(在后面的章节有讨论),该节只考虑和->的原始意义。解除对指针的引用,允许直接访问这个指针指向的值,->是解除引用之后再接.成员选择操作的简写。下面的代码演示了这两者的一致性:

      SpreadsheetCell* cell = new SpreadsheetCell;
      (*cell).set(5);     // 解除引用加成员函数调用
      cell->set(5);       // 单箭头解除引用和成员函数调用
    
  • 在类中重载解除引用运算符,可以使这个类的对象行为和指针一致。这种能力的主要用途是实现智能指针,还能用于STL使用的迭代器。本节通过智能指针类模板的例子,讲解重载相关运算符的基本机制。

  • 警告:C++有两个标准的智能指针:std::shared_ptr和std::unique_ptr。强烈使用这些标准的智能指针而不是自己编写。本节列举的例子是为了演示如何编写解除引用运算符。

  • 下面是这个示例智能指针类模板的定义,其中还没有填入解引用运算符:

      template <typename T> class Pointer
      {
      public:
          Pointer(T* inPtr);
          virtual ~Pointer();
          // 阻止赋值和按值传值
          Pointer(const Pointer<T>& src) = delete;                // C++11 禁用拷贝构造函数
          Pointer<T>& operator=(const Pointer<T>& rhs) = delete;  // C++11 禁用赋值函数重载
    
          // 解引用运算符将会在这里
      private:
          T* mPtr;
      };
    
  • 这个智能指针只是保存了一个普通指针,在智能指针销毁时,删除这个指针指向的存储空间。这个实现同样十分简单:构造函数接受一个真正的指针(普通指针),该指针保存为类中仅有的数据成员。析构函数释放这个指针引用的存储空间。

      template <typename T> Pointer<T>::Pointer(T* inPtr) : mPtr(inPtr);
      {
      }
      template <typename T> Pointer<T>::~Pointer()
      {
          delete mPtr;
          mPtr = nullptr;
      }
    
  • 可以采用以下方式使用这个智能指针模板:

      Pointer<int> smartInt(new int);
      *smartInt = 5;                  //智能指针解引用
      cout << *smartInt << endl;
      Pointer<SpreadsheetCell> smartCell(new SpreadsheetCell);
      smartCell->set(5);              //解引用同时调用set方法
      cout << smartCell->getValue() << endl;
    
  • 从这个例子可以看出,这个类必须提供operator*operator->的实现。其实现部分在下两节中讲解。

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*
*
Website