💖 作者简介:大家好,我是菀枯😜
🎉 支持我:点赞👍+收藏⭐️+留言📝
💬格言:不要在低谷沉沦自己,不要在高峰上放弃努力!☀️
我们都知道面向对象语言的三大特点是:**封装,继承,多态。**之前在类和对象部分,我们提到了C++中的封装,那么今天呢,我们来学习一下C++中的继承。
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
看概念是一件很让人疑惑的东西,接下来我就来举个例子来看看继承具体是什么东西👀
首先我们定义两个类,一个Student类,一个Teacher类,二者都有年龄和姓名,学生有学号,老师有工号。
class Student
{
private:
int _age; //年龄
string _name; //姓名
int _stuid; //学号
};
class Teacher
{
private:
int _age; //年龄
string _name; //姓名
int _jobid; //工号
};
我们发现这两个类有一些重复的地方,比如年龄和姓名,这二者是他们的成员,此时代码就产生了冗余。那么我们可不可以像个方法去复用这两个成员呢?继承此时就可以发挥它的重大作用。
我们将他们重复的地方提取出来,重新定义一个Person类。而Student类和Teacher类将Person类继承下来,此时我们就实现了代码的复用。
class Person
{
protected:
int _age; //年龄
string _name; //姓名
};
class Student : public Person
{
private:
int _stuid; //学号
};
class Teacher : public Person
{
private:
int _jobid; //工号
};
我们首先分别用Student和Teacher类来创建两个对象,来看看对象里面有什么。
int main()
{
Teacher t;
Student s;
return 0;
}
此时我们可以看到我们创建的两个对象里面都含有从Person类中继承过来的 age 和 name 两个成员。
所以继承实际上是一个代码的复用,我们可以借用已完成的类的代码来完善我们需要创造的新类。
以我们刚刚创建的Student类来举例:我们看到Person是父类,也称作基类。Student是子类,也称作派生类。
我们在类和对象的时候介绍了三种访问限定符:public(公有),protected(保护)和private(私有)。访问限定符限定了我们在类外如何去访问类中的成员。
在继承中我们一样使用这三种限定符来限定子类该如何去访问父类的的成员,下面有一张表来表示他们的关系。
类成员\继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
父类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
父类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
父类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
首先解释一下在派生类中不可见是什么意思,就如同我们在类外无法直接去修改类中的private成员一样,我们在子类中也无法直接修改父类的private成员。
如何简洁的去记这个表呢?在C++中权限的关系:public > protected > private。在继承的时候呢,父类成员的权限取的是:父类成员原本权限和继承方式中较小的那个。
比如父类的A成员原本权限为public,而子类的继承方式为private。此时A成员相对子类来说就为private成员
子类的对象可以赋值给 父类的对象/父类的指针/父类的应用,那么是如何进行赋值的呢?形象一点来说就是切片,将子类中父类的部分切割父类。
但我们无法反过来,将父类对象赋值给子类对象。
在继承体系中父类和子类都有独立的作用域,如果子类和父类中有同名的成员,子类成员将屏蔽对父类成员的直接访问,这种情况叫隐藏,也叫重定义。
下面还是用我们的Person类和Student类来举个栗子,我们分别在Person类和Student类中加入一个print函数,通过打印内容来区分调用的为哪一print函数。
class Person
{
protected:
int _age;
string _name;
public:
void print()
{
cout << "Person"<< endl;
}
};
class Student : public Person
{
private:
int _stuid; //学号
public:
void print()
{
cout << "Student" << endl;
}
};
接下来我们创建一个对象然后来试一下结果。
int main()
{
Student s;
s.print();
return 0;
}
我们可以看到我们调用的为Student中的print函数。此时子类的print函数已经对父类的print函数进行了重定义。重定义不代表子类无法去调用父类的同名函数,只是不那么直接而已。使用下面这种方法我们就可以调用父类中的同名函数。
int main()
{
Student s;
s.Person::print();
return 0;
}
通过指定类域,我们就可以去调用父类的print函数。但在实际中最好不要去定义同名函数以免带来问题。
首先我们来回顾一下有哪几个默认成员函数。
那么在子类中,这些默认成员函数是怎么生成的呢?
子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在派生类构造函数的初始化列表中显式调用。还是用我们的Person类和Student类举例。
情况一:有默认构造函数
class Person
{
protected:
int _age;
string _name;
public:
Person()
{
cout << "Person" << endl; //调用就打印
}
};
class Student : public Person
{
private:
int _stuid; //学号
public:
Student()
{
cout << "Student" << endl; //调用就打印
}
};
int main()
{
Student s;
return 0;
}
情况二:无默认构造函数
class Person
{
protected:
int _age;
string _name;
public:
Person(int age, string name)
{
cout << "Person" << endl;
}
};
class Student : public Person
{
private:
int _stuid; //学号
public:
Student()
: Person(19, "wanku") //无默认构造,此时我们需要在初始化列表中初始化
{
cout << "Student" << endl;
}
};
int main()
{
Student s;
return 0;
}
int main()
{
Student s;
return 0;
}
子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化化。
class Person
{
protected:
int _age;
string _name;
public:
Person(int age = 10, string name = "wanku")
{
cout << "Person" << endl;
}
Person(const Person &p)
: _age(p._age), _name(p._name)
{}
};
class Student : public Person
{
private:
int _stuid; //学号
public:
Student()
{
cout << "Student" << endl;
}
Student(const Student &s)
: Person(s) /*显示调用父类的拷贝构造*/, _stuid(s._stuid)
{}
};
有些朋友可能会疑惑,在Person类中的拷贝构造函数参数明明是Person类,为什么我们的Student类可以传过去呢?那是因为我们刚刚讲的切片原理,当我们把子类对象传过去时,编译器会进行切分,然后再传给父类。
派生类的operator=必须要调用基类的operator=完成基类的复制。(原理和拷贝构造大体相似,值得注意的是:当我们在子类直接想去调用父类的operator= 时,会发生重定义,使用时记得加上父类的作用域)
在继承中一个对象的历程如下:父类的构造函数 –> 子类的构造函数 –> 子类的析构函数 –> 父类的析构函数。这个过程相当于把这些行为存在一个栈中,然后再把行为从栈中拿出来一般
一个子类只有一个直接父类。
一个子类有两个及以上的父类
多继承的一种特殊情况
菱形继承会带来一些问题,如数据的冗余和二义性等等,该如何解决这些问题呢?
篇幅有限,今天就到这里把,这个问题我会在下篇博客来解决😼
欢迎各位参考与指导!!!