一、继承的相关基本概念
1、继承的定义
在C++中,可以使用继承来使新类得到已定义的一些类中的特性,这就好比与孩子从父亲母亲得到遗传类似,所以我们称原有的类为基类或父类,用原有类来生成新的类的过程称为派生,所以生成的新类称之为派生类或者子类。
2、 继承的声明
在继承中和上面所说的遗传是有区别的,孩子只能遗传其父母的一些基因,而在C++的继承中,一个新的类是可以继承多个不同的类,被称为多重继承。所以继承分为单继承和多继承。
继承的定义格式如下:
在一个类中,我们知道其成员的访问类型分为三种,公有(public),保护(protected)和私有(private)。相应的继承方式也分为这三种,他们的区别也就在于派生类的成员和类外对象在访问它从基类继承来的的成员的访问权限不同。
关于三种继承方式后的成员访问权限变换关系如下表所示:
其实这个表我自己是看着很烦,其实很容易记的,根本就不需要看这个表的。你只要记得基类的private成员在派生类中始终是不可访问的,然后记着private>protected>public,高访问权限的继承方式会改变低的访问权限成员的访问权限。说起来还是比较绕口,其实并不需要记,你看一遍肯定会牢记在心的,不信你试试看。
可以看出来,不管是哪一种继承方式,在派生类内都可以访问基类的非私有成员,基类的私有成员虽然被继承了,但是并不是可见的。而对于保护和私有继承方式,类外对象并不能访问基类的成员,公有继承方式下可以访问基类的公有成员。
二、构造函数和析构函数
在派生类中,从基类继承而来的成员初始化需要调用基类的构造函数,派生类中新增的成员初始化则调用自己的构造函数。
1.继承中的构造函数
这里,当基类没有定义自己的构造函数,则在派生类中也可以不用定义,在构造对象时自行调用默认构造函数,但一般在编译器里会认为构造函数并不做什么事情所以对代码进行优化,并不生成默认构造函数,当你在转到反汇编看汇编代码的时候是看不到调用构造函数的语句的。
当基类显式地定义了自己的构造函数,则编译器为派生类生成默认构造函数,而且当基类没有缺省的构造函数的时候,就必须为派生类定义构造函数且在初始化列表中显式的给出基类名和其参数列表,不然在派生过程中编译器就不知道如何调用基类构造函数。
上面这两点其实与在类中包含另一个类的对象的情况差不多,我们也很容易理解。那么派生类到底能不能继承基类的构造函数呢?
这一点很多人说法都不一样,有人说派生类继承了基类的构造函数,因为你在构造一个派生类对象的时候会调用基类的构造函数。其实准确的说应该是带引号的继承,之所以能够调用基类的构造函数,是因为编译器使基类的构造函数在派生类中可见,在创建派生类的对象时会先调用基类的构造函数。
构造函数的调用顺序:
因为在创建派生类对象的时候,要先给它继承基类的成员,所以当程序走到派生类的构造函数的时候会先调用基类的构造函数,所以对于构造函数的调用顺序,是按照继承列表的顺序依次调用每一个基类的构造函数,然后调用派生类自己的构造函数,再执行它的函数体。当然如果派生类中还有别的类对象,则先调用此对象类的构造函数再调用派生类自己的构造函数。
2.继承中的析构函数
析构函数是不能被继承的,同时一般会把基类的析构函数定义成虚析构函数:
1 class Base 2 { 3 public: 4 Base(){} 5 virtual ~Base() 6 { 7 cout << "~Base()" << endl; 8 } 9 public: 10 int _pub1; 11 }; 12 class Derived:public Base 13 { 14 public: 15 Derived(int k=1) 16 { 17 buf = new char[k]; 18 } 19 ~Derived() 20 { 21 delete[] buf; 22 cout << "~Derived()" << endl; 23 } 24 public: 25 char* buf; 26 }; 27 28 void test() 29 { 30 31 Base* b = new Derived(5); 32 delete b; 33 } 34 int main() 35 { 36 test(); 37 return 0; 38 }
如果不定义成虚函数,在delete时只会调用基类的析构函数而不会调用派生类的析构函数而导致内存泄漏。虚函数在这里不细讲了。
析构函数的调用顺序:
析构函数的调用情况,我们一般认为和压栈类似,所以析构函数的调用顺序如下:
三、继承中的同名隐藏
在C++中我们知道有重载,当在同一作用域内函数名相同且函数的参数列表不同,就会构成重载,这样就可以根据传参的不同来调用相应的函数,而不会存在二义性。
但是在派生类中如果有一个和基类中同名的函数,那么在派生类中基类的这个函数就是被屏蔽的,当你用派生类对象调用这个函数一定是调用派生类中的函数,即使两个函数的参数列表不同。但其实基类的这个函数还是被继承了的,要想调用可以使用作用域解析符进行调用。(对于一个同名的成员也是同样的道理)。例子如下:
1 class A 2 { 3 public: 4 void test(int) 5 { 6 cout << "test1()" << endl; 7 } 8 9 public: 10 int _pub1; 11 protected: 12 int _pro1; 13 private: 14 int _pri1; 15 }; 16 17 class B :public A 18 { 19 public: 20 void test() 21 { 22 cout << "test2()" << endl; 23 } 24 public: 25 int _pub2; 26 protected: 27 int _pro2; 28 private: 29 int _pri2; 30 }; 31 int main() 32 { 33 B b; 34 b.test(); //编译错误 35 b.test(3); //编译错误 36 b.A::test(3); //正确 37 return 0; 38 }
四、继承中的赋值兼容规则
在此之前,我们看看基类和派生类的对象模型:
1 class Base 2 { 3 public: 4 Base() { cout << "Base()" << endl; } 5 ~Base() { cout << "~Base()" << endl; } 6 public: 7 int _pub1; 8 protected: 9 int _pro1; 10 11 }; 12 class Derived :public Base 13 { 14 public: 15 Derived() { cout << "Derived()" << endl; } 16 ~Derived() { cout << "~Derived()" << endl; } 17 public: 18 int _pub2; 19 protected: 20 int _pro2; 21 }; 22 int main() 23 { 24 return 0; 25 }
假如就这样定义基类和派生类,那么派生类继承了基类的_pub1和pro1成员
赋值兼容规则如下:
1、派生类对象可以直接赋值给基类对象
基类对象不能赋值给派生类对象
2、基类类型指针可以指向派生类对象(派生类对象可以初始化基类的引用)
派生类类型指针不可以指向基类对象
这里也比较容易理解(当然是在public继承下),一个派生类对象本来就是从它的基类继承而来的,向上图中的对象模型,在派生类中有与基类对应的一个模块是继承来的成员,那么在赋值过程中编译器是可以把相应的基类部分赋值给基类对象。而对于把一个基类对象赋值给派生类对象的话,
五、理解“is a”和“has a”
我一开始就很不理解为啥要总结这么样的关系,还这么抽象,什么东西啊。后来写写代码也确实慢慢领会了一点,还是有那吗一丝丝韵味在其中的。
is a:
is a就是有一个,对于public继承,就有着is a特性。在上面的赋值兼容规则中也说到了,一个派生类对象时可以赋值给基类对象的,所以派生类可以代替任何需要直接基类的地方。is a就是代表了这种继承关系。对于多继承或者在派生类对象中有新增加的东西,这种关系相当于is like。
has a:
has a一般用来描述是组合这种关系的,就是在某一个类中有另一个类。那么在这个类中就可以用它包含的类的成员及成员函数,有时候我们可以把保护和私有继承也看成是一种has a关系,因为只有在类内才可以访问基类的成员。
is a相当于父亲在儿子家干活,而has a相当于雇别人在家里干活。大概就是这么个意思,大家能理解就行了,对于组合和继承这里就不展开讲了,它们各有优缺点和用武之地。在我们利用继承的时候,并不是说我们需要在这个类中使用另一个类的某些东西就继承它,要保证这个类是和基类是有is a关系的,比如说老虎是动物的一种,这才叫继承。