C++类中的一些细节(重载、重写、覆盖、隐藏,构造函数、析构函数、拷贝构造函数、赋值函数在继承时的一些问题)

1 函数的重载、重写(重定义)、函数覆盖及隐藏

其实函数重载与函数重写、函数覆盖和函数隐藏不是一个层面上的概念。前者是同一个类内,或者同一个函数作用域内,同名不同参数列表的函数之间的关系。而后三者是基类和派生类函数不同情况下的关系。

1.1 函数重载

正如上文说的函数的重载是指类内部,同名不同参数列表函数之间的关系。如下:

void show();
void show(int);
void show(int , double);

以上是多个同名参数列表不同的函数,这种情况就是函数重载。不同的函数返回值不作为判断函数重载的依据,比如如下两个函数会被判定为函数重定义。

void show();
int show();

编译器错误:“错误(活动) E0311 无法重载仅按返回类型区分的函数”。

不过函数重载要注意的是有默认参数和类型的隐式转换造成的问题,这里需要了解一下编译器是如何选择使用那个版本的函数的。其大致过程是

  • 第一步,创建候选函数列表。其中包含与被调用函数的名称相同的函数
  • 第二步,使用候选函数列表创建可行函数列表,这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参数类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配。
  • 第三步,确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错(没有匹配项或者有多个匹配项都出错)

所以再重载函数的时候要注意有默认参数值和隐式转换,如下:

#include <iostream>
void show()
{
  std::cout << "无参数" << std::endl;
}
void show(int n = 1)
{
  std::cout << "有参数" << std::endl;
}
void show(double d)
{
    std::cout << "有参数 double" << std::endl;
}
void show(double & d)
{
    std::cout << "有参数 double 引用" << std::endl;
}
int mian()
{
  show();   //E0308 有多个 重载函数 "show" 实例与参数列表匹配:
  double d = 10;
    show(d);  //E0308   有多个 重载函数 "show" 实例与参数列表匹配
}

对于有默认参数值的情况不必多说,对于隐式转换则要注意在匹配的第三步编译器确认那些是最佳的。它查看为使函数调用参数与可行的时候选函数的参数匹配所需要进行的转换。通常从最佳到最差的顺序如下所述。

  1. 完全匹配
  2. 提升转换(如:char自动转换为int,float自动转换为double)
  3. 标准转换(如:int转换为char,long转换为double)
  4. 用户定义的转换(如:类种定义的转换构造函数)

进行完全匹配时,C++允许一些无关紧要的转换,如下表:
|实参|形参|
|---|---|
|Type | Type & |
|Type & | Type |
|Type[] | Type * |
|Type(argument-list) | Type()(argument-list) |
|Type | const Type |
|Type | volatile Type |
|Type
| const Type * |
|Type * | volatile Type * |

1.2 函数覆盖

这个概念都是描述基类和派生类之间函数关系的。函数覆盖:是基类虚函数在派生类种被重新定义。如:

class Base
{
  virtual void show()
  {
    ...
  }
}

class BasePlus : pulic Base
{
  void show()
  {
    ...
  }
}

这种形式是基类函数有virtual关键字,且派生类与基类函数名相同,参数列表也相同。如果参数列表不相同的话就是函数的隐藏了。而这种类型其实就是派生类函数把基类函数隐藏了。总之就是,在派生类种重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。所以,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。如果基类声明被重载了,则应在派生类种重新定义所有的基类版本。因为如果只定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。如果不需要修改,则可以在派生类定义函数种只显示调用基类方法即可。

1.3 函数隐藏

函数隐藏则分两种情况,一种是基类有virtual关键字,但参数里列表不同。另一种是在基类中无virtual关键字,但是派生类中有同名函数(参数列表相不相同都无所谓)。这时如果派生类对象调用该函数则执行派生类的同名函数,所有基类的同名函数都会被隐藏。此时派生类是不能调用基类的任何同名函数的。如下:

class Base
{
public:
    void show()
    {
        std::cout << "Base is running!" << std::endl;
    }
    void show(int n)
    {
        std::cout << "Base : " << n << std::endl;
    }
};

class BasePlus:public Base
{
public:
    void show()
    {
        std::cout << "BasePlus is running !" << std::endl;
    }
};

int main()
{
    BasePlus ob;
    ob.show();
    // ob.show(1); // 错误    1   error C2660: “BasePlus::show”: 函数不接受 1 个参数
    system("pause");
    return 0;
}

如果基类对象调用该函数则执行基类函数。如果是指向派生类对象的基类引用或指针则调用基类的函数。

1.4 小结

其实了解一下类的动态绑定就没有这么麻烦了,本来就是基类对象执行基类的函数,派生类对象执行派生类函数。如果基类有virtual关键字,则基类中则会有一个指向虚函数表的指针,此时如果派生类定义了同名函数,则虚函数标中的函数指针则定位到了派生类的函数。这就是函数覆盖的现象。至于函数隐藏则更简单了,没有virtual关键字的情况下都是静态联编,自然是基类对象调用基类函数,派生类对象调用派生类函数了。如果有virtual关键字时,但是参数列表不一样,自然会调用派生类的函数了。

2 构造函数、析构函数、拷贝构造函数、重载=在继承的时候的一些问题

2.1 构造函数和拷贝构造函数

构造函数不能是虚函数。创建派生类对象时,会先调用基类的构造函数,然后调用派生类的构造函数。如果基类没有默认构造函数,则基类需要通过参数列表显示地调用基类的构造函数,并传递参数给基类构造函数。

class Base_1
{
public:
    Base_1()
    {
        std::cout << "Base_1 defaut Constructor" << std::endl;
    }
};

