一纸聊完C++继承
1.引言
谈到继承(Inheritance),我们自然会想到遗产继承、基因继承等等… 正是因为有了继承才有小猫咪出生一周就会抓老鼠,也有龙生龙凤生凤,老鼠儿子会打洞。 那么在C++的类中,也存在这样的一种"血缘关系",将存在相关性的两个类会分为子父类或者基类派生类。呈现了面向对象的层次结构,提高了类的代码的复用性。
2.继承规则介绍
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保 持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类(子类)。继承呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继 承是类设计层次的复用。
继承的基本语法规则:class + 派生类名 : 继承方式 + 基类名
例如: class Student :public Person {};
类似于下面方式就能简单创建出一个继承结构
class Person //人类
{
public:
string name= "小明";
int _age = "20";
};
class Student : public Person //学生类 :因为人中含有学生,所以可以继承且继承方式为public型继承
{
public:
string ID = "2000";
};
int main()
{
Student s;
return 0;
}
这样利用student创建的对象s不仅有自己本身的属性ID,还具有了父类的属性name和_age
3.继承权限介绍
既然类中变量有权限限定符public、protected和private等限制**,继承方式**也是由这几个关键字所选定的,那么子类中的变量根据类内访问限定符和继承方式的不同应该具有什么样的权限呢?
通过表中的数据我们不难观察出一种规律:
1.但凡是父类中的private的成员在派生类中都是不可见的。注意:这里说的不可见并不是不存在或者不会被继承。实际上确实是存在且会被继承,只是无法通过常规渠道访问到。若硬要访问,可以通过写公有性质的函数等方法对private成员直接打印。例如:
2.大多数情况当并不涉及基类的private成员时,两者权限综合起来看总是取其中最小权限者。例如基类的protected类成员经过private私有继承最终的权限应当是派生类的private成员。
4.子父类对象的赋值转换
先说结论:子类对象可以赋值给父类对象/父类指针/父类引用。这里可以称之为切割或者切片。但是父类对象不能赋值给子类对象。因为父亲有的成员儿子一定有,但是儿子有的成员父亲却不一定有。这里的寓意为儿子可以满足父亲,但是父亲满足不了儿子♂。
上述规则反映到代码上是:
Student s;
Person p = s; //子类对象赋值给父类对象
Person* ptr1 = &s; //子类对象赋值给父类指针
Person& ptr2 = s; //子类对象赋值给父类引用
而不能写成
Person p;
Stdent s = p; //错误
5.继承作用域
直接上结论再解释:
1.在继承体系中基类和派生类都有独立的作用域。
-
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
-
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
-
注意在实际中在继承体系里面最好不要定义同名的成员。
需要注意的是构成隐藏的函数往往是在不同作用域的子父类下,且函数名等大体保持相同,参数等可能不同。
class Person { public: void f()//与子类函数构成隐藏 {cout<< "Person"<<endl;} }; class Student :public Person { public: void f()//与父类函数构成隐藏 {cout<<"Student"<<endl;} };
实际调用中,是哪类的对象调用的函数就优先使用哪一类的函数。
6.继承的默认成员函数
关于派生类有几点我们需要注意它默认成员的性质:
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序
- .派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
class Person
{
public:
Person(){cout << "Person" << endl;}
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
Student() { cout << "Student()" << endl; }
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Student s;
return 0;
}
输出结果应该是:
这样才符合栈存储的先进后出规则。
7.多继承引发的菱形继承问题
C++允许使用多继承,但是如果当多继承所形成的类造成了以上类似菱形的环状结构,我们称之为菱形继承的话,那么问题就来了。
首先,如果Person中有一个变量a,学生和老师类的都继承了a变量,到助教类中就会有两个a,那么这两个a中地址是否一样?值是否一样?两个a是否会重复冲突引发指代错误问题? 显而易见,菱形继承会引发数据冗余和二义性的问题。
1.数据冗余是指同一个变量名有两份存储空间太繁琐。在正式场合下例如身份证信息的话需要有唯一值,是不宜出现的。
2.二义性问题是指会发生指代不明确问题。意为不知道到底是指向的哪个a,是学生类的a,还是教师类的a?
涉及到菱形继承的代码是:
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
8.菱形继承解决方案以及原理
解决菱形继承的方法是**:虚拟继承,简称虚继承**。在多继承的继承方式前加上virtual关键字。
即如下方式:
class A
{
public:
int _a;
};
class B : virtual public A //继承方式前加上virtual关键字
{
public:
int _b;
};
class C :virtual public A//继承方式前加上virtual关键字
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
return 0;
}
加上virtual后它的解决原理是:既然有两个a变量,那我整体就只设置一个a,既不放到B类中,也不放到C类中,将其放到公共区域,然后在两个不同继承类中设置不同的偏移量,通过相对地址+偏移量的方式解决。后面修改也是处理的同一份数据。
打开内存我们可以看到:
二者都是通过偏移量去找到相同A中的a变量,解决了数据冗余和二义性的问题。
如果觉得文章对你有帮助的话,麻烦手动点个赞吧!!!
如果你还想关注我更多的文章内容,请访问:
C++优先级队列PriorityQueue模拟实现
C++List的简单模拟实现
Linux保姆级介绍系统接口fork()函数