前言
本章我们要学习,面向对象编程的三大特性之一的继承。
我们简要阐述继承的使用场景
多个类中存在
相同的属性和行为
时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只需要继承那个类
即可。
多个类可以称为子类
,单独这个类称为父类或者超类,基类
等。
文章目录
一. 继承基本语法
在我们日常生活中,人是我们的共性,但是在人之下,我们可以有很多的身份,一个人可以是学生,老师,工程师…所以,人就可以作为一个基类
,而细化的学生,老师,工程师则是人的子类
。
代码如下
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "张三";
int _age = 18;
};
而细化的学生,老师,可以是这样的。因为身份的细化
,可能会有新的属性
,这里我们仅举例一两个
class Student : public Person
{
protected:
int _stuid;
};
class Tercher :public Person
{
protected:
int _jodid;
};
我们先实例化一个学生,这里看到,我们在学生类里并没有定义_name,_age,Print函数。但是,因为是继承关系,子类将会拥有父类的一切属性
int main()
{
Student s1;
s1.Print();
return 0;
}
这里Student类成功继承了Person的_name,_age和Print函数。
二. 继承基类访问限制
其实,子类继承基类属性有三种方式,以上我们使用的public公有继承
,其实还有protected保护继承
,private私有继承
以下是继承后访问限制规则
访问限定符的访问权限:public>protected>private
- 公有继承,子类继承的父类的属性访问限定都
不变
- 保护继承,子类继承的父类属性,在
保护以上的会发生降级,降为保护。
- 私有继承,子类继承的父类属性,在
私有以上的会发生降级,降为私有。
- 基类的私有成员,子类不管已任何方式继承,
都不可访问。
- protected和private的属性,
类外都不可访问
。区别是父类的protected,子类可以访问
,父类的private子类也不可访问
6.子类继承如果不显示写继承方式,class默认是private继承
,struct默认是public继承
但在实际运用中,一般都是public继承
,几乎很少使用protected/private继承
,也不提倡使用,因为protected/private继承的成员只能在子类中使用,实际运用中扩展维护性不强
三. 基类和子类对象赋值转换
我们在赋值时,如果是内置类型,相近的类型赋值会发生隐式类型转换。
比如,我们将double赋值给int,会产生临时对象。
那我们能否将子类对象赋值给父类呢?会不会发生隐式类型转换,产生临时对象呢?
答案是:不会发生隐式类型转换,也不会产生临时对象
但是会发生一个被称为切片或者切割的现象
切片/切割
:派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用
。过程发生切片 / 切割。寓意把子类中父类部分的那部分切回给父类
基类对象不能赋值给派生类对象
我们再看一个现象
这里可以证明,子类给父类赋值没有产生临时对象。
同时,对于引用和指针的理解,可以如下图
指针和引用都是指向子类中父类的那一部分。
四. 继承中的作用域
基类和子类都有其自己的作用域。
在子类中,我们可以定义和父类一样的变量或者函数呢?
答案是可以的。因为子类和父类是不同的作用域,并且如果不显示调用,则会遵循就近原则。
测试代码如下;
//父类
class Person
{
public:
protected:
string _name = "张三";
int _age = 30;
};
//子类
class Student : public Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
int _age = 18;
};
int main()
{
Student s1;
s1.Print();
return 0;
}
如果要使用父类的同名变量,需要指定作用域
void Print()
{
cout << "name:" << _name << endl;
cout << "age" << Person::_age << endl;
cout << "age:" << _age << endl;
}
- 在继承体系中,父类和子类都有独立的作用域
- 子类和父类中有同名成员,
子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也就重定义
。(在子类成员函数中,可以使用父类::父类成员
访问)- 需要注意的是如果是
成员函数的隐藏,只需要函数名相同就构成隐藏
- 注意在实际中的继承体系里面,
最好不要定义同名成员
五. 子类的默认成员函数
子类会继承父类的所有属性,无论是变量还是函数,那么默认成员函数呢?构造,析构,拷贝,赋值…
我们接下来学习子类继承的默认成员函数
我们先举个例子
//父类
class Person
{
public:
Person(const string&name="张三")
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person&p)
:_name(p._name)
{
cout << "Person(const Person&p)" << endl;
}
Person&operator=(const Person&p)
{
cout << "Person operaot=(const Person&p)" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
//子类
class Student : public Person
{
public:
protected:
int _num = 18;
};
1.构造函数
我们定义一个Person父类,Student子类。我们在父类中,写好四个默认成员函数,构造,拷贝,赋值,析构。但是在子类中并没有显示写默认成员函数,接下来我们实例化一个Student。
我们可以看到,实例化的子类Student,其内部不仅有赋值好的_name,还调用了父类的构造和析构函数。
我们再更改一下子类的内容。
我们增加一个子类的默认构造(全缺省也是默认构造函数)
Student(const string&name="张三",const int&num=18)
:_name(name)
,_num(num)
{
cout << "Student()" << endl;
}
但是这并不行,会报错
在继承中规定,父类的成员初始化,必须使用父类的构造函数,子类的构造会自动调用父类的相应的构造函数。
我们可以这么处理
Student(const string&name = "张三", const int&num = 18)
:Person(name)
,_num(num)
{
cout << "Student()" << endl;
}
调用父类的构造函数。
2. 拷贝构造
通过构造函数的学习,我们知道父类部分的成员变量。需要调用父类的相应构造 / 函数初始化。拷贝构造同样如此
Student(const Student&s)
:Person(s)
,_num(s._num)
{}
而我们在上面讲到的,子类对象赋值给父类发生的切片。实际是调用了父类的拷贝构造
,将子类中父类部分的数据切片
,用拷贝构造实例化
3. 赋值重载
赋值重载也同样遵循规则
Student& operator = (const Student&s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
4.析构函数
析构函数的规则有些不一样,父类的析构函数不需要显示调用,其会在调用子类的析构函数之前调用。
我们在Student类中补上析构函数,以便观察
~Student()
{
cout << "~Student()" << endl;
}
我们看到,
构造是先构造父类,再构造子类
。而析构时先析构子类,再析构父类
。
编译器在编译子类析构函数时,会在子类析构函数最后一条语句之后,插入调用基类析构函数的语句
比如我们在Student析构内部什么都不写
通过反汇编,我们可以看到,在Student析构的最后,有call Person父类析构。
六. 继承的小知识点
父类的友元函数
不可继承,父类的友元函数,在子类中没有继承,如果需要,则还需要将该函数设置为子类的友元函数- 父类的静态成员,
共同属于父类和子类
,且无论是子类访问,还是父类访问,都是同一个
- 将
父类的构造函数或者析构函数私有化
,就实现了一个不可以被继承的类
。因为子类构造需要调用父类的构造,子类的析构也需要调用父类的析构,如果二者其一私有化,则子类也无法调用,故不可以继承
七. 菱形继承
现实生活中,一个事物具有多种特征,抽象过来,就是一个类可以有多个父类。这就可能会出现菱形继承的问题。
因为Assistant的两个父类
有相同的成员_name。而隐藏是发生在父子类中的
,所以这边不会发生隐藏
,但直接指定_name是不可以的,因为不知道指定的是哪个作用域的_name,发生二义性
。
如果要访问,需要指明作用域
。但成员变量一般不需要同名。
就像记录我们信息时,其实是不需要两个名字的。即使我们在生活中有很多小名,外号啥的,但是信息记录只需要记录一个就好
菱形继承会造成数据冗余和二义性
结束语
本章是对继承的初次学习,感谢您的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。