继承(2):
我们继续接着上一节的来讲:
派生类的默认成员函数:
1. 构造函数:
我们之前学习了四个常见的默认构造,我们今天继续来看:
对于我们的派生类的构造函数:
当我们初始化我们的派生类的时候,我们的派生类的自己的那部分,我们就正常的构造函数里面初始化,然后我们的派生类里面的基类的那部分,我们要调用我们的基类的构造函数来进行初始化。
我们看这个图片,我们的派生类的构造函数的书写:我们写这个类的构造函数的时候,初始化我们的基类的时候,会调用我们的基类的默认构造来初始化我们的派生类继承下来的基类的那部分。
如果有默认构造的话,我们可以直接不管。
如果基类没有默认构造的话,我们也可以在派生类里面的构造函数里面传值,在初始化列表里面初始化类。
如果基类没有默认构造的话,也没有在派生类里面的构造函数里面传值,在初始化列表里面初始化类,就会报错。
我们再来看我们的上面的图片,我们的派生类是没有写构造函数的,我们说过,当我们的没有写我们的构造函数(或者是构造函数没有参数,或者是构造函数的参数都是全缺省,(就是我们不需要传参的时候))的时候,这时候我们的编译器就会默认生成构造函数来。
但是我们明白,我们现在的这种程度的默认构造函数是完全不能满足我们的要求的。(我们还是不想自己写构造函数的话,我们可以给我们的成员变量都传上默认的缺省值)。
如果派生类不写构造函数,在调用默认构造函数时,其自定义类型的成员变量会调用自身的默认构造函数。,内置类型的话,看他有没有缺省值,如果没有缺省值的话,他初始化的结果是什么那就要看编译器了。
我们看我们的这两个图片:我们自己写我们的派生类的构造函数,然后我们的派生类继承下来的基类的成员变量_name(我们的这个成员变量,在基类里面是protected类型的,然后public继承下来的,这时候我们的派生类里面可以使用这个成员变量,但是类的外面不可以使用我们的成员变量)是怎么初始化,也是在我们的派生类的初始化列表吗?显然不是的,,,
我们刚才说了,我们在派生类里面初始化我们的基类的成员变量我们要调用基类里面的构造函数来进行初始化:
我们看这个,这个就是我们的派生类对我们的基类里面的成员变量进行初始化,这个有点类似于我们的定义匿名对象的样子;
我们看这个图片:
我们看我们的派生类里面,我们初始化我们的成员变量的时候,我们是吧name成员变量放到了我们的最后面,但是我们看我们的this指针,其实他还是先初始化我们的name的,因为我们之前讲过,他的初始化顺序,是按照我们的声明的顺序来的,我们看我们的派生类,我们的顺序就是先是address,然后是num,但是我们的person(name)这个是继承下来的,这个优先级更高。
2. 拷贝构造:
我们的派生类的拷贝构造可以不需要自己写,因为里面没有需要动态开辟的内存空间。
对于内置类型的成员变量,我们的编译器的默认的拷贝构造我们就可以满足,对于自定义类型的成员变量的话,它会调用自己的拷贝构造来进行拷贝;
我们的父类其实可以和我们的自定义类型看成同一类,他们都是调用自己的拷贝构造来进行拷贝;
但是:
当我们需要进行深拷贝的时候,我们这时候就需要自己来写拷贝构造了;
我们写拷贝构造,我们还是,我们的派生类继承下来的基类的成员变量还是需要我们调用我们的基类的拷贝构造来进行。
但是:我们要拷贝构造,我们要把一个Student类拷贝给另一个新的Student类,但是我们的里面的从基类里面继承下来的name怎么传过去?(你要知道,我们的这个name是已经经历了我们的派生类的构造函数之后的,里面是我们的初始化过的数据,这个我们怎么拿到?)
我们看上面的图片,我们就按照这个逻辑来。
我们看,这个是我们的基类的拷贝构造,我们把我们的派生类的对象传给我们的Person,让p引用我们的派生类对象,但是其实p只能引用派生类里面的从基类里面继承下来的那部分。这时候我们的p就可以得到我们的派生类里面的继承的基类的(初始化过的)name。
//这个就是我们的切片切割;
3. 复制重载:
我们再看我们的派生类的复制重载:
还是一样的,当我们的成员变量里面没有需要我们动态的开辟内存的时候,我们就不需要自己手动的写复制重载,编译器自动实现的就够用了;
我们现在手动来实现一下:
我们来看这个函数,我们判断两个对象不一样的时候,我们把s的里面的成员变量赋值给我们的this指针指向的对象,然后我们的父类的成员变量,我们要调用我们的父类的复制重载,来把我们的s对象里面的父类的成员变量赋值给this指针指向的对象。
这里的父类的赋值重载,我们和我们的拷贝构造比较类似,还是需要切片;
把派生类的对象传给我们的基类的成员函数,这时候我们的基类的对象就引用了我们的派生类对象,但是实际上只是引用了派生类里面的基类的那部分。这时候就可以了。
然后这时候我们需要调用我们的基类的复制重载,但是我们上节讲过,我们的基类和派生类都是各自有各自的域的,我们的这时候我们的基类和派生类里面都有我们的赋值重载函数,所以,当我们的派生类继承下来的时候,如果有两个同名的函数,基类的这个函数就会被隐藏,所以,我们要调用基类里面的复制重载,我们就要说明他的域;
普通的pubilc继承下来的话,我们的派生类里面,如果没有基类和派生类里面相同的函数名构成隐藏的话,我们是可以直接的调用我们的基类里面的成员函数的。
4. 析构函数:
还是一样的,没有资源释放的时候,这个析构函数,我们是不需要自己来写的,我们的派生类里面,自定义类型会调用它的析构,基类当作一个自定义类型的整体,会调用基类的析构;
我们的析构函数,我们是不需要显示的对我们的父类进行析构的,我们的派生类析构结束的时候,会自动地调用我们的父类析构,这里不需要我们显示的调用。
因为我们的构造的顺序是先是父类,然后是子类,析构的顺序是先是子类,后是父类,所以我们在析构完我们的派生类以后,我们就不用管了,编译器会回去调用我们的父类的析构。
目的就是为了保证先子后父,如果我们在派生类的析构函数里面还给我们的父类进行了析构,我们的编译器出去以后还会对我们的父类进行析构,这时候我们的父类就被析构了两次,这就错了。
对于基类:前三个函数,我们的基类要调用的时候,他是自己主动的进行调用,最后的析构函数,编译器会自动的析构父类;
补充:
其实我们的大多数的情况下,我们的构造函数可能需要自己写,其实剩下的三个默认的成员函数,没有动态的内存开辟于释放的话,我们都是不需要自己来写的。
其实我们的派生类的默认的成员函数和我们的普通的类的成员函数是一样的,我们只是把我们的继承下来的基类的,我们看作一个自定义的类型,我们把基类看成一个整体,需要的时候调用它自己的构造,拷贝构造,复制重载,析构。
实现一个不能被继承的类:
我们先看第一种方式:
我们看上面的这个图片:我们把我们的父类的构造函数,我们把他放到我们的private的环境之下,这时候我们进入到我们的派生类的时候,根据我们上面学习的,我们要先是构造我们的基类,但是我们的private的环境下,不论你怎么继承下来,他都是在派生类里面是不可见的,所以,我们就调用不了我们的基类的构造函数,所以,也就不能初始化我们继承下来的基类的成员变量,这时候就会报错。
第二种:
C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了
我们看这个图片:我们给我们的Base类的后面加上一个final,我们的这个类就表示最终类,这个类就不能被继承;
我们的第二种方法比较直观;
继承和友元:
友元关系不能继承:
我们看,我们的派生类可以继承基类的成员变量和成员函数,但是不能继承他的友元关系;
我们看下面的函数,因为我们的两个类里面的成员变量都是protected类型的,外部是不能访问到的,只能在类的里面进行访问,所以,我们就把它定义为友元来访问类里面的成员变量,但是我们只定义了父类的友元,子类没有定义,,我们的子类是继承不了父类的友元的,所以函数访问不了子类的里面的成员变量;
继承和静态成员:
我们的父类的静态的成员变量我们是可以继承下来的。
我们看我们的基类和派生类,我们的这两个类的name不一样,但是静态的成员变量被继承了下来,两个类里面都可以使用我们的这个静态的成员变量,并且他们的静态的成员变量都是同一个成员变量。
强调:
我们在这里要强调一下,我们之前讲解类和对象的时候,我们就说了,我们的静态的成员变量是不包含在我们的任何一个对象里面的,他属于我们的公共的区域。
静态成员变量(也被称作类变量)并非被任何对象单独持有,而是归属于类本身。
静态成员变量在类加载时就会被分配内存,并且在整个程序运行期间只会存在一份,所有对象共享这同一个静态成员变量。
静态成员变量和具体的对象无关,即便没有创建对象,静态成员变量也已经存在。所有对象对静态成员变量的操作,实际上都是对同一个内存位置的数据进行操作。
多继承及其菱形继承问题:
我们的第一个是单继承,第二个是多继承;
我们的这个是菱形继承:我们的Student类和Teacher类,这两个类都是继承的Preson类,但是我们的Assistant继承了Student和Teacher,这时候,继承就有了冗余和二义性的问题;
二义性指的是,当我们的访问Assistant的name的时候,因为我们的Student和Teacher都继承了name变量,所以我们的访问name编译器不知道我们访问的是谁。
那有没有办法来解决这个问题呢?
有的:我们可以使用虚继承来解决,但是这个解决方法也不好,底层很复杂,消耗了一些性能。
在Student和Teacher两个类上加上virtual来修饰。就可以解决我们的冗余和二义性问题。
看一个题目:
我们看一个题目:
我们有三个类,第三个类继承了我们的前两个类,我们的第三个类先继承的Base1,然后继承的Base2,我们刚才的上面我们讲了,在我们的派生类,我们要先构造我们的基类,然后在构造我们的派生类,我们的基类Base是最开始的,我们就先构造他,然后按顺序是Base2。
我们之前还说过,我们的派生类的对象或者地址给我们的基类的变量或者指针,引用和指向的时候,只能引用我们的派生类的里面的基类的部分,指针也是只能指向派生类里面的基类的部分。
我们继续看:
我们看这个图片,这个也是我们的菱形继承:这次我们的虚继承放在我们的B和D的位置。
因为我们的B,D是一起继承了我们的A类;
继承和组合:
在 C++ 里,继承与组合是两种构建类之间关系的重要手段,它们各自具备独特的特点与应用场景。
继承:
在这个示例中,Dog
类继承自 Animal
类,因此 Dog
类的对象可以调用 Animal
类的 eat
方法,同时还能调用自身的 bark
方法。继承的话,派生类可以使用基类的protected域里面的函数和变量
组合:
组合代表的是一种 “has-a” 关系,即一个类 把另一个类 的对象作为其成员。通过组合,能够把不同类的功能组合起来,构建出更复杂的类。
在这个示例中,Car
类包含一个 Engine
类的对象,Car
类的 startCar
方法调用了 Engine
类的 start
方法。但是组合的话,包含类是不能使用对象类里面的protected区域的变量和函数。
继承和组合的比较:
组合的话,耦合度低,各个类的独立性强,修改一个类的话,对另一个类的影响比较小。
继承的话,耦合度高,各个类的独立性弱,修改一个类的话,对其他的类的影响比较大。
继承的话,派生类对基类里面几乎除了private的,都可以使用,但是组合的话,包含类只能让成员类的对象调用函数。