"你在酒杯还未干的时间里,收藏这份情谊"
一、回顾继承
什么是继承?
继承是面向对象编程语言的三大特征之一。通过继承机制,面向对象的程序设计可以很大限度地对代码进行复用。
它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
继承 呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用, 继承是类设计层次的复用。
C++中继承定义格式/继承关系与访问限定符
二、切片对象vs临时对象
我们先来看看下面的代码。
int main()
{
double i = 2.3;
int j = i;
cout << j << endl;
return 0;
}
初始化一个int类型的变量j,但是我们用的不是同一类型的其他变量i。事实上,不同类型的变量是不能进行这样的操作的,因此,为了保证可行性,这里会发生“隐式类型转换”。不是将i的值赋值给j,而是在这个过程中产生一个临时变量 ,先将i的值给临时变量,再由临时变量赋值给j。
口说无凭,我们来看看下面的现象。临时变量具有常量性,因此不是不能用int&,而是需要将"权限"缩小 。
那么上面提到了,当不同类型进行赋值的时候,是会发生隐式类型转换的。那么我回到继承上来。这时我们想用父类对象引用子类对象。
int main()
{
Student s;
Person p = s;
Person& rp = s;
return 0;
}
当父类对象引用子类时,并不需要+const修饰。也就意味着,编译器认为这种情况不同于上述发生隐式类型转换的条件。
"父类对象引用子类,不会发生隐式类型转换,也就不会生成临时变量。"
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
三、基类析构函数
我们先来回顾类里的6个重要的默认成员函数。
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student :public Person
{
public:
Student()
{
cout << "Student()" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
};
我们定义Student s,那么它不仅仅会去调用它自身的构造函数,还会去调用继承的父类对象部分的构造函数进行初始化。其次,当析构的时候,先调用自己的析构函数,完成清理本类的资源。
我们没有调用析构,为什么会自动调用父类的析构函数呢?我们能否手动调用父类的析构?
这两个函数析构在我们看来不是 不同吗?为什么需要指定类域?难道构成隐藏了吗?
这个知识牵涉到后面多态在析构函数的处理。
不管是Student的析构函数、还是Person中的析构函数。在编译器看来,最后都会被特殊处理成destructor函数名。
什么时候调用父类的析构函数?
在本段的开头,我们就发现,当定义一个派生类时,即便我们在该类的析构函数中没有显式调用其基类的析构函数,但是最后打印出来却是调用了基类的析构函数。
当子类调用析构后,会自动调用父类的析构函数。
四、友元关系不能继承
也就是说基类友元不能访问子类私有和保护成员。
五、解引用/静态成员变量
在开讲本段之前,我们先来回顾类成员你的存储方式。
(1)类对象的存储方式
类体中的成员分为两类,类变量与类方法(函数)。
上述结果清晰地告知我们,一个类的大小,取决于成员变量的大小。
而类里的方法(函数),是被放在一个公共的代码段。
(3)静态成员变量
我们来看看下面的代码段;
class Person
{
public:
Person() { }
void Print()
{
cout << this << endl;
}
public:
static int _count; // 统计人的个数。
string _name;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
此时基类中有一个静态变量count,那么student继承下去后,会生成一份新的count吗?
答案是不会!静态成员变量在基类,在派生类中也是同一个。"静态变量属于整个类"。
(3)nullptr解引用
Person* ptr = nullptr;
ptr->Print();
cout << ptr->_name << endl;
看完上面的一份代码,是否觉得都会奔溃?
(*ptr).Print();
cout << (*ptr)._name << endl;
要理解这个点和类成员的存储方式十分密切。解引用的本质是,访问地址处的类型大小的字节。当我们用ptr->Print()是在解引用吗?当然不是!因为并非是在访问类成员变量,而是直接访问的是类成员方法,这些方法早不在类的大小里!
这也就是为什么ptr->name \ (*ptr).name 才会访问出错,因为name是类成员的变量!此时解引用是对空指针的解引用。
当我们再在print中打印_name时,此时this就是空指针(ptr->name),所以也就 成了对空指针的解引用。我们用ptr->Print()调用时,只传了一个值给Print()函数,那就是this(nullptr)。
六、菱形继承
(1)多继承
像一个子类只有一个直接父类时称这个继承关系为:单继承。
像一个子类有两个或以上直接父类时称这个继承关系为:多继承。
(2)二义性与数据冗余
C++设计继承有一个很大的坑,就是支持了菱形继承。那么什么是菱形继承呢?我们来看看这模型。
这是一种特殊的多继承情况。
也许仅凭图中的模型,不会让你对这菱形继承望而生畏或者攥紧拳头,我们简单地设计一套继承体系。
class Person
{
public:
string _name; // 姓名
// id 家庭住址 身份证号码
};
class Student:public Person
{
protected:
int _num; //学号
};
class Teacher:public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
这是一个人,但是他有"学生+老师"的双重身份。
我们定义对象Assistant,此时我们想要给这个对象的内部成员name初始化。但是无法直接进行初始化!因为继承的缘故,_name不仅仅在Student有一份,Teacher中也有一份,从而导致了这样的"二义性"问题!因此,我们不得不指名类域,初始化我们想要的值。
但是,一个人是否仅仅需要一个名字就可以了(在当前这个条件下)?当他是学生的时候有一个名字,当他是老师的名字又有另外一个名字?显然这是很不符合常理的。也许你会说,哎呀一个名字就几个字节大小罢了,多了的那份变量似乎显得不过尔尔。但是如果你发现的这份变量是一个100字节、1000字节甚至更多呢?本来我们仅仅需要一份代码不过100byte,却因为菱形继承,足足增加到了200byte大小却是一模一样的类型。 这种 情况,也叫做"数据冗余"。
(3)虚继承
当我们翻看C++的发展史,不难发现,当支持多继承后,肯定会出现菱形继承这样的不好的场景。为此,后一个版本C++又为解决多继承产生的二义性问题 增添了一个关键字virtual。
我们先来看看没有virtual时的菱形继承;
从内存角度来看,很清楚地看到d继承的各个类变量的分布情况。此时,有两个地址指向同一份int a基类。
virual继承
class Base
{}
//virtual加在腰部类上
class Derived1:virtual public Base
{}
class Derived2:virtual public Base
{}
class Example:public Derived1,public Derived2
{}
当加上virtual虚继承后,本来两份的a变成了一份。最先被初始化为4的a,后来被覆盖成了5。但是,我们却在d对象里发现两份像地址一样的数字。
我们找到地址处,得到它们的内容,其实记录的是从该位置到变量a的偏移量。
同样,当变为虚继承时,存储的方式也会发生变化,我们来看看子类B。
这里是通过了B和C的两个指针,指向的一张表。 这两个指针叫虚基表指针 ,这两个表叫虚基表。虚 基表中存的偏移量 。通过偏移量可以找到下面的A。
总结:
①父类对象引用子类 不产生临时变量。不是发送隐式类型转换。
②当子类析构调用完成后,会自动调用父类的析构函数。
③友元关系不能继承
④类的静态成员属于整个类,类对象指针解引用并不全都是"解引用"。
⑤菱形继承是不好的,如果实在遇到菱形继承,为避免代码冗余和二义性,应当使用virtual虚继承。
本篇到此结束,感谢你的阅读
祝你好运,向阳而生~