目录
1.多态
多态属于动态函数重载,它是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。
实现多态的过程中,不是直接让子类指针(引用)指向子类对象,而要让父类指针(引用)指向子类对象,这样做有以下好处:
程序的逻辑结构清晰,便于维护、调式
子类型一般是具体类型,个数有可能是不可预知的,有可能会变化的;
父类型一般是抽象类型,只要抽象的好,发生变化的可能性比较小;
你针对子类写的一个函数就只能用于操作这个类的对象
你针对父类写的一个函数可以操作所有从这个父类继承的子类的对象
这是对 一个东西进行操作 和 对一类东西进行操作 的区别;
多态的精髓在于:对外提供一个通用的抽象接口,运行的时候能够根据具体的内容调用相应的函数处理。而父类要能够调用子类的函数,是通过虚函数机制实现的,父类指针通过使用其指向的子类的虚函数表调用相应的函数,前提该函数在父类中被声明为虚函数。
2.虚函数表和虚函数表指针
虚函数表本质上是一个函数指针数组,指向这个表的指针称为虚函数表指针(每个对象的隐藏指针)
使用多态时,父类指针会对子类的虚函数表查找;
父类的虚函数按声明顺序存放在一个虚函数表中;到了子类,如果重定义了父类的虚函数,虚函数表中的地址会指向新的地址,如果子类增加了新的虚函数,则地址会被添加到子类的虚函数表中;
3.构造函数可以为虚函数吗?
不能,因为虚函数的执行依赖于虚函数表,虚函数表有点像类的static成员,是所有对象共有,需要通过虚函数表指针找到虚函数表的地址。而虚函数表指针在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表指针还没有被初始化,将无法进行。
4.虚析构函数
与构造函数不同,析构函数可以为虚函数,而且最好把基类的析构函数设置为虚析构函数。在多态继承中,因为如果父类析构函数不是虚函数,当父类指针指向子类对象时,只会调用父类的析构函数,子类的析构i函数不会被调用。如果子类的构造函数中有动态内存分配,则这些空间不会被释放,导致内存泄漏。如果基类有虚析构函数,那么会先调用子类的析构函数,再调用父类的析构函数。
注意析构函数一般是用来释放构造函数动态分配的空间(堆空间)。如果子类中定义了新的成员变量,但没有使用动态内存分配,而是c++的内置类型,那么基类析构函数不是虚函数是不会导致内存泄漏的。栈的空间系统是自动回收的。
如果在父类的虚析构函数中调用了虚函数,子类重写了这个虚函数,那在析构时,父类的析构函数中执行的虚函数是父类的还是子类的?为什么?
在父类虚析构函数中调用虚函数是没有意义的,因此,在调用父类虚析构函数之前,必然已经调用了子类的析构函数,子类的部分已经被虚构调了,此时无法访问子类重写的函数,因此这个虚函数调用的是父类的。一般来说,不要在构造函数和析构函数中调用虚函数。
5.派生类中复制构造函数,赋值运算符函数
类的成员变量包含指针,必须对复制构造函数、赋值运算符重载函数进行显示重写,将指针的拷贝改为深拷贝(默认的函数中是浅拷贝,直接赋值,对于指针而言,很容易出错),拷贝的内容不是地址,而是指针所指向的数据。
假设基类Cd,子类Classic定义如下(来源于C++primer 6,13.2):
class Cd
{
private:
char* preformers;
char* label;
int selections;
double playtime;
public:
Cd(const char* s1,const char* s2,int n,double x);
Cd(const Cd& d);
Cd();
virtual void report() const;
virtual Cd& operator=(const Cd& d);
};
class Classic :public Cd
{
char* article;
public:
Classic();
Classic(const char* s1, const char* s2,const char* s3, int n, double x);
Classic(const Classic& d);
void report() const;
Classic& operator=(const Classic& d);
};
对于一般的构造函数,实现比较常规,子类构造函数参数中包含父类构造函数的参数,并通过:调用父类构造函数并传递参数,如下所示:
Cd::Cd(const char* s1,const char* s2, int n, double x)
{
preformers = new char[strlen(s1)+1];
strcpy_s(preformers, strlen(s1) + 1,s1);//C 风格字符串处理
label = new char[strlen(s2) + 1];
strcpy_s(label, strlen(s2) + 1, s2);
selections = n;
playtime = x;
}
Classic::Classic(const char* s1, const char* s2, const char* s3, int n, double x)
:Cd(s1, s2, n, x)
{
article = new char[strlen(s3) + 1];
strcpy_s(article, strlen(s3) + 1,s3);
}
但是对于复制构造函数,其参数只有一个对象,子类调用复制构造函数时,先调用父类的复制构造函数,并将子类函数参数传递给父类。这里,传到父类复制构造函数的参数是一个子类对象的引用,父类构造函数使用子类的引用将父类的成员变量初始化。子类对象中一定包含了父类的信息,即使有的信息在子类无法访问。如果不这样做,会调用基类的默认构造函数。如下所示:
Cd::Cd(const Cd& d)
{
preformers = new char[strlen(d.preformers) + 1];
strcpy_s(preformers, strlen(d.preformers) + 1,d.preformers);//C 风格字符串处理
label = new char[strlen(d.label) + 1];
strcpy_s(label, strlen(d.label) + 1, d.label);
selections = d.selections;
playtime = d.playtime;
}
Classic::Classic(const Classic& d)
:Cd(d)//子类复制构造函数
{
article = new char[strlen(d.article) + 1];
strcpy_s(article, strlen(d.article) + 1,d.article);
}
对于赋值运算符重载,当子类调用赋值运算符重载函数时,不仅需要对子类的特有成员重新赋值,还需要对父类的成员重新赋值,解决的方法是在子类重载函数中使用父类名+类作用域限定符::调用父类的赋值运算符重载函数对父类成员进行重新赋值(子类成员函数访问父类成员函数和变量的方法父类名+作用域限定符)。如下所示:
Cd& Cd::operator=(const Cd& d)
{
if (&d == this)
return *this;
delete this->preformers;
delete this->label;
this->preformers = new char[strlen(d.preformers) + 1];
this->label = new char[strlen(d.label) + 1];
strcpy_s(this->preformers, strlen(d.preformers) + 1,d.preformers);
strcpy_s(this->label, strlen(d.preformers) + 1, d.label);
this->selections = d.selections;
this->playtime = d.playtime;
return *this;
}
Classic& Classic::operator=(const Classic& d)
{
if (&d == this)
return *this;
Cd::operator=(d);//子类赋值运算符重载
delete this->article;
this->article = new char[strlen(d.article) + 1];
strcpy_s(this->article, strlen(d.article) + 1,d.article);
return *this;
}