在面试中,面向对象语言中经常会提到代码重用和接口重用的概念,有些同学会很疑惑,其实仔细翻阅下书本就不难发现,代码重用就是继承父类的方法,达到方法(代码)重用的目的,而接口重用就是在多态中能够通过父类指针来调用子类的方法,当然这个需要用到虚函数和类型转化的支持,达到一个指针能够达到重复使用的目的(即接口重用)
1.代码重用(继承)
先来讲讲继承,基础的就不讲了,说下c++特有的多重继承,在说多重继承之前,先谈谈覆盖和重写
覆盖,举个例子
class A
{
public:
int a;
void display();
};
class B
{
public:
int a;
void display();
};
<pre name="code" class="cpp">class C:public A , public B
{
public:
int a;
int b;
void show();
};
对于成员变量如果通过类C的对象来引用成员a,如
C c1;
cout<<c1.a<<endl;
那么class A和class B中的成员a会被覆盖,变为不可见,但不是不可访问,也是可以访问的
可以这样访问:
cout<<c1.A::a<<endl;
而对于函数是同样的道理,如果函数名和参数个数相同、类型相匹配,也可以覆盖基类中的成员函数,也就是我们习惯说的函数重写,这个是多态的重要实现的方法
对于继承的二义性:
将class C改写如下:
class C:public A , public B
{
public:
//int a;
int b;
void show();
};
如果现在通过类C的对象来引用继承下来的成员a,那么会出现二义性问题
c1.a这种引用到底是引用A中的a,还是B中的a呢?
所以解决的办法只有用作用域限定符来限定出处,如:
c1.A::a//引用类A中的a
c1.B::b//引用类B中的a,
当然函数方法,也用同样的二义性问题,不再赘述
这里有一个问题,无论是A中的a,还是B中的a,一般情况下在具体的类中作用是类似的,所以到子类中会出现两个同名成员,而且作用一样的情况,即浪费空间,有容易产生二义性,导致代码可阅读性和维护性变差,那么我们可以运用虚基类来过滤掉冗余成员和函数方法
不多说,举个例子:
class A
{
public:
int a;
void display();
};
class B : virtual public A//A是B的虚基类
{
public:
int b;
}
class C: virtual public A//A是C的虚基类
{
public:
int c;
}
class D : public B,public C
{
public:
int d;
}
此时类D中的成员实际是这样的
class D
{
public:
int a;
int b;
int c;
int d;
void display();
};
最后来说说代码重用的问题
举个例子,例如我们在一个类A中封装了一个比较好的实现显示验证码和验证验证码的两个方法函数,当我们需要用在其他模块中运用的时候,可以通过模块中的类来继承类A中的方法,从而我们能够使用这个方法,而不需要重新粘贴复制这个方法,当然你会说c里边用extern 申明再包含个头文件一样可以实现呀,是的,的确可以实现代码的复用,但是对于面向对象语言来说,继承才是实现代码重用的途径,因为面向对象语言必须要保证它的可封装性,所以才需要用继承来达到代码重用的目的,只是方法不同而已,没有对与错,或者是好与坏的区别,只是不同的语言实现不同而已,不过就效率而言,c的实现肯定是要高得多的,因为c语言中代码复用的方法,是用到那个函数就复用那个函数,而c++可能要继承一些用不上的函数方法,但是面向对象继承的复用的这种方法能够很好的为程序员服务,因为这种方法更加合乎人类的思维模式,即逐层细化,逐层分工,逐层实现的思维模式,而这是c做不到的,如果想深究这个问题,就请好好理解面向对象语言的优点。
对于代码重用,再大一点就是软件重用,都是建立在继承基础之上的,但是如何设计类,来达到重用的高效率,这需要长期的开发经验的积累和实践的,推荐看看贝尔大牛写的一本书《c++代码设计与重用》(我是没看过,虽然老了点,但是豆瓣评价还可以)
2.接口重用(多态)
说到接口重用,先说说多态,什么是多态,很简单,就是孩子从父辈那里继承的DNA,到了孩子这一代出现了不同的分支方向,有的可能喜欢文学,有的可能喜欢数理化,这个是多态的一个比较感性的描述
至于多态的代码实例我就不在这里列举了,下面会有更深入的例子,其实c++实现多态的方法,就是函数方法的重写(在这里区别下函数的重载)和基类与派生类的类型转换
基类与派生类的转换:
1)派生类对象可以向基类对象赋值。
2)派生类对象可以代替基类对象向基类对象的引用进行赋值或者初始化。
3)如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。
4)派生类对象的地址可以赋给基类对象的指针变量。//很重要
上述转换的核心思想是,基类对象永远只能访问父类的成员,哪怕是派生类赋值还是引用,都限定在基类中
派生实例:
class point
{
public:
point{float,float};
void setpoint(float,float);
float getX() const{return x;}
float getY() const{return y;}
void display();
protected:
float x,y;
};
point::point(float a,float b):x(a),y(b){}
void point::setpoint(float a,float b)
{
x=a;
y=b;
}
void point::display()
{
std::cout<<"x="<<x<<";"<<"y="<<y<<endl;
}
/
class circle:public point
{
public:
circle(float,float,float);
//中间的功能函数无关紧要,所以没有写出来
void display();
private:
float radius;
}
circle::circle(float a,float b,float c):point(a,b),radius(c){}
void circle::display()
{
std::cout<<"x="<<x<<";"<<"y="<<y<<";"<<"radius="<<radius<<endl;
}
可以看出派生类对方法display进行了重写,即多态性表现了出来
如果这样调用
point pt(1,1);
circle cl(1,1,2);
point *p=&pt;
p->display();
p=&cl;
p->display();
则输出结果是一致的都是
x=1;y=1
怎么样才能实现接口重用呢,即基类指针调用派生类的方法,这时候我们需要用到虚函数(区别下虚基类)
我们只需要将point类中display函数方法前面加一个关键字virtual就可以了,即:
virtual void display();
同样的是上面的调用过程
结果如下:
x=1;y=1
x=1;y=1;radius=2
最后说下虚析构函数和纯虚函数与抽象类
1>虚析构函数,是在接口重用时,如果派生类对象是动态建立的(new出来的),则需要用到
如果析构函数不是虚函数,在释放动态内存时,只会释放指向基类部分的成员,而不会释放派生类的派生出的成员,所以一般将基类的析构函数申明为虚函数
2>纯虚函数
例子:
virtual float area()const{return 0;}
或者
virtual float area() const = 0;
此时这个函数并没有做什么,纯粹是为了方便让派生类去重写而申明的,只是做了简单的初始化而已
至于抽象类,基本是纯虚函数的一个集合类,也是为了方便派生类去做派生,实现多态的
小结:
代码重用(代码复用),是面向对象语言通过继承来实现的,而代码重用并不是面向对象语言所特有的,但继承是特有的;
接口复用,其实就是基类指针的重用,只是实现起来需要一些方法来支撑;