C++里的继承和多态(上)
继承
1、私有继承:基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
公有继承:基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的访问权限,而基类的私有成员在派生类中是不可见的。
在公有继承时,派生类的成员函数可以访问基类中的公有成员和保护成员;派生类的对象仅可以访问基类中的公有成员。
保护继承:基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数访问,不能被它派生类的对象访问。
2、注意:
1》基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外被直接访问,但需要在派生类中访问,就需要定义成protected。可以看出保护成员限定符是因继承才出现的。
2》public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可以用,因为每个子类对象也都是一个父类对象。
3》procted/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是has-a的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大部分的场景下使用的都是公有继承。
4》不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存在但是在子类中不可见(不可以访问)
5》使用class关键字是默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
6》在实际应用中一般使用的都是public继承,极少场景下才会使用protected/private继承。
3、派生类的默认成员函数:
在继承关系里面,在派生类中如果没有显示的定义这六个成员函数,编译则会默认合成这六个默认的成员函数。
类的六个默认成员函数:
1》构造函数
2》拷贝构造函数
3》析构函数
4》赋值操作重载
5》取地址操作符重载
6》const修饰的取地址操作符重载
4、继承体系中的作用域:
1》在继承体系中基类和派生类是两个不同的作用域
2》子类和父类中有同名成员,子类成员将屏蔽父类对成员的直接访问(在子类成员函数中,可以使用 基类::基类成员 访问)
——隐藏:子类可以隐藏继承的成员变量。对于子类可以从父类继承成员变量,只要子类中定义的成员变量和父类中的成员变量相同时,子类就隐藏了继承的成员变量,即子类对象以及子类自己声明定义的方法操作的变量是子类重新定义的成员变量。 子类也可以隐藏已经继承的方法,子类通过重写来隐藏继承的方法。
方法重写是指:子类中定义一个方法,并且这个方法的名字、返回值类型、参数个数与父类继承的方法完全相同。子类通过方法的重写可以把父类的状态和行为改变为自身的状态和行为。
如果子类想使用被隐藏的父类方法,必须使用super关键字。
——重定义
3》注意在实际继承体系里面最好不要定义同名成员。
5、派生类的构造函数:
1》派生类的数据成员包含了基类中说明的数据成员和派生类中说明的数据成员。
2》构造函数不能被继承,因此,派生类的构造函数必须通过调用基类的构造函数来初始化基类的数据成员。
3》如果派生类中还有子对象时,还应包含对子对象初始化的构造函数。
6、派生类构造函数的调用顺序:
1》基类的构造函数(按照继承列表中的顺序调用)
2》派生类中对象的构造函数(按照在派生类中成员对象声明顺序调用)
3》派生类构造函数
注意:
1》基类没有缺省构造函数,派生类必须要在初始化列表中显示给出基类名和参数列表。
2》基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数。
3》基类定义了带有形参列表的构造函数,派生类就一定要定义构造函数。
补充:缺省构造函数又叫默认构造函数(default constructor)。当声明对象时,编译器会调用一个构造函数。若声明的类中没有声明构造函数,编译器就会自动调用一个缺省构造函数,该函数相当于一个不接受任何参数,不进行任何操作的构造函数。而当类中已经有声明的构造函数时,编译器就不会调用缺省构造函数。
7、派生类的析构函数:由于析构函数也不能被继承,因此在执行派生类的析构函数时,基类的析构函数也将被调用。
执行顺序是:
1》先执行派生类的析构函数,
2》派生类包含成员对象的析构函数(调用顺序和成员对象在类中的声明顺序相反)
3》基类析析构函数(调用顺序和基类在派生列表中声明的顺序相反)
8、构造函数的功能是在创建对象时,用给定的值对对象进行初始化。
没有参数的构造函数称为默认构造函数(缺省构造函数)。默认构造函数有两种:一种是系统自动提供的;另一种是程序员定义的。
使用系统提供的默认构造函数给创建的对象初始化时,外部类对象和静态类对象的所有数据成员为默认值,自动对象的所有数据成员为无意义值。
9、构造函数分两类:一类是带参数的构造函数,可以是一个参数,也可以是多个参数;另一类是默认构造函数,即不带参数的构造函数。
11、子类型:用来描述类型之间的一般和特殊的关系。当有一个已知类型s,它至少另一个类型t的行为,它还可以包含自己的行为,这时,则称类型s是类型t的子类型。子类型的概念涉及行为共享,即代码重用问题,它与继承有着密切的关系。在继承中,公有继承可以实现子类型。
子类型的重要性就在于减轻编写代码的负担,提高了代码重用率。
因此一个函数可以用于某类型的对象,则它也可以用于该类型的各个子类型的对象,这样就不用为处理这些子类型的对象去重载该函数。
12、类型适应:子类型与类型适应是一致的,a类型是b类型的子类型,那么b类型必将适应于a类型。
13、赋值兼容规则:通常在公有继承方式下,派生类是基类的子类型,这时派生类对象与基类对象之间的一些关系规则称为赋值兼容规则.
在公有继承方式下,兼容规则规定:
1》子类对象可以赋值给父类对象
2》父类对象不能赋值给子类对象
3》父类的指针/引用可以指向子类对象
4》子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
使用上述规则,必须注意两点:
1》具有派生类公有继承类的条件。
2》上述三条规定是不可逆的。
——赋值兼容规则:
1》为什么派生类可以给基类赋值?
——派生类访问空间大于基类
3》父类指针/引用可以指向子类对象(多态实现)
14、单继承、多继承、菱形继承:
单继承:一个子类只有一个直接父类时,称这个继承关系为单继承
多继承:一个子类有两个或者两个以上直接父类时,称这个继承关系为多继承
菱形继承:菱形继承存在二义性和数据冗余的问题
——虚继承解决了菱形继承的二义性和数据冗余的问题。
1》虚继承解决了在菱形继承体系里面子类对象包含多分父类对象的数据冗余和浪费空间的问题。
2》虚继承体系看起来很复杂,在实际应用中我们通常不会定义如此复杂的继承体系,一般不到万不得已都不要定义菱形继承的体系结构,英文使用虚继承解决数据冗余问题也带来了性能上的损耗。
15、什么情况下编译器会合成一种缺省构造函数?《深入理解c++对象模型》
——1》类中有类类型成员对象,该成员对象它有自己的缺省构造函数,这时编译器也会在该类中合成一个缺省的构造函数(不一定是继承和派生的关系)
2》继承层次,基类中有缺省构造函数,而派生类中没有构造函数,这时编译器就会在派生类中合成一个缺省的构造函数
3》虚拟继承时,在派生类中会合成缺省构造函数
4》基类含有纯虚函数时,在派生类中会合成缺省构造函数。
——默认构造函数是编译器默认合成的
——缺省构造函数是里面带缺省值的
16、友元与继承:
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
注意:
友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。
在c++中,自定义函数可以充当友元,友元只是能访问指定类的私有和保护成员的自定义函数,不是指定类的成员,自然不能被继承。
使用友元时要注意:
1》友元关系不能被继承
2》友元关系是单向的,不具有交换性
3》友元关系不具有传递性
注意事项:
1》友元可以访问类的私有成员
2》友元只能出现在类定义的内部,友元声明可以出现在任何地方,一般放在类定义的开始或者结尾。
3》友元可以是普通的非成员函数,或者前面定义的其它类的成员函数,或者整个类。
4》类必须将重载函数集中每一个希望设置为友元的函数都声明为友元。
5》友元关系不能被继承,基类的友元对派生类的成员没有特殊的访问权限。如果基类被授予友元关系,则只有几类具有特殊的访问权限。该基类的派生类不能访问授予友元关系的类。
17、继承与静态成员:
基类定义了一个static成员,则整个继承体系里面只有一个这样的成员,无论派生多少个子类,都只有一个static成员实例
多态
1、对象的类型:
静态多态:编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用哪一个函数,如果有对应的函数就调用该函数,否则编译错误。
动态多态:(动态绑定)在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
class base { }; class derive :public base { }; int main() { int a; base b; derive d; base *pb = &b;//pb 基类指针类型(静态类型--编译器在编译过程中确定的类型);;动态类型(它所指向的类型)---base* pb = &d; system( "pause"); return 0; } base *pb=&b; pb=&d;
——这里pb的类型发生了变化,也就是它的动态类型,它的动态类型为derive *
2、多态:函数重载也是一种多态。意思是具有多种形式或形态的情形
静态类型的多态:在编译期间就确定的关系(早绑定)
动态类型的多态:在执行期间判断所引用对象啊的实际类型,根据其实际类型决定调用的相应方法(晚绑定)(通过虚函数的机制实现)
——1》使用virtual实现,
2》通过基类类型的引用或指针的调用。。在运行过程中找指针的指向,先定义一个基类的指针,在指向要调用的派生类
3》在基类中一定要加vietual,,派生类中可以不加;
4》在派生类中需要重新实现这个基类中方法。
3、
class base { public: virtual void funtest()//虚函数 {
cout << "base::funtest()" << endl; } }; class derive :public base//在派生类中包含funtest(),除了基类中的构造函数和析构函数其余的均会被继承 { public: virtual void funtest()//虚函数 { cout << "derive::funtest()" << endl; } }; int main() { derive d; d.funtest(); base b; b.funtest(); base* pb = &b; pb->funtest(); //这里调用的是b的funtest pb = &d; pb->funtest(); //这里调用的是d的funtest system( "pause"); return 0; }
virtual:这个关键字可以实现多态。
需要注意:1》在基类中的虚函数前一定要加virtual关键字。在派生类中重写该函数时,可加可不加virtual关键字。
2》调用时,要用基类的指针/引用指向派生类的对象
动态绑定条件:1》必须是虚函数 2》通过基类类型的引用或者指针调用
4、继承体系中同名成员函数关系:
1》重载:
1》在同一个作用域;
2》函数名相同、参数不同
3》返回值可以不同
2》重写(覆盖):
1》不在同一作用域(分别在基类和派生类)
2》函数名相同、参数相同、返回值相同(协変例外)
3》基类函数必须有virtual关键字
4》访问修饰符可以不同
3》重定义(隐藏):
1》在不同作用域中(分别在基类和派生类)
2》函数名相同
3》在基类和派生类中只要不构成重写就是重定义
5、构造函数不能定义成virtual
---1》构造函数是用来构造对象,virtual需要通过对象调用,,而在构造函数中的virtual,还没有出构造函数,此时并没有成功构造对象,所以不行。
2》当一个构造函数被调用时,它要做的首要事情之一是初始化它的vptr。因此,它只能知道它是当前类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(因为类不知道谁继承他)。所以它使用的vptr必须是对于这个类的vtable。而且,vptr的状态是由最后调用的构造函数确定的。
但是,当这一系列构造函数正在发生时,每个构造函数都已经设置vptr指向他自己的vtable。如果函数调用使用虚机制,它将只产生通过他自己的vtable的调用,而不是最后的vtable(所有构造函数被调用之后才会有最后的vtable)
6、拷贝构造不能定义成virtual,
7、赋值运算符重载可以定义成virtual一般情况下不建议这样做。
8、静态成员函数、友元函数不可以定义成虚函数----因为它们两个没有this指针,虚函数底层是用this指针实现的
——类的普通成员函数(包括虚函数)在编译时加入this指针,通过这种方式可以与对象捆绑,而静态函数编译时不加this指针,因为静态函数是给所有的类对象公用的,因此在编译时没有加this指针,所以无法与对象捆绑,而虚函数是靠着与对象捆绑加上虚函数列表才实现了动态捆绑,所以没有this指针虚函数无从谈起。
——因为在一个类中声明友元时,该友元不是自己的成员函数,自然不能把它声明为虚函数。
但是友元本身可以是虚函数。这个类将她声明为自己的友元时,只是让它可以存取自己的私有变量。
9、纯虚函数
在成员函数的形参后面写上=0,则成员函数为虚函数,包含虚函数的类叫做抽象类(也叫接口类)抽象类不能实例化出对象,
纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。
class test { virtual void funtest() = 0;//没有函数体,只是一个接口,必须在派生类中重新实现 }; int main() { test t;//不允许使用抽象类类型的对象,会报错 system( "pause"); return 0; } 10、虚表 class base { public: virtual void funtest(){}//有一个虚指针 virtual void funtest1(){} virtual void funtest2(){} virtual void funtest3(){} virtual void funtest4(){} }; int main() { base base; base.funtest(); base.funtest1(); base.funtest2(); base.funtest3(); base.funtest4(); system( "pause"); return 0; } class drive :public base { public: virtual void funtest1() { ; } virtual void funtest2() { ; } }; int main() { drive d;//d里面包含一个base,而base里面同上,存的是一个虚指针,指向一个虚表,只不过在派生类中实现了的函数,它的函数地址会发生改变 d.funtest(); d.funtest1(); d.funtest2(); d.funtest3(); d.funtest4();//在派生类中实现了的虚函数,就调用派生类中的,没有在派生类中实现的就调用基类中的 system( "pause"); return 0; } int main() { base base; drive d; base *pa = &base;//pa里面会有(指向)一个虚指针,指向虚表,再来调动虚函数 pa->funtest(); pa->funtest1(); pa->funtest2(); pa->funtest3(); pa->funtest4(); pa = (base*)&d;//d中会有一个base,base里面会包含一个虚指针,指向另一个虚表;;;这里的类型转换是不起作用的,并不会指向上一个虚表 pa->funtest(); pa->funtest1(); pa->funtest2(); pa->funtest3(); pa->funtest4(); system( "pause"); return 0; }