C++语言程序设计5第五讲——多态性_第1页
C++语言程序设计5第五讲——多态性_第2页
C++语言程序设计5第五讲——多态性_第3页
C++语言程序设计5第五讲——多态性_第4页
C++语言程序设计5第五讲——多态性_第5页
已阅读5页,还剩189页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第八章 多态性,C+语言程序设计,本章主要内容,多态性 运算符重载 虚函数 纯虚函数 抽象类,多态性的概念,(这里讲的多态性是狭义的,仅指动态多态。广义的多态应包括静态多态。) 动态多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。即,在用户不作任何干预的环境下,类的成员函数的行为能根据调用它的对象类型自动作出调整。 多态性是面向对象程序设计的重要特征之一。 是扩充性在“继承”之后的又一重大表现 。,函数重载 运算符重载,多态性的分类,强制多态,参数多态,重载多态,包含多态,多态性,通用多态,专用多态,虚函数,const_cast static_cast reinterpret_cast dynamic_cast 以及老式的 (),函数模板 类模板,多态的实现可分为编译时多态和运行时多态,它们分别对应静态联编和动态联编。 联编又称为绑定(binding),是指计算机程序中的语法元素(标识符、函数等)彼此相关联的过程。 从绑定的时机看,在编译时就完成的绑定叫静态绑定;直到运行时才能确定并完成的绑定叫动态绑定。 静态绑定消耗编译时间,动态绑定消耗运行时间。 静态绑定的程序到了运行阶段其功能就固定了,即使情况发生了变化,功能无法改变。 动态绑定的程序由于绑定发生在运行阶段,其功能是未定的,当情况变化了,功能也跟着变。于是表现出会聪明的判断及具有灵活的行为。,多态性的实现,6,指针 地址的抽象; 形参 数值的抽象; 对象 事物的抽象; 类 对象的抽象; 超类 类的抽象; 模板 类型的抽象; 多态 行为的抽象; 函数 过程的抽象; 类型 数据标识的抽象; 异常 错误的抽象; 函数对象 函数的一元化抽象; 流 文件的抽象; ,关于“抽象”,1、系统提供了大量运算符,可用于基本数据类型。 如:int x,y; x=x+y; 表达简洁,使用方便。 2、可是对于串类的对象合并: char *a=“abcd” , *b = “xyz”; strcat(x,y);不如上述运算那样简单,希望能改造为:当 string x, y; x=x+y; 更加简单和直观! 因此,需要对“+”进行不同的解释,即让它具有新的功能。,问题的提出,又例复数的运算,class complex /复数类声明 public: complex(double r=0.0,double i=0.0) /构造函数 real=r; imag=i; void display(); /显示复数的值 private: double real; double imag; ;,运算符重载,比如有complex a,b,c; 能用“+”、“-” 实现复数的加减运算吗? c = a+b; 只有在复数类中实现复数加减运算的方法,即重载“+”、“-”运算符。 “重载”就是原来有了,又重复增加一个。,运算符重载,运算符重载的实质,运算符重载是对已有的运算符赋予多重含义。其实是将运算符函数化。 基本认识 C+中预定义的运算符其运算对象只能是基本数据类型,而不适用于用户自定义类型(如类)。 实现机制 将指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参。 编译系统对重载运算符的选择,遵循函数重载的选择原则。,运算符重载,如何理解“将指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参。”,人们习惯用运算符写计算基本类型值的表达式,如 a + b 。对于用户自定义类型的对象,如Clock c1, c2 ,”+”无法计算,就要用函数来完成。于是这个函数就应这样定义:+(形参表) 为了便于系统识别,函数名前缀 operator。最终的运算符重载函数形如: 为了不改变人们的习惯,调用式最好不写成“ +(c1,c2)”,仍沿用老写法:c1+c2 。其中的转换由系统代劳。,返回类型 operator (形参表);,运算符重载,规则和限制,可以重载C+中除下列运算符外的所有运算符:. .* : ?: sizeof typeid # static_cast dynamic_cast const_cast reinterpret_cast | & , 只能重载C+语言中已有的运算符,不可臆造新的; 不改变原运算符的优先级和结合性; 不能改变操作数个数; 不可声明为类属性;Why? 要符合该运算符的使用习惯。,运算符重载向用户提供了“重塑语言”的机制。,为何加上它们!,因为静态函数没有this,无法使用当前对象。,重载的两种形式,重载函数的表现形式仅有两种: 重载为类成员函数。 要受类成员的存取权限约束 重载为友元函数。,运算符重载,运算符重载函数,声明形式: 函数类型 operator 运算符(形参表) 重载为类成员函数时: 形参个数=原操作数个数- 1(后置+、-除外) 这一规则限制了重载函数写成静态形式。除非作特殊安排。 重载为友元函数时:形参个数=原操作数个数,运算符重载,运算符重载,例 8.1,将“+”、“-”运算重载为复数类的成员函数。 规则: 实部和虚部分别相加减。 操作数: 两个运算操作数都是复数类的对象。,#include using namespace std; class complex /复数类声明 public: /外部接口 complex(double r=0.0,double i=0.0)real=r;imag=i; /构造函数 complex operator + (complex c); /+重载为成员函数 complex operator - (complex c); /-重载为成员函数 void display(); /输出复数 private: /私有数据成员 double real; /复数实部 double imag; /复数虚部 ;,16,其实还应加const,那是为了堵塞a+b=c的漏洞。,complex complex: operator +(complex c2) /重载函数实现 complex c; c.real=c2.real+real; c.imag=c2.imag+imag; return complex(c.real,c.imag); ,17,complex complex: operator -(complex c2) /重载函数实现 complex c; c.real=real-c2.real; c.imag=imag-c2.imag; return c; ,这是个什么式子?做了件什么事? 是否可以简化?,如何创建的?,void complex:display() cout“(“real“,“imag“)“endl; void main() /主函数 complex c1(5,4),c2(2,10),c3; /声明复数类的对象 cout“c1=“; c1.display(); cout“c2=“; c2.display(); c3=c1-c2; /使用重载运算符完成复数减法 cout“c3=c1-c2=“; c3.display(); c3=c1+c2; /使用重载运算符完成复数加法 cout“c3=c1+c2=“; c3.display(); ,18,程序输出的结果为: c1=(5,4) c2=(2,10) c3=c1-c2=(3,-6) c3=c1+c2=(7,14),重载运算符的显式和隐含调用,运算符也是函数,可以显式调用和隐含调用。 当运算符作为成员函数时,对象本身是函数的调用者,另一个对象则作为默认的参数: complex a, b; complex c = a + b; /隐含调用 complex d = a.operator+( b ); /显式调用,20,运算符成员函数的设计,运算符 使用形式 等价式 双目运算符 oprd1 oprd2 oprd1 . operator (oprd2 ) 前置单目 oprd oprd . operator () 后置单目 oprd oprd . operator (0 ),例8.2,运算符前置+和后置+重载为时钟类的成员函数。 前置单目运算符重载函数没有形参,对于后置单目运算符,重载函数需要有一个整型形参。 操作数是时钟类的对象。 实现时间增加1秒钟。,运算符重载,/8_2.cpp #include using namespace std; class Clock /时钟类声明 public: /外部接口 Clock(int NewH=0, int NewM=0, int NewS=0); void ShowTime(); Clock ,22,为何要返回本类的引用?,为何不要返回引用?,Clock ,23,/后置单目运算符重载 Clock Clock:operator +(int) /注意形参表中的整型参数 Clock old=*this; +(*this); /调用了另一个成员函数 return old; ,24,这是哪个对象?,改了哪个对象?,/其它成员函数的实现略 void main() Clock myClock(23,59,59); cout“First time output:“; myClock.ShowTime(); cout“Show myClock+:“; (myClock+).ShowTime(); cout“Show +myClock:“; (+myClock).ShowTime(); ,25,程序运行结果为: First time output: 23:59:59 Show myClock+: 23:59:59 Show +myClock: 0:0:1,谜底在这里!,重提赋值运算符重载的话题,赋值运算符重载函数应做成类的成员函数; 这个函数应返回本类类型的引用; 在这个函数中应为所有的数据成员复制; 这个函数应能检查出“自赋值”的情况。 对以上四条,你能回答为什么吗?,运算符重载,运算符友元函数的设计1,如果需要重载一个运算符,使之能够用于在类外操作某类对象的私有成员,可以此将运算符重载为该类的友元函数。 函数的形参代表了依自左至右次序排列的各操作数。 为了区分前置后置运算,后置单目运算符 +和-的重载函数,其形参表中要增加一个int,但不必写形参名。,运算符重载,运算符友元函数的设计2,运算符 使用形式 等价式 双目运算符 oprd1 oprd2 operator (oprd1,oprd2 ) 前置单目 oprd operator (oprd ) 后置单目 oprd operator (oprd,0 ),如果一个运算符想用某个基本类型的数据调用,它就必须定义成全局的友元函数。如: complex a; complex b= 2+ a; 这个operator+不能是complex类的成员函数,也不能是其他类的成员函数,因为不存在一个函数: complex & int.operator+ ( complex& rhs)。,运算符友元函数的设计3,例8-3,将+、-(双目)重载为复数类的友元函数。 两个操作数都是复数类的对象。 (可以对比 例8-1 重载为复数类的成员函数),运算符重载,#include using namespace std; class complex /复数类声明 public: /外部接口 complex(double r=0.0,double i=0.0) real=r; imag=i; /构造函数 friend complex operator + (complex c1,complex c2); /运算符+重载为友元函数 friend complex operator - (complex c1,complex c2); /运算符-重载为友元函数 void display(); /显示复数的值 private: /私有数据成员 double real; double imag; ;,31,加上const。为的是阻塞(a+b)=c之类的漏洞。,/运算符重载友元函数实现 complex operator +(complex c1,complex c2) return complex(c2.real+c1.real, c2.imag+c1.imag); ,32,/运算符重载友元函数实现 complex operator -(complex c1,complex c2) return complex(c1.real-c2.real, c1.imag-c2.imag); ,/ 其它函数和主函数同例8.1,从这句话你能想到什么?,例题,运算符前置+和后置+重载为时钟类的友元函数。 前置单目运算符重载函数有一个形参,后置单目运算符重载函数需要有两个整型形参。 操作数是时钟类的对象。 实现时间增加1秒钟。,运算符重载,#include using namespace std; class Clock /时钟类声明 public: /外部接口 Clock(int NewH=0, int NewM=0, int NewS=0) Hour=NewH;Minute=NewM;Second=NewS; void ShowTime(); friend Clock ,34,Clock ,35,/后置单目运算符重载 Clock operator +(Clock ,36,改了哪个对象?,void main() Clock myClock(23,59,59); cout“First time output:“; myClock.ShowTime(); cout“Show myClock+:“; (myClock+).ShowTime(); cout“Show +(+myClock):“; (+(+myClock).ShowTime(); ,37,程序运行结果为: First time output: 23:59:59 Show myClock+: 23:59:59 Show +myClock: 0:0:2,谜底在这里!,重载赋值运算符,一、概述 1、“”赋值运算符可以被重载; 2、重载后不能被继承; 3、必须重载为成员函数。 二、格式: X X : operator = (const X & from) /复制X类的成员; 三、一般说,当缺省的赋值函数不能正确工作时,考虑“”的重载。,重载运算符()和 ,一、重载函数调用运算符() 1、函数为:operator(); 2、格式: 当 X x; x(arg1, arg2) 可被解释为: x. operator ()(arg1,arg2); /在C中()作用:提高运算优先级,强制类型转换, 函数调用运算符 二、重载下标运算符 1、函数为:operator; 2、格式:当 X x; xy 可被解释为:x. operator (y); 三、两者都只能重载为成员函数,不能使用友元函数。,40,运算符重载时不要改变其功能,下面是一个改变了功能的反例: 始作俑者想给 complex 类加上幂功能,但C+没有幂符号,于是想到了 让ab等价于ab. 于是写出了: friend Complex operator (cons Complex 符号可以借用,但随之而来的优先级、结合性还好用吗?,41,运算符重载的设计原则(一),当运算符是: 规则: 全部单目运算符 建议设计为非静态成员函数 = () - * 必须是非静态成员函数 建议设计为非静态成员函数 双目运算符 建议设计为友元函数 必须是友元函数 虚函数 建议设计为成员函数 若允许链式运算 该函数应返回本类型的引用 若定义了 + - * / % 千万别忘了定义” operator = “ 理由:“必须是非静态成员函数”是为了确保调用它的对象必然 是函数所在类的对象; “必须是友元函数”是因为使调用它的对象可以不是函数 所在类的对象;,+= -= *= /= %= = = |= &=,42,对于任何形参,若仅读取值而不修改,则应设为const 所有的赋值运算符皆可以改变左值。为满足链式运算要求,函数应返回同类型的非常引用。(记住:编译器是自左向右扫描表达式的,但却是从右向左来处理的。) 对于逻辑运算符,应返回bool 类型,至少是 int 型;,运算符重载的设计原则(二),43,对于自增自减运算符,可以返回本类的对象,亦可处理成返回是否继续迭代的逻辑值; 应尽量避免数值类型和指针类型重载,因为人们往往猜不透编译器的“心思”; 下标运算符必须是成员函数,单个形参,返回应是引用; ()运算符是唯一允许带任意个参数的函数,必须设计为成员函数; 指针运算符-最神奇也最复杂,不但重载了,甚至演化为类,作用于容器,叫作迭代子,是特例,那是后话了。,运算符重载的设计原则(三),44,运算符重载是必要的吗?,运算符重载的必要性毋庸置疑,它是支持数据抽象和泛型化编程的利器。 一般说来,凡是要作为元素使用容器的类,必须至少重载“=”、“=”、“”等运算符,因为容器要对其中的元素排序。实际上,在STL中,泛型算法是以元素已重载了“=”、“=”、“”等运算符为前提的。 这也是我们设计类的指导思想。,45,1. 若重载为成员函数,则this指针所指对象发起调用; 2. 若重载为全局 / 友元函数,则由第一形参发起调用; 3. 禁止程序员臆造运算符集中不存在的运算符; 4. 除了“函数调用运算符()”外,其他运算符重载函数不得有默认参数值; 5. 不要试图改变运算符的语义,一定要与原运算符的语义保持一致。 6. 经重载的运算符,其操作数中至少应该有一个是自定义类型,即不可重载使用基本类型的运算符。,运算符重载函数的调用规则,7、运算符被重载,只是相对一特定类、在特定的环境下作出特定的解释。当离开这个特定环境后,运算符恢复原来的意义(系统定义); 8、当重载运算符解释失败时,编译器使用该运算符的预定义版本(系统); 9、当现有运算符不够用时,只能用函数调用来实现; 10、除“=”以外,重载的运算符可以被任何派生类所继承,“=”需要每个类明确定义自己的解释; 11、重载能使程序的可读性下降,在使用时应模仿原运算符的习惯用法 。,47,转换可以在任何类型间进行 : 基本类型 基本类型 (有哪些规则?) 基本类型 用户自定义类型; 用户自定义类型 用户自定义类型。,类型转换,在算术表达式中:短服从长 在赋值表达式中:右服从左 在条件(三目运算)表达式中:取长的 在函数调用形实结合时:实服从形 函数返回和函数类型不匹配时:表达式服从函数返回。,48,对于自定义类型而言,类型转换有两个方向: 外 内 :由构造函数承担; 内 外 :由转换函数承担。,类型转换,class Test int num; public : Test ( int n) num = n; cout“lnitializing ”numendl; Test() cout“Destorying ”numendl; void Print() cout numendl; ;,49,类型转换,void main() Test t(0); / A t = 5 ; / B t . Print(); t = Test(10); / C t . Print(); ,请说出每条语句发生了什么?,调用构造函数,创建0值对象,又调构造函数,创建5值对象,赋给t 后被析构,又调构造函数,创建10值对象,赋给t 后被析构,最后析构 t 对象,5和t 类型不同,5能赋值给t靠的是构造函数,即构造函数提供了类型转换功能。,50,转换函数 格式: operator 类型名();,类型转换,该函数不得写返回类型; 该函数不得写参数; 其中的类型名可以是 int char.各种类型,还可以是: 函数体内定要有 return 语句; 可以被继承; 可以是虚函数; 一个类可以有多个转换函数。 该函数应是类的非静态公有成员,为常函数更佳;注意此时的const应该放在这儿,它与类型名中的const 无关。,const volatile,类型名,* ( ),51,包含了两个转换方向的类, class String char strN; public : String ( char s = 0) / 从外向内转换 strcpy( str , s ); operator char * () return str ; / 从内向外转换 void display( ) const cout str endl; ; void main() String s1; char s2 = “ china”; s1 = s2; cout ( char * ) s1; / 显式调用 cout s1; / 隐含调用 。 这两种都可以。 ,但若类中没写 operator char * () 就不可用cout s1;,52,类型转换的三种方法:,函数法: 用函数来实现类型转换的工作内容,将被转换的对象做实参,函数的返回是目标类型; 运算符法: 用 (目标类型)对象;或 目标类型(对象); 或 static_cast 或 dynamic_cast 定义法: 用类型定义运算符 typedef 旧类型名 新类型名;,53,注意:对于多继承类族,以上转换都会移动子类对象中所含各个父类部分的偏移量。 转换所发生的场合: 赋值、形实结合、 、各种表达式 使用注意: 一旦同时出现,则会出现 二义,故要避免。,这叫“内转外”。由转换函数承担。,这叫“外转内”。由构造函数承担。,54,以下几种转换的组合是安全的:,“内转外”,“外转内”,类型转换的歧义性,class X public: X() X(int) X(char*) ;,class Z public: Z() Z(X void g(Z z ),class Y public: Y() Y(int) ; void f(X x) void f(Y y),void main() f( 1 ); /歧义:调用f( X(1) )还是f( Y(1) ) f( X(1) ); / OK f( Y(1) ); / OK g( “mark” ) ; /错,不能直接从char*转换到Z g( X(“mark”) ); / OK,调用了X(char*)和Z(X&) ,构造函数天然地承担从外向内的数据转换任务,这种身兼二职的功能,有时会给程序添乱:本不想转换的地方它悄悄地给转换了。 为了关闭类的构造函数的这种隐式转换功能,可以用explicit关键字通知编译器。其效果是该构造函数只能用于创建对象,不能用于隐含(不动声色的)转换。要想转换,必须显式地写出转换语句。 用法:在构造函数前面加explicit关键字 如:explicit X(int); 注:1 . 只能用于构造函数; 2 . 只能用于带参构造函数;,explicit关键字的使用,class B public: explicit B(int x=0,bool b = true) ;,void main() void doSomething( B ); / 另外的函数声明 B bObj1; / 创建一个对象 doSomething( bObj1 ); / 没问题 B bObj2(28); / 又创建一个对象 doSomething( 28); / 参数类型不符,由于构造函 数的转换功能被关闭,也没能转换,因此报错! doSomething( B (28) ); / 这就可以了 ,#include class B; class A public: A(const B ,仅一个转换函数也有歧义 (伪拷贝构造函数竟有构造功能),伪拷贝构造函数就是由别的类的对象复制产生本类的对象的函数。 但这个函数根本没使用B类,仅虚晃一枪,class B public: B() x = 100; operator A() const int get() return x; private: int x; ;,转换函数:它想把B对象变成A类的对象。但该函数起着助纣为虐的作用。,若写return A 则错:类型不符; 写return A(*this)则对,但啥都没干。因为这正是A类的构造函数所干的事情。 可见转换函数必须写return语句,否则,它形同虚设。,void main() B bb; cout bb.get() endl; / 100 A a(bb); a.show(); / 是200而非100 B b; cout b.get() endl; / 100 / a = b.operator A(); /将b对象显式调用转换函数,转 换为A类的对象a。 a.show(); / 显示200。 a = A(b); /调用A 类的伪拷贝构造函数为对象a 赋值。 a.show(); / 200 ,通过调用伪拷贝构造函数,只完成了对象的创建,但并没完成对象的拷贝。,若上句去掉注释将出错。为何?,这个转换函数也没完成任务,因为它没有返回,反而破坏了a.,#include class number / 父类 int val; public: number(int i) val=i; operator int () return val; ; class sub_number:public number / 子类 public: sub_number(int i):number(i) ;,转换函数可以被继承,void main() sub_number s_nu(50); cout“对象s_nu 的值:“s_nuendlendl; 50 /子类的对象也享受隐式转换 int j; j=s_nu; / 调用了父类的operator int () cout“整形数j 的值:“jendlendl; / 50 j+=s_nu; / 同上 cout“整形数j 与对象s_nu 的和”jendlendl; / 100,转换函数尽管可以被继承,但仅能完成子类对象中的父类部分的转换,对子类新增的无能为力。,/转换函数以及强制类型转换的破坏作用 #include #include class string public: string(const char *value); / 构造 string(); / 析构 void set(char * val); void show() const; operator char *() const; / 自身尽管是常函数,可是能把对象的数据转换成 非常的char* 交给外界。 private: char *data; ;,一个可怕的例子:,string:string(const char *value) if (value) data = new charstrlen(value) + 1; strcpy(data, value); else data = new char1; *data = 0; inline string:string() delete data; inline void string:set(char * val) strcpy(data, val); inline void string:show() const / 常函数 cout“对象的值:”dataendlendl; ,inline string:operator char*() const / 常函数 return data; void main() const string b(“hello world”); / b是个const对象 string / republic ,静态绑定与动态绑定,绑定 程序自身彼此关联的过程,确定程序中的调用与执行代码间的关系。其实就是确定绝对地址的过程。 然后就是个绑定的时机问题何时绑定。 静态绑定(静态联编) 由编译器在编译时发出调用,由链接器在链接时确定被调用函数的绝对地址。这都固定在代码中了。 动态绑定 编译器在编译时仅对被调函数语法检查,直到程序运行时才通过附加(机制)代码确定要调用的函数。,#include using namespace std; class Point public: Point(double i, double j) x=i; y=j; double Area() const return 0.0; private: double x, y; ; class Rectangle:public Point public: Rectangle(double i, double j, double k, double l); double Area() const return w*h; private: double w,h; ;,静态绑定例,67,Rectangle:Rectangle(double i, double j, double k, double l) :Point(i,j) w=k; h=l; void fun(Point ,68,运行结果: Area=0,#include using namespace std; class Point public: Point(double i, double j) x=i; y=j; virtual double Area() const return 0.0; private: double x, y; ; class Rectangle:public Point public: Rectangle(double i, double j, double k, double l); virtual double Area() const return w*h; private: double w,h; ; /其它函数同例 8.8,动态绑定例,69,void fun(Point ,70,运行结果: Area=375,动态绑定(多态)的功能,通俗地讲,就是使现在写的程序可以调用将来(按规定)写的函数。简直是“未卜先知”。 这一点只有诸葛亮能作到。他在刘备将赴东吴娶亲前,给了赵云三个锦囊,让他在危难时依次打开看,里面分别给出了应对办法。果然料事如神,逢凶化吉。 预设了处理办法。 这种思想和处理技术被充分运用到了COM和CORBA中了。,虚 函 数,虚函数,虚函数是动态绑定的技术基础。 只能用于非静态非友元非内联的成员函数。 在类的声明中,在函数原型之前写virtual。如果一个函数被定义为虚函数,那么,使用基类类型的指针来调用该成员函数,C+能保证所调用的是特定于实际对象的成员函数。 virtual 只用来说明类声明中的原型,不能用在函数实现时。 具有继承性。基类中声明了虚函数,派生类中无论是否说明,同原型的函数都自动为虚函数。 本质:不是重载(overload)而是重写(override)。,虚 函 数,稍后,请解释为什么?,虚函数,调用方式:通过基类类型的指针或引用,执行时会根据指针指向的对象的类型,决定调用哪个类的成员函数。一个指向基类的指针可用来指向从基类公有派生的任何对象,这一事实非常重要,它是C+实现运行时多态的关键。 virtual 的含义:迫使在继承于该类的派生类必须予以重新实现。 virtual 的作用:使继承于该基类的各派生类,都肩负了重写虚函数的责任。 虚函数虽然不可以是友元函数,但可以外派成为另一个类的友元函数。,虚 函 数,虚函数的实现机制,编译器发现某类含有虚函数,则对其生成的对象悄悄地加入一个void 型的指向指针的指针:vptr,并让其指向一个由类维护的虚函数表vtable(其实是个指针数组),每个表项是一个虚函数名(地址),排列次序按虚函数声明的次序排列 。 在类族中,无论是基类还是派生类,都拥有各自的vptr和vtable。相同类型所生成的对象共享了同一个vtable。 该项技术的实质是“将找谁变成到哪去找”不用管找到的是哪一个。,虚 函 数,void *vtable ,类,B,A,. .,void * ptr,virtual func,虚函数机制的图示,派生类新增的虚函数依次排在表的后面。当然,派生类的vtable表项中放的是新的覆盖了父类同名函数的首址。 对象中加入的vptr的位置,会因类是class还是struct而不同:或在对象之前端,或在后端。于是造成了父子是class还是struct的不和(不兼容)。 对象中加入的vptr的位置,还会因编译器的不同而异:有的编译器将vptr加在对象之前端,有的加在后端。VC+是加在前端。 非虚函数不进入vtable表。,虚 函 数,能昭示对象内部结构的例子: #include #include class point3d public: float x,y,z; point3d(float xx, float yy, float zz) x=xx; y=yy; z=zz; virtual point3d() void show() /普通成员函数 coutx“ “y“ “zendl; static point3d origin; /类定义中含有本类的静态对象 ;,虚函数。,point3d point3d:origin( 10,18,20);/静态成员的类外初始化 void main(void) / cout“ ,运行结果: &point3d:dd =0x0012ff64 &point3d:x = 0x0012ff68 (x不在对象的最前面) &point3d:y = 0x0012ff6c &point3d:z = 0x0012ff70 &point3d:x = 4(也证明不在最前面) &point3d:y = 8 &point3d:z = C,只有用C方式才能显示各成员的偏移量,这种方式只能显示各成员的绝对地址,若想用C+的方式,观察其成员的偏移量,可用下面的办法: #define offsetof(S,m) (size_t) S 应是平凡类的类型名;m 是公有成员名; (size_t)&(S*)0)-m)是将0视为S类型的指针后,取其m成员的(地址)偏移量,再强转成整型。,vtable,基类,B,公有继承,void * ptr,继承类族中虚函数机制的图示,virtual func,vtable,子类,D,void * ptr,基类的虚函数,换成子类的虚函数,例 8.4,#include using namespace std; class B0 /基类B0声明 public: /外部接口 virtual void display() /虚成员函数 cout“B0:display()“endl; ; class B1: public B0 /公有派生 public: void display() cout“B1:display()“endl; ; class D1: public B1 /公有派生 public: void display() cout“D1:display()“endl; ;,虚 函 数,自动成为虚函数。,void fun(B0 *ptr) /普通函数 ptr-display(); void main() /主函数 B0 b0, *p; /声明基类对象和指针 B1 b1; /声明派生类对象 D1 d1; /声明派生类对象 p= /调用派生类D1函数成员 ,运行结果: B0:display() B1:display() D1:display(),82,83,当用指针指向一个对象,用指针调用对象的虚函数时, 比如有:class B ; 这个类含有虚函数fun(), 当 B obj; B * p = ,为何使用指针时会动态联编,通过间址找所指向的函数体。,是虚函数在vtable表中的下标位置。,虚函数所带的实参列表。,将对象首址传给this。,84,( 缺一不可) 必须有继承产生的类族; 必须是公有继承(类型兼容); 基类的某成员函数使用了virtual; 派生类的成员函数要重写该虚函数; 派生类的对象要使用指针或引用来调用该虚函数;,动态多态的前提,在派生类重定义虚函数(及重写)时,子类重写的函数必须与父类的函数具有相同的函数签名,包括返回类型,函数名、参数个数、参数类型以及声明次序,只是函数体实现不同。 这几条必须严格遵守,缺一不可。若仅函数名相同,那不是重写,是隐藏,或干脆错误,自然不享受动态联编的机制。 若在派生类中没有重新定义虚函数,则该类的对象将使用其基类的虚函数代码只是继承,没有创新。,“重写”的含义,86,#include class A public: A(void) fc(); virtual void fc() cout“in Class A“endl; virtual A()cout“destructing A object“endl; ; class B:public A public: B(void) fc(); void f() fc(); B() fd(); void fd() cout“destructing B object“endl; ;,调用虚函数不一定动态联编,87,class C:public B public: C(void) void fc() coutA:fc()B()A:fc()C() coutA:fc()B()A:fc() coutB:fd()A:A()只有此句启用了动态联编 coutC:fd()B:B()B:fd()A:A(),同名的成员函数可分为两种:普通和虚函数。 普通的同名函数们在同一个类中只能是重载关系, 例如:void Show ( int , char ) ; void Show ( char * , float ) ; 若在继承类族中,子类又新增了与父类同名的普通函数,此时是隐藏,调用时可以用“ : ”区分, 例如:A : Show ( ); B : Show ( ); 若在继承类族中,子类又新增了与父类同函数签名的虚函数,此时是重写, 但能表现出动态多态。 例如: Aobj.Show ( );是调用 A : Show ( ) 。 Bobj.Show ( );是调用 B : Show ( ) 。,同名成员函数的表现,89,许多人分不清重载,重写和隐藏! 重载:在同一作用域内,函数名相同却有不同的代码实现。 隐藏:在继承树中,子类中再度出现了父类的同名函数,无论形参是否相同,那都是隐藏不是重载。 重写:在继承树中,子类中再现了父类的用虚函数或纯虚函数修饰的同函数签名(此时的同名最严格:函数名、返回类型、形参表都必须相同),这种现象叫重写(覆盖)。注意,若返回类型符合“类型兼容”亦可。即子类中的同名函数返回了父类类型或子类类型。这称为“协变类型”。,重载、重写和隐藏的区别,#include using namespace std; class Base public: void f(int x) cout“Base:f(int)”xendl; void f(float x) cout“Base:f(float)”xendl; void g(float x) cout“Base:g(float)”xendl; void h(float x) cout“Base:h(float)”xendl; virtual void m(float x)cout“Base:m(float)”xendl; ; class derived :public Base public: void f(int x) cout“derived :f(int)”xendl; void g(float x) cout“derived :g(float)”xendl; void h(int x) cout“derived :h(int )”xendl; void m(float x)cout“derived :m(float)”xendl; void m(int x)cout“derived :m(int)”xendl; ;,重载,隐藏,重写,隐藏,重载。 但m(int)和基类的m(float)无关。,若子类只有一个m(int),则为隐藏。,91,class Base public: virtual void foo() const ; / A ; class D:public Base public: /尽管父类的 foo()有virtual,

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论