1.同名函数的关系:
- 重载
- 隐藏
- 覆盖
重载:
重载是指在同一作用域下的同名函数具有不同的参数列表。
- 在同一访问区域内声明的几个具有不同参数列表(参数的类型、个数、顺序不同)的同名函数,程序会根据不同的参数列表来确定具体调用哪个函数。
- 对于重载函数的调用,编译期间确定,属于静态联编,它们的地址在编译期间就绑定了
- 重载不关心函数的返回值类型
函数重载的特征:
- 相同的作用域
- 函数名相同
- 参数列表不同
- virtual关键字可有可无
代码:
class Base
{
public:
//函数的重载
void function(int res) {}
void function(double res) {}
void function() {}
private:
int ma;
};
隐藏(作用域的隐藏):
隐藏是指派生类的函数屏蔽了与其同名的基类函数。
- 如果派生类的函数与基类的函数同名,但参数不同,则无论有无virtual关键字,基类的函数都被隐藏。
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字,此时基类的函数被隐藏。
隐藏的特征:
- 必须分别位于基类和派生类中
- 必须同名
- 参数不同的时候本身已经不构成覆盖关系了,所以此时有无virtual关键字不重要
- 参数相同时就要看是否有virtual关键字,有就是覆盖关系,无就是隐藏关系
代码:
class Base
{
public:
//基类方法
void function(int res){}
};
class Derive : public Base
{
public:
//派生类的同名方法隐藏基类方法
void function(int res) {}
};
覆盖(虚函数表中的覆盖):
覆盖是指派生类中存在重新定义基类的函数,其函数名、参数列表、返回值类型必须同基类中相应被覆盖的函数严格一致,覆盖函数和被覆盖函数只有函数体不同。
- 当基类指针指向派生类对象,调用该同名函数时会自动调用派生类类中的覆盖版本,而不是基类中的被覆盖函数版本。
覆盖的特征
- 不同的范围(分别位于派生类和基类)
- 函数名相同
- 参数列表相同
- 返回值类型相同
- 基类函数必须有virtual关键字
代码:
class Base
{
public:
//基类方法
virtual void function(int res)
{
cout << "Base::function" << endl;
}
};
class Derive : public Base
{
public:
//派生类此方法将覆盖基类方法
void function(int res)
{
cout << "Derive::function" << endl;
}
};
int main()
{
Derive derive;
Base* p = &derive;
p->function(5);
}
重载和覆盖的关系:
覆盖是派生类和基类之间(不同作用域)的关系,是垂直关系;重载是在同一个作用域方法之间的关系,是水平关系。
覆盖只能由一对方法产生关系;重载是两个或多个方法之间的关系。
覆盖要求参数列表相同;重载要求参数列表不同。
覆盖关系中,调用方法是根据对象的类型来决定的,重载关系是根据函数的不同形参列表来选择方法体的。
2.派生类的构造和析构函数:
派生类的构造函数:
我们来看一个代码:
class Base
{
public:
Base(int a):ma(a){}
{
cout<<"Base::Base()"<<endl;
}
~Base()
{
cout<<"Base::~Base()"<<endl;
}
protected:
int ma;
};
//派生类
class Derive : public Base
{
public:
Derive(int b):mb(b);
{
cout<<"Derive::Derive()"<<endl;
}
~Derive()
{
cout<<"Derive::~Derive()"<<endl;
}
private:
int mb;
};
int main()
{
//生成一个派生类对象
Derive d(20);
}
我们分析下:系统在开始的时候会调用的基类(Base)的构造函数,来进行初始化;接下来对派生类进行初始化,但派生类中没有默认的构造函数,系统将无法对派生类对象基类部分初始化。
解决方式:
Derive(int b):mb(b);//要指明基类的构造方式
Derive(int b):mb(b),Base(b);
派生类不能继承基类的构造函数,必须自己定义构造函数进行新增数据成员初始化工作,如果想同时初始化基类数据成员,必须调用基类构造函数。
创建派生类对象时,程序首先创建基类对象,从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作。
代码:
class Base
{
public:
Base(int data):ma(data){}
private:
int ma;
};
class Derive : public Base
{
public:
Derive(int data) : Base(data),mb(data){}
private:
int mb;
};
如果省略成员初始化列表,那么程序还是必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数,除非要使用默认构造函数,否则应该显式调用正确的基类构造函数。
有关派生类构造函数的要点如下:
- 首先创建基类对象
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
- 派生类构造函数应初始化派生类新增的数据成员
- 创建对象时,先执行基类构造函数,然后自动调用派生类的构造函数
构造函数作用:
基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数。可以使用初始化列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。
派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
派生类的析构函数:
释放对象的顺序即首先执行派生类的析构函数,然后自动调用基类的析构函数。
class Base
{
public:
Base(int a):ma(a)
{
cout<<"Base()"<<endl;
}
~Base()
{
cout<<"~Base()"<<endl;
}
private:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 0) : Base(data),mb(data)
{}
~Derive()
{
cout<<"~Derive()"<<endl;
}
private:
int mb;
};
int main()
{
Base* p =new Derive();
delete p ;
return 0;
}
delete语句将调用基类Base的析构函数。释放的是派生类Derive对象中的Base部分指向的内存,不会释放新的类成员(派生类新定义的成员)指向的内存。
但如果析构函数是虚的,则上述代码将先调用派生类Derive的析构函数释放由Derive组件指向的内存,然后,调用基类Base的的析构函数来释放由Base组件指向的内存。
这意味着,即使基类不需要显式析构函数提供服务,也不应该依赖默认析构函数,而应该提供一个虚析构函数,即使它不执行任何操作。
这样做是为了确保释放派生类对象时,按正确的顺序调用析构函数。
注意:如果要在派生类对象中重新定义基类的方法,通常应该把基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。
3.派生类的相互指向:
总结规则如下:
- 基类的指针或者引用可以指向或者引用派生类对象
- 派生类的指针或者引用不可以指向或者引用基类对象
基类的指针或者引用可以指向或者引用派生类对象
Base base(10);
Derive derive(20);
Derive* pd = &base;//不可以
Base* pb = &derive;
派生类的指针或者引用不可以指向或者引用基类对象
Base base(10);
Derive derive(20);
Base& rb = derive;
Derive& rd = base;//不可以