继承

来源:互联网 发布:北京铁路软件开发 编辑:程序博客网 时间:2024/05/22 12:30

(一):代码重用 继承 公有、私有、保护继承 默认继承保护级别 接口继承与实现继承 继承与重定义 继承与组合

注:没有使用继承  就没有使用到面向对象  使用对象  仅仅是使用了数据抽象

OO: Object-Oriented

OOD:面向对象的设计    OOAD: UML  面向对象原则  设计模式  设计原则


C++很重要的一个特征就是代码重用。在C语言中重用代码的方式就是拷贝代码、修改代码。C++可以用继承或组合的方式来重用。通过组合或继承现有的的类来创建新类,而不是重新创建它们。

组合:

class A{public:    void FunA()    {        ...    }}; class B{public:    void FunB()    {        ...        a_.FunA();        ...    }private:    A a_;};

继承是使用已经编写好的类来创建新类,新的类具有原有类的所有属性和操作,也可以在原有类的基础上作一些修改和增补。

新类称为派生类或子类,原有类称为基类或父类,派生类是基类的具体化


派生类的声明语法为:

class 派生类名 : 继承方式  基类名

{

           派生类新增成员的声明;

}

保护成员可以在派生类中访问  派生类无法访问私有成员


默认继承保护级别:

class Base {};

struct D1 : Base {}; // 公有继承

class D2 : Base {}; // 私有继承

接口继承与实现继承:

我们将类的公有成员函数称为接口。

公有继承,基类的公有成员函数在派生类中仍然是公有的,换句话说是基类的接口成为了派生类的接口,因而将它称为接口继承。

实现继承,对于私有、保护继承,派生类不继承基类的接口。派生类将不再支持基类的公有接口,它希望能重用基类的实现而已,因而将它称为实现继承。


重定义:

1、对基类的数据成员的重定义  基类的数据成员会被覆盖

2、对基类成员函数的重定义分为两种:

overwrite:与基类完全相同,与基类成员函数名相同,参数不同(只是隐藏了基类的成员,)

override:虚函数

#include <iostream>using namespace std; class Base{public:    Base() : x_(0)    {     }    int GetBaseX() const    {        return x_;    }     void Show()    {        cout<<"Base::Show ..."<<endl;    }    int x_;}; class Derived : public Base{public:    Derived() : x_(0)    {     }    int GetDerivedX() const    {        return x_;    }    void Show(int n)    {        cout<<"Derived::Show "<<n<<endl;    }     void Show()    {        cout<<"Derived::Show ..."<<endl;    }    int x_;};  class Test{public:    Base b_;    int x_;};int main(void){    Derived d;    d.x_ = 10;    d.Base::x_ = 20;    cout<<d.GetBaseX()<<endl;    cout<<d.GetDerivedX()<<endl;     d.Show();    d.Base::Show();//可以暴露出来     cout<<sizeof(Derived)<<endl;    cout<<sizeof(Test)<<endl;     return 0;}

继承与组合

无论是继承与组合本质上都是把子对象放在新类型中,两者都是使用构造函数的初始化列表去构造这些子对象。

组合通中是在希望新类内部具有已存在的类的功能时使用,而不是希望已存在类作为它的接口。组合通过嵌入一个对象以实现新类的功能,而新类用户看到的是新定义的接口,而不是来自老类的接口。(has-a)

如果希望新类与已存在的类有相同的接口(在这基础上可以增加自己的成员)。这时候需要用继承,也称为子类型化。(is-a)


如:汽车和引擎和轮胎是组合的功能  而不是说汽车是引擎

教师与人类 就是继承      LSP:里氏代换原则可以检验继承的质量



(二):不能自动继承的成员函数  继承与构造函数   友元关系与继承    静态成员与继承

不能自动继承的成员函数:

构造函数   只对基类进行初始化

析构函数

=运算符    与构造函数的功能类似


继承与构造函数:

基类的构造函数不被继承,派生类中需要声明自己的构造函数。

声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员的初始化(调用基类构造函数完成)。

派生类的构造函数需要给基类的构造函数传递参数

由内存模型可以看出:先调用基类的构造函数,再调用派生类的构造函数



class Base{public:    Base(int b) : b_(b), objb_(111)    {        cout<<"Base ..."<<endl;    }    Base(const Base& other) : objb_(other.objb_), b_(other.b_)    {     }    ~Base()    {        cout<<"~Base ..."<<endl;    }    int b_;    ObjectB objb_;}; class Derived : public Base{public:    Derived(int b, int d) : d_(d), Base(b), objd_(222)    {        cout<<"Derived ..."<<endl;    }    Derived(const Derived& other) : d_(other.d_), objd_(other.objd_), Base(other)    {     }    ~Derived()    {        cout<<"~Derived ..."<<endl;    }    int d_;    ObjectD objd_;};
Derived(int b, int d) : d_(d), Base(b), objd_(222)

