该引用摘自,  通过类继承可以完成的工作9

作者: 编程  发布:2019-11-05

不同于隐式转换,显式转换运算符必须通过转换的方式来调用。 如果转换操作会导致异常或丢失信息,则应将其标记为 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->的实现。其实现部分在下两节中讲解。

  *可以修改类的行为。

求和:

使用implicit

7.1 实现operator*

  • 当解除对指针的引用时,常常希望能访问这个指针指向的内存。如果那块内存包含了一个简单类型,例如int,应该可以直接修改这个值。如果内存中包含了复杂的类型,例如对象,那么应该能通过.运算符访问它的数据成员或方法。

  • 为了提供这些语义,operator*应该返回一个变量或对象的引用。在Pointer类中,声明和定义如下所示:

      template <typename T> class Pointer
      {
      public:
          // 构造部分同上,所以省略
          T& operator*();
          const T& operator*() const;
          // 其它部分暂时省略
      };
      template <typename T> T& Pointer<T>::operator*()
      {
          return *mPtr;
      }
      template <typename T> const T& Pointer<T>::operator*() const
      {
          return *mPtr;
      }
    
  • 从这个例子中可以看出,operator*返回的是底层普通指针指向的对象或变量的引用。与重载下标运算符一样,同时提供方法的const版本合非const版本也很有用,这两个版本分别返回const引用和非const引用。

  继承机制只需要提供新特性,甚至不需要访问源代码就可以派生出类。

    class Program
    {
        static void Main(string[] args)
        {
            var a = new Student
            {
                Chinese = 90.5d,
                Math = 88.5d
            };

            //a的总成绩 语文和数据的总分数
            Console.WriteLine(a.Chinese + a.Math);          
        }
    }
    class Student
    {
        /// <summary>
        /// 语文成绩
        /// </summary>
        public double Chinese { get; set; }

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

        /// <summary>
        /// 隐式求和
        /// </summary>
        /// <param name="a"></param>
        public static implicit operator double(Student a)
        {
            return a.Chinese + a.Math;
        }
    }

7.2 实现operator->

  • 箭头运算符稍微复杂一些,应用箭头运算符的结果应该是对象的一个成员或方法。然而,为了实现这一点,应该要实现operator*operator.;而C++有充足的理由不实现运算符operator.:不可能编写单个原型,来捕捉任何可能选择的成员或方法。因此,C++将operator->当成一个特例。例如下面的这行代码:

      smartCell->set(5);
    
  • C++将这行代码解释为:

      (smartCell.operator->())->set(5);
    
  • 从中可以看出,C++给重载的operator->返回的任何结果应用了另一个operator->。因此,必须返回一个指向对象的指针,如下所示:

      template <typename T> class Pointer
      {
      public:
          // 省略构造函数部分
          T* operator->();
          const T* operator->() const;
          // 其它部分省略
      };
      template <typename T> T* Pointer<T>::operator->()
      {
          return mPtr;
      }
      template <typename T> const T* Pointer<T>::operator->() const
      {
          return mPtr;
      }
    

 

使用explicit

求和:

7.3 operator->*的含义

  • 在C++中,获得类成员和方法的地址,以获得指向这些成员和方法的指针是完全合法的。然而,不能在没有对象的情况下访问非static数据成员或调用非static方法。类数据成员和方法的重点在于它们依附于对象。因此,通过指针调用方法和访问数据成员时,必须在对象的上下文中解除这个指针的引用。下面的例子说明了.和->运算符:

      SpreadsheetCell myCell;
      double (SpreadsheetCell::*methodPtr)() const = &SpreadsheetCell::getValue;
      cout << (myCell.*methodPtr)() << endl;
    
  • 注意,.*运算符解除对方法指针的引用并调用这个方法。如果有一个指向对象的指针而不是对象本身,还有一个等效的operator->*可以通过指针调用方法。这个运算符如下所示:

      SpreadsheetCell *myCell = new SpreadsheetCell();
      double (SpreadsheetCell::*methodPtr)() const = &SpreadsheetCell::getValue();
      cout << (myCell->*methodPtr)() << endl;
    
  • C++不允许重载operator.*(就像不允许重载operator.一样),但是可以重载operator->*。然而这个运算符的重载非常复杂,标准库中的share_ptr模板也没有重载operator->*

