C++(继承)
目录
前言:
进入到c++面向对象的第二大板块继承,对此做一个复习巩固。所有的OO(面向对象)类型的语言都具备三大基本特征:封装-继承-多态,在累和对象复习篇章已经介绍了封装,今天主要复盘一下继承。
正文:
1.继承的概念及定义
1.1继承的概念
????????继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
需要注意的是:
- 被继承对象:父类/基类(base)
- 继承方:子类/派生类(derive)
1.2继承的本质
????????继承的本质就是 代码复用,只不过是类层次的复用,比如我定义了一个人的类,包含打印显示函数,作为学生我也有人的特征,我就可以继承这个类,作为教职工我也有人的特征,也可以继承这个类,这样就避免了代码的复写。比如:
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; //年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
//Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可
//以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
? 我们可以看到两个子类都具备父类中 public属性且相互不干扰。
?
2.继承的定义
2.1继承格式
?????????继承的格式很简单,子类:继承方式 父类,例如 上面那个例子?class Sdudent :public Person,需要注意的是 java中继承符号为 extern。
2.2继承关系和访问限定符
????????继承方式有 共有继承(public)保护继承(protect)和私有继承(private)当然相应的访问限定符也是一样。如下图所示:
?需要注意的是:
权限大小:公有 > 保护 > 私有
保护?protected
?比较特殊,只有在?继承?中才能体现它的价值,否则与?私有?作用一样
访问权限和继承方式各有三种,排列组合就是9种,如下所示:? ? ? ?
?
父类成员 / 继承权限 | ?public | ?protected | private |
父类的 public 成员 | ?外部可见,子类中可见 | 外部不可见,子类中可见 | ?外部不可见,子类中可见 |
父类的 protected 成员 | ?外部不可见,子类中可见? | 外部不可见,子类中可见? | 外部不可见,子类中可见 |
父类的 private 成员 | ?都不可见 | 都不可见 | 都不可见 |
总结:无论是哪种继承方式,父类中的?private
?成员始终不可被 [子类 / 外部] 访问;当外部试图访问父类成员时,依据?min(父类成员权限, 子类继承权限)
,只有最终权限为?public
?时,外部才能访问。
假设不注明继承权限,
class
?默认为?private
,struct
?默认为?public
,最好是注明继承权限
小case:
实际使用中。权限是可以很好保护成员的,如何设计一个不能被继承的类呢?
? 答:我们只要将我们不想被继承的类设置为私有,这样该类就不能被继承了,代码如下:
class base
{
private:
base();
~base();
};
class derived public base
{};
int main()
{
derived d;
return 0;
}
?
3 继承中的作用域
3.1隐藏
隐藏也叫重定义,也可叫做重写(覆盖)?。子类中只要出现和父类中同名的成员就叫重定义不考虑返回值,参数。假设出现同名函数时,默认会将父类的同名函数隐藏调,进而执行子类的同名函数,
//父类
class Base
{
public:
void func() { cout << "Base val: " << val << endl; }
protected:
int val = 123;
};
//子类
class Derived : public Base
{
public:
int func()
{
cout << "Derived val: " << val << endl;
return 0;
}
private:
int val = 668;
};
int main()
{
Derived d;
d.func();
return 0;
}
执行结果:
此时,父子类中的方法和成员均被隐藏,执行的是子类方法,输出的是子类成员
?只修改子列方法名为funa
int funA()
{
cout << "Derived val: " << val << endl;
return 0;
}
?执行结果:
函数名不同,不构成隐藏,结果是 父类方法+子类成员。
只修改子类成员为 num
?
int num = 668;
执行结果:
隐藏也消失了,执行结果:子类方法+父类成员
综上所述,当子类中的方法出现 隐藏 行为时,优先执行 子类 中的方法;当子类中的成员出现 隐藏 行为时,优先选择当前作用域中的成员(局部优先)
这已经证明了?父子类中的作用域是独立存在的
如何显式的使用父类的方法或成员?
?
- 利用域作用限定符?
::
?进行访问范围的限制?
?
?4 基类和派生类对象赋值转换
4.1切片
?将?父类对象?看作一个结构体,子类对象?看作结构体Plus 版
将?子类对象?中多余的部分去除,留下?父类对象?可接收的成员,最后再将?对象?的指向进行改变就完成了?切片
?
5 派生类中的默认成员函数
?派生类(子类)也是?类,同样会生成?六个默认成员函数(用户未定义的情况下)
不同于单一的?类,子类?是在?父类?的基础之上创建的,因此它在进行相关操作时,需要为?父类?进行考虑。
?5.1隐式调用
子类在继承父类后,构建子类对象时?会自动调用父类的 默认构造函数,子类对象销毁前,还会自动调用父类的 析构函数。
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;
}
注意:?自动调用是由编译器完成的,前提是父类存在对应的默认成员函数;如果不存在,会报错?
?
5.2显示调用
?因为存在?隐藏?的现象,当父子类中的函数重名时,子类无法再自动调用父类的默认成员函数,此时会引发?浅拷贝?相关问题
class Person
{
public:
Person() { cout << "Person()" << endl; }
void operator=(const Person& P) { cout << "Person::operator=()" << endl; }
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
Student() { cout << "Student()" << endl; }
void operator=(const Student&) { cout << "Student::operator=()" << endl; }
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Student s1;
cout << "================" << endl;
Student s2;
s1 = s2;
return 0;
}
?
?此时可用通过 域作用限定符?::
?显式调用父类中的函数
?
总的来说,子类中的默认成员函数调用规则可以概况为以下几点:
1.子类的构造函数必须调用父类的构造函数,初始化属于父类的那一部分内容;如果没有默认构造函数,则需要显式调用。
2.子类的拷贝构造、赋值重载函数必须要显式调用父类的,否则会造成重复析构问题
3.父类的析构函数在子类对象销毁后,会自动调用,然后销毁父类的那一部分
注意:
子类对象初始化前,必须先初始化父类那一部分
*子类对象销毁后,必须销毁父类那一部分
不能显式的调用父类的析构函数(因为这不符合栈区的规则),父子类析构函数为同名函数 destructor,构成隐藏,如果想要满足我们的析构需求,就需要将其变为虚函数,构成重写
析构函数必须设为 虚函数,这是一个高频面试题,同时也是 多态 中的相关知识
?
?
6 友元和继承
友元关系不能被继承
场景:友元函数?Print
?可以访问父类中的私有成员,但子类继承父类后,友元函数无法访问子类中的私有成员
?
class Base
{
friend void Print();
private:
static const int a = 10;
};
class Derived : public Base
{
private:
static const int b = 20;
};
void Print()
{
cout << Base::a << endl;
cout << Derived::b << endl;
}
int main()
{
Print();
return 0;
}
爸爸的朋友不可以是儿子的朋友,如果要想成为儿子的朋友,需要在儿子家里声明为友元。
?总结:友元关系不能被继承
7 继承与静态成员
?????????静态成员是唯一存在的,无论是否被继承
????????????????静态变量为于静态区,不同于普通的堆栈区,静态变量的声明周期很长,通常是程序运行结束后才会被销毁,因此?假设父类中存在一个静态变量,那么子类在继承后,可以共享此变量
用计数的demo证明一下:
class Base
{
friend void Print();
public:
Base() { num++; }
static int num; //静态变量
};
int Base::num = 0; //初始化静态变量
class Derived : public Base
{
public:
Derived() { num++; }
};
void Print()
{
cout << Base::num << endl;
}
int main()
{
Derived d1;
Derived d2;
Derived d3;
Print();
return 0;
}
由此可见:静态成员是唯一存在的,并且被子类共享?
?
?8 菱形继承
单继承:一个子类只能继承一个父类
多继承:一个子类可以继承多个父类(两个及以上)
C++ 支持多继承,即支持一个子类继承多个父类,使其基础信息更为丰富,但凡事都有双面性,多继承 在带来巨大便捷性的同时,也带来了个巨大的坑:菱形继承问题
注:其他面向对象的高级语言为了避免出现此问题,直接规定了不允许出现多继承
8.1菱形继承的问题
class Person
{
public:
string _name; //姓名
};
//本科生
class Undergraduate : public Person
{};
//研究生
class Postgraduate : public Person
{};
//毕业生
class Graduate : public Undergraduate, public Postgraduate
{};
int main()
{
Graduate g1;
g1._name = "zhangsan";
return 0;
}
8.2原因
Undergraduate?
中继承了?Person
?的?_name
,Postgraduate?
也继承了?Person
?的?_name
Graduate?
多继承?Undergraduate?
、Postgraduate?
后,同时拥有了两个?_name
,使用时,无法区分
?
8.3解决方案
方法一:通过域访问限制符 ::?
Graduate g1;
g1.Undergraduate::_name = "zhangsan";
cout << g1.Undergraduate::_name << endl;
?
?这种方法并没有从本质上解决数据冗余的问题
方法二:虚继承
虚继承是专门用来解决 菱形继承 问题的,与多态中的虚函数没有直接关系
虚继承:在菱形继承的腰部继承父类时,加上?virtual
?关键字修饰被继承的父类
?
class Person
{
public:
string _name; //姓名
};
//本科生
class Undergraduate : virtual public Person
{};
//研究生
class Postgraduate : virtual public Person
{};
//毕业生
class Graduate : public Undergraduate, public Postgraduate
{};
int main()
{
Graduate g1;
g1._name = "zhangsan";
cout << g1._name << endl;
return 0;
}
?
虚继承是如何解决菱形继承问题的?
- 利用?虚基表?将冗余的数据存储起来,此时冗余的数据合并为一份
- 原来存储 冗余数据 的位置,现在用来存储?虚基表指针
?
虚继承底层是如何解决菱形继承问题的?
对于冗余的数据位,改存指针,该指针指向相对距离
对于冗余的成员,合并为一个,放置后面,假设想使用公共的成员(冗余成员),可以通过相对距离(偏移量)进行访问
这样就解决了数据冗余和二义性问题
为何在冗余处存指针?指针指向空间有预留一个位置,可以用于多态
因此虚继承用的是第二个位置
新建对象进行兼容赋值时,对象指向指针处该指针(偏移量)指向的目标位置不定
无论最终位置在何处,最终汇编指令都一样(得益于偏移量的设计模式)
虚函数是否会造成空间浪费?不会,指针大小固定为 4/8 字节
指针所指向的空间(虚基表)是否浪费空间?可以忽略不计,所有对象共享
假设存在多个共享成员,需要新增指针(偏移量),因为这些成员都是连续的,找到第一个,即可找到其他即使涉及内存对齐问题,编译器也会根据规则做出调整
?
注意:为了解决?菱形继承?问题,想出了?虚继承?这种绝妙设计,但在实际使用中,要尽量避免出现?菱形继承问题
?
9 补充
继承是面向对象三大特性之一,非常重要,需要对各种特性进行学习
关于多继承时,哪个父类先被初始化的问题
谁先被声明,谁就会先被初始化,与继承顺序无关
除了可以通过继承使用父类中的成员外,还可以通过 组合 的方式进行使用
公有继承:is-a —> 高耦合,可以直接使用父类成员
组合:has-a —> 低耦合,可以间接使用父类成员
实际项目中,更推荐使用 组合 的方式,这样可以做到 解耦,避免因父类的改动而直接影响到子类
当然,使用哪种方式还要取决于具体场景,具体问题具体分析
?
//父类
class A {};
//继承
class B : public A
{
//直接继承,直接使用
};
//组合
class C
{
private:
A _aa; //创建 A 对象,使用成员及方法
}
可能有的人问?继承?到底有什么用?答案很简单,为后面的?多态?实现铺路,也就是说,多态的实现离不开继承!
关于之前的?适配器?模式,除了可以使用 组合 的方式进行适配外,还可以通过?继承?的方式进行适配
queue
?->?deque
、list
reverse_iterator
?->?iterator
在通过后者实现前者时,可以通过?组合,也可以通过?继承
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!