跟顺序没有关系,都是先调用基类的构造函数,然后再初始化自身的

在构造函数里其实不是初始化 是赋值


注意:

1const 2引用成员 3 基类没有默认构造函数的时候 4类的对象成员没有默认构造函数的时候  要在派生类的构造函数中初始化

如果对象成员中有一个类对象怎么办?在基类构造完后再构造,所以派生类的构造有一个顺序

所以派生类的构造次序:

先构造基类、派生类的对象成员,最后是派生类自身


如果基类也有一个对象成员呢?

先调用基类的对象成员的构造函数、然后构造基类的构造函数、派生类的对象成员,最后是派生类自身


如果对象成员不止有一个,那么是按照声明的顺序来进行构造的,与实际的初始化列表的顺序无关

每个类要负责对自己的对象成员的构造


假如基类提供了自己的拷贝构造函数?也需要对自己的对象成员进行构造,参考构造函数来写!

#include <iostream>using namespace std;  class ObjectB{public:    ObjectB(int objb) : objb_(objb)    {        cout<<"ObjectB ..."<<endl;    }    ~ObjectB()    {        cout<<"~ObjectB ..."<<endl;    }    int objb_;}; class ObjectD{public:    ObjectD(int objd) : objd_(objd)    {        cout<<"ObjectD ..."<<endl;    }    ~ObjectD()    {        cout<<"~ObjectD ..."<<endl;    }    int objd_;}; class Base{public:    Base(int b) : b_(b), objb_(111)    {        cout<<"Base ..."<<endl;    }    Base(const Base& other) : objb_(other.objb_), b_(other.b_)    {     }    ~Base()    {        cout<<"~Base ..."<<endl;    }    int b_;    ObjectB objb_;}; class Derived : public Base{public:    Derived(int b, int d) : d_(d), Base(b), objd_(222)    {        cout<<"Derived ..."<<endl;    }    Derived(const Derived& other) : d_(other.d_), objd_(other.objd_), Base(other)    {     }    ~Derived()    {        cout<<"~Derived ..."<<endl;    }    int d_;    ObjectD objd_;}; int main(void){    Derived d(100, 200);//全部放在栈上,所以从栈上销毁会调用析构函数    cout<<d.b_<<" "<<d.d_<<endl;     Base b1(100);    Base b2(b1);    cout<<b2.b_<<endl;     //要注意对基类的拷贝构造函数!要完成对基类的构造,对象成员的构造,最后是自身成员    //拷贝构造函数与普通的构造函数的写法是一致的    Derived d2(d);    return 0;}

友元关系是不能被继承的:

A是B的友元类:A可以访问B的所有成员,包括私有的保护的成员

C是A的派生类,那么C不是B的友元类

友元关系是单向的,


静态成员无所谓继承

因为可以通过派生类来进行访问,因为它是共享的

用法:

#include <iostream>using namespace std; class Base{public:    static int b_;}; int Base::b_ = 100;class Derived : public Base{ }; int main(void){    Base b;    Derived d;    cout<<Base::b_<<endl;    cout<<b.b_<<endl;     cout<<Derived::b_<<endl;    cout<<d.b_<<endl;     return 0;}


(三):转换与继承、派生类到基类的转换、基类到派生类的转换

派生类对象也是基类对象。这意味着在使用基类的地方可以用派生类来替换。

static_cast用于编译器认可的静态转换,比如说从char到int,从double到int 或者具有转换构造函数,或者类型转换运算符

reinterpret_cast用于编译器不认可的静态转换,从int*到int的转换(指针的字节和int可能不一致),不做任何对齐

const_cast去除常量性  dynamic_cast不同于以上的静态转换,属于动态转换,安全的向下转型,比如多态

当派生类以public方式继承基类时,编译器可自动执行的转换(向上转型 upcasting 安全转换)

1、派生类对象指针自动转化为基类对象指针

2、派生类对象引用自动转化为基类对象引用

3、派生类对象自动转换为基类对象(特有的成员消失)


当派生类以private/protected方式继承基类时

1、派生类对象指针(引用)转化为基类对象指针(引用)需用强制类型转化。但不能用static_cast,要用reinterpret_cast

