C++:面向对象编程(封装、继承、多态等)

1. 面向对象概述

面向对象有三个基本特征:封装、继承、多态。其中,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了:代码重用。而多态则是为了实现另一个目的:接口重用
这里写图片描述

1.1 封装

其实,封装只是针对类而言的。我们知道,类的成员属性有public、protected和private,体现了从外部访问类成员时,成员的可见性(可操作性)。封装就是指把过程和数据包围起来,对数据的访问只能通过已定义的界面。
在面向对象编程上可理解为:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

1.2 继承

继承:子类能够继承父类的成员,并重定义那些与子类相关的成员函数(虚函数)。
继承概念的实现方式有三类:实现继承、接口继承和可视继承。
1. 实现继承是指使用父类的属性和方法而无需额外实现;
2. 接口继承是指仅使用属性和方法的名称、但是子类必须提供具体实现(虚函数);
3. 可视继承是指子窗体(类)使用基窗体(类)的外观和实现代码。

1.3 多态

多态性,一句话解释:允许将子类类型的指针赋值给父类类型的指针。而这个功能,是通过虚函数实现的。后面会介绍虚函数,现在先看一个例子:


class A  
{  
public:  
    virtual void foo()  
    {  
        cout<<"A::foo() is called"<<endl;  
    }  
};  
class B:public A  
{  
public:  
    void foo()  
    {  
        cout<<"B::foo() is called"<<endl;  
    }  
};  
int main(void)  
{  
    A *a = new B;  
    a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!  
    return 0;  
}  

这里就实现了将B类型的指针赋值给了A类型的指针。

1.4 静态多态(重载)和动态多态(覆盖)

实现多态,有二种方式:重载和覆盖。而这两种方式,也就对应了两种多态:静态多态和动态多态。

1.4.1 静态多态

静态多态是用重载实现的。
重载:允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
实现静态多态的重载特指:在同一个类中有相同的返回类型和方法名的成员函数,但是参数的个数和类型不同。
值得一提的是,重载不仅包括函数重载,也包括运算符重载:

int operator +{...}
char operator +{...}

其实,重载的概念并不属于“面向对象编程”,重载的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)
如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了。
可以这么理解:重载只是一种语言特性,与多态无关,用它实现的静态多态也不是真正意义上的多态。

1.4.2 动态多态

其实一般所说的多态,就是指动态多态,它涉及到动态绑定。动态多态是用覆盖实现的,而覆盖具体又是用虚函数实现的。

覆盖:是指子类重新定义父类的虚函数的做法。
注意到,覆盖是发生在不同的类中的。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出),因此成为动态绑定。

1.5 重载、多态、继承、虚函数、覆盖等概念的厘清

重载和多态没有必然联系,重载只是cpp的语言特性。但利用重载,可以实现一种静态多态。
覆盖是和多态联系在一起的,用覆盖可以实现一种动态多态,即常规意义上的多态。覆盖就涉及到继承虚函数。也就是说,子类成员对父类成员的覆盖,是通过子类先继承父类,再重写父类里的虚函数来实现的。

重载、覆盖容易混淆,简单的区别是:
1)重载发生在同一个类中,覆盖发生在子类与父类中;
2)子类与父类中,如果使用了虚函数,则属于覆盖。

1.6 补充:隐藏(与重载、覆盖)

1.6.1 隐藏的具体含义

我们知道,作为一种标识符,函数也有作用域,它的原则如下:
如果存在两个或多个具有包含关系的作用域,外层声明了一个标识符,而内层没有再次声明同名标识符,那么外层标识符在内层依然可见。
如果在内层声明了同名标识符,则外层标识符在内层不可见,这时称内层标识符隐藏了外层同名标识符。


在类的继承中,父类的成员和子类新增的成员都具有类作用域。二者的作用范围不同,是相互包含的两个层,子类在内层。
这时,如果子类声明了一个和父类成员同名的新成员,这个子类的同名新成员就隐藏了外层的父类同名成员,直接使用成员名只能访问到子类的成员。如果这个成员是函数,那么,即使函数的参数表不同,从父类继承的同名函数(或其重载形式)也都被隐藏。
如果一定要访问被隐藏的成员,就需要使用类作用域分辨符和父类名来限定。

#include<iostream>  
using namespace std;  
class A  
{  
    public:  
    void print2(){  
        cout<<"A print2 !"<<endl;  
    }  
};  

class B:public A  
{  

    public:  
    void print2(int x){  
        cout<<"B print2 !"<<x<<endl;  
    }  
};  


int main(){  
    B b;  
    b.A::print2();//用作用域分辨符定位从父类继承的成员函数
    return 0;  
}  

结果就是

A print2 !

除了利用作用域分辨符定位继承自父类并被子类隐藏的成员以外,还可以利用using关键字加以说明。
using的一般功能是将一个作用域中的名字引入到另一个作用域中,它还有一个非常有用的用法:将using用于父类中的函数名,这样子类中如果定义同名但参数不同的函数,父类的函数将不会被隐藏,这两个函数作为重载函数将会并存在子类的作用域中。

