欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

关于基类与派生类相关,以及继承

程序员文章站 2022-07-04 12:35:24
多态就是一个基类可以指向其任意派生类的能力,即基类的指针或者引用可以指其派生类,作用就就是希望使用派生具体的实现接口功能1.多态性只存在类的继承层次中2.必须使用基类的指针或者引用才有效.3.多态可以动态调用的函数必须是由virtual声明的函数4.多态性是运行时的动态加载,不是链接时也不是编译时决 ......

多态就是一个基类可以指向其任意派生类的能力,即基类的指针或者引用可以指其派生类,作用就就是希望使用派生具体的实现接口功能
1.多态性只存在类的继承层次中
2.必须使用基类的指针或者引用才有效.
3.多态可以动态调用的函数必须是由virtual声明的函数
4.多态性是运行时的动态加载,不是链接时也不是编译时决定的
5.当出现虚拟函数时,为避免基类指针或者引用的的局部析构,那么基类的析构必须声明为virtual。派生构造因为在初始化时就是调用派生类构造函数的,
而且构造是深度优先的,所以构造不用
(即基类指针只调用了基类的析构函数,但派生类未调用,故根源的基类的析构声明为virtual,那么就会先调用派生类的析构定义实例了)


一个类对象的静态和动态类型是相同的故虚拟函数机制只在使用指针和引用时才会如预期般地起作用。

抽象基类(只声明调用接口函数纯虚拟函数)<-实现基类(声明虚拟函数 + 共用功能函数)<-派生类(接口的具体实现 + 自有业务处理逻辑)
一般基类不应包括太多变量,尤其是抽象基类,基本就不应该含有变量,因为含有太多变量会导致类在构造和复制时性能和效率都会降低。


构造顺序base->devide,析构顺序devide->base
基类构造函数被调用的顺序反映了派生类继承层次结构中深度优先的遍历过程,派生类的析构函数调用顺序与它的构造函数调用顺序相反
故不能基类的构造函数或者析构函数里面调用虚拟函数,因为这样会引起程序的崩溃。
对于派生类对象在基类构造函数因早于派生,基类指针会先构造基类,但如果基类构造函数中调用的派生类的虚拟实现,就有可能使用到派生构造的对象,故有冲突
对于派生类对象在基类析构函数中也是如此,基类指针会先析构派生类,而到基类析构里面调用派生的实现,如果对象不再可能引发错误,派生类部分也是未定义的但是这一次不是因为它还没有被构造而是因为它已经被销


如果类不是基类,或者没有多态的情况出现,就不需要声明为virtual,但如果是继承关系,基类的析构必须是虚拟的。

缺省情况下函数是在编译时刻被静态解析的,在c++中通过一种被称为虚拟函数virtual function的机制来支持动态绑定
在进程运行时刻需要解析出被调用的函数这个解析过程被称为动态绑定dynamic bindng,

纯虚拟函数(purl virtual):virtual t func(t arg) = 0;(例如通常使用的virtual const char* what() const throw() {};)
purl virtual函数会导致类抽象化,即该类不能被实体化,也就是不能创建一个类对象(实体变量),只能创建一个类指针;
包含一个或多个纯虚拟函数的类被编译器识别为抽象基类,试图创建一个抽象基类的独立类对象会导致编译时刻错误;

类似地通过虚拟机制调用纯虚拟函数也是错误的
继承的派生类则需要将所有的纯虚拟函数进行实现,如果没有实质的实现,也需用通过空函数来实例化,这样在创建对象时才不会因抽象而无法引用函数.
否则也是无法创建对象,只能通过引用或者指针进行使用;


在基类与派生类如果不想按照动态绑定则可以通过类域来指定实现方法,不指定的话就会导致基类无法使用派生类的函数了。
如果基类和派生类存在同样函数不用virtual会怎么样? 那么实现的内容具体会由指针的类型是基类还是派生类决定,
如果是基类指针指向派生类对象则仍按基类的定义实例实现, 从而失去多态效应,即不会按照对应派生的定义实例去实现功能
如果是派生类指针指向派生类则按照派生类的定义实例实现,这样会引起隐藏基类相同名称的函数,是hide而非override
如果想明确指定可以使用类域来实现,而不是直接访问。