2、不能把派生类对象强制转换为基类对象


基类对象指针(引用)可用强制类型转换为派生类对象指针(引用), 而基类对象无法执行这类转换.

向下转型不安全,没有自动转换的机制

#include <iostream>#include <string>using namespace std; class Employee{public:    Employee(const string& name, const int age, const int deptno) : name_(name),        age_(age), deptno_(deptno)    {     }private:    string name_;    int age_;    int deptno_;}; class Manager : public Employee{public:    Manager(const string& name, const int age, const int deptno, int level)        : Employee(name, age, deptno), level_(level)    {     }private:    int level_;}; class Manager2 : private Employee{public:    Manager2(const string& name, const int age, const int deptno, int level)        : Employee(name, age, deptno), level_(level)    {     }private:    int level_;}; int main(void){    Employee e1("zhangsan", 25, 20);    Manager m1("lisi", 38, 20, 10);    Manager2 m2("wangwu", 40, 15, 8);    Employee* pe;    Manager* pm;    Manager2* pm2;     pe = &e1;    pm = &m1;    pm2 = &m2;     pe = &m1;   // 派生类对象指针可以转化为基类对象指针。将派生类对象看成基类对象    //pm = &e1; // 基类对象指针无法转化为派生类对象指针。无法将基类对象看成是派生类对象     e1 = m1;    // 派生类对象可以转化为基类对象。将派生类对象看成基类对象                // 会产生对象切割(派生类特有成员消失)。object slicing     //pe = pm2; //私有或保护继承的时候,派生类对象指针不可以自动转化为基类对象指针    pe = reinterpret_cast<Employee*>(pm2);     //e1 = m2;  // 私有或保护继承的时候,派生类对象无法转化为基类对象。    //e1 = reinterpret_cast<Employee>(m2); // 私有或保护继承的时候,派生类对象无法强制转化为基类对象。      pm = static_cast<Manager*>(pe);   // 基类指针可以强制转化为派生类指针,但是不安全                                    //reinterpret_cast也可以 所以转型时尽量使用reinterpret_cast    pm2 = reinterpret_cast<Manager2*>(pe);  //无所谓私有继承     //m1 = reinterpret_cast<Manager>e1;   // 基类对象无法强制转化为派生类对象    return 0;}
注:向上和向下转型要知道!!  向上是不会丢失精度,把基类放在上面,派生类放在下面


基类对象到派生类对象的转换有两种方法:  (仅仅从语法上演示  一般不这么做)

1  转换构造函数  2、将其他类型转换为类类型

针对1:将其他类型转换为类类型

针对2:将类类型转换为其它类型

转换构造函数:

#include <iostream>#include <string>using namespace std; class Employee{public:    Employee(const string& name, const int age, const int deptno) : name_(name),        age_(age), deptno_(deptno)    {     }private:    string name_;    int age_;    int deptno_;}; class Manager : public Employee{public:    Manager(const string& name, const int age, const int deptno, int level)        : Employee(name, age, deptno), level_(level)    {     }     // 从语法上来演示基类对象可以转化为派生类对象,但是没有意义    Manager(const Employee& other) : Employee(other), level_(-1)    {     }private:    int level_;}; int main(void){    Employee e1("zhangsan", 25, 20);    Manager m1("lisi", 38, 20, 10);    m1 = e1;//先转换构造   然后再赋值    return 0;}
类型转换运算符重载:

#include <iostream>#include <string>using namespace std;class Manager;class Employee{public:    Employee(const string& name, const int age, const int deptno) : name_(name),        age_(age), deptno_(deptno)    {     }    //因为看不到manager  所以编译会失败      //所以必须放在Manager的后面        operator Manager();private:    string name_;    int age_;    int deptno_;}; class Manager : public Employee{public:    Manager(const string& name, const int age, const int deptno, int level)        : Employee(name, age, deptno), level_(level)    {     }private:    int level_;};// 仅仅只是从语法上演示基类对象可以转化为派生类对象,但是没有意义Employee::operator Manager(){    return Manager(name_, age_, deptno_, -1);}int main(void){    Employee e1("zhangsan", 25, 20);    Manager m1("lisi", 38, 20, 10);    m1 = e1;    return 0;}


(四) 多重继承、虚继承与虚基类、虚基类及其派生类构造函数

举例:

蝙蝠:是鸟也是兽  称为鸟兽

多重继承优点:派生类同时继承多个基类的成员,更好的软件重用

缺点:可能会有大量的二义性,多个基类中可能包含同名变量或函数

