要是这个继承方式我们不显示去写的话,那么默认就是私有继承,当然我们一般都是去显示的写出继承方式,而且最常用的继承方式也是public
而且在继承这里就体现出来了protected的意义了,它在类里面和private没啥区别,但是在继承这里可以显示在子类里面,而private类虽然可以被子类继承但是不能显示在子类里面
下面这个是切片
看这个图,首先先明白一点就是子类对象的值可以赋给父类对象,但是反过来父类对象的值是不能赋给子类的,原理很简单,父类里面的东西子类里面都有,因为是子类继承父类呀,但是子类里面的东西父类里面不一定有。
然后就是像这个p=s这个中间是不存在类型转换的,跟下面的这个是不一样的
这个i=d 实际上是有一个int类型的中间变量,先将这个d的值赋给这个中间变量,然后再把这个中间变量的值赋给i,然后为啥是写成const int& ri=d而不能写成int & ri=d呢,原因就是这个赋值的过程产生的中间变量具有常性,所以不能直接赋给引用。
然后这里有一个问题就是当子类和父类里面都有一个同名的成员,就像这个子类和父类里面都有一个_num,那么再子类里面去调用这个_num的时候调用的是哪个呢??
答案是调用子类自己的,这个就跟那个局部全局那个作用域一样,遵循的是就近原则!!
这个过程叫隐藏
所以的话如果我们非要在这种重定义(隐藏)的情况下在子类里面去调用父类,就可以使用 基类:基类成员 这样去显示访问
还有要注意的一点就是如果是成员函数的隐藏,隐藏条件是函数名相同就可以隐藏,不必考虑参数以及参数类型
看下面这个题,注意首先明确重载的前提是在同一个作用域内!!!
而下面这个题父类和子类肯定是在两个不同的作用域里面的,所以肯定不是重载,重写后面会讲,这里也不是重写
// 两个func什么关系
// 重载 重写 重定义/隐藏 编译报错
// ->重定义/隐藏
// 父类和子类的同名成员函数,函数名相同就构成隐藏
//class A
//{
//public:
// void fun()
// {
// cout << "A::func()" << endl;
// }
//};
//class B : public A
//{
//public:
// void fun(int i)
// {
// cout << "B::func(int i)->" << i << endl;
// }
//};
//
//void Test()
//{
// B b;
// b.fun(10);
//};
这里这个 b.fun(10); 就是一个重定义
对比这个题下面还有一个编译报错的
分析:就是b.fun()肯定会是去调用子类的这个fun函数的,但是我们调用的时候没有传参,又因为这个当父类和子类有同名的成员的时候,我们调用原则遵循的是就近原则(隐藏父类成员)所以这里肯定也调用不了父类里面的fun函数,所以只能编译报错,当然我们要想显示的去调用父类里面的fun函数也是可以的
写成这样:b.A::fun();
下面还有一个东西就是
// 派生类中
// 1、构造函数,父类成员调用父类的构造函数完成初始化
这个是实现这个子类里面的构造、拷贝构造以及赋值的重载,可以明显的看出就是遵循那个父类成员都是调用的父类里面的构造函数完成的初始化,只有这个_num是子类里面的成员所以直接在子类里面初始化,这里我们在实现子类的拷贝构造的时候首先还是要用父类的拷贝构造拷贝子类里面的父类成员,但是这里我们传参是一个s(子类对象),这个就是前面的切片的概念
还有一个就是为啥在实现子类的赋值运算符重载的时候里面显示调用了父类的赋值重载
就是这个
那是因为这个operator和子类的这个operator函数名相同所以就会默认就近原则调用子类的赋值重载,就循环下去了,所以肯定完蛋,所以这里我们显示调用父类!
前面这个说明派生类(子类),首先里面的基类对象也就是父类对象它的初始化/清理/拷贝这些工作都是需要去调用父类对应函数去完成的,然后对于派生类里面自己的内置类型和自定义类型遵循的原则就跟之前讲的普通类成员一样,对于像析构和构造函数这种的,内置类型编译器不处理(需要我们自己去写构造析构),而自定义类型会调用其对应的构造和析构
而对于拷贝赋值,内置类型是浅拷贝(值拷贝),自定义类型调用对应的拷贝/赋值
下面是继承子类里面的析构,有两怪
第一怪:1、子类析构函数和父类析构函数构成隐藏关系。(由于多态关系需求,所有析构函数都会特殊处理成destructor函数名)
所以就像这个我们必须指明作用域跟前面调用父类的构造什么的去初始化基类的父类成员一样,但是我们又不能这么写,原因是第二怪
第二怪:子类先析构,父类再析构。子类析构函数不需要显示调用父类析构,子类析构后会自动调用父类析构
下面要说的就是这个继承是不会继承父类里面的友元关系的(就好比你爹的朋友会是你得朋友吗???)
所以就像这个Display函数(父类的友元函数)中要是想调用stuNum(子类的成员变量)的话是不允许的
子类是有独立的对象模型的,继承只是说明父类里面有的成员子类里面也要有,但是二者是相对独立的所以像这个给父类的这个_name成员赋值的时候父类对象和子类对象各管各的,这个是对于普通类型的成员变量来说的
但是对于静态成员的话就不一样,对于静态成员,子类和父类是同一份的
还有就是父类的静态成员就用父类去初始化就行
下面会解释
下面这个题我们首先要知道一个事情就是静态成员变量是被这个类所有而不是被某一个单独的对象所有,它是被这个类的所有对象所共有的!同时也是属于所有的派生类及其对象的!(所以static是没有this指针的)
分析:
首先*ptr定义为一个空指针,然后第一个当我们想要去调用这个_name(是一个成员变量,所以需要到ptr这个所指向的对象里面去找,那么就得需要ptr去解引用得到这个对象)的时候是调用不了的,因为它传递的是一个空指针,相当于这里,像下面的print函数是没有在这个对象里面的,对象里面只会存储成员,而函数在代码段,所以这里当调用ptr->Print()的时候传递的只是一个空指针,并不会去解引用(所以我们不要看到->或者(*).就以为是解引用获得这个对象啊!!!!)当然啊像下面要是在print函数里面调用成员_name的话那么也会报错
所以像下面的ptr->_count 和(*ptr).Print()和(*ptr)._count也是同理,并没有发生解引用。这个些个传空指针的其实就是起一个告诉编译器我们要去哪个类里面去找到这个我们要调用的函数或者静态成员变量
下面我们引入多继承(就是一个类继承了多个基类),但是这个时候会出现一个恶心的bug就是菱形继承,像下面这个样子,所以为了解决菱形继承带来的问题我们引入了虚拟继承(virtual)
这个虚继承是在这个腰部的位置去虚继承而不是最后这个Assistant去虚继承
多重继承
#include<iostream>
using namespace std;
//下面是基类1
class base1{
public:
base1()
{
m_a = 10;
}
void fun()
{
cout << "base1 的fun方法" << endl;
}
int m_a;
};
class base2 {
public:
base2()
{
m_b = 10;
m_a = 30;
}
void show()
{
cout << "base2 的show方法" << endl;
}
int m_b;
int m_a;
};
//下面是子类继承
class son :public base1, public base2 {
public:
int m_c;
int m_d;
};
int main()
{
son s;
cout << "s.m_a: " << s.m_a<< endl;
cout << "s.m_b: " << s.m_b << endl;
s.fun();
s.show();
//多继承,子类会继承多个父类中的成员(成员变量和成员函数)
//如果两个不同的父类中出现了同名的成员,就需要加上作用域去访问
cout << s.base2::m_a << endl;
return 0;
}
菱形继承和虚继承
如果有两个派生类继承了同一个基类,又有一个基类同时继承了这两个派生类,那么我们就叫这个为菱形继承
这个是多继承的一个坑(菱形继承)
虚继承实现原理
虚基类对象放在最下面,为了让B和C方便找到这个虚基类,然后此时就会出现一个虚基表的指针B和C,然后这个B和C的虚基表指针分别指向B和C这两个虚基表,而虚基表里面就存储了二者相对于这个虚基类的偏移量,通过这个偏移量就可以找到这个虚基类
这里是通过不同的偏移量找到这个虚基类
这二者的区别就是继承的权限相对更大一点,就像后面这个组合,M中的保护成员那么N就没法用