#include<iostream>  
using namespace std;  
class A  
{  
    public:  

    void print2(){  
        cout<<"A print2 !"<<endl;  
    }  
};  

class B:public A  
{  

    public:  
    using A::print2;  
    void print2(int x){  
        cout<<"B print2 !"<<x<<endl;  
    }  
};  

int main(){  
    B b;  
    b.print2();  
    return 0;  
}  

注意到,用using的前提是,父类和子类中的同名函数必须参数不同。

1.6.2 隐藏、重载和覆盖的区别

函数重载的特性:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual关键字可有可无。
因为函数参数不同,可以简单的理解为:两个重载函数是不同的函数,调用者能够明确根据不同的参数来调用不同的函数。

重载和隐藏、覆盖的本质区别在于:重载是发生在同一个类中的,而隐藏和覆盖都发生在父类和子类中。


那么,隐藏和覆盖该如何区分呢?
覆盖,是指子类函数覆盖父类函数,只作用于子类函数,其特性为:
(1)不同的范围(分别位于子类与父类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual关键字。

我们发现,这里用到了虚函数,实际上虚函数就是为了就是实现覆盖而存在的,这两个概念紧密联系。


隐藏,是指子类函数将父类函数给藏起来了,同样只作用于子类函数,其特性为:
(1)如果子类的函数与父类的函数同名,但是参数不同。此时,不论有无virtual关键字,父类的函数都将被隐藏。
(2)如果子类的函数与父类的函数同名,并且参数也相同,但是父类函数没有virtual关键字。此时,父类的函数被隐藏。


可以这样理解隐藏和覆盖:

类型参数表相同参数表不同
有virtual覆盖隐藏
没有virtual隐藏隐藏

2. 引申:公有、私有和受保护的继承

公用继承 class x : public base{...}:基类成员保持自己的访问级别:基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员,基类的private成员为派生类的private成员,但是在派生类中却不可见,也即派生类不能访问基类的private成员
私有继承 class x : private base{...}:基类的所有成员在派生类中为private属性,但是基类的private成员在派生类中却不可见,也即派生类不能访问基类的private成员
受保护继承 class x : protected base{...}:基类的public成员和protected成员为派生类的protected成员,基类的private成员为派生类的private成员,但是在派生类中却不可见,也即派生类不能访问基类的private成员

注意到,无论是哪种方式的继承,子类都能使用父类的public成员和protected成员,而且都不能直接访问private成员。那么继承方式的不同是对谁而言的呢?其实是对子类以外的类或函数调用子类的成员时而言的,好比父类能够调用它的任何成员,但用它的子类(看成从父类之外)就无法调用父类的私有成员了,但子类都能使用父类的public成员和protected成员。


实际上,公有、私有、受保护和继承没有关系,他们只是体现了成员的可见性。
我们知道:
public允许来自任何类的访问;
private只允许来自改该类内部的方法访问,不允许任何来自该类外部的访问;
protected允许来自同一包中的任何类以及该类的任何子类的方法访问。

另一方面,父类的任何成员都会被子类继承下来,而继承下来的私有成员之所以对子类来说不可见,是因为子类已经属于父类的外部了。但是注意,子类仍然可以用父类的函数操作它们。

这样的设计有何意义呢?我们可以用这个方法将我们的成员保护得更好,让子类的设计者也只能通过父类指定的方法修改父类的私有成员,这对一个完整的继承体系是尤为可贵的。

3. 引申:虚函数和纯虚函数

3.1 虚函数

class A  
{  
public:  
    virtual void foo()  
    {  
        cout<<"A::foo() is called"<<endl;  
    }  
};  
class B:public A  
{  
public:  
    void foo()  
    {  
        cout<<"B::foo() is called"<<endl;  
    }  
};  
int main(void)  
{  
    A *a = new B;  
    a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!  
    return 0;  
}  

在父类中用virtual 修饰一个函数,它就成了一个虚函数。继承了该父类的子类中,不用virtual 再显式声明虚函数。
虚函数的“虚”,就在于所谓的“动态绑定”上:一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。编译代码的时候并不能确定被调用的是父类的函数还是子类的函数。

虚函数只能借助于指针或者引用来实现动态绑定,以达到多态的效果。

3.2 纯虚函数

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何子类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”

class Disc : public Item{
public:
    virtual void funtion1()=0;
};

将函数定义为纯虚函数,说明该函数为该父类的子类们提供了可以覆盖(也必须覆盖)的接口。
含有纯虚函数的类是抽象基类,用户不能创建抽象基类的对象。

3.3 虚函数和纯虚函数的区别

定义一个函数为虚函数,不代表函数就是不被实现的函数。
定义它为虚函数,是为了允许用基类指针来调用子类的这个函数。


定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个父类的子类必须实现这个函数。


实现了纯虚函数的子类,该纯虚函数在子类中就变成了一个虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值