class Base_2
{
public:
    Base_2(int n)
    {
        std::cout << "Base_2 self define Constructor:" << n << std::endl;
    }
};

class BasePlus_1 : public Base_1
{
public:
    BasePlus_1()
    {
        std::cout << "BasePlus_1 defaut Constructor" << std::endl;
    }

};

class BasePlus_2 : public Base_2
{
public:
    BasePlus_2():Base_2(2) // 默认构造函数
    {
        std::cout << "BasePlus_2 defaut Constructor" << std::endl;
    }
};

class BasePlus_3 : public Base_1
{
public:
    BasePlus_3(int n)
    {
        std::cout << "BasePlus_3 self define Constructor:" << n << std::endl;
    }
};

class BasePlus_4 : public Base_2
{
public:
    BasePlus_4(int n):Base_2(n)
    {
        std::cout << "BasePlus_4 self define Constructor : " << n << std::endl;
    }
};
int main()
{
    BasePlus_1 ob_1;
    std::cout << "------------------------------------" << std::endl;
    BasePlus_2 ob_2;
    std::cout << "------------------------------------" << std::endl;
    BasePlus_3 ob_3(3);
    std::cout << "------------------------------------" << std::endl;
    BasePlus_4 ob_4(4);

    system("pause");
    return 0;
}

运行结果如下:
这里写图片描述

同样的拷贝构造函数也是如此的。不可是虚拟的,不可被继承。调用机制和构造函数一样。拷贝构造函数通常会在一下情况被调用:

  • 用一个对象来初始化的时候
Base ob_0;
Base ob_1 = ob_0; // 此时会调用拷贝构造函数
  • 用一个对象来构造另一个对象的时候

    Base ob_0;
    Base ob_1(ob_0); // 此时会调用拷贝构造函数
  • 传递对象的时候

     Base Base::fun()
     {
       ....
       return * this; // 此时会调用拷贝构造函数
     }
  • 创建临时对象时

Base ob_0;
Base(ob_0); // 此时会调用拷贝构造函数,该对象为临时对象
2.2 析构函数

析构函数应当是虚函数,除非类不用作基类。如果派生类对象销毁时,会调用派生类析构函数然后调用基类析构函数,这样运行时正确的。但如果基类的引用或指针指向派生类对象的时候,这时候该对象销毁的时候会只调用基类的析构函数,此时如果派生类构造函数里动态分配了内存的话,这部分内存就泄漏了。如果基类的析构函数是虚的,则会先调用派生类析构函数,然后再调用基类的析构函数。

class Base_3
{
public:
    ~Base_3()
    {
        std::cout << "Base_3 destructor" << std::endl;
    }
};

class Base_4
{
public:
    virtual ~Base_4()
    {
        std::cout << "Base_3 destructor" << std::endl;
    }
};

class BasePlus_5 : public Base_3
{
public:
    ~BasePlus_5()
    {
        std::cout << "BasePlus_5 destructor" << std::endl;
    }
};

class BasePlus_6 : public Base_4
{
public:
    ~BasePlus_6()
    {
        std::cout << "BasePlus_6 destructor" << std::endl;
    }
};

int main()
{
    Base_3 * pBase_3= new BasePlus_5;
    Base_4 * pBase_4 = new BasePlus_6;

    delete pBase_3;
    std::cout << "------------------------------------" << std::endl;
    delete pBase_4;

    system("pause");
    return 0;
}

运行结果如下:
这里写图片描述

通常应给基类提供一个虚析构函数,即使它并不需要析构函数

2.2 重载操作符=函数(赋值函数)

=操作符号与拷贝构造函数的调用情况是相同的,如果派生类没有重载该操作符的时候。会调用基类的操作符函数(无论是重载的还是默认的),如果派生类定义了则会调用派生类重载的函数。所以为了保持基类部分数据的正确赋值,必须在派生类的重载函数中显示调用基类的重载函数。如:

BasePlus & BasePlus::operator= (const BasePlus & ob)
{
  Base::operator= (ob); // 显示调用基类构造函数
  ...
}

这里唯一不同的是,operator=(),函数可以是虚函数。那再什么情况下我们需要operator=是虚函数呢?只有在两个指向派生类对象的基类引用赋值的时候需要赋值派生类部分数据的时候我们希望通过多态调用派生类的赋值函数。但是很不幸目前这种操作实现不了,如下:

virtual Base & operator= (const Base & ob);

BasePlus & operator= (const BasePlus & ob);

虽然基类赋值函数是虚函数,但是派生类函数并不是该函数的重新定义。也许是编译器内部并没有把virtual Base & operator= (const Base & ob)映射到虚函数表的原因,总之两个指向派生类对象的基类引用赋值的时候调用的是基类的构造函数。如下:

// 基类
class Base
{
public:
    virtual Base & operator= (const Base & ob)
  {
    std::cout << "BasePlus operator = is running" << std::endl;
    return * this;
  }
};
// 派生类
class BasePlus : public Base
{
    BasePlus & operator= (const BasePlus & ob)
  {
    Base::operator= (ob);// 显示调用基类函数
      std::cout << "BasePlus operator = is running" << std::endl;
      return * this;
  }
};

int main()
{
    BasePlus objBasePlus_0;
    BasePlus objBasePlus_1;

    Base & obj_0 = objBasePlus_0;
    Base & obj_1 = objBasePlus_1;

    obj_0 = obj_1;

  system("pause");
    return 0;
}

输出结果如下:
这里写图片描述

猜你喜欢

转载自www.cnblogs.com/konglongdanfo/p/9135250.html
今日推荐