C++多态的完结版
1.虚函数重写的特例及预防
1.虚函数重写的特例
大家都知道,要实现多态,就必须有虚函数的加入,但是虚函数的重写必须遵循三个规则:函数名相同,参数相同,返回值相同。而下面这两个特例却违反了规则,但又可以实现虚函数的重写,情况如下:
①:协变(基类与虚函数返回值类型不同)即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时,称为协变。
情况是这样的,先上代码:
class A
{
public:
virtual A* func()
{
return new A;
}
};
class B : public A
{
public:
virtual B* func()
{
return new B;
}
};
如上,该函数运行的时候不会出错
②:析构函数的重写(基类和派生类虚构函数的名称不同)
情况是这样的,如果基类虚写了析构函数,此时,只要派生类自己定义了虚构函数,不管是否添加virtual,即使名字不相同,都会与虚函数的基函数构成重写。虽然名字不一样,看起来好像违背了规则,但是在底层,编译器对析构函数的名称做了特殊的处理,编译后虚构函数的名称统一为destructor。
请看如下代码:
class A
{
public:
A()
{
cout << "A::A()" << endl;
}
~A()
{
cout << "~A::A()" << endl;
}
};
class B : public A
{
public:
B()
{
cout << "B::B()" << endl;
}
~B()
{
cout << "~B::B()" << endl;
}
};
void main()
{
A* a = new B;
delete a;
}
这个代码在运行的时候,由于没有虚写基类的析构函数,如果此时在主函数中直接delete对象a,那么会发生这样的情况:
没有对B进行析构,原因是什么呢,由于B的对象是动态申请的,动态申请的对象需要delete去完成释放,但是由于我们定义的是基类的指针去接收,那么用基类的delete去释放时,只能调用基类的析构函数,而基类无法调用派生类的析构函数,所以造成了空间内存泄漏,所以这也是一个非常重要的一点,在动态内存申请的时候,我们必须虚写基类析构函数。
大家可以去尝试将上述代码的基类虚构函数进行虚写,再看看运行结果是不是动态申请的对象是否被释放了。
2.两个预防关键词,可以防止我们在重写基类虚函数的时候出现失误:C++11的finial和override
①:finial(修饰虚函数,表示该虚函数不能再被继承)
这个函数主要是中止对一个虚函数的继承,用法如下:
class A
{
public:
virtual void func()final
{}
};
class B : public A
{
public:
virtual void func()
{}
};
这段程序是无法运行的,因为基类虚函数后面加了final,所以在派生类中使用时会出现错误,情况如下图:
②:override(检查派生类虚函数是否重写了基类的某个虚函数,如果没有,那就报错)
主要是帮助我们在写代码的期间就让我们知道我们出现了错误,防止程序写完后去找出现麻烦的步骤。
用法如下:
class A
{
public:
virtual void func()
{}
};
class B : public A
{
public:
virtual void func()override
{}
};
注意override是添加在派生类的虚函数后面,检查这个函数是否重写了基类的虚函数。
2.抽象类
1.抽象类的概念:在虚函数后面加上=0,则这个函数为纯虚函数,包含纯虚函数的类叫抽象类,也叫接口类,不能实例化对象。派生类继承了也无法实例化对象,只有重写虚函数,才能实例化对象。纯虚函数的规范了的派生类必须重写,纯虚函数更能体现接口继承。
2.使用方法如下:
class A
{
public:
virtual void func() = 0
{}
};
class B : public A
{
public:
virtual void func()
{
cout << "B :: func()" << endl;
}
};
class C : public A
{
public:
virtual void func()
{
cout << "C :: func()" << endl;
}
};
int main()
{
A* a = new B;
a->func();
A* a1 = new C;
a1->func();
return 0;
}
更体现了一个接口的继承,就如同A只提供了接口,什么都不用干了,用的时候只需要接口就好了。
3.实现继承和接口继承
①:实现继承:普通函数的继承就是实现继承,派生类继承了基类的函数们可以使用函数,继承是函数的实现。
②:接口继承:虚函数继承是一种接口继承,目的是为了重写,达成多态。
3.单继承和多继承的虚函数表
1.单继承中的虚函数表
上代码:
class A
{
public:
virtual void func1()
{
cout << "A :: func1()" << endl;
}
virtual void func2()
{
cout << "A :: func2()" << endl;
}
};
class B : public A
{
public:
virtual void func1()
{
cout << "B :: func1()" << endl;
}
virtual void func3()
{
cout << "B :: func3()" << endl;
}
virtual void func4()
{
cout << "B :: func4()" << endl;
}
};
int main()
{
A* a = new B;
return 0;
}
监视中的虚函数表可以看出,基类的第一个func1函数已经被替换了,但是我们发现,派生类定义了的虚函数为什么不见了,其实是存在的,并且是在已存在的虚函数表后面按顺序放的,只是编译器故意把他们隐藏了,怎样才能看到呢,通过如下代码:
其中我们定义了函数指针:
typedef void(*func_t)();
由于在该处理器下,虚表的位置位于实例化对象的最开始的位置,所以&b的就是指向的虚表地址,对其取虚表地址所以先强转成地址在对其取地址,就是虚表的地址,然后对其取里面的内容再进行强转和解引用即可,这样我们就可以发现派生类自己的虚函数是在虚函数表的后面根据声明的顺序依次排列的。
2.多继承中的虚函数表
看下列代码:
class A
{
public:
virtual void func1()
{
cout << "A :: func1()" << endl;
}
};
class B
{
public:
virtual void func2()
{
cout << "B :: func2()" << endl;
}
};
class C : public A, public B
{
public:
virtual void func1()
{
cout << "C :: func1()" << endl;
}
virtual void func2()
{
cout << "C :: func2()" << endl;
}
virtual void func3()
{
cout << "C::func3()" << endl;
}
};
int main()
{
C c;
return 0;
}
通过调试我们能看到,两个基类分别有自己的虚表,但是派生类自己的虚函数到底放在哪个虚表里呢,还是通过函数指针的方法去看:
通过观察,可以清楚看到,派生类自己的虚函数是在继承的第一个基类的虚函数表后面。