• 认真地记录技术中遇到的坑!

浅谈继承机制

程序开发语言 Moxun 7个月前 (05-28) 260次浏览 0个评论

封装、继承、多态是面向对象的三大特征。继承可以机制可以说是起到了承上启下的作用,封装的本质就是类设计,继承的根本是使一个类具有另一个类的特征,多态则是在继承的基础上演进而来的(其实就是父类和子类的替换而已),这里说的多态,在C++种特指运行时多态。

在C++类中,类有三种方法:(1)纯虚函数;(2)虚函数;(3)非虚函数。在设计一个类时,尤其是基类时,要考虑把方法设计成什么类型的。

纯虚函数:派生类只继承接口;虚函数:派生类可以继承一个接口和一个默认的实现,派生类也可以重写(注意是重写不是重载)它;非虚函数:派生类继承基类的一个强制实现。对这三种类型的方法示例如下:

#include <iostream>
class Base
    {
    public:
    //纯虚函数
    virtual void prueVirtualFunc() = 0;
    //虚函数,这里提供的是一个默认的实现,派生类可以重写它
    virtual void virtualFunc()
        {
        std::cout << "I am a virtual function!" << std::endl;
    }
    //这是一个非虚函数,派生类不可重写它,只能继承继承基类的提供的这个强制实现
    void nonVirtualFunc()
        {
        std::cout << "I am a non-virtual function!" << std::endl;
    }
}

当我们使用纯虚函数时,我们希望派生类只是继承基类的方法名,然后派生类根据自己的具体需求去实现这个方法。

当我们使用虚函数时,基类将向派生类提供一个方法名和默认的实现,派生类可以根据自己的情况定义符合自身的实现(这个叫做覆盖或者重写),也可以调用基类默认的实现。

非虚函数:派生类继承了基类提供的方法名和一个对应强制实现。但是派生类也可以定义一个相同名字的函数,这样就是派生类中的同名函数将会隐藏基类中的同名函数。隐藏规则如下:
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

跟虚函数相关的一些概念—-

1.动态绑定(dynamic binding)
在设计父类方法时,我们希望派生类拥有自己实现的方法通常定义为虚函数,不希望派生类自己实现的方法我们把它定义为非虚函数。
动态绑定发生在使用基类指针或引用时,通过基类指针或引用调用虚函数时,在编译期间我们是无法获知它调用的具体是基类的虚函数版本还是派生类的虚函数版本,只有在运行时才能确定,这取决于,在运行期间,和基类指针或引用绑定的实际对象的具体类型。

2.静态类型和动态类型:
静态类型指的是变量声明时给定的类型或者表达式生成的结果类型,它是在编译期已知的。动态类型指的是变量或表达式表示的内存中的对象类型,它只有在运行期间才可以得知。当且仅当通过基类的指针或引用调用虚函数时才会解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同,如果表达式既不是指针也不是引用,那么它的静态类型和动态类型永远相同。

3.final和override
派生类中如果定义了一个函数与基类中虚函数同名,但是形参列表不同,此时,编译器会认为这个函数是派生类新定义的函数,如果,我们的意图是在派生类中重写这个虚函数,那么这类错误很难发现。解决的办法是,在派生类的虚函数,函数声明后加关键字override表明派生类要重写基类对应虚函数的意图,此时,如果派生类没有重写基类的虚函数,那么编译器将报错,示例:

class Base
    {
    public:
    virtual void print()
        {
        std::cout << "I am Base!" <<std::endl;
    }
}

class A:public Base
    {
    public:
    void print() override
        {
        std::cout <<"I am A!" << std::endl;
    }
}

如果我们定义了一个类且不希望它被继承或者一个方法且不希望它被覆盖,那么我们可以用final标记它。示例:

class A final       //A不能被继承
    {

}

class D:public A        //编译将报错
    {

}

class B
    {
    void func() final;      //不允许后序的其它类覆盖它。
}

4.回避虚函数的机制
在某些情况下,我们不希望进行类的动态绑定,而只想让它执行某个特定的版本,这时我们可以使用作用域运算符来实现这一目的。示例:

class Base
    {
    public:
    virtual int func(int i)
        {
return i;
    }
}

Base * baseP;
/*
baseP指针具体绑定类别的代码
*/

int a = baseP->Base::func(5);
//这样运行的结果是,编译器强行调用基类的func函数,而不管baseP具体绑定的对象是哪一个派生类。

