C++ 的基础概念(3)——多态详解。
最近两次面试都问到了多态,我也不得不重视起来了,最近最大的收获就是:基础知识很重要,就算你很会写代码,但是面试官问你基础知识答不上来的话,也很难被人赏识和录用,所以还是要多补补基础概念,这一篇就说多态。
之前第一篇提到过,多态是指同样的消息被不同的对象接受时导致不同的行为。分四类:重载多态,强制多态,包含多态和参数多态。
多态从实现的角度分为 编译时多态 运行时多态。不同处就是确定操作针对的具体对象的时间是编译的时候还是运行的时候。
重载多态:
我们知道的普通函数及类的成员函数的重载都属于重载多态。
函数的重载因为很常用相信一般都很熟悉了,就是指函数名相同而形参的个数和类型不同,编译器调用的时候根据参数类型个数来判断调用哪一个函数。
运算符的重载也是重载多态,比如我们定义了一个类Counter,希望这个类内部可以实现'+'的运算,比如Counter a,b,c; 初始化后可以有c=a+b;或类似的运算,那么我们需要对‘+’进行重载,给它以运算规则,来保证这句话能通过编译。
(有5个运算符不可以被重载,分别是: . .* :: sizeof ?:)
运算符重载分两种:作为成员函数重载 作为友元函数重载。
语法形式为:
作为成员函数重载运算符: | 作为友元函数重载运算符: |
函数类型 operator 运算符(形参表){ ............; } |
friend 函数类型 operator 运算符(形参表){ ............; } |
之前也说过,友元函数其实是定义在函数外的,所以跟函数成员的区别就是友元函数需要两个形参作为运算符前后的值。
比如我要重载上面的Counter 类型的+ - 运算:
Counter operator +(Counter c2){
return Counter(this.number+c2.number);
}
//或者友元重载减号:
friend Counter operator -(Counter c1, Counter c2){
return Counter(c1.number-c2.number);
}
//调用时:
Counter a(5),b(3),c,d;
c=a+b;
d=a-b;
//都是可以通过的。
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
除了这种加减等常用的外,还有比如-- ++这种前置或后置运算符,比如我们要定义一个counter++; 那么运算符重载为Counter的成员函数,同时函数要带有一个整数形参(int),它的作用就是区别说明它是后置的++ --。如果是前置,则没有int形参。如下:
//前置++的重载
Counter Counter::operator ++(){
.........;
}
//后置++的重载
Counter Counter::operator ++(int){
...........;
}
//这样,当出现++符号时,系统就可以根据位置知道调用哪个函数了:
Counter a(5);
a++; // 相当于调用了 operator ++(0);
++a; // 相当于调用了 operator ++();
强制多态:
就是指讲一个变元的类型加以变化,符合一个函数或者操作的具体要求。
比如加法符号,在进行整型和浮点型的运算时,会先强制将整型转为浮点型,然后再进行运算。
包含多态:
包含多态是研究类族中定义于不同类中的同名函数成员的多态行为,主要通过虚函数来实现.
虚函数必须是非静态成员,经过多次派生之后,族类中可以实现运行过程中的多态。
一般虚函数成员声明语法: |
virtual 函数类型 函数名(形参表){ .......................; } |
虚函数的声明只能在类定义中的函数原型声明时就写清楚,而不能在写函数体时才声明。运行过程中要满足三个规则:
1. 类之间要满足类型兼容规则。
2. 声明时虚函数。
3*. 要由成员函数来调用,或者是通过指针,引用来访问虚函数。
理论了解了之后,我们写个小例子就懂了。
以下是完整代码:
#include <iostream>
using namespace std;
class Father{
public:
virtual void outputTest(){
cout<<"Father"<<endl;
}
};
class Son1:public Father{
public:
void outputTest(){
cout<<"Son1"<<endl;
}
};
class Son2:public Son1{
public:
void outputTest(){
cout<<"Son2"<<endl;
}
};
// 调用时
int main(){
Father f,*p;
Son1 s1;
Son2 s2;
p=&f;
p->outputTest();
p=&s1;
p->outputTest();
p=&s2;
p->outputTest();
}
话不多说,我们截图为证:
可见,通过使用virtual这个关键词,我们实现了outputTest这个函数的包含多态。这三个类中,outputTest都是虚函数。
添一句话:如果子类中的一个函数fun,跟父类中的一个虚函数fun,函数名相同,参数表完全一致,返回值也相同,那么这函数就自动被判定为了虚函数。
虚析构函数:它的存在时为了防止某些情况下的空间泄露。所以虚构造函数是不允许存在的。同时,如果一个父类的析构函数是虚函数,那么子类的析构也同样是虚函数。
下面就说说空间泄露的情况:假如我定义了一个父类Father,里面没有数据成员,然后我定义了一个子类Son公有继承父类,并且有一个私有数据成员*int t; Son的构造函数里有 t=new int(0);,析构函数里会delete t;,但是父类自然没有这一句。这样我们在定义对象的时候如果有这样的情况: Father *f=new Son(); delete *f; 那么,啊~非常不意外的,可怜的t就被这样无情的丢在了角落,不使用也不释放。这样如果在一个较大成程序里发生的话,很可能会出现内存不足的情况。
解决的办法,就是在Father的析构函数前面加virtual,具体形式为: virtual ~Father(); 因为很简单,就再给一次代码好了,三分钟搞定:
#include <iostream>
using namespace std;
class Father{
public:
Father(){};
virtual ~Father(){cout<<"Father delete;"<<endl;};
};
class Son:public Father{
public:
Son(){t=new int(0);};
~Son(){delete t; cout<<"Son delete;"<<endl;};
private:
int *t;
};
// 调用时
int main(){
Father *f=new Son();
delete f;
}
运行结果:
可见,子类的析构函数被调用了,这样就避免了内存泄露带来的危害。所以虚析构还是很有用的~!
除了上述两种虚函数之外,我们还应该知道一种类 叫做:抽象类。
抽象类处于类的上一层,它本身无法被实例化,只能通过继承机制,由抽象类生成非抽象的派生类,再对之实例化。
注意:带有纯虚函数的类就是抽象类。但是什么是纯虚函数呢?
纯虚函数就是在一个基类中声明的虚函数,它在这个基类里面没有具体要操作的内容,它存在的意义就是为了让子类根据自己的需求对这个函数实现不同的接口。
纯虚类的定义语法是: virtual 函数返回类型 函数名(参数表)=0; 其实就是有了一个‘=0’。这样就不需要给它定义函数体了,所以纯虚函数是没有函数体的。
参数多态:
参数多态,就是将程序所处理对象的类型参数化,使得一段程序可以用于处理多种不同类型的对象。
参数的多态,可以通过函数模板或类模板实现。
所谓函数模板,我们可以想一个例子:我们通常使用math.h里的很多函数比如绝对值函数abs()的时候,参数可以有int, double等不用类型的参数,那么如果为了每一种类型建立一个重载函数的话,就太麻烦了,因为函数体本身是相同的,所以我们就用一个模板来代替之,如下:
template <typename T>
T abs(T x){
return x<0?-x:x;
}
这样,我们在调用的时候,如果给的参数是int,那么编译器就会把typename里的T变为int,这个函数就是个返回int,参数为int的函数了,double等类型也一样。
函数模板的语法定义: |
template <typename T> 或 template <class T> //表示T是一个类型名或者类名,可以更换为int 、 double、 某个类 等等 返回类型 函数名(参数表){ .........; }(注意返回类型不一定为T的~。) |
类模板:使用类模板可以为类声明一种模式,是的类中某些数据成员、某些成员函数的参数、某些成员函数的返回值能取任意类型。
类模板的语法定义: | 在类的外部定义成员函数的函数体时: |
template <模板参数表> class 类名{ ...........; } |
template <模板参数表> 返回类型 类名<模板参数表成员>::函数名(参数表){ ........; } |
同样,我们用一个小例子,一秒搞懂类模板:
#include <iostream>
using namespace std;
template<class Input, class Output>
class Test{
public:
Test(Input in){input=in;}
Output fun();
private:
Input input;
Output output;
};
// 类之外定义函数时的格式:
template<class Input, class Output>
Output Test<Input,Output>::fun(){
return input>0?input:-input;
}
int main(){
Test<int,int> test(-30);
cout<<test.fun()<<endl;
}
查看输出:
成功~! 呵呵 虽然没有含金量,但是该用到的格式这个小程序都用到了,不错吧~