内联析构函数可能是程序代码膨胀的一个源泉,因为它被插入到函数中的每个退出点,在每个return 语句之前析构函数都必须被内联地展开, 以析构每一个活动的局部类对象.
比如在函数中的每一个return语句都会触发一个析构函数,解决方法是在函数中减少return语句的调用,尽量集中到一点使用return退出.
要么就在实现源代码中显式的声明析构为非内联的.
这里提醒如果析构包括太多操作,请记住应该如何控制内联和调用函数内局部对象的操作

*************************************************************************************************************
类继承构造函数的顺序
1.基类构造函数。基类一般必须含有默认的构造函数,否则会引起派生类编译因没有缺省构造函数而无法通过。
如果有多个基类则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们在成员初始化表中的顺序
2.成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序
3.派生类对象构造函数

class c: public a, public b {
private:
g g;
public:
c():e(1), c(2), g(3) {};

private:
f c;
e e;
}

  

定义一个c的构造顺序为:a->b->g->f->e->c


*************************************************************************************************************
虚拟继承virtual inheritance,在虚拟继承下只有一个共享的基类子对象被继承而无论该基类在派生层次中出现多少次。
共享的基类子对象被称为虚拟基类virtual base class,在虚拟继承下基类子对象的复制及由此而引起的二义性都被消除了。

任何层次的多重继承如果出现类域二义性都会导致编译时刻的错误。

因为当中间派生类虚拟基础基类时,作为中间派生类,所有对虚拟基类的构造函数调用都被自动抑制了,对于构造函数最好提供
显式的构造函数,一个公共(public)函数作为最终派生类使用,和一个protected()最为中间派生类使用,以避免不必要的参数

虚拟继承只是消除了多个中间派生类会重复复制相同基类和由此引发的重复基类二义性的问题,但对于中间派生类的虚拟函数却无法保证能消除二义性。比如

class a { 
virtual void func();
virtual void func1();
virtual void func2(){...};
...
};
class b:public virtual a { 
virtual void func(){...};
virtual void func1(){...};
...
};
class c:public virtual a { 
virtual void func(){..};
...
};

class d:public b, public c {
void func(){a::func();}; /* must be specific */
void func1(func1();); /* b::func1() */
void func2(func2();); /* a::func2() */
...
}

 

以上虚拟基类的虚拟函数有3种情况:
1.没有被中间派生类重新定义具体实现。a::func2()
2.中间派生类同一层次仅有一个特例。a::func1()->b::func1()
3.中间派生类同一层次出现多个派生类(>=2)定义其具体实现。a::func(), b::func(), c::func()

在非虚拟继承基类的中间派生类下,所有非限定修饰(不用类域)的引用100%都会引起二义性的编译错误,
对于虚拟继承,
第一种为共享基类的单个定义实例,故不存在二义性,最终派生类均可直接无二义的访问。
第二种为同一层次仅有一个定义实例,最终派生类会根据继承层次的优先级(派生类高于基类),取最接近最终派生类的实现实例进行访问。
第三种为同一层次出现多个派生类的实例,直接访问则会引起二义性的编译错误。
如果想要清晰的的引用不同层次的中间派生类,最好通过类域特指访问特定定义实例,也可以解除因第三种情况引起的二义性编译错误


虚拟构造函数顺序:虚拟构造函数层次(从左至右, 深度优先) -> 非虚拟构造函数层次(从左至右, 深度优先)
比如:

class a{...}; 
class b{...};
class c: public a{...};
class d: public virtual b{...};
class e: public virtual b{...};
class f:{...};
class g: public c, public d, public e, public virtual f{...};

那么定义一个g类对象构造顺序为:
b->f->a->c->d->e->g

 

析构则刚刚好相反。