6.纯虚函数的引入:
在类中,有时候我们并不能给出虚函数的有意义的定义,此时引入了纯虚函数的概念,纯虚函数是在函数声明后加 =0 声明前加virtual的方式来声明的,这种形式的声明必须要在类的内部,纯虚函数不需要实现。但是纯虚函数也可以实现,如果要实现的话,必须放在类的外部。

7.含有纯虚函数的类称为抽象类,纯虚函数没有对应实现的类不能实例化(不能创建这个类的对象),但是可以声明指向抽象基类的指针或引用。抽象基类类似于JAVA中的接口概念。

8.析构函数通常应该是虚函数,这样能保证在析构时调用正确的析构函数版本。

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI(运行时类型识别)技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

以下内容摘自:https://blog.csdn.net/three_bird/article/details/51479175
RTII(Run Time Type Identification)机制:即通过运行时类型识别,程序能够使用基类的指针或引用来检查这些指针或引用所指对象的实际派生类型。

typeid和dynamic_cast
RTTI机制提供了两个很有用的操作:typeid和dynamic_cast。

typeid返回指针或引用所指的实际类型。
dynamic_cast操作符将基类类型的引用或指针安全的转为派生类类型的引用或指针。

为了确定一个对象的类型,可以对该对象使用typeid函数,该对象返回一个对type_info对象的引用。要使用typeid必须包含头文件。不允许用户创建自己的type_info类对象,唯一要使用type_info的只有typeid函数。
typeid函数的主要作用就是让用户知道当前的变量是什么类型的,比如使用typeid(a).name()就能知道变量a是什么类型的。typeid()函数的返回类型为typeinfo类型的引用。typeid使用示例。

class A{
private:
    int a;
};

class B :public A{
public:
    virtual void f(){ cout << "HelloWorld\n"; }
private:
    int b;
};

class C :public B{
public:
    virtual void f(){ cout << "HelloWorld++\n"; }
private:
    int c;
};

class D :public A{
public:
    virtual void f(){ cout << "HelloWorld--\n"; }
private:
    int d;
};
int main()
{
    int a = 2;
    cout << typeid(a).name() << endl;
    A objA;
    //打印出class A  
    cout << typeid(objA).name() << endl;
    B objB;
    //打印出class B  
    cout << typeid(objB).name() << endl;
    C objC;
    //打印出class C  
    cout << typeid(objC).name() << endl;

    //以下是多态在VC 6.0编译器不支持,但是在GCC以及微软更高版本的编译器却都是
    //支持的,且是在运行时候来确定类型的,而不是在编译器,会打印出class c
    B *ptrB=new C();
    cout<<typeid(*ptrB).name()<<endl;

    A *ptrA = new D();
    //打印出class A而不是class D  
    cout << typeid(*ptrA).name() << endl;
    return 0;
}

dynamic_cast强制转换运算符
该转换符用于将一个指向派生类的基类指针或引用转换为派生类的指针或引用,注意dynamic_cast转换符只能用于含有虚函数的类,其表达式为dynamic_cast<类型>(表达式),其中的类型是指把表达式要转换成的目标类型,比如含有虚函数的基类B和从基类B派生出的派生类D,则B pb; D *pd, md; pb=&md; pd=dynamic<D>(pb); 最后一条语句表示把指向派生类D的基类指针pb转换为派生类D的指针,然后将这个指针赋给派生类D的指针pd。

因为有些时候我们需要强制转换,比如如果指向派生类的基类指针B想访问派生类D中的除虚函数之外的成员时就需要把该指针转换为指向派生类D的指针,以达到访问派生类D中特有的成员的目的,比如派生类D中含有特有的成员函数g(),这时可以这样来访问该成员dynamic_cast<D*>(pb)->g();因为dynamic_cast转换后的结果是一个指向派生类的指针,所以可以这样访问派生类中特有的成员。但是该语句不影响原来的指针的类型,即基类指针pb仍然是指向基类B的。

dynamic_cast转换符只能用于指针或者引用。dynamic_cast转换符只能用于含有虚函数的类。dynamic_cast转换操作符在执行类型转换时首先将检查能否成功转换,如果能成功转换则转换之,如果转换失败,如果是指针则反回一个0值,如果是转换的是引用,则抛出一个bad_cast异常,所以在使用dynamic_cast转换之间应使用if语句对其转换成功与否进行测试,比如pd=dynamic_cast<D>(pb); if(pd){…}else{…},或者这样测试if(dynamic_cast<D>(pb)){…}else{…}。

虚函数的底层实现:
见浅谈数据成员、成员函数指针、虚函数实现


转载请注明出处 浅谈继承机制
喜欢 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址