一、一个简单的基类

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

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

        public static explicit operator double(Student a)
        {
            return a.Chinese + a.Math;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var a = new Student
            {
                Chinese = 90.5d,
                Math = 88.5d
            };

            double total = a;

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

8. 编写转换运算符

  • 回到SpreadsheetCell例子,考虑如下两行代码:

      SpreadsheetCell cell(1.23);
      string str = cell;          //不能编译通过
    
  • SpreadsheetCell包含一个字符串表达式,因此将SpreadsheetCell赋值给string变量看上去是符合逻辑的。但不能这么做,编译器会表示不知道如何将SpreadsheetCell转换为string。你可能会通过下述方式迫使编译器进行这种转换:

      string str = (string)cell;  //仍然不能编译通过
    
  • 首先,上述代码依然无法编译,因为编译器还是不知道如何将SpreadsheetCell转换为string。从这行代码中编译器已经知道你想让编译器做转换,所以编译器如果知道如何转换,就会进行转换。其次,一般情况下,最好不要在程序中添加这种无理由的类型转换。如果想允许这类赋值,必须告诉编译器如何执行它。也就是说,可编写一个将SpreadsheetCell转换为string的转换运算符。其原型如下:

      operator std::string() const;
    
  • 函数名为operator std::string。它没有返回类型,因为返回类型是通过运算符的名称确定的:std::string。这个函数时const,因为这个函数不会修改被调用的对象。实现如下:

      SpreadsheetCell::operator string() const
      {
          return mString;
      }
    
  • 这就完成了从SpreadsheetCell到string的转换运算符的编写。现在的编译器可以接受下面这行代码,并在运行时正确的操作。

      SpreadsheetCell cell(1.23);
      string str = cell;          //按照预期的执行
    
  • 可以同样的语法编写任何类型的转换运算符。例如,下面是从SpreadsheetCell到double的转换运算符:

      SpreadsheetCell::operator double() const
      {
          return mValue;
      }
    
  • 现在可以编写以下代码:

      SpreadsheetCell cell(1.23);
      double d1 = cell;
    

  首先我们定义一个简单的基类Person,其设计如下:

求和:

8.1 转换运算符的多义性问题

  • 注意,为SpreadsheetCell对象编写double转换运算符时会引入多义性问题。例如下面这行代码:

      SpreadsheetCell cell(1.23);
      double d2 = cell + 3.3;     // 不能编译通过,如果你已经重载了operator double()
    
  • 现在这一行无法成功编译。在编写运算符double()之前,这行代码可以编译,那么现在出现了什么问题?问题在于,编译器不知道应该通过operator double()cell转换为double,再执行double加法,还是通过double构造函数将3.3转换为SpreadsheetCell,再执行SpreadsheetCell加法。在编写operator double()之前,编译器只有一个选择:通过double构造函数将3.3转换为SpreadsheetCell,再执行SpreadsheetCell加法。然而,现在编译器可以执行两种操作,存在二义性,所以编译器便报错。

  • 在C++11之前,通常解决这个难题的方法是将构造函数标记为explicit,以避免使用这个构造函数进行自动转换。然而,我们不想把这个构造函数标记为explicit,通常希望进行从doubleSpreadsheetCell的自动类型转换。自C++11以后,可以将double类型转换运算符标记为explicit,来解决这个问题:

      explicit operator double() const;
    
  • 下面的代码演示了这种方法的应用:

      SpreadsheetCell cell = 6.6;                     // [1]
      string str = cell;                              // [2]
      double d1 = static_cast<double>(cell);          // [3]
      double d2 = static_cast<double>(cell + 3.3);    // [4]
    
  • 下面解释了上述代码中的各行:

  • [1]使用隐式类型转换从double转换到SpreadsheetCell。由于这是在声明中,所以这个是通过调用接受double参数的构造函数进行的。

  • [2]使用了operator string()转换运算符。

  • [3]使用了operator double()转换运算符。注意,由于这个转换运算符现在声明为explicit,所以要求强制类型转换。

  • [4]通过隐式类型转换将3.3转换为SpreadsheetCell,再进行两个SpreadsheetCelloperator+操作,之后进行必要的显式类型转换来调用operator double()

Person.h

    class Program
    {
        static void Main(string[] args)
        {
            var a = new Student
            {
                Chinese = 90.5d,
                Math = 88.5d
            };

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

8.2 用于布尔表达式的转换

  • 有时,能将对象用在布尔表达式中会非常有用。例如,程序员常常在条件语句中这样使用指针:

      if (prt != nullptr) { /* 执行一些解除引用的操作 */}
    
  • 有时候程序员会编写这样的简写条件:

      if (prt) { /* 执行一些解除引用的操作 */}
    
  • 有时还能看到这样的代码:

      if (!prt) { /* 执行一些操作 */}
    
  • 目前,上述任何表达式都不能和此前定义的Pointer智能指针类一起编译。然而,可以给类添加一个转换运算符,将它转换为指针类型。然后,这个类型和nullptr的比较,以及单独一个对象在if语句中的形式都会触发这个对象向指针类型的转换。转换运算符常用的指针类型为void*,因为这个指针类型除了在布尔表达式中测试之外,不能执行其他操作。

      operator void*() const
      {
          return mPtr;
      }
    
  • 现在下面的代码可以成功编译,并能完成预期的任务:

      void process(Pointer<SpreadsheetCell>& p)
      {
          if (p != nullptr)
          {
              cout << "not nullptr" << endl;
          }
          if (p != NULL)
          {
              cout << "not NULL" << endl;
          }
          if (p)
          {
              cout << "not nullptr" << endl;
          }
          if (!p)
          {
              cout << "nullptr" << endl;
          }
      }
      int main()
      {
          Pointer<SpreadsheetCell> smartCell(nullptr);
          process(smartCell);
          cout << endl;
          Pointer<SpreadsheetCell> anotherSmartCell(new SpreadsheetCell(5.0));
          process(anotherSmartCell);
      }
    
  • 输出结果如下所示:

      nullprt
    
      not nullptr
      not NULL
      not nullptr
    
  • 另一种方法是重载operator bool()而不是operator void*()。毕竟是在布尔表达式中使用对象,为什么不能直接转换为bool呢?

      operator bool() const
      {
          return mPtr != nullptr;
      }
    
  • 下面的比较仍可以运行:

          if (p != NULL)
          {
              cout << "not NULL" << endl;
          }
          if (p)
          {
              cout << "not nullptr" << endl;
          }
          if (!p)
          {
              cout << "nullptr" << endl;
          }
    
  • 然而,使用operator bool()时,下面和nullptr的比较会导致编译器错误:

      if (p != nullptr)   { cout << "not nullptr" << endl; } //Error
    
  • 这是正确的行为,因为nullptr有自己的类型nullptr_t,这个类型没有自动类型转换为整数0。编译器找不到接受Pointer对象和nullptr_t对象的operator!=。可以把这样的operator!=实现为Pointer类的友元:

      template <typename T>
      bool operator!=(const Pointer<T>& lhs, const std::nullptr_t& rhs)
      {
          return lhs.mPtr != rhs;
      }
    
  • 然而,实现这个operator!=后,下面的比较会无法工作,因为编译器知道该用哪个operator!=

      if (p != NULL)
      {
          cout << "not NULL" << endl;
      }
    
  • 通过这个例子,你可能得出以下结论:operator bool()技术看上去只适合于不表示指针的对象,以及转换为指针类型并没有意义的对象。遗憾的是,添加转换至bool的转换运算符会产生其他一些无法预知的后果。当条件允许时,C++会使用“类型提升”规则将bool类型自动转换为int类型。因此,采用operator bool()时,下面的代码可以编译运行:

      Pointer<SpreadsheetCell> smartCell(new SpreadsheetCell);
      int i = smartCell;      //转换smartCell指针从bool到int
    
  • 这通常并不是期望或需要的行为。因此,很多程序员更偏爱使用operator void*()而不是operator bool()

  • 从中可以看出,重载运算符时需要考虑设计因素。哪些操作符需要重载的决策会直接影响到客户对类的使用方式。

 1 #include <iostream>
 2 #include <string>
 3 using std::string;
 4 class Person{
 5 private:
 6     string name_;
 7     int age_;
 8 public:
 9     Person(const string & name = "none", int age = 0);//形参类型声明为const string &,那么实参既可以是string对象,也可以是字符串常量。
10     void setName(const string &name);
11     void setAge(int age);
12     string getName()const;
13     int getAge() const;
14     friend std::ostream & operator<<(std::ostream & os, const Person & p);
15 };

9. 重载内存分配和释放运算符

  • C++允许重定义程序中内存分配和释放的方式。既可以在全局层次也可以在类层次进行这种自定义。这种能力可能产生内存碎片的情况下最有用,当分配和释放大量小对象时会产生内存碎片。例如,每次需要内存时,不适用默认的C++内存分配,而是编写一个内存池分配器,以重用固定大小的内存块。本节详细讲解内存分配和释放例程,以及如何定制化它们。有了这些工具,就可以根据需求编写自己的分配器。

Person.cpp

9.1 new和delete的工作原理

  • C++最复杂的地方之一就是newdelete的细节。考虑下面几行代码:

      SpreadsheetCell* cell = new SpreadsheetCell();
    
  • new SpreadsheetCell()这部分称为new表达式。它完成了两件事情。首先,通过调用opetator newSpreadsheetCell对象分配了内存空间。然后,为这个对象调用构造函数。只有这个构造函数完成了,才返回指针。

  • delete的工作方式与此类似。考虑下面这行代码:

      delete cell;
    
  • 这行称为delete表达式。它首先调用cell的析构函数,然后调用operator delete9159.com,来释放内存。

  • 可以重载operator newoperator delete来控制内存的分配和释放,但不能重载new表达式和delete表达式。因此,可以自定义实际的内存分配和释放,但不能自定义构造函数和析构函数的调用。

  • (1). new表达式和operator new

  • 有6种不同形式的new表达式,每种形式都有对应的operator new。前4种new表达式:newnew[]nothrow newnothrow new[]。下面列出了<new>头文件种对应的4种operator new形式:

      void* operator new(size_t size);                                //For new
      void* operator new[](size_t size);                              //For new[]
      void* operator new(size_t size, const nothrow_t&) noexcept;     //For nothrow new
      void* operator new[](size_t size, const nothrow_t&) noexcept;   //For nothrow new[]
    
  • 有两种特殊的new表达式,它们不进行内存分配,而在已有的存储段上调用构造函数。这种操作称为placement new运算符(包括单对象和数组形式)。它们在已存在的内存上构造对象,如下所示:

      void* ptr = allocateMemorySomehow();
      SpreadsheetCell* cell = new(prt) SpreadsheetCell();
    
  • 这个特性有点偏门,但知道这项特性的存在非常重要。如果需要实现内存池,以便在不释放内存的情况下重用内存,这项特殊性就非常方便。对应的operator new形式如下,但C++标准禁止重载它们:

    void* operator new(size_t size, void* p) noexcept;
    void* operator new[](size_t size, void* p) noexcept;
  • (2). delete表达式和operator delete

  • 只有两种不同形式的delete表达式可以调用:deletedelete[];没有nothrowplacement形式。然而, operator delete有6种形式。为什么有这种不对称性?两种nothrowplacement的形式只有在构造函数抛出异常时才会使用。这种情况下,匹配调用构造函数之前分配内存时使用的operator newoperator delete会被调用。然而,如果正常地删除指针,delete会调用operator deleteoperator delete[](绝不会调用nothrowplacement形式)。在实际中,这并没有关系:C++标准指出,从delete抛出异常的行为是未定义的,也就是说delete永远都不应该抛出异常,因此nothrow版本的operator delete是多余的;而placement版本的delete应该是一个空操作,因为在placement operator new中并没有分配内存,因此也不需要释放内存。下面是operator delete各种形式的原型:

      void operator delete(void* ptr) noexcept;
      void operator delete[](void* ptr) noexcept;
      void operator delete(void* ptr, const nothrow_t&) noexcept;
      void operator delete[](void* ptr, const nothrow_t&) noexcept;
      void operator delete(void* ptr, void*) noexcept;
      void operator delete[](void* ptr, void*) noexcept;
    
 1 #include "Person.h"
 2 Person::Person(const string & name, int age){
 3     name_ = name;
 4     age_ = age;
 5 }
 6 std::ostream & operator<<(std::ostream & os, const Person & p){
 7     os << "name:" << p.name_ << ", age:" << p.age_;
 8     return os;
 9 }
10 void Person::setName(const string &name){
11     name_ = name;
12 }
13 void Person::setAge(int age){
14     age_ = age;
15 }
16 string Person::getName()const{
17     return name_;
18 }
19 int Person::getAge()const{
20     return age_;
21 }

9.2 重载operator new和operator delete

  • 如有必要,可以替换全局的operator newoperator delete例程。这些函数会被程序中的每个new表达式和delete表达式调用,除非在类中有更特别的版本。然而,引用Bjarne Stroustrup的一句话:“……替换全局的operator newoperator delete是需要胆量的。”。所以我们也不建议替换。

  • 警告:如果决定一定要替换全局的operator new,一定要注意在这个运算符的代码中不要对new进行任何调用:否则会产生无限循环。

  • 更有用的技术是重载特定类的operator newoperator delete。仅当分配或释放特定类的对象时,才会调用这些重载的运算符。下面是一个类的例子,它重载了4个非placement形式的operator newoperator delete

      #include <new>
      class MemoryDemo
      {
      public:
          MemoryDemo();
          virtual ~MemoryDemo();
          void* operator new(std::size_t size);
          void operator delete(void* ptr) noexcept;
          void* operator new[](std::size_t size);
          void operator delete[](void* ptr) noexcept;
          void* operator new(std::size_t size, const std::nothrow_t&) noexcept;
          void operator delete(void* ptr, const std::nothrow_t&) noexcept;
          void* operator new[](std::size_t size, const std::nothrow_t&) noexcept;
          void operator delete[](void* ptr, const std::nothrow_t&) noexcept;
      };
    
  • 下面是这些运算符的简单实现,这些实现将参数传递给了这些运算符全局版本的调用。注意nothrow实际上是一个nothrow_t类型的变量:

      void* MemoryDemo::operator new(size_t size)
      {
          cout << "operator new" << endl;
          return ::operator new(size);
      }
      void MemoryDemo::operator delete(void* ptr) noexcept
      {
          cout << "operator delete" << endl;
          ::operator delete(ptr);
      }
      void* MemoryDemo::operator new[](size_t size)
      {
          cout << "operator new[]" << endl;
          return ::operator new[](size);
      }
      void MemoryDemo::operator delete[](void* ptr) noexcept
      {
          cout << "operator delete[]" << endl;
          ::operator delete[](ptr);
      }
      void* MemoryDemo::operator new(size_t size, const nothrow_t&) noexcept
      {
          cout << "operator new nothrow" << endl;
          return ::operator new(size, nothrow);
      }
      void MemoryDemo::operator delete(void* ptr, const nothrow_t&) noexcept
      {
          cout << "operator delete nothrow" << endl;
          ::operator delete(ptr, nothrow);
      }
      void* MemoryDemo::operator new[](size_t size, const nothrow_t&) noexcept
      {
          cout << "operator new[] nothrow" << endl;
          return ::operator new[](size, nothrow);
      }
      void MemoryDemo::operator delete[](void* ptr, const nothrow_t&) noexcept
      {
          cout << "operator delete[] nothrow" << endl;
          ::operator delete[](ptr, nothrow);
      }
    
  • 下面的代码以不同方式分配和释放这个类的对象:

      MemoryDemo* mem = new MemoryDemo();
      delete mem;
      mem = new MemoryDemo[10];
      delete[] mem;
      mem = new (nothrow) MemoryDemo();
      delete mem;
      mem = new (nothrow) MemoryDemo[10];
      delete[] mem;
    
  • 下面是运行结果:

      operator new;
      operator delete;
      operator new[];
      operator delete[];
      operator new nothrow;
      operator delete;
      operator new[] nothrow;
      operator delete[];
    
  • 这些operator newoperator delete的实现非常简单,但作用不大。它们旨在介绍语法形式,以便在实现真正版本时参考。

  • 警告:当重载operator new时,要重载对应形式的operator delete。否则,内存会根据指定的方式分配,但是根据内建的语义释放,这两者可能不兼容。

  • 重载所有不同形式的operator new看上去有点过分。但是在一般情况下最好这么做,从而避免内存分配不一致。如果不想提供任何实现,可使用=delete显示地删除函数,以避免别人使用。具体内容可参考下一节。

 

9.3 显示地删除/默认化operator new和operator delete

  • 显示地删除或默认化不局限用于构造函数和赋值运算符。例如,下面的类删除了operator newnew[],也就是说这个类不能通过newnew[]动态创建:

      class MyClass
      {
      public:
          void* operator new(std::size_t size) = delete;
          void* operator new[](std::size_t size) = delete;
      };
    
  • 按以下方式使用这个类会产生编译器错误:

      int main()
      {
          MyClass* p1 = new MyClass;      // Error
          MyClass* p2 = new MyClass[2];   // Error
          return 0;
      }
    

  提示:在设计一个类的时候,我们需要考虑一下几个问题:

9.4 重载带有额外参数的operator new和operator delete

  • 除了重载标准形式的operator new之外,还可以编写带有额外参数的版本。例如下面是MemoryDemo类中有额外整数参数的operator newoperator delete原型:

      void* operator new(std::size_t size, int extra);
      void operator delete(void* ptr, int extra) noexcept;
    
  • 实现如下所示:

      void* MemoryDemo::operator new(size_t size, int extra)
      {
          cout << "operator new with extra int arg: " << extra << endl;
          return ::operator new(size);
      }
      void MemoryDemo::operator delete(void* ptr, int extra) noexcept
      {
          cout << "operator delete with extra in arg: " << extra << endl;
          return ::operator delete(ptr);
      }
    
  • 编写带有额外参数的重载operator new时,编译器会自动允许编写对应的new表达式。因此可以编写这样的代码:

      MemoryDemo* pmem = new (5) MemoryDemo();
      delete pmem;
    
  • new的额外参数以函数调用的语法传递(和nothrow new一样)。这些额外参数可用于向内存分配例程传递各种标志或计数器。例如,一些运行时库在调试模式中使用这种形式,在分配对象的内存时提供文件名和行号,这样,在发生内存泄漏时,可以识别出发生问题的分配内存所在的代码行数。

  • 定义带有额外参数的operator new时,还应该定义带有额外参数的对应operator delete。不能自己调用这个带有额外参数的operator delete,只有在使用了带额外参数的operator new且对象的构造函数抛出异常时,才会调用这个operator delete

  • 另一种形式的operator delete提供了需释放的内存大小和指针。只需声明带有额外大小参数的operator delete原型。

  • 警告:如果类声明了两个一样版本的operator delete,只不过一个接受大小参数,另一个不接受,那么不接受额外参数的版本总是会调用。如果需要使用带大小参数的版本,则请只编写这一个版本。

  • 可独立地将任何版本的operator delete替换为接受大小参数的operator delete版本。下面是MemoryDemo类的定义,其中的第一个operator delete改为接受要释放的内存大小作为参数:

      class MemoryDemo
      {
      public:
          // 省略其他内容
          void* operator new(std::size_t size);
          void operator delete(void* ptr, std::size_t size) noexcept;
          // 省略其他内容
      };
    
  • 这个operator delete实现调用没有大小参数的全局operator delete,因为并不存在接受这个小大参数的全局operator delete

      void MemoryDemo::operator delete(void* ptr, size_t size) noexcept
      {
          cout << "operator delete with size" << endl;
          ::operator delete(ptr);
      }
    
  • 只有需要为自定义类编写复杂的内存分配和释放方案时,才使用这个功能。

    *是否需要显式提供默认构造函数;

    *是否需要显式提供析构函数;

    *是否需要显式提供复制构造函数;

    *是否需要显式提供赋值运算符重载函数;

    *是否需要显式提供地址运算符函数;

  一般来说,如果在类的构造函数中使用了new运算符,或者在其他成员函数中使用了new运算符来修改类的成员,那么就需要考虑显式提供复制构造函数、赋值运算符重载函数、析构函数。在Person类中,我们使用编译器提供的默认析构函数、默认复制构造函数和默认的赋值运算符重载函数即可。

  1、派生一个类

  下面我们设计一个Teacher类继承自Person类。首先将Teacher类声明为从Person类派生而来:

1 #include <iostream>
2 #include "Person.h"
3 
4 class Teacher:public Person{
5    // ...
6 };

  冒号指出Teacher类的基类是Person类。上述特殊的生命头表明Person是一个公有基类,这杯称为公有派生。派生类对象包含基类对象。

  使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。

  派生类将具有以下特征:

    *派生类对象存储了基类的数据成员(派生类继承了基类的实现);

    *派生类对象可以使用基类的方法(派生类继承了基类的接口)。

  接下来,我们就可以在继承特性中添加下面的内容:

    *派生类需要自己的构造函数;

    *派生类可以根据需要添加额外的数据成员和成员函数。

  在我们设计的Teacher类需要一个数据成员来存储工作的单位、工资以及所教授的课程。还应包括检查这些信息和重置这些信息的方法:

 1 #include <iostream>
 2 #include "Person.h"
 3 
 4 class Teacher:public Person{
 5 private:
 6     string workUnit_;//工作单位
 7     float salary_;//工资
 8     string course_;//教授的课程
 9 public:
10     Teacher(const string & , int , const string &, float, const string &);
11     Teacher(const Person &, const string &, float, const string &);
12   Teacher();
13     void setWorkUnit(const string & );
14     void setSalary(float );
15     void setCourse(const string &);
16     string getWorkUnit()const;
17     float getSalary()const;
18     string getCourse()const;
19     friend std::ostream & operator<<(std::ostream & os , const Teacher &);
20 };

  构造函数必须给新成员(如果有新成员)和继承的成员提供数据。

  2、构造函数:访问权限的考虑

  派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。例如,派生类构造函数不能直接设置继承来的成员,而必须使用基类的公有方法来访问私有的基类成员。具体地说,派生类构造函数必须使用基类的构造函数。

  创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作。例如,下面是第一个Teacher类的构造函数代码:

1 Teacher::Teacher(const string & name, int age, const string & workUnit, float salary, const string & course):Person(name,age){
2     workUnit_ = workUnit;
3     salary_ = salary;
4     course_ = course;
5 }

  必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数,因此下面的两段代码是等效的:

1 Teacher::Teacher(const string & name, int age, const string & workUnit, float salary, const string & course){
2     workUnit_ = workUnit;
3     salary_ = salary;
4     course_ = course;
5 }

1 Teacher::Teacher(const string & name, int age, const string & workUnit, float salary, const string & course):Person(){
2     workUnit_ = workUnit;
3     salary_ = salary;
4     course_ = course;
5 }

 

   除非要使用默认的构造函数,否则应显式调用正确的基类构造函数。

  

   下面来看第二个构造函数的代码:

1 Teacher::Teacher(const Person & per, const string & workUnit, float salary, const string & course):Person(per){
2     workUnit_ = workUnit;
3     salary_ = salary;
4     course_ = course;
5 }

  由于per的类型为Person,因此调用基类的复制构造函数。在这里,基类Person没有定义复制构造函数,如果需要复制构造函数但又没有定义,编译器将生成一个。在这种情况下,执行成员复制的隐式复制构造函数是合适的,因为这个类没有使用动态内存分配。

  同样,也可以对派生类使用成员初始化列表语法。在这种情况下,应在列表中使用成员名,而不是类名。所以,第二个构造函数可以按照下面的方式编写:

Teacher::Teacher(const Person & per, const string & workUnit, float salary, const string & course):Person(per),workUnit_(workUnit),salary_(salary),course_(course){}

  有关派生类构造函数的要点有如下几点:

  *首先创建基类对象;

  *派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;

  *派生类构造函数应初始化派生类新增的数据成员。

  这个例子没有提供显式析构函数,因此使用隐式析构函数。释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。

 

  3、使用派生类

  要使用派生类,程序必须要能够访问基类声明。可以将基类和派生类的声明置于同一个头文件中,也可以将每个类放在独立的头文件中,但由于这两个类是相关的,所以把其类声明放在一起更合适。

  下面是Teacher的完整方法实现文件:

 1 #include "Teacher.h"
 2 Teacher::Teacher(const string & name, int age, const string & workUnit, float salary, const string & course):Person(name,age){
 3     workUnit_ = workUnit;
 4     salary_ = salary;
 5     course_ = course;
 6 }
 7 Teacher::Teacher(const Person & per, const string & workUnit, float salary, const string & course):Person(per){
 8     workUnit_ = workUnit;
 9     salary_ = salary;
10     course_ = course;
11 }
12 Teacher::Teacher(){
13     workUnit_ = "none";
14     salary_ = .0;
15     course_ = "none";
16 }
17 void Teacher::setCourse(const string & course){
18     course_ = course;
19 }
20 void Teacher::setWorkUnit(const string & workUnit){
21     workUnit_ = workUnit;
22 }
23 void Teacher::setSalary(float salary){
24     salary_ = salary;
25 }
26 string Teacher::getWorkUnit()const{
27     return workUnit_;
28 }
29 string Teacher::getCourse()const{
30     return course_;
31 }
32 float Teacher::getSalary()const{
33     return salary_;
34 }
35 std::ostream & operator<<(std::ostream & os,const Teacher & te){
36     os << "name:" << te.getName() << ",age:" << te.getAge() << ", workUnit:" << te.workUnit_ << ", salary:" << te.salary_ << ", course:" << te.course_;
37     return os;
38  }

 

  4、派生类和基类之间的特殊关系

  派生类和基类之间有一些特殊关系。

  *派生类可以使用基类的方法,条件是方法不是私有的。

  *基类指针可以在不进行显式类型转换的情况下指向派生类对象;

  *基类引用可以在不进行显式类型转换的情况下引用派生类对象。

  但是,基类指针或引用只能调用基类方法。

  通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。然而,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针。

  

二、继承:is-a关系 

  派生类和基类之间的特殊关系是基于C++继承的底层模型的。实际上,C++有3种继承方式:共有继承、保护继承和私有继承。公有继承是最常用的方式,它建立一种is-a关系,即派生对象也是一个基类对象,可以对基类执行的操作,也可以对派生类对象执行。

  但是公有继承不具有下列关系:

  *公有继承不建立has-a关系;

  *公有继承不建立is-like-a关系;

  *公有继承不建立is-implemented-as-a(作为....来实现)关系。

 

三、多态公有继承

  多态:方法的行为取决于调用该方法的对象,即同一个方法的行为随上下文而异。

  有两种重要的机制可用于实现多态公有继承:

  *在派生类中重新定义基类的方法;

  *使用虚方法。

  下面我们重新设计Person类和Teacher类,

Person.h

 1 #ifndef __Demo__Person__
 2 #define __Demo__Person__
 3 
 4 #include <iostream>
 5 #include <string>
 6 using namespace std;
 7 
 8 class Person{
 9 private:
10     string name_;
11     int age_;
12 public:
13     Person(const string & name = "无名氏", int age = 0 );
14     virtual ~Person(){};
15     void setName(const string & name);
16     void setAge(int age);
17     const string & getName()const;
18     int getAge()const;
19     virtual void showMessage()const;
20     void setMessage(const string & name, int age);
21     friend ostream & operator<<(ostream & os, const Person & per);
22     
23 
24 };
25 #endif /* defined(__Demo__Person__) */

 

Person.cpp

 1 #include "Person.h"
 2 Person::Person(const string & name, int age){
 3     name_ = name;
 4     age_ = age;
 5 }
 6 void Person::setAge(int age){
 7     age_ = age;
 8 }
 9 void Person::setName(const string &name){
10     name_ = name;
11 }
12 const string & Person::getName()const{
13     return name_;
14 }
15 int Person::getAge()const{
16     return age_;
17 }
18 void Person::showMessage()const{//虚方法
19     cout <<"调用了Person对象的showMessage()方法:"<< *this;
20 }
21 void Person::setMessage(const string &name,int age){
22     cout << "调用了Person对象的setMessage()方法n";
23     name_ = name;
24     age_ =age;
25 }
26 ostream & operator<<(ostream & os, const Person & per){
27     os << "name:" << per.name_ << ", age:" << per.age_;
28     return os;
29 }

 

Teacher.h

 1 #ifndef __Demo__Teacher__
 2 #define __Demo__Teacher__
 3 
 4 #include <iostream>
 5 #include "Person.h"
 6 
 7 class Teacher:public Person{
 8 private:
 9     string school_;
10     float salary_;
11 public:
12     Teacher(const string & name = "无名氏", int age = 0, const string & school = "无", float salary = .0);
13     void setSchool(const string &);
14     void setSalary(float salary);
15     const string & getSchool()const;
16     float getSalary()const;
17     virtual void showMessage()const;
18     void setMessage(const string & school, float salary);
19     friend ostream & operator<<(ostream & , const Teacher &);
20 };
21 
22 #endif /* defined(__Demo__Teacher__) */

 

Teacher.cpp

 1 #include "Teacher.h"
 2 Teacher::Teacher(const string & name , int age, const string & school, float salary ):Teacher(name, age){
 3     school_ = school;
 4     salary_ = salary;
 5 }
 6 void Teacher::setSchool(const string & school){
 7     school_ = school;
 8 }
 9 void Teacher:: setSalary(float salary){
10     salary_ = salary;
11 }
12 const string & Teacher:: getSchool()const{
13     return school_;
14 }
15 float Teacher:: getSalary()const{
16     return salary_;
17 }
18 void Teacher:: showMessage()const{
19     cout << "调用了Teacher对象的showMessage()方法:" << *this;
20 }
21 void Teacher:: setMessage(const string & school, float salary){
22     cout << "调用了Teacher的setMessage()方法n";
23     school_ = school;
24     salary_ = salary;
25 }
26 ostream & operator<<(ostream & os, const Teacher & per){
27     os <<"调用了Teacher对象的<<运算符方法,"<< "name:" << per.getName() << ", age:" << per.getAge() << ", school:" << per.school_ << ", salary:"<< per.salary_;
28     return os;
29 }

 

main.cpp

 1 #include <iostream>
 2 #include "Teacher.h"
 3 
 4 using namespace std;
 5 
 6 int main(int argc, const char * argv[]) {
 7     Person *per = new Person{"王晓红",24};
 8     Person *per2 = new Teacher{"刘晓东",30,"成都七中",5000.0};
 9     per->showMessage();
10     per2->showMessage();
11     per->setMessage("王晓玲", 40);
12     per2->setMessage("刘翔情", 35);
13     per->showMessage();
14     per2->showMessage();
15     return 0;
16 }

 

输出结果:

1 调用了Person对象的showMessage()方法,name:王晓红, age:24
2 调用了Teacher对象的showMessage()方法,name:刘晓东, age:30, school:成都七中, salary:5000
3 调用了Person对象的setMessage()方法
4 调用了Person对象的setMessage()方法
5 调用了Person对象的showMessage()方法,name:王晓玲, age:40
6 调用了Teacher对象的showMessage()方法,name:刘翔情, age:35, school:成都七中, salary:5000

  说明:

    首先,在上面的代码中,在基类Person和Teacher类声明中声明showMessage()方法时都使用了C++关键字virtual,这些方法方法叫做虚方法。从输出结果中可以看出,虽然在main.cpp函数中Person对象和Teacher对象都是用Person指针指向的,但是在调用showMessage()方法的时候,都调用了对象各自的方法,即继承类Teacher对象没有调用基类的showMessage()方法。

    其次,在基类Person和Teacher类声明中声明setMessage()方法的时候没有使用关键字virtual。从输出结果可以看出,用Person指针指向的Person对象和Teacher对象在调用setMessage()方法的时候,都是调用的基类Person类的setMessage()方法。Teacher类虽然重载了setMessage()方法,但是在用指向Teacher对象的基类Person指针或引用调用该方法的时候并没有调用Teacher对象本身的setMessage()方法。

  有上述可以得出以下结论:

  如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象类型来选择方法。

  因此,我们需要在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。然而,在派生类声明中使用关键字virtual来指出哪些函数是虚函数也不失为一个好办法。

  另外,基类声明一个虚析构函数,可以确保释放派生对象的时候,按正确的顺序调用析构函数。

  注意,关键字virtual只用于类声明的方法原型中,而不能用于方法定义中。

 

  非构造函数不能使用成员初始化列表语法,但是派生类方法可以调用公有的基类方法。

  在重定义派生类继承方法的代码中调用基类中被继承的同名方法时,如果不使用作用域解析运算符很有可能带来不必要的麻烦,将会创建一个无限递归函数,为避免这种错误必须对基类被继承的同名方法使用作用域解析运算符。例如下面的代码将会创建一个无限递归函数:

    void Teacher::showMessage()const{

      .....

      showMessage();//这样将会创建一个无限递归函数, 因为该函数的默认调用对象是自己本身,即该语句与this->showMessage();等效

      .....

    }

    但是下面的不会出错:

    void Teacher::showMessage() const{

      ....

      Person::showMessage();//这将调用基类的showMessage()方法,在这里并不会出现任何错误。

      .....

    }

  

  虚析构函数

  在上面代码中,在基类Person声明中,我们使用了虚析构函数,即virtual ~Person();这样做的理由在于:

    如果析构函数不是虚的,则将调用对应于指针或引用类型的析构函数;如果析构函数是虚的,将调用相应对象类型的析构函数。因此,使用虚析构函数可以确保正确的析构函数序列被调用。

四、静态联编和动态联编 

  静态联编:在编译过程中进行联编,又称为早期联编;

  动态联编:编译器在程序运行时生成选择正确虚方法的代码,称为动态联编,又称为晚期联编。

  1、指针和引用类型的兼容性

  在C++中,动态联编与通过指针和引用调用方法相关,从某种成都上说,这是由继承控制的。公有继承建立的is-a关系的一种方法是如何处理指向对象的指针和引用。通常,C++不允许将一种类型的地址赋给另一种类型的指针,也不允许将一种类型的引用指向另一种类型。

  指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换。

  将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显式类型转换。该规则是is-a关系的一部分。向上强制转换是可以传递的,即A是B的基类,B是C的基类,则A引用或指针可以引用A对象、B对象和C对象。

  将基类指针或引用转换为派生类指针或引用称为向下强制转换。如果不使用显式类型转换,则向下强制类型转换是不允许的。原因是is-a关系是不可逆的。派生类可以新增数据成员,因此使用这些数据成员的类成员函数不能应用于基类。

  对于使用基类引用或指针作为参数的函数调用,将进行向上转换。隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。

  2、虚成员函数和动态联编

  编译器对非虚方法使用静态联编,对虚方法使用动态联编。

  编译器将静态联编设为默认联编方案,原因如下:

  (1)静态联编效率更高。仅当在程序设计时确实需要虚函数时,才使用它们。提示:如果要在派生类中重定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。

  (2)使用虚方法时,在内存和执行速度方面将有一定的成本,包括:

        *每个对象都将增大,增大量为存储地址的空间;

        *对于每个类,编译器都将创建一个虚函数地址表(数组);

        *对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。

  3、有关虚函数的注意事项

  *在基类方法的声明中使用关键字virtual可以使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的;

  *如果使用指向对象的指针或引用来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种类型非常重要,因为这样基类指针或引用可以指向派生类对象。

  *如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。

  对于虚方法,还需要了解下面的知识:

  (1)构造函数

   构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数。

  (2)析构函数

   析构函数应当是虚函数,除非类不用做基类。即使基类不需要显式析构函数提供服务,也不应依赖于默认的析构函数,而应提供虚析构函数,即使它不做任何操作。因此,通常应该给基类提供一个虚析构函数,即使它并不需要析构函数。

  (3)友元

   友元不能是虚函数,因为友元不是类成员,而只有成员函数才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。

  (4)没有重新定义

  如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。

  (5)重新定义将隐藏方法

  假设创建了如下的代码:

  class Dwelling{

  public:

    virtual void showperks(int a)const;

  ....

  };

  class Hovel:public Dewlling{

  public:

    virtual void showperks()const;

  ...

  };

  这将导致问题,可能会出现类似于下面这样的警告:

  Warning :Hovel::showperks(void) hides Dewlling::showperks(ing)

  也可能不出现警告。但不管怎么样,代码将具有如下含义:

  Hovel trump;

  trump.showperks();//允许

  turmp.showperks(5);//不允许

  新定义将showperks()定义为一个不接受任何参数的函数。重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。总之,重新定义继承的方法并不是重载。如果重新定义派生类中的函数,将不只是使用相同的函数参数列表覆盖基类声明,无论参数列表是否相同,该操作将隐藏所有的同名基类方法。

  这里引出了两条经验规则: 

  第一、如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化:

  

class Dwelling{

  public:

    virtual Dewlling& build(int a);

  ....

  };

  class Hovel:public Dewlling{

  public:

    virtual Hovel& build(int a);

  ...

  };

  注意,这种例外只适用于返回值,而不适用于参数。

  第二、如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。

  

五、访问控制:protected

  关键字protected与private类似,在类外只能用公有类成员函数来访问protected部分中的类成员。private与protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部来说,保护成员的行为与私有成员类似;但对于派生类来说,保护成员的行为与公有成员类似。

  警告:最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。

  对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能直接使用的内部函数。

 

六、抽象基类

  抽象基类(abstract base class, ABC)

  C++通过使用纯虚函数来提供未实现的函数。纯虚函数声明的结尾处为=0.

  当类声明中包含纯虚函数时,则不能创建该类的对象。这里的概念是,包含纯虚函数的类只用作基类。要成为真正的ABC,必须至少包含一个纯虚函数。纯虚函数可以有函数定义,也可以没有函数定义。

  ABC理念

  设计ABC之前,首先应开发一个模型——指出编程问题所需的类以及他们之间的相互关系。一种学院派思想认为,如果要设计类继承层次,则只能将那些不会被用作基类的类设计为具体的类。

  可以将ABC看作是一种必须实施的接口。ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则。这种模型在基于组件的编程模式中很常见,在这种情况下,使用ABC使得组件设计人员能够制定“接口约定”,这样确保了从ABC派生的所有组件都至少支持ABC指定的功能。

七、继承和动态内存分配

 

  一般来说,在设计类的时候,我们会根据类是否使用了动态内存分配来考虑是否需要提供显式析构函数、复制构造函数和赋值运算符,对于派生类同样需要考虑这些因素。一般在设计派生类的时候会有一下两种情况:

  1、派生类不使用new

  (1)析构函数

  派生类的默认析构函数总是要执行下面的操作:执行自身的代码后调用基类的析构函数。因此,对于没有使用动态内存分配的派生类来说,默认析构函数是合适的。

  (2)复制构造函数

  默认复制构造函数执行成员复制,成员复制将根据类成员类型采用相应的复制方式;并且在复制类成员和继承的类组件时,则是使用该类的复制构造函数完成的。因此,对于没有使用动态内存分配的派生类来说,默认复制构造函数是合适的。

  (3)赋值运算符

  类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值,因此对于没有使用动态内存分配的派生类来说,默认的赋值运算符是合适的。

  

  2、派生类使用new

  下面的讨论都是基于A是B的基类,并且A和B使用了动态内存分配。

  (1)析构函数

  派生类的析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的进行清理。

  (2)复制构造函数

  派生类B的复制构造函数只能自身的数据,因此它必须调用基类A的复制构造函数来处理共享的基类数据,派生类的复制构造函数的基本形式如下:

    B::B(const B & b):A(b){

    //复制基类自身的数据

    ....

    }

  需要注意的一点是,成员初始化列表将一个派生类B的引用传给基类A的复制构造函数,这里使用了向上强制类型转换(基类引用或指针可以指向派生类对象),这样基类A的复制构造函数将使用派生类B引用对象参数共享的基类数据部分来构造新对象的共享基类数据部分。

  (3)赋值运算符

  派生类的复制运算符应遵循下面的基本格式:

    B & B::operator=(const B & b){

      if (this == & b)

        return *this;

      A::operator=(b);

      //.....

      return *this;

    }

   在派生类的赋值运算符中,必须使用作用域解析运算符显式调用基类的赋值运算符,不然将会导致无限递归。同时,给基类赋值运算符提供参数的时候只需要提供派生类对象引用即可,这里会自动使用向上强制类型转换,这样基类赋值运算符就只会使用派生类共享的基类数据部分来进行赋值操作。

  总之,当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数和赋值运算符都必须使用相应的基类方法来处理基类元素。这种要求是通过三种不同方式来满足的。对于析构函数,这是自动网承德。对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的,如果不这样做,将自动调用基类的默认构造函数。对于赋值运算符,这是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的。

  3、使用动态内存分配和友元的继承示例

   由于友元不是成员函数,所以不能使用作用域解析运算符来指出要使用哪个函数。这个问题的解决方法是使用强制类型转换,以便匹配原型时能够选择正确的函数。在这里,假设类A是类B的基类,operator<<(ostream &, const A &)为基类A的<<重载函数原型,那么派生类B的<<运算符重载函数应使用下面的定义:

    ostream & operator<<(ostream & os, const B & b){

      os << (const A &)b;//必须显式使用向上强制类型转换,这样将会调用基类A的友元<<运算符重载函数;否则将会导致无限递归

      //......

      return os;

    }

 

八、类设计回顾

   1、编译器生成的成员函数

   (1)默认构造函数

  默认构造函数要么没有参数,要么所有的参数都有默认值。如果没有定义任何构造函数,编译器将定义默认构造函数。

  自动生成的默认构造函数的一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。

  另外,如果派生类的构造函数的成员初始化列表中没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有过构造函数,将导致编译阶段错误。

  如果定义了某种构造函数,编译器将不会定义默认构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供。

  提供构造函数的动机之一是确保对象总能被正确地初始化。另外,如果类包含指针成员,则必须初始化这些成员。因此,最好提供一个显式默认构造函数,将所有的类数据成员都初始化为合理的值。

  (2)复制构造函数

  复制构造函数接受其所属类的对象作为参数。

  在下述情况下将使用复制构造函数:

  *将对象初始化为另一个同类对象;

  *按值将对象传递给函数;

  *函数按值返回对象;

  *编译器生成临时对象。

  如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。

  在某些情况下,成员初始化是不合适。例如,使用new初始化的成员指针通常要求深度复制,或者类可能包含需要修改的静态变量。在上述情况下,需要定义自己的复制构造函数。

  (3)赋值运算符

  默认的赋值运算符用于处理同类对象之间的赋值。不要将赋值和初始化混淆了。如果语句创建新的对象,则用初始化;如果语句修改已有对象的值,则是赋值。

  默认赋值为成员赋值。如果成员为类对象,则默认赋值运算符将使用相应类的赋值运算符。如果需要显式定义复制构造函数,则基于相同的原因,也需要显式定义赋值运算符。

  编译器不会生成将一种类型赋给另一种类型的赋值运算符。

  2、其他的类方法

   (1)构造函数

  构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用。这是构造函数不被继承的原因之一。继承意味着派生类对象可以使用基类的方法,然而,构造函数在完成其工作之前,对象并不存在。

  (2)析构函数

  一定要定义显式析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需的任何特殊的清理工作。对于基类,即使它不需要析构函数,也应提供一个虚析构函数

  (3)转换

  使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。

  将可转换的类型传递给以类为参数的函数时,将调用转换构造函数。

  在带一个参数的构造函数原型中使用explicit将禁止进行隐式转换,但仍允许显式转换。

  要将类对象转换为其他类型,应定义转换函数。转换函数可以是没有参数的类成员函数,也可以是返回类型被声明为目标类型的类成员函数。即使没有声明返回类型,函数也应返回所需的转换值。

  但是,对于某些类,包含转换函数将增加代码的二义性。可以将关键字explicitshiyong1于转换函数,这样将禁止隐式转换,但仍允许显式转换。

  (4)按值传递对象与传递引用

  通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。这样做的原因之一是为了提高效率。按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数。调用这些函数需要时间,复制大型对象比传递引用花费的时间多得多。如果函数不修改对象,应将参数声明为const引用。

  按引用传递传递对象的另外一个原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。 

  (5)返回对象和返回引用

  有些类方法返回对象。有些成员函数直接返回对象,而另一些返回引用。有时方法必须返回对象,但如果可以不返回对象,则应返回引用。来具体看一下:

  首先,在编码方面,直接返回对象与返回引用之间唯一的区别在于函数原型和函数头:

  Star noval1(const Star &);//返回Star对象

  Star noval2(const Star &);//返回Star引用

   其次,应返回引用而不是返回对象的原因在于,返回对象涉及生成返回对象的临时副本,这是调用函数的程序可以使用的副本。因此,返回对象的时间成本包括调用复制构造函数来生成副本所需的时间和调用析构函数删除副本所需的时间。返回引用可以节省时间和内存。直接返回对象与按值传递对象相似:它们都生成临时副本。同样,返回引用与按引用传递对象相似:调用和被调用的函数对同一个对象进行操作。

  然而,并不总是可以返回引用。函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种引用是非法的。在这种情况下,应返回对象,以生成一个调用程序可以使用的副本。

  通用的规则是,如果函数返回在函数中创建的临时对象,则不要使用引用。

  如果函数返回的是通过引用或指针传递给他的对象,则应按引用返回对象。

  (6)使用const

  使用const时应特别注意。可以用它来确保方法不修改参数:

    Star:: Star(const char * s){.....}

  使用const可以来确保方法不修改调用它的对象:

    void Star::show()const{....}//这里const表示const Star* this,而this指向调用的对象。

  通常,可以将返回引用的函数放在赋值语句的左侧,这实际上意味着可以将值赋给引用的对象。但可以使用const确保引用或指针的值不能用于修改对象中的数据:

    const Stock & Stock::topval(const Stock & s)const{

      if(s.total_val > total_val)

        return s;

      else

        return *this;

    }

  该方法返回对this或s的引用。因为this和s被声明为const,所以函数不能对它们进行修改,这意味着返回的引用也必须被声明为const。

  注意,如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也确保了参数不会被修改。

  3、公有继承的考虑因素

   通常,在程序中使用继承时,有很多问题需要注意。下面来看其中的一些问题。

  (1)is-a关系

   要遵循is-a关系。如果派生类不是一种特殊的基类,则不要使用公有派生。

  在某些情况下,最好的方法可能是创建包含纯虚函数的抽象数据类,并从它派生出其他的类。

  表示is-a关系的方式之一是,无需进行显式类型转换,基类指针就可以指向派生类对象,基类引用可以引用派生类对象。另外,反过来是行不通的,即不能在不进行显式类型转换的情况下,将派生类指针或引用指向基类对象。这种显式类型转换(向下强制转换)可能有意义,也可能没有,这取决于类声明。

  (2)什么不能被继承

  构造函数是不能被继承的,也就是说,创建派生类时,必须调用派生类的构造函数。然而,派生类构造函数通常使用成员初始化列表语法来调用基类构造函数,以创建派生类对象的基类部分。如果派生类构造函数没有使用成员初始化列表语法显式调用基类构造函数,将使用基类的默认构造函数。在继承链中,每个类都可以使用成员初始化列表将信息传递给相邻的基类。C++11新增了一种能够继承构造函数的机制,但默认仍不继承构造函数。

  析构函数也是不能继承的。然而,在释放对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数。如果基类有默认析构函数,编译器将为派生类生成默认析构函数。通常,对于基类,其析构函数应设置为虚的。  

  赋值运算符是不能被继承的,原因很简单。派生类继承的方法的特征标与基类完全相同,但赋值运算符的特征标随类而异,这是因为它包含一个类型为其所属类的形参。

  (3)赋值运算符

  如果编译器发现程序将一个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值运算符。这个运算符的默认或隐式版本将采用成员赋值,即将原对象的相应成员赋给目标对象的每个成员。然而,如果对象属于派生类,编译器将使用基类赋值运算符来处理派生类对象中基类部分的赋值。如果显式地为基类提供了赋值运算符,将使用该运算符。于此类似,如果成员是另一个类的对象,则对于该成员,将使用其所属类的复制运算符。

  正如多次提到,如果类构造函数使用new来初始化指针,则需要提供一个显式赋值运算符。因为对于派生类对象的基类部分,C++将使用基类的赋值运算符,所以不需要为派生类重新定义赋值运算符,除非它添加了需要特别留意的数据成员。

  然而,如果派生类使用了new,则必须提供显式复制运算符。必须给类的每个成员提供赋值运算符,而不仅仅是新成员。

  另外,将派生类对象赋给基类对象,将调用基类赋值运算符,基类赋值运算符的参数为一个基类引用,它可以指向派生类对象。只是,赋值运算符只处理基类成员,而忽略派生类新增加的成员(如果派生类新增加了成员)。总之,可以将派生类对象赋给基类对象,但这只涉及基类的成员。

  相反,如果把基类对象赋给派生类对象,除非派生类有将基类对象转换为其类型的转换构造函数(可以接受一个类型为基类的参数和其他参数,条件是其他参数有默认值);否则,将会导致错误(派生类引用不能自动引用基类对象)。

   总之,问题“是否可以将基类对象赋给派生类对象?”的答案是“也许”。如果派生类包含了这样的构造函数,即对将基类对象转换为派生类对象进行了定义,则可以将基类对象赋给派生类对象。如果派生类定义了将基类对象赋给派生类对象的赋值运算符,则也可以这样做。如果上述两个条件都不满足,则不能这样做,除非使用显式强制类型转换。

  (4)私有成员与保护成员

  对派生类而言,保护成员类似于公有成员;但对于外部而言,保护成员与私有成员类似。派生类可以直接访问基类的保护成员,但只能通过基类的成员函数来访问基类的私有成员。因此,将基类成员设置为私有成员可以提高安全性,而将他们设置为保护成员则可以简化代码的编写工作,并提高访问速度。

  (5)虚方法

  设计基类时,必须确定是否将类方法声明为虚的。如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的,这样可以启用晚期联编(动态联编);如果不希望重新定义方法,则不必将其声明为虚的,这样虽然无法禁止他人重新定义方法,但是却表达了这样的意思:不希望它被重新定义。

  注意,不适当的代码将阻止动态联编。例如,请看下面的两个函数:

    void show(const Brass &rba){

      rba.ViewAcct();

      cout << endl;

    }

     void inadequate(Brass ba){

      ba.ViewAcct();

      cout << endl;

    }

  第一个函数按引用传递对象,第二个按值传递对象。

  现在假设派生类参数传递给上述两个函数:

    BrassPlus buzz(....);

    show(buzz);

    inadequate(buzz);

  show()函数调用使rba成为BrassPlus对象buzz的引用,因此,rba.ViewAcct()被解释为BrassPlus版本,正如应该的那样。但在inadequate()函数中(它是按值传递参数的),ba是Brass(const Brass &)构造函数创建的一个对象(自动向上强制转换使得构造函数可以引用一个BrassPlus对象)。因此,在indaquate()中,ba.ViewAcct()是Brass版本,所以只有buss的Brass部分被显示。

  (6)析构函数

  正如前面介绍的,基类的析构函数应当是虚的。这样,当通过指向对象的基类指针或引用来删除派生类对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数,而不仅仅是调用基类的析构函数。

  (7)友元函数

  由于友元函数并非类成员,因此不能继承。然而,我们可能希望派生类的友元函数能够使用基类的友元函数。为此,可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数。

  (8)有关使用基类方法的说明

  以公有方式派生的类的对象可以通过多种方式来使用基类的方法。

  *派生类对象自动使用继承而来的基类方法,如果派生类没有重新定义该方法;

  *派生类的构造函数自动调用基类的构造函数;

  *派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数;

  *派生类构造函数显式地调用成员初始化列表中指定的基类构造函数

   *派生类方法可以使用作用域解析运算符来调用公有的和受保护的基类方法;

  *派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换成基类引用或指针,然后使用该引用或指针调用基类的友元函数。

  4、类函数小结

  C++类函数有很多不同的变体,其中有些可以继承,有些不可以。有些运算符函数既可以是成员函数,也可以是友元,而有些运算符函数只能是成员函数。下面的表总结了这些特征,其中op=表示诸如+=、*=等格式的赋值运算符。注意,op=运算符的特征与“其他运算符”类别并没有区别。单独列出op=旨在指出这些运算符与=运算符的行为不同。

函数 能够继承 成员还是友元 默认能否生成 能否为虚函数 是否可以有返回类型
构造函数 成员
析构函数 成员
成员
& 任意
转换函数 成员
() 成员
[] 成员
-> 成员
op= 任意
new 静态成员 void*
delete 静态成员 void
其他运算符 任意
其他成员 成员
友元 友元

 

 

 

 

 

      

 

本文由9159.com发布于编程,转载请注明出处:该引用摘自,  通过类继承可以完成的工作9

关键词:

上一篇:没有了
下一篇:没有了