继承
1.继承的概念与定义
继承 是面向对象编程中的一个重要概念。它的由来可以追溯到软件开发中的模块化设计 和代码复用的需求。
在软件开发过程中,我们经常会遇到需要为多个类添加相同的行为或属性的场景,这样就产生了代码重复的问题。为了解决这个问题,工程师们开始寻找一种方法来实现代码的复用。
继承就是一种解决代码复用问题的方式。它允许我们创建一个新的类,继承自一个已经存在的类,从而继承和复用父类的属性和方法。通过继承,我们可以在不改变父类的前提下,为子类添加额外的属性和方法,实现功能的扩展。
继承方式的由来可以追溯到早期的面向对象编程语言。早期的面向对象编程语言如Smalltalk、Simula等提供了基于类的继承机制。后来的编程语言如Java、C++等也引入了类似的继承机制。继承方式的由来和发展是为了提高软件开发的效率和可维护性,同时也体现了面向对象编程的思想和原则。
总而言之,在C++中,继承是代码复用的最重要手段。
2.继承的方式
那么我们要如何实现继承呢?
先看个样例,再看语法规则:
cpp
class parent
{
public:
int _age;
};
class child : public parent
{
public:
int _id;
};
-
以上就是一个简单的继承结构,child继承了parent。
-
其中parent这种被别人继承的类叫做:基类 / 父类
-
child这种继承别人的类叫做:派生类 / 子类
-
public parent这条语句,紧跟在child的类名后面,说明child继承了parent。而public是继承方式
这个继承方式有什么作用呢?
继承方式与基类的成员访问限定符共同决定了派生类对基类成员的访问权限
基类成员 \ 继承方式 | public继承 | protect继承 | private继承 |
---|---|---|---|
public成员 | 派生类的public成员 | 派生类的protect成员 | 派生类的private成员 |
protect成员 | 派生类的protect成员 | 派生类的protect成员 | 派生类的private成员 |
private成员 | 不可见 | 不可见 | 不可见 |
-
以上表格展示了所有情况下的继承,基类的成员会根据自身的访问属性以及继承方式,共同决定最终继承到派生类的成员是什么属性。
-
其中,不可见不是指不继承,基类中的private成员继承后在派生类中不可见,就是派生类无法直接访问到这个成员,但是派生类依然是存储着这个成员的。
当然,与访问限定符一样,继承方式也是有默认方式的
- 用class定义的类,默认继承方式是private
- 用struct定义的类,默认的继承方式是public
- 多继承:
一个派生类可以同时继承多个基类:
cpp
class parent1
{
public:
int _age;
};
class child : public parent1, public parent2
{
public:
int _id;
};
2.1继承基本特性
- 继承后,派生类有可能只增改了基类的成员函数,而成员变量是一样的,所以基类和派生类的大小可能是一样的
- 友元关系不能继承,基类的友元不能访问子类的私有和保护成员
- 对于基类的静态成员,派生类和基类共用,派生类不会额外创建静态成员
- 如果不希望一个类被继承,可以将这个类的构造函数或者析构函数用private修饰
继承后,派生类的初始化列表指向顺序为继承顺序
2.2继承的作用域
基类与派生类有两个分别独立的作用域
2.2.1隐藏
当派生类继承了基类的成员后,如果派生类自己创建了与基类同名的成员,那么派生类成员将屏蔽对同名基类成员的直接访问,这种情况叫做隐藏。
cpp
class A
{
void func()
{}
public:
int num;
};
class B : public A
{
void func()
{}
public:
int num;
};
在以上继承关系中,B继承了A的num变量与func函数,而B类自己还创建了同名的func与num。那么此时在B内部直接访问num与func,就是访问B自己的num。如果想要访问A的成员,需要限定作用域。
cpp
B b;
b.func();//访问B的func函数
b.A::func();//访问A的func函数
- 此外,函数重载要求两个函数在同一个作用域,而基类与派生类是两个不同作用域,所以就算参数不同也不能构成重载。所以只要基类与派生类内的函数名相同就构成隐藏,不考虑参数。
赋值兼容
赋值兼容是一个基类与派生类之间的转换规则,其可以让派生类转换为父类。
以如下的继承关系做讲解:
cpp
class person
{
public:
string _name;
string _sex;
int _age;
};
class student : public person
{
public:
int _No;
};
规则:
派生类的对象可以赋值给基类的对象
cpp
student s;
person p = s;
赋值如下图
我们可以将一个派生类的成员赋值给基类成员,此时会发生一个切片效果,基类只取出派生类中属于基类的部分来构造基类。
派生类的指针可以转换为基类的指针
派生类的引用可以转换为基类的引用
cpp
student s;
person* pp = &s;
person& rp = s;
基类是被包含在派生类中的,所以我们用基类的指针去访问派生类,相当于只访问了基类的部分。上图中就是只访问了红色的部分。
派生类的创建和销毁
派生类是如何创建销毁的?因为派生类内部还包含了一个基类,那么基类这一部分要如何处理?
其实想要理解这一部分,就记住一句话:派生类的默认成员函数,把基类当作一个类成员变量处理。
接下来我为大家讲解构造函数,拷贝构造,赋值重载,析构函数这几个与创建销毁相关的函数,来理解派生类是如何创建销毁的。
构造函数
派生类构造函数将基类当作一个成员变量,不会直接初始化基类的成员,而是通过调用基类的构造函数。
在一般的类中,类内部如果有其他类的成员变量,构造函数会在初始化列表调用其构造函数。如果不直接调用,那么会隐式调用其相应的默认构造函数。
cpp
class person
{
public:
string _name;
};
class child : public parent
{
public:
child(string name, int num)
:parent(name)
,_num(num)
{}
private:
int _num;
};
:parent(name) 就是在初始化列表显式地调用构造函数。
拷贝构造
派生类拷贝构造将基类当作一个成员变量,不会直接拷贝基类的成员,而是通过调用基类的拷贝构造。
在一般的类中,类内部如果有其他类的成员变量,拷贝构造会在初始化列表调用其拷贝构造。如果不直接调用,那么会隐式调用其相应的默认构造函数。
cpp
class person
{
public:
string _name;
};
class child : public parent
{
public:
child(const child& c)
:parent(c)
,_num(c.num)
{}
private:
int _num;
};
- parent ( c ) 就是在显式调用基类的拷贝构造,不过我们在调用基类的拷贝构造时,传入的却是派生类的引用。这是为什么?❓❔❓
- 我们刚在赋值兼容处说过:派生类的引用可以转化为基类的引用
- 所以此处在传参时会发生一次隐式的切片,基类的拷贝构造只访问派生类的基类部分,来拷贝出一个基类。
要注意:拷贝构造也属于构造函数,所以拷贝构造在初始化列表中如果没有显式调用拷贝构造,就会隐式调用默认构造函数。
赋值重载
在派生类拷贝构造中,必须显式调用基类的赋值重载,因为赋值重载也把基类当作一个类成员做处理。赋值重载不会直接调用成员的赋值重载,而是需要我们显式调用。
cpp
class person
{
public:
string _name;
};
class child : public parent
{
public:
child& operator=(const child& c)
{
parent::operator=(c);
_num= c._num;
}
private:
int _num;
};
parent::operator=(c );就是在显式地调用基类的拷贝构造,这里不能直接调用operator=(c );,因为派生类中存在operator=;这个函数,基类的函数被隐藏了,所以我们要指定作用域,来调用基类的赋值重载。