#include <iostream>using namespace std; class Bed{public:    Bed(int weight) : weight_(weight)    {     }    void Sleep()    {        cout<<"Sleep ..."<<endl;    }    int weight_;}; class Sofa{public:    Sofa(int weight) : weight_(weight)    {     }    void WatchTV()    {        cout<<"Watch TV ..."<<endl;    }    int weight_;}; class SofaBed : public Bed, public Sofa{public:    SofaBed() : Bed(0), Sofa(0)    {        FoldIn();    }    void FoldOut()    {        cout<<"FoldOut ..."<<endl;    }    void FoldIn()    {        cout<<"FoldIn ..."<<endl;    }}; int main(void){    SofaBed sofaBed;    //sofaBed.weight_ = 10;    //sofaBed.weight_ = 20;     //可以显示指定     //从逻辑上来看   只能有一个重量  所以可以把重量提升到家具sofabed    sofaBed.Bed::weight_ = 10;    sofaBed.Sofa::weight_ = 20;     sofaBed.WatchTV();    sofaBed.FoldOut();    sofaBed.Sleep();     return 0;}
更好点的表示:

#include <iostream>using namespace std; class Furniture{public:    Furniture(int weight) : weight_(weight)    {     }    int weight_;}; class Bed : public Furniture{public:    Bed(int weight) : Furniture(weight)    {     }    void Sleep()    {        cout<<"Sleep ..."<<endl;    }     }; class Sofa : public Furniture{public:    Sofa(int weight) : Furniture(weight)    {     }    void WatchTV()    {        cout<<"Watch TV ..."<<endl;    }}; class SofaBed : public Bed, public Sofa{public:    SofaBed(int weight) : Bed(weight), Sofa(weight)    {        FoldIn();    }    void FoldOut()    {        cout<<"FoldOut ..."<<endl;    }    void FoldIn()    {        cout<<"FoldIn ..."<<endl;    }}; int main(void){    SofaBed sofaBed(10);    //sofaBed.weight_ = 10;  //会有歧义    //sofaBed.weight_ = 20;     sofaBed.Bed::weight_ = 10;//可以显示指定    sofaBed.Sofa::weight_ = 20;     sofaBed.WatchTV();    sofaBed.FoldOut();    sofaBed.Sleep();     return 0;}


多重继承中解决访问歧义的方法

基类名::数据成员名(或成员函数(参数表))

明确指明要访问定义于哪个基类中的成员


虚继承与虚基类:

当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类中的成员时,将产生二义性(比如家具被继承了两次)

——采用虚基类来解决。

虚基类的引入:用于有共同基类的场合

声明:以virtual修饰说明基类 例:class B1:virtual public BB

作用:

1、主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题.

2、为最远的派生类提供唯一的基类成员,而不重复产生多次拷贝


注:钻石形状的继承体系采用虚继承

#include <iostream>using namespace std;class Furniture{public:    //Furniture(int weight) : weight_(weight)    //{     //}    int weight_;}; class Bed : virtual public Furniture{public:    //Bed(int weight) : Furniture(weight)    //{     //}    void Sleep()    {        cout<<"Sleep ..."<<endl;    } }; class Sofa : virtual public Furniture{public:    //Sofa(int weight) : Furniture(weight)    //{     //}    void WatchTV()    {        cout<<"Watch TV ..."<<endl;    }}; class SofaBed : public Bed, public Sofa{public:    //SofaBed(int weight) : Bed(weight), Sofa(weight)    //{    //  FoldIn();    //}    void FoldOut()    {        cout<<"FoldOut ..."<<endl;    }    void FoldIn()    {        cout<<"FoldIn ..."<<endl;    }}; int main(void){    SofaBed sofaBed;    sofaBed.weight_ = 10;    sofaBed.WatchTV();    sofaBed.FoldOut();    sofaBed.Sleep();    return 0;}

1、虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。

2、在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中给出对虚基类的构造函数的调用。如果未列出,则表示调用该虚基类的默认构造函数。

3、在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用被忽略。

以上的例子还是不够完美  weight类在哪里构造呢??

虚基类及其派生类构造函数

