继承与多态
合集下载
相关主题
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
class A { public: virtual void f1() {} void f2() { cout << "f2()" << endl; } void f3() { cout << this->_a << endl; } int _a; }; int main() { A* p = NULL; p->f1(); p->f2(); p->f3(); } //× //√ //×
①普通的菱形继承对象模型
此时Assistant的大小为20,8+8+4=20. ②菱形虚拟继承对象模型
class B { public: int a = 1; }; class B1 :virtual public B { int b = 2; }; class B2 :virtual public B { int c = 3; }; class D :public B1 , public B2 { public: int d = 4; };
继承与多态
一、继承
1.概念 继承就是面向对象程序设计使代码复用的的手段,好比“龙生龙凤生凤”,在原有类的基础上添加新的功能所产生的新类叫做派生 类。 2.定义格式 class D:public B //三种继承类型:public protected private //class:默认的都是private,struct:默认的都是public;
剖析:
此时Assistant的大小为24,8+8+4+4=24.
二、多态
1.概念 字面意思就是多种形态,不同的函数完成不同的功能,就是多态,C++是通过虚函数来实现多态性。例如买票对象就是不同的,普 通人全票,大学生半价票,军人免费,多态就可以实现多种情况的调用。 示例:
class B { public: virtual void print() { cout << "B"; } }; class D :public B { public: void print() { cout << "D"; } }; void Fun(B& p) { p.print(); } void test() { B b; D d; Fun(b); Fun(d); //B &p = b; //p.print(); //B &pp = d; //pp.print(); }
直接上结果
剖析:只有虚函数才会放到虚表中,派生类和基类不共用虚表,也就是说每个类各自最多只有一张虚表,同一个类对象是共用一张 剖析 虚表的。对于派生类B对象来说,它继承了父类,先将父类对象A拷贝过来,如果B有该虚函数,就将该函数进行覆盖,如果没有, 就使用父类的,最后,再按次序,将自己独有的虚函数加入到虚表中。
typedef void(*FUN_TEST)(); void PrintVtable(int *table) { printf("table:%p\n", table); for (int i = 0; NULL != table[i]; i++) { printf("i:%d table:%p--->", i,table[i]); FUN_TEST f = (FUN_TEST)table[i]; f(); } } void test() { D d; B1 b1; B2 b2; PrintVtable((int*)*(int*)&d); PrintVtable((int*)*((int*)&d+sizeof(b1)/2)); } //走两步,一步跨四个字节
剖析:虽然指针是空指针,但是编译期间都没有什么问题,没有类型错误。运行时,第一个和第三个错误,第二个因为是虚函数, 头四个字节是指向虚表的指针,一旦解引用空指针,会造成问题。第三个也是因为解引用了空指针。 第二题:单继承派生类对象模型
class A { public: virtual void f1() { cout<<"A::f1()"<<endl; } virtual void f2() { cout<<"A::f2()"<<endl; } void f3() { cout<<"A::f3()"<<endl; } private: int _a = 5; }; class B : public A { public: virtual void f1() { cout<<"B::f1()"<<endl; } virtual void f3() { cout << "B::f3()" << endl;
8.单继承虚表剖析(虚函数成员覆盖) 对于有虚函数的类,编译器都会维护一张虚表,对象的前四个字节就是指向虚表的指针,可以通过虚表查找到实际需要调用哪个函
数,这张表相当于一个指针数组。如果没有使用基类对派生类进行操作,就不能定义为虚函数,编译器维护的这张虚表存放虚函数 指针,需要进行间接寻址,增加了内存开销。 第一题:
} void f4() { cout << "B::f4()" << endl; } private: int _b=10; };
//定义一个函数指针 typedef void(*FUN_TEST)();
void PrintVTable(int *vTable) { printf("vTable:%p\n", vTable); //虚表以NULL结尾,是一个指针数组。 for (size_t i=0; vTable[i] != NULL; vTable++) { printf("vTable[i]:%p->", vTable[i]); FUN_TEST f = (FUN_TEST)vTable[i]; f(); } } int main() { A a; B b; //先取对象a的地址,取4个字节,并解引用得到里面的内容,是函数的地址,再取4个字节。 PrintVTable((int*)(*(int*)&a)); PrintVTable((int*)(*(int*)&b)); getchar(); return 0; }
(2) 子类对象可以给父类对象赋值。(分割/切片) 父类对象不能赋值给子类对象。(访问_d空间会出错) 父类的指针/引用可以指向子类。 子类的指针/引用不可以指向父类。(可以通过通过强制类型转换)
Байду номын сангаас
8.友元关系不能继承,基类友元不能访问子类私有成员和保护成员。 9.继承与静态成员 注意事项: (1)静态成员变量需要在类外进行声明。 (2)一个静态成员变量在整个继承体系中只有一份,既属于父类又属于子类,无论派生了多少个子类。 10.单继承&多继承&菱形继承 (1)单继承 一个子类只有一个直接父类。 (2)多继承 一个子类有两个或以上直接父类。 (3)菱形继承 由两个单继承和一个多继承构成。但是存在二义性和数据冗余问题(子类去访问父类成员变量不知道访问哪一个)。可以利用 虚拟继承解决,虽然可以解决,但是也会带来一些性能上的损耗,还要利用地址去查找虚基表(偏移量表格),进行间接寻址。 (4)虚拟继承 形如:class class D :virtual public B
9.多继承虚表剖析(虚函数成员覆盖) 例题:
class B1 { public: virtual void f1() { cout << "B1::f1()"<<endl; } virtual void f2()
{ cout << "B1::f2()" << endl; } int _b1 = 5; }; class B2 { public: virtual void f1() { cout << "B2::f1()" << endl; } virtual void f2() { cout << "B2::f2()" << endl; } int _b2 = 10; }; class D :public B1, public B2 { public: D() {} virtual void f1() { cout << "D::f1()" << endl; } virtual void f3() { cout << "D::f3()" << endl; } int _d = 20; };
2.种类
(1)普通调用和类型有关,多态调用和对象有关。 (2)静态多态:编译器在编译期间完成,根据函数实参类型判断需要调用哪个函数,如果没有就进行报错。 (3)动态多态:也称动态绑定,在程序执行期间根据所引用对象的实际类型,判断调用哪个函数。 3.动态绑定条件 (1)必须是虚函数,在派生类中必须对基类的虚函数进行重写。 (2)通过基类类型的引用或者指针调用虚函数。 4.如何构成重写(覆盖)? (1)概念:重写又叫覆盖,在继承体系中,如果基类有虚函数,并且在派生类中有和基类虚函数原型完全相同,会发生重写。将 基类的虚函数拷贝下来,如果派生类将某个虚函数重写了,那么替换为派生类的,最后将自己的虚函数跟在后面。 (2)重写条件:派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同 (协变除外)。 基类的virtual关 键字必须有,派生类最好也写上。(父类是虚函数,子类中继续保持) (3)协变:返回值可以不同,依然可以构成重写,分别是父子关系类型的指针和引用。 5.哪些函数可以定义为虚函数? (1)构造函数-->不可以 (2)拷贝构造-->不可以 (3)赋值运算符-->可以,但最好不要 (4)友元函数-->不可以 (5)析构函数-->最好将基类的析构函数声明为虚函数 1.虽然派生类与基类的构造函数名不同,但是可以构成重载,编译器做了特殊处理。 2.析构函数构成多态,可以保证正确调用对应的虚函数。 3.例如: B* p = new D; 会去调用派生类的析构函数, delete p; //如果没有将基类的析构函数声明为虚函数,当D中有资源的释放时,此时编译器不 造成内存泄漏。(因为没有构成多态) (6)注意事项 1.虚表是所有类对象实例共用的。 2.不要在构造函数和析构函数里面调用虚函数,有可能对象还不完整,可能会出现未定义的情况。 3.如果在类外定义虚函数,只能在声明时加virtual,定义时不用加(为了不给编译器造成负担)。 4.内联函数不能为虚函数,直接展开代码进行替换,都没有地址。 6.纯虚函数 (1)概念 在成员函数的形参列表后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。纯虚 函数在派生类中重新定义后,派生类才能实例化出对象。 (2)一句话,纯虚函数就是为了强制派生类进行重写。就怕你不重写。 7.继承体系同名成员函数的关系
取小的访问限定符
3.访问限定符的作用 (1)如果基类成员不想在类外直接访问,但需要在派生类函数 函数内访问,就定义为protected,所以说明保护成员限定符是因为继承 才出现的。 (2)基类的私有成员是不能在派生类中被访问的(不可见的),虽然它确实存在于派生类里。 (3)不管是哪种继承方式,在派生类函数 函数里都可以访问公有成员和保护成员。 (4)实际应用一般都是使用public继承。 4.派生类的默认成员函数 (1)与基类一样,如果没有显示定义,编译器系统会默认合成这六个默认的成员函数。 (2)如果基类没有缺省的构造函数,派生类必须要在初始化列表中显示给出基类名和参数列表。(编译器不会为派生类自动合成 默认的构造函数)。 (3)基类定义了带有形参表的构造函数,派生类就一定定义构造函数。(派生类合成的默认构造函数没有参数,不能调用有参的 基类) 5.继承关系中构造函数(析构函数)的调用顺序 正:基类的构造函数-->派生类中对象构造函数-->派生类构造函数体。 反:派生类析构函数-->派生类中对象析构函数-->基类的析构函数。 6.继承体系中的作用域 (1)基类和派生类属于两个不同的作用域。 (2)同名隐藏:同名成员,子类优先访问派生类,屏蔽父类(可以使用D.B::_d D.B::_d)。 (3)继承体系里面最好不要定义同名的成员。 (4)同名隐藏与类型无关,与返回值与参数列表无关。 7.赋值兼容规则(public继承) (1)派生类对象是基类的对象
①普通的菱形继承对象模型
此时Assistant的大小为20,8+8+4=20. ②菱形虚拟继承对象模型
class B { public: int a = 1; }; class B1 :virtual public B { int b = 2; }; class B2 :virtual public B { int c = 3; }; class D :public B1 , public B2 { public: int d = 4; };
继承与多态
一、继承
1.概念 继承就是面向对象程序设计使代码复用的的手段,好比“龙生龙凤生凤”,在原有类的基础上添加新的功能所产生的新类叫做派生 类。 2.定义格式 class D:public B //三种继承类型:public protected private //class:默认的都是private,struct:默认的都是public;
剖析:
此时Assistant的大小为24,8+8+4+4=24.
二、多态
1.概念 字面意思就是多种形态,不同的函数完成不同的功能,就是多态,C++是通过虚函数来实现多态性。例如买票对象就是不同的,普 通人全票,大学生半价票,军人免费,多态就可以实现多种情况的调用。 示例:
class B { public: virtual void print() { cout << "B"; } }; class D :public B { public: void print() { cout << "D"; } }; void Fun(B& p) { p.print(); } void test() { B b; D d; Fun(b); Fun(d); //B &p = b; //p.print(); //B &pp = d; //pp.print(); }
直接上结果
剖析:只有虚函数才会放到虚表中,派生类和基类不共用虚表,也就是说每个类各自最多只有一张虚表,同一个类对象是共用一张 剖析 虚表的。对于派生类B对象来说,它继承了父类,先将父类对象A拷贝过来,如果B有该虚函数,就将该函数进行覆盖,如果没有, 就使用父类的,最后,再按次序,将自己独有的虚函数加入到虚表中。
typedef void(*FUN_TEST)(); void PrintVtable(int *table) { printf("table:%p\n", table); for (int i = 0; NULL != table[i]; i++) { printf("i:%d table:%p--->", i,table[i]); FUN_TEST f = (FUN_TEST)table[i]; f(); } } void test() { D d; B1 b1; B2 b2; PrintVtable((int*)*(int*)&d); PrintVtable((int*)*((int*)&d+sizeof(b1)/2)); } //走两步,一步跨四个字节
剖析:虽然指针是空指针,但是编译期间都没有什么问题,没有类型错误。运行时,第一个和第三个错误,第二个因为是虚函数, 头四个字节是指向虚表的指针,一旦解引用空指针,会造成问题。第三个也是因为解引用了空指针。 第二题:单继承派生类对象模型
class A { public: virtual void f1() { cout<<"A::f1()"<<endl; } virtual void f2() { cout<<"A::f2()"<<endl; } void f3() { cout<<"A::f3()"<<endl; } private: int _a = 5; }; class B : public A { public: virtual void f1() { cout<<"B::f1()"<<endl; } virtual void f3() { cout << "B::f3()" << endl;
8.单继承虚表剖析(虚函数成员覆盖) 对于有虚函数的类,编译器都会维护一张虚表,对象的前四个字节就是指向虚表的指针,可以通过虚表查找到实际需要调用哪个函
数,这张表相当于一个指针数组。如果没有使用基类对派生类进行操作,就不能定义为虚函数,编译器维护的这张虚表存放虚函数 指针,需要进行间接寻址,增加了内存开销。 第一题:
} void f4() { cout << "B::f4()" << endl; } private: int _b=10; };
//定义一个函数指针 typedef void(*FUN_TEST)();
void PrintVTable(int *vTable) { printf("vTable:%p\n", vTable); //虚表以NULL结尾,是一个指针数组。 for (size_t i=0; vTable[i] != NULL; vTable++) { printf("vTable[i]:%p->", vTable[i]); FUN_TEST f = (FUN_TEST)vTable[i]; f(); } } int main() { A a; B b; //先取对象a的地址,取4个字节,并解引用得到里面的内容,是函数的地址,再取4个字节。 PrintVTable((int*)(*(int*)&a)); PrintVTable((int*)(*(int*)&b)); getchar(); return 0; }
(2) 子类对象可以给父类对象赋值。(分割/切片) 父类对象不能赋值给子类对象。(访问_d空间会出错) 父类的指针/引用可以指向子类。 子类的指针/引用不可以指向父类。(可以通过通过强制类型转换)
Байду номын сангаас
8.友元关系不能继承,基类友元不能访问子类私有成员和保护成员。 9.继承与静态成员 注意事项: (1)静态成员变量需要在类外进行声明。 (2)一个静态成员变量在整个继承体系中只有一份,既属于父类又属于子类,无论派生了多少个子类。 10.单继承&多继承&菱形继承 (1)单继承 一个子类只有一个直接父类。 (2)多继承 一个子类有两个或以上直接父类。 (3)菱形继承 由两个单继承和一个多继承构成。但是存在二义性和数据冗余问题(子类去访问父类成员变量不知道访问哪一个)。可以利用 虚拟继承解决,虽然可以解决,但是也会带来一些性能上的损耗,还要利用地址去查找虚基表(偏移量表格),进行间接寻址。 (4)虚拟继承 形如:class class D :virtual public B
9.多继承虚表剖析(虚函数成员覆盖) 例题:
class B1 { public: virtual void f1() { cout << "B1::f1()"<<endl; } virtual void f2()
{ cout << "B1::f2()" << endl; } int _b1 = 5; }; class B2 { public: virtual void f1() { cout << "B2::f1()" << endl; } virtual void f2() { cout << "B2::f2()" << endl; } int _b2 = 10; }; class D :public B1, public B2 { public: D() {} virtual void f1() { cout << "D::f1()" << endl; } virtual void f3() { cout << "D::f3()" << endl; } int _d = 20; };
2.种类
(1)普通调用和类型有关,多态调用和对象有关。 (2)静态多态:编译器在编译期间完成,根据函数实参类型判断需要调用哪个函数,如果没有就进行报错。 (3)动态多态:也称动态绑定,在程序执行期间根据所引用对象的实际类型,判断调用哪个函数。 3.动态绑定条件 (1)必须是虚函数,在派生类中必须对基类的虚函数进行重写。 (2)通过基类类型的引用或者指针调用虚函数。 4.如何构成重写(覆盖)? (1)概念:重写又叫覆盖,在继承体系中,如果基类有虚函数,并且在派生类中有和基类虚函数原型完全相同,会发生重写。将 基类的虚函数拷贝下来,如果派生类将某个虚函数重写了,那么替换为派生类的,最后将自己的虚函数跟在后面。 (2)重写条件:派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同 (协变除外)。 基类的virtual关 键字必须有,派生类最好也写上。(父类是虚函数,子类中继续保持) (3)协变:返回值可以不同,依然可以构成重写,分别是父子关系类型的指针和引用。 5.哪些函数可以定义为虚函数? (1)构造函数-->不可以 (2)拷贝构造-->不可以 (3)赋值运算符-->可以,但最好不要 (4)友元函数-->不可以 (5)析构函数-->最好将基类的析构函数声明为虚函数 1.虽然派生类与基类的构造函数名不同,但是可以构成重载,编译器做了特殊处理。 2.析构函数构成多态,可以保证正确调用对应的虚函数。 3.例如: B* p = new D; 会去调用派生类的析构函数, delete p; //如果没有将基类的析构函数声明为虚函数,当D中有资源的释放时,此时编译器不 造成内存泄漏。(因为没有构成多态) (6)注意事项 1.虚表是所有类对象实例共用的。 2.不要在构造函数和析构函数里面调用虚函数,有可能对象还不完整,可能会出现未定义的情况。 3.如果在类外定义虚函数,只能在声明时加virtual,定义时不用加(为了不给编译器造成负担)。 4.内联函数不能为虚函数,直接展开代码进行替换,都没有地址。 6.纯虚函数 (1)概念 在成员函数的形参列表后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。纯虚 函数在派生类中重新定义后,派生类才能实例化出对象。 (2)一句话,纯虚函数就是为了强制派生类进行重写。就怕你不重写。 7.继承体系同名成员函数的关系
取小的访问限定符
3.访问限定符的作用 (1)如果基类成员不想在类外直接访问,但需要在派生类函数 函数内访问,就定义为protected,所以说明保护成员限定符是因为继承 才出现的。 (2)基类的私有成员是不能在派生类中被访问的(不可见的),虽然它确实存在于派生类里。 (3)不管是哪种继承方式,在派生类函数 函数里都可以访问公有成员和保护成员。 (4)实际应用一般都是使用public继承。 4.派生类的默认成员函数 (1)与基类一样,如果没有显示定义,编译器系统会默认合成这六个默认的成员函数。 (2)如果基类没有缺省的构造函数,派生类必须要在初始化列表中显示给出基类名和参数列表。(编译器不会为派生类自动合成 默认的构造函数)。 (3)基类定义了带有形参表的构造函数,派生类就一定定义构造函数。(派生类合成的默认构造函数没有参数,不能调用有参的 基类) 5.继承关系中构造函数(析构函数)的调用顺序 正:基类的构造函数-->派生类中对象构造函数-->派生类构造函数体。 反:派生类析构函数-->派生类中对象析构函数-->基类的析构函数。 6.继承体系中的作用域 (1)基类和派生类属于两个不同的作用域。 (2)同名隐藏:同名成员,子类优先访问派生类,屏蔽父类(可以使用D.B::_d D.B::_d)。 (3)继承体系里面最好不要定义同名的成员。 (4)同名隐藏与类型无关,与返回值与参数列表无关。 7.赋值兼容规则(public继承) (1)派生类对象是基类的对象