用的最重要的手段它允许程序员在保持原有类特性的基.ppt
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构。体现了由简单到复杂的认识过程。,第八章 继承与多态,多态性(polymorphism)多态性是考虑在不同层次的类中,以及在同一类中,同名的成员函数之间的关系问题。函数的重载,运算符的重载,属于编译时的多态性。以虚函数为基础的运行时的多态性是面向对象程序设计的标志性特征。体现了类推和比喻的思想方法。,第八章 继承与多态,8.1 继承与派生的概念,8.4 虚基类(选读),8.3 多重继承与派生类成员标识,8.6 多态性与虚函数,8.5 派生类应用讨论,8.2 派生类的构造函数与析构函数,8.1 继承与派生的概念,层次概念是计算机的重要概念。通过继承(inheritance)的机制可对类(class)分层,提供类型/子类型的关系。C+通过类派生(class derivation)的机制来支持继承。被继承的类称为基类(base class)或超类(superclass),新产生的类为派生类(derived class)或子类(subclass)。基类和派生类的集合称作类继承层次结构(hierarchy)。如果基类和派生类共享相同的公有接口,则派生类被称作基类的子类型(subtype)。派生反映了事物之间的联系,事物的共性与个性之间的关系。派生与独立设计若干相关的类,前者工作量少,重复的部分可以从基类继承来,不需要单独编程。,8.1 继承与派生的概念,8.1.1 类的派生与继承,8.1.2 公有派生与私有派生,由基类派生出派生类的定义的一般形式为class 派生类名:访问限定符 基类名1,访问限定符 基类名2,访问限定符 基类名n private:成员表1;/派生类增加或替代的私有成员public:成员表2;/派生类增加或替代的公有成员protected:成员表3;/派生类增加或替代的保护成员;/分号不可少其中基类1,基类2,是已声明的类。在派生类定义的类体中给出的成员称为派生类成员,它们是新增加成员,它们给派生类添加了不同于基类的新的属性和功能。派生类成员也包括取代基类成员的更新成员。,8.1.1 类的派生与继承,(a)多重继承,(b)单继承,图8.1 多重继承与单继承,一个基类可以直接派生出多个派生类,派生类可以由多个基类共同派生出来,称多重继承。,8.1.1 类的派生与继承,如果一个派生类可以同时有多个基类,称为多重继承(multiple-inheritance),这时的派生类同时得到了多个已有类的特征。一个派生类只有一个直接基类的情况称为单一继承(single-inheritance)。,编制派生类时可分四步,吸收基类的成员,改造基类成员,发展新成员,重写构造函数与析构函数,8.1.1 类的派生与继承,不论是数据成员,还是函数成员,除构造函数与析构函数外全盘接收,声明一个和某基类成员同名的新成员,派生类中的新成员就屏蔽了基类同名成员称为同名覆盖(override),派生类新成员必须与基类成员不同名,它的加入保证派生类在功能上有所发展。,8.1.1 类的派生与继承,上面的步骤就是继承与派生编程的规范化步骤。第二步中,新成员如是成员函数,参数表也必须一样,否则是重载。第三步中,独有的新成员才是继承与派生的核心特征。第四步是重写构造函数与析构函数,派生类不继承这两种函数。不管原来的函数是否可用一律重写可免出错。,访问控制,亦称为继承方式,是对基类成员进一步的限制。访问控制也是三种:公有(public)方式,亦称公有继承保护(protected)方式,亦称保护继承私有(private)方式,亦称私有继承。,8.1.2 公有派生与私有派生,访问限定符有两方面含义:派生类成员(新增成员)函数对基类(继承来的)成员的访问(调用和操作),和从派生类对象之外对派生类对象中的基类成员的访问。,公有派生是绝对主流。,派生类的构造函数的定义形式为:派生类名:派生类名(参数总表):基类名1(参数表1),基类名2(参数表2),基类名n(参数表n),成员对象名1(成员对象参数表1),成员对象名m(成员对象参数表m)/派生类新增成员的初始化;/所列出的成员对象名全部为新增成员对象的名字,在构造函数的声明中,冒号及冒号以后部分必须略去。所谓不能继承并不是不能利用,而是把基类的构造函数作为新的构造函数的一部分,或者讲调用基类的构造函数。基类名仅指直接基类,写了底层基类,编译器认为出错。冒号后的基类名,成员对象名的次序可以随意,这里的次序与调用次序无关。,8.2 派生类的构造函数与析构函数,派生类构造函数各部分的执行次序为:1.调用基类构造函数,按它们在派生类定义的先后顺序,对基类成员进行初始化。2.调用成员对象的构造函数,按它们在类定义中声明的先后顺序,顺序调用。3.执行派生类的构造函数。,*在派生类构造函数中,只要基类不是使用缺省构造函数都要显式给出基类名和参数表。如果基类没有定义构造函数,则派生类也可以不定义,全部采用系统给定的缺省构造函数。如果基类定义了带有形参表的构造函数时,派生类就应当定义构造函数。,析构函数各部分执行次序与构造函数相反,首先对派生类新增一般成员析构,然后对新增对象成员析构,最后对基类成员析构。,【例8.1】由在册人员类公有派生学生类,【例8.1】由在册人员类公有派生学生类。我们希望基类和派生类共享相同的公有接口,只能采用公有派生来实现。,首先来看基类:,class Personstring IdPerson;/身份证号,18位数字string Name;/姓名Tsex Sex;/性别enum Tsexmid,man,woman;int Birthday;/生日,格式1986年8月18日写作19860818string HomeAddress;/家庭地址,public:Person(string,string,Tsex,int,string);/构造函数 Person();/缺省的构造函数 Person();/析构函数,【例8.1】由在册人员类公有派生学生类,void SetName(string);/修改名字string GetName()return Name;/提取名字void SetSex(Tsex sex)Sex=sex;/修改性别Tsex GetSex()return Sex;/提取性别void SetId(string id)IdPerson=id;/修改身份证号string GetId()return IdPerson;/提取身份证号void SetBirth(int birthday)Birthday=birthday;/修改生日int GetBirth()return Birthday;/提取生日void SetHomeAdd(string);/修改住址string GetHomeAdd()return HomeAddress;/提取住址void PrintPersonInfo();/输出个人信息;,接口函数:,【例8.1】由在册人员类公有派生学生类,派生的学生类:,class Student:public Person/定义派生的学生类string NoStudent;/学号course cs30;/30门课程与成绩public:Student(string id,string name,Tsex sex,int birthday,string homeadd,string nostud);/注意派生类构造函数声明方式 Student();/缺省派生类构造函数 Student();/派生类析构函数 SetCourse(string,int);/课程设置 int GetCourse(string);/查找成绩 void PrintStudentInfo();/打印学生情况;,struct course char*coursename;int grade;,在VC+平台上运行例8.1,验证主函数,8.3 多重继承与派生类成员标识,由多个基类共同派生出新的派生类,这样的继承结构被称为多重继承或多继承(multiple-inheritance),图8.3 大学在册人员继承关系,8.3 多重继承与派生类成员标识,派生出来的新类同样可以作为基类再继续派生出更新的类,依此类推形成一个层次结构。,例:输出派生类构造函数与析构函数的调用关系,#includeclass Base1int x;public:Base1(int a)x=a;cout调用基类1的构造函数!n;Base1()cout调用基类1的析构函数!n;class Base2int y;public:Base2(int a)y=a;cout调用基类2的构造函数!n;Base2()cout调用基类2的析构函数!n;,class Derived:public Base1,public Base2int z;public:Derived(int a,int b):Base1(a),Base2(20)z=b;cout调用派生类的构造函数!n;Derived()cout调用派生类的析构函数!n;void main(void)Derived c(100,200);,例:派生类中包含对象成员,#inlucde“liti11-4.h”class Der:public Base1,public Base2int z;Base1 b1,b2;public:Der(int a,int b):Base1(a),Base2(20),b1(200),b2(a+b)z=b;cout调用派生类的构造函数!n;Der()cout调用派生类的析构函数!n;void main(void)Der d(100,200);,本例中类Circle为圆;类Line为高;类Cone为圆锥,由Circle和Line公有派生而来。在Cone类中,Circle和Line类的接口完全不变,可以直接调用,这就是公有派生的优点。在Cone的成员函数中可直接访问Circle和Line中的公有成员,但不能直接访问私有成员。,【例8.2】由圆和高多重继承派生出圆锥,检证主程序:,圆类Circle定义,高类Line定义,圆锥类Cone定义,8.3.1 冲突 在多重派生时,若一个公有的派生类是由两个或多个基类派生,当基类中成员的访问权限为public,且不同基类中的成员具有相同的名字时,出现了重名的情况。这时在派生类使用到基类中的同名成员时,出现了不唯一性,这种情况称为冲突。,class Apublic:int x;void Show()coutx=xn;A(int a)x=a;A();class Bpublic:int x;void Show()coutx=xn;B(int a)x=a;B();,class C:public A,public Bint y;public:void Setx(int a)x=a;void Sety(int b)y=b;int Gety()return y;void main(void)C c1;c1.Show();,8.3 多重继承与派生类成员标识(冲突、支配规则、赋值兼容规则),下面是编译时的出错提示:D:VCsourceL11_7.cpp(19):error C2385:C:x is ambiguous(不明确的)D:VCsourceL11_7.cpp(19):warning C4385:could be the x in base A of class CD:VCsourceL11_7.cpp(19):warning C4385:or the x in base B of class CD:VCsourceL11_7.cpp(26):error C2385:C:Show is ambiguousD:VCsourceL11_7.cpp(26):warning C4385:could be the Show in base A of class CD:VCsourceL11_7.cpp(26):warning C4385:or the Show in base B of class C说明:在派生类C中访问由基类继承来的变量x时,编译系统无法确定是要访问属于基类A中的x还是属于基类B中的x,因此编译出错。同样,在基类C的对象c1中调用函数Show()时,也是无法确定是要调用类A中继承来的公有成员函数Show(),还是调用类B中继承来的公有成员函数Show()。,解决冲突问题的方法:各基类中定义的成员函数名不同;对于成员数据,在基类中说明其访问权限为private,并在相应的基类中提供成员函数对这些成员数据进行操作。使用作用域分辨符来限定所访问成员的属性,其格式为:类名:成员名;,class C:public A,public Bint y;public:void SetAx(int a)A:x=a;/对类A中的x设置void SetBx(int a)B:x=a;/对类B中的x设置void Sety(int b)y=b;int Gety()return y;void main(void)C c1;c1.SetAx(35);c1.SetBx(100);c1.Sety(300);c1.A:Show();/调用类A中的成员函数c1.B:Show();/调用类B中的成员函数couty=c1.Gety()n;,注意:当把派生类作为基类,又派生出新的派生类时,这种限定作用域的运算符不能嵌套使用,即:类名1:类名2:.:成员名;也就是说,限定作用域的运算符只能直接限定其成员。,8.3.2 支配规则 在C+中,允许派生类中新增加的成员名与其基类的成员名相同,这种相同不产生冲突。当没有使用作用域运算符时,则派生类中定义的成员名优先于基类中的成员名,这种优先关系称为支配规则。,class Apublic:int x;void Show()coutx=xn;class Bpublic:int y;void Show()couty=yn;class C:public A,public Bpublic:int y;,void main(void)C c1;c1.x=100;c1.y=200;c1.B:y=300;c1.A:Show();c1.B:Show();couty=c1.yn;couty=c1.B:yn;运行结果为:X=100Y=300Y=200Y=300,8.3.3 继承和对象成员,C+规定,任一基类在派生类中只能继承一次,否则会造成成员名的冲突。Class Aclass B:public A,public A public:float x;.;此时在派生类B中包含了两个继承来的成员X,在使用时产生冲突。解决的方法是,在类B的定义中加入类A的两个对象作为类B的成员(对象成员)。Class B A a1,a2;注意:在派生类中,如果权限允许的话可以直接使用基类的成员,但这里使用的是对象成员的成员,必须在对象名后加上成员运算符“.”和成员名。,/例:基类成员与对象成员在使用上的差别#includeclass Apublic:int x;A(int a=0)x=a;class Bpublic:int y;B(int a=0)y=a;class C:public Aint z;B b1;public:C(int a,int b,int m):A(a),b1(b)z=m;void show()coutx=xt;/直接使用成员名couty=b1.yt;/不能直接使用对象的成员名ycoutz=zn;,void main(void)C c1(100,200,300);c1.show();,8.3.4 赋值兼容规则 简单的说就是:对于公有派生类来说,可以将派生类的对象赋给其基类的对象,反之是不允许的。,class Apublic:int x;.;,class C:public A,public Bpublic:int y;.;C c1,c2,c3;A a1,*pa1;B b1,*pb1;,class Bpublic:int y;.;,不能将基类的对象赋给派生类对象。例如:c2=a1;c3=b1;可以将一个派生类对象的地址赋给基类的指针变量。例如:pa1=,说明:派生类的对象可以赋给基类的对象,系统是将派生类对象中从对应基类中继承来的成员赋给基类对象。例如:a1=c1;/将c1中从类A中继承来的对应成员x分别赋给a1的对应成员。b1=c1;/将c1中从类B中继承来的对应成员y分别赋给b1的对应成员。,8.4 虚基类,图11-2 派生类中包含同一基类的两个拷贝,对于某一共公共基类A,设类B由类A公有派生,类C也由类A公有派生,而类D是由类B和类C共同公有派生。这时在类D中将包含类A的两个拷贝。,基类A,基类B,基类C,类D,public,public,基类A,class Apublic:int x;A(int a=0)x=a;class B:public A/class B:A(200)x=200 public:int y;B(int a=0,int b=0):A(b)/调用基类的构造函数y=a;/y=100,void PB()coutx=xty=yn;class C:public A/class C:A(400)x=400public:int z;C(int a=0,int b=0):A(b)/调用基类的构造函数 z=a;/z=300void PC()coutx=xtz=zn;,class D:public B,public Cpublic:int m;D(int a,int b,int d,int e,int f):B(a,b),C(d,e)m=f;/m=500void Print(void)PB();/DPC();/Ecoutm=mn;void main(void)D d1(100,200,300,400,500);d1.Print();,注:由于在类D中包含了公共基类A的两个不同的拷贝,当D、E改为下列情况时:coutx=xty=yn;coutx=xtz=zn;,编译器认为有错,因无法确定成员x是从类B中继承来的还是从类C中继承来的,从面产生了冲突。为此,必须用作用域运算符来限定成员的属性:B:x;C:x。VC+中提供了将基类说明为虚基类的方法,使得在多重派生的过程中公共基类只产生一个拷贝。在派生类的定义中,只要在基类的类名前加上关键字virtual,就可以将基类说明为虚基类,格式为:class 类名:vitual 基类名;,#includeclass Apublic:int x;A(int a=0)x=a;cout虚基类A的构造函数endl;A()cout虚基类A的析构函数endl;,class B:virtual public Apublic:int y;B(int a=0,int b=0):A(b)/不调用虚基类的构造函数y=a;cout派生类B的构造函数endl;B()cout派生类B的析构函数endl;void PB()coutx=xty=yn;class C:virtual public Apublic:int z;C(int a=0,int b=0):A(b)/不调用虚基类的构造函数z=a;cout派生类C的构造函数endl;C()cout派生类C的析构函数endl;void PC()coutx=xtz=zn;,class D:public B,public Cpublic:int m;D(int a,int b,int d,int e,int f):B(a,b),C(d,e)m=f;cout派生类D的构造函数endl;D()cout派生类D的析构函数endl;void Print(void)PB();PC();coutm=mn;,void main(void)D d1(100,200,300,400,500);d1.Print();d1.x=400;/对象d1中只有基类A的一个拷贝d1.Print();,X的初值为0,这是因为调用虚基类的构造函数的方法与调用一般基类的构造函数的方法不同,编译器约定,在执行类B和类C的构造函数时都不调用虚基类A的构造函数,而是在类D的构造函数中直接调用虚基类A的缺省的构造函数,使得x的初值为0。因此,虚基类定义中一般要包含有缺省的构造函数。2.若虚基类中没有定义缺省的构造函数,则在派生的每一个派生类的构造函数的初始化成员例表中都必须有对虚基类构造函数的调用。例如将F行改为:A(int a)x=a;则在类D的构造函数初始化成员列表中必须增加直接调用虚基类A的构造函数:D(int a,int b,int d,int e,int f):B(a,b),C(d,e),A(1000)此时则将类D中的x成员初值置为1000。3.在派生类D的对象d1中只有基类A的一个拷贝,当改变成员X的值时,由基类B和C中的成员函数输出的X的值是相同的;,4.在派生类对象的创建中,首先是虚基类的构造函数并按它们声明的顺序构造。第二批是非虚基类的构造函数按它们声明的顺序调用。第三批是成员对象的构造函数。最后是派生类自己的构造函数被调用。,书:P207【例8.3】在采用虚基类的多重继承中,构造与析构的次序。,class Dclass:public Bclass1,virtual Bclass3,virtual Bclass2 Object object;public:Dclass():object(),Bclass2(),Bclass3(),Bclass1()cout派生类建立!n;Dclass()cout派生类析构!n;void main()Dclass dd;cout主程序运行!n;,运行结果Constructor Bclass3/第一个虚拟基类,与派生类析构函数排列无关Constructor Bclass2/第二个虚拟基类Constructor Bclass1/非虚拟基类Constructor Object/对象成员派生类建立!主程序运行!派生类析构!deconstructor Object/析构次序相反deconstructor Bclass1deconstructor Bclass2deconstructor Bclass3/析构的次序与构造的次序相反。,8.6 多态性与虚函数,多态性是面向对象程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能。,在C+中有两种多态性,编译时的多态性,运行时的多态性,运行时的多态性是指在程序执行前,无法根据函数名和参数来确定该调用哪一个函数,必须在程序执行过程中,根据执行的具体情况来动态地确定。它是通过类继承关系和虚函数来实现的。目的也是建立一种通用的程序。通用性是程序追求的主要目标之一。,通过函数的重载和运算符的重载来实现的。,8.6 多态性与虚函数,8.6.1 虚函数的定义,8.6.4 动态联编(选读),8.6.2 纯虚函数,8.6.3 继承与多态的应用单链表派生类(选读),虚函数既虚拟函数,它必须是成员函数并且不能是静态函数。虚函数与成员函数一样包含一定的功能,所不同的是虚函数可以在其派生类的定义中被重新定义。虚函数的定义格式如下:Virtual funcname();当在派生类中定义了一个与该虚函数名同名的成员函数,并且该成员函数的参数个数、参数的类型以及函数的返回值类型都与基类中的同名虚函数一样,则无论是否使用virtual来修饰该成员函数,它都成为一个虚函数。,8.6.1 虚函数的定义,例:使用虚函数#includeconst double PI=3.14159;class point double x,y;public:point(float i=0,float j=0)x=i;y=j;virtual double area()return 0.0;class circle:public pointdouble radius;public:circle(double r=0)radius=r;double area()return PI*radius*radius;,void main(void)point p;coutarea()endl;运行结果:the area of the point p is:0the area of the circle c is:41.8538now the area is:41.8538,程序说明:在基类point中定义了一个虚函数area(),其派生类circle中也定义了area()的成员函数,由于与基类中的虚函数同名且返回值、形参均相同,因此派生类circle中的area()也为虚函数。Main函数中定义了基类point的对象p,派生类circle的对象c,因此p.area()以及c.area()在编译时就有了确定的含义,既调用基类point中的成员函数(虚函数)area()以及调用派生类circle的成员函数(虚函数)area(),这就是编译时的多态性,输出结果分别为:0、41.8538;C+中,基类指针*pp可以用来指向派生类对象的地址,此时调用的将是派生类对象的虚函数,既pp-area()调用的是派生类circle的成员函数area()。可见,当基类指针指向不同的派生类对象时,尽管调用的形式完全相同,但却是调用了不同对象中的虚函数,这就是运行时的多态性。因此,输出结果仍为:41.8538。,关于虚函数的几点说明:当在基类中把成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中的虚函数同名,且参数的类型、顺序、个数及函数的返回值的类型也相同。要实现动态(运行时)的多态性,必须使用基类类型的指针变量,使该指针指向不同派生类的对象,并通过调用指针所指向的虚函数才行。虚函数必须是类的一个成员函数,它不能是友元函数,或静态成员函数。在派生类中没有重新定义虚函数时,与一般的成员函数一样,当调用这种派生类对象的虚函数时,则调用其基类中的虚函数。5.可以把析构函数定义为虚函数,但是不能将构造函数定义为虚函数。6.虚函数与一般的成员函数相比较,调用时的执行速度稍慢一些,这是因为,为了实现多态性,在每一个派生类中均要保存相应的虚函数的入口地址表,函数的调用机制也是间接实现的。,思考:在上例中若将基类point中的虚函数virtual area()改为一般成员函数,程序运行的结果将会如何?class pointdouble x,y;public:point(float i=0,float j=0)x=i;y=j;double area()return 0.0;程序运行的结果变为:the area of the point p is:0the area of the circle c is:41.8538now the area is:0,结果分析:point*pp;pp=,虚函数怎样实现多态性:【例8.6】计算学分。可由本科生类派生出研究生类,但它们各自的从课程学时数折算为学分数的算法是不同的,本科生是16个学时一学分,而研究生是20个学时一学分。,【例8.7】计算学分。派生类定义不再重复。,纯虚函数(pure virtual function)是指被标明为不具体实现的虚拟成员函数。它用于这样的情况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。,8.6.2 纯虚函数,定义纯虚函数的一般格式为:virtual 返回类型 函数名(参数表)=0;,含有纯虚函数的基类是不能用来定义对象的。纯虚函数没有实现部分,不能产生对象,所以含有纯虚函数的类是抽象类。,1 定义纯虚函数时,不能定义虚函数的实现部分。即使是函数体为空也不可以,函数体为空就可以执行,只是什么也不做就返回。而纯虚函数不能调用。2“=0”表明程序员将不定义该函数,函数声明是为派生类保留一个位置。“=0”本质上是将指向函数体的指针定为NULL。3 在派生类中必须有重新定义的纯虚函数的函数体,这样的派生类才能用来定义对象。,8.6.2 纯虚函数,定义纯虚函数必须注意:,#includeclass Aprotected:int x;public:A()x=1000;virtual void print()=0;/纯虚函数;class B:public A int y;public:B()y=2000;void print()couty=yendl;class C:public A int z;public:C()z=3000;void print()coutz=zendl;,void main(void)A*pa;B b;C c;pa=也要产生运行错误,因为pp的值也是不确定的。,【例8.8】学校对在册人员进行奖励,依据是业绩分,但是业绩分的计算方法只能对具体人员进行,如学生,教师,行政人员,工人,算法都不同,所以可以将在册人员类作为一个抽象类,业绩计算方法作为一个纯虚函数。在主函数中全部用指向基类的指针来调用,业绩分基类定义,业绩分学生派生类定义,业绩分教师派生类定义,验证主函数,8.6.3 继承与多态的应用单链表派生类(选读),【例8.10】通用单链表派生类。首先改造【例7.4】的头文件,不采用模板类,而采用虚函数实现多态性,达到通用的目的。结点类数据域被改造为指针,而把数据放在一个抽象类中,由指针与之建立联系。,图8.9 结点构造,class Object/数据类为抽象类public:Object()virtual bool operator(Object/析构函数可为虚函数,构造函数不行,首先看结点组织,采用结点类加数据类,数据类定义:,本题两个要点:采用虚函数实现多态性,达到通用的目的。堆内存的分配与释放,关键不是创建,而是释放!,数据抽象类中含有三个纯虚函数:比较函数和输出函数。当抽象类在派生时重新定义纯虚函数,可以进行各种类型,包括类和结构对象的比较和输出。,本例介绍程序总体组成为主,链表的操作由学生自己仔细阅读。,抽象类中的析构函数也是虚函数,这一点非常重要,当抽象类派生的数据类的数据部分是动态产生,而由结点类删除释放数据类对象时,必须由数据类的析构函数来释放该类对象数据部分占用的动态分配的内存。这时必须重新定义析构函数。,Class Node Object*info;/数据域用指针指向数据类对象 Node*link;/指针域public:Node();/生成头结点的构造函数 Node();/析构函数 void InsertAfter(Node*P);/在当前结点后插入一个结点 Node*RemoveAfter();/删除当前结点的后继结点,返回该结点备用 void Linkinfo(Object*obj);/把数据对象连接到结点 friend class List;/以List为友元类,List可直接访问Node的私有函数,;,结点类定义:,class List Node*head,*tail;/链表头指针和尾指针public:List();/构造函数,生成头结点(空链表)List();/析构函数 void MakeEmpty();/清空链表,只余表头结点 Node*Find(Object/删除指定结点,定义链表类,第二步,由抽象类派生数据类(取代模板定义泛型类型为具体类型)步骤是。这里数据采用的是字符串,字符串是放在动态分配的堆内存中的,所以析构函数必须重新定义。为了完成字符串的比较和输出,重新定义了比较和输出函数(虚函数)。,class StringObject:public Object string sptr;public:StringObject()sptr=;StringObject(string s)sptr=s;StringObject();/析构函数 bool operator(Object,验证主函数运行结果,在该程序中,特别要仔细揣摩堆内存的分配与释放。删除一个结点时系统自动调用结点类析构函数释放结点占用的动态内存,而结点类析构函数自动调用数据域类虚析构函数,数据域类析构函数自动调用string类的析构函数释放所占用的动态内存。一环套一环,一步都不能错。这是使用动态内存分配的关键。即关键不是创建,而是释放!,在VC+平台上运行例8.10。,动态联编(dynamic binding)亦称滞后联编(late binding),对应于静态联编(static binding)。,如果使用对象名和点成员选择运算符“.”引用特定的一个对象来调用虚函数,则被调用的虚函数是在编译时确定的(称为静态联编),如果使用基类指针或引用指明派生类对象并使用该指针调用虚函数(成员选择符用箭头号“-”),则程序动态地(运行时)选择该派生类的虚函数,称为动态联编。,8.6.4 动态联编(选读),联编是指计算机程序自身彼此关联的过程,是把一个标识符名和一个存储地址联系在一起的过程,也就是把一条消息和一个对象的操作相结合的过程。,图8.9 虚函数调用的控制流程,8.6.4 动态联编(选读),C+编译器编译含有一个或几个虚函数的类及其派生类 时,对该类建立虚函数表(Virtual function table,vtable)。虚函数表使执行程序正确选择每次执行时应使用的虚函数。多态是由复杂的数据结构实现的,参见图8.10。图8.10是以【例8.10】为基础的,不过增加了一个由抽象类Object派生的复数数据类ComplexObject。图中列出了基类和各派生类的虚函数表,这些表是由指向函数的指针组成的。,8.6.4 动态联编(选读),还有第二层指针,在实例化带虚函数的类(创建对象)时,编译器在对象前加上一个指向该类的虚函数表的指针。第三层指针是链表结点类对象中指向抽象基类Object的指针(这也可以是引用,但本例是指针)。虚函数的调用是这样进行的,考虑虚函数Compare(),则看含“cat”的结点。由该结点的info指针找到含“cat”的无名对象,再由对象前的指针找到StringObject虚函数表,移动4个字节(一个指针占4个字节)找到比较函数指针,进入串比较函数。,完,第八章 继承与派生,谢谢!,Person:Person(string id,string name,Tsex sex,int birthday,string homeadd)IdPerson=id;Name=name;Sex=sex;Birthday=birthday;HomeAddress=homeadd;/作为一个管理程序,这个构造函数并无必要,因为数据总是另外输入的。仅为说明语法存在。,分析构造函数:,Person:Person()IdPerson=#;Name=#;Sex=mid;Birthday=0;HomeAddress=#;,分析缺省的构造函数:,分析析构函数:,Person:Person()/string内部动态数组的释放,由string自带的析构函数完成,void Person:SetName(string name)Name=name;/拷入新姓名,修改名字:,void Person:SetHomeAdd(string homeadd)HomeAddress=homeadd;,修改住址:,void Person:PrintPersonInfo()int i;cout身份证号:IdPersonn姓名:Namen性别:;if(Sex=man)cout男n;else if(Sex=woman)cout女n;else cout n;cout出生年月日:;i=Birthday;couti/10000年;i=i%10000;couti/100月i%100日n 家庭住址:HomeAddressn;,输出个人信息:,Student:Student(string id,string name,Tsex sex,int birthday,string homeadd,string nostu