#include <iostream>using namespace std; class Furniture{public:    Furniture(int weight) : weight_(weight)    {        cout<<"Furniture ..."<<endl;    }    ~Furniture()    {        cout<<"~Furniture ..."<<endl;    }    int weight_;}; class Bed : virtual public Furniture{public:    Bed(int weight) : Furniture(weight)    {        cout<<"Bed ..."<<endl;    }    ~Bed()    {        cout<<"~Bed ..."<<endl;    }    void Sleep()    {        cout<<"Sleep ..."<<endl;    } }; class Sofa : virtual public Furniture{public:    Sofa(int weight) : Furniture(weight)    {        cout<<"Sofa ..."<<endl;    }    ~Sofa()    {        cout<<"~Sofa ..."<<endl;    }    void WatchTV()    {        cout<<"Watch TV ..."<<endl;    }}; class SofaBed : public Bed, public Sofa{public:    //要在基类中构造weight   只跟继承的顺序有关    SofaBed(int weight) : Bed(weight), Sofa(weight), Furniture(weight)    {        cout<<"SofaBed ..."<<endl;        FoldIn();    }    ~SofaBed()    {        cout<<"~SofaBed ..."<<endl;    }    void FoldOut()    {        cout<<"FoldOut ..."<<endl;    }    void FoldIn()    {        cout<<"FoldIn ..."<<endl;    }}; int main(void){    SofaBed sofaBed(5);    sofaBed.weight_ = 10;      sofaBed.WatchTV();    sofaBed.FoldOut();    sofaBed.Sleep();     return 0;}
构造的顺序很重要  因为是虚继承,所以底层的基类只初始化一次,如果不是的话,会多次调用基类构造函数


(五):虚继承对C++对象内存模型造成的影响

原则:

1、类大小计算遵循前面学过的结构体对齐原则

2、类的大小与数据成员有关与成员函数无关

3、类的大小与静态数据成员无关

4、虚继承对类的大小的影响

5、虚函数对类的大小的影响

两个概念:

virtual base table :

1、本类地址与虚基类表指针地址的差

2、虚基类地址与虚基类表指针地址的差

virtual base table pointer(vbptr)


继承模型:


代码:

#include <iostream>using namespace std;  class BB{public:    int bb_;}; class B1 : virtual public BB{public:    int b1_;}; class B2 : virtual public BB{public:    int b2_;}; class DD : public B1, public B2{public:    int dd_;}; int main(void){    cout<<sizeof(BB)<<endl;    cout<<sizeof(B1)<<endl;    cout<<sizeof(DD)<<endl;     B1 b1;    long** p;         cout<<&b1<<endl;    cout<<&b1.bb_<<endl;    cout<<&b1.b1_<<endl;        p = (long**)&b1;    cout<<p[0][0]<<endl;    cout<<p[0][1]<<endl;     DD dd;    cout<<&dd<<endl;    cout<<&dd.bb_<<endl;    cout<<&dd.b1_<<endl;    cout<<&dd.b2_<<endl;    cout<<&dd.dd_<<endl;    p = (long**)ⅆ    cout<<p[0][0]<<endl;    cout<<p[0][1]<<endl;    cout<<endl;    cout<<p[2][0]<<endl;    cout<<p[2][1]<<endl;     BB* pp;     pp = &dd;    pp->bb_;     // 通过间接访问,这需要运行时的支持     return 0;}
DD本来应该是4个Int  也就是16个字节  为什么会这样子呢

推导B1内存模型: 

cout<<&b1<<endl;

cout<<&b1.bb_<<endl;

cout<<&b1.b1_<<endl;

这是B1类的内存模型 空的位置表示vbptr(虚基类表指针),指向了虚基类表

注:

要打印虚基类表中的内容,必须用指针的指针

如果有虚函数的话  会更加复杂,会有虚表指针,指向虚表而不是虚基类表!在后面会进行讨论  

推导DD的内存模型

C++把基类放在最后一个位置,因为都是通过查找虚基类表,确定它的虚基类地址是多少,

virtual:存在!共享!间接!

虚基类的访问是间接的,需要通过虚基类表来进行访问!

如果虚基类不是共享的,那是非常简单的一件事情,就不需要虚表指针!

清楚了以上的特点后,有一个问题

对象访问bb_是直接访问吗?在编译时刻内存模型已经确定了,因为编译时刻要确定内存

DD dd;

dd.bb_;

而间接访问:

BB* pp;

pp = &dd;

pp->bb_;//访问内存前4个字节   是虚基类指针  肯定出错

需要运行时的支持,编译时刻访问肯定出错,在运行时刻会做调整的

会自己找指针并且做出相应的偏移


更多的C++对象模型,可以参考一本书:深入C++对象模型(有点过时了)

内存模型因为编译器的不同而不同,

虚基类因为是共享的,所以是放在末尾的,如果基类指针访问派生类指针,需要调整地址,这就是设计者思考的一种方向

0 0
原创粉丝点击