文章目录
什么是继承?一、继承的概念二、继承的语法三、继承方式(访问限定符)先说记忆方法:再举例总结:再说注意事项: 四、基类和派生类对象赋值转换五、继承中的作用域概念解释判断题目 六、派生类的默认成员函数行为总结原理解释对于构造:对于拷贝构造对于赋值重载对于析构函数 七、继承与友元八、继承与静态成员总结
什么是继承?
继承是面向对象编程(OOP)中的一个核心概念,它允许我们在已有类的基础上创建新的类,从而实现代码的复用和扩展。派生类可以继承基类的属性和方法,并可以添加新的功能或重写现有的方法,这样不仅提高了代码的可维护性,还促进了设计的灵活性。
继承通常呈现出一种层次结构,比如类之间的父子关系,这种结构使得设计变得更具组织性。从简单到复杂的认知过程让我们能更好地理解和构建系统。与函数复用相比,继承不仅仅是代码复用,它也改变了类之间的关系,促进了更高层次的重构和设计。
一、继承的概念
假设我们要写一个职工管理系统:
一个学校有老师,学生保安大叔等等,对于每一种人都需要一个类,但是老师,学生,保安大叔这些都有共同的成员变量,如:姓名,年龄,电话号码等等,难道我们需要每个都写一遍吗?
这样的话代码就会显得非常冗余,我们可以抽象出一个父类
–人类,它包含姓名,年龄住址电话号这些信息,然后让学生和老师去继承这些信息。
学生和老师作为Person类
的孩子他即包含了person类的元素,还包含自己独有的元素,这就是继承。
二、继承的语法
现在我们有一个人类:
class Person{public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name = "peter"; // 姓名int _age = 18; // 年龄};
想让学生类和老师类继承人类,语法是这样的:
class 子类 : 继承方式(访问限定符) 父类
{}
具体代码如下:
class Student : public Person{protected:int _stuid; // 学号};class Teacher : public Person{protected:int jobbit; //工号};
三、继承方式(访问限定符)
先说记忆方法:
继承一共分9种情况,对于父类有公有,保护,私有三种,继承方式也有公有,保护,私有三种,两两组合公有9种继承方式,具体情况如下:
只要基类是private,都是不可见,基类不是private,在基类成员访问限定符与继承方式访问限定符取小。
再举例总结:
对于同一个类三种访问限定符的区别:
对于继承来说,public继承
总结:对于类内可以访问公有和保护,对于类外可以访问公有
对于protected继承
总结:对于类内可以访问公有和保护,对于类外都不可以
对于private:
总结:三种继承方式对于类内来说是一样的,都是可以访问父类公有和保护的成员
对于类外来说,public继承可以类外访问父类public的成员,其余继承方式什么都访问不了
三种继承的区别:
再说注意事项:
基类的private成员在派生类中无论以何种方式继承,都是不可见的。这里的不可见是指,尽管基类的私有成员仍然被继承到派生类对象中,但由于语法限制,派生类对象无法在类内部或外部访问这些成员。
基类的private成员在派生类中无法被访问。如果希望基类的成员在类外不可直接访问,但又需要在派生类中能够访问,可以将其定义为protected。保护成员限定符是为了支持继承而出现的,允许派生类访问这些成员。
从上述表格可以总结出,基类的私有成员对子类始终不可见。对于基类的其他成员,子类的访问方式取决于基类的访问限定符及继承方式,具体优先级为public > protected > private。
使用关键字class时,默认的继承方式为private;而使用struct时,默认的继承方式为public。为了代码的可读性和明确性,建议显式地写出继承方式。
在实际应用中,通常使用public继承,而很少使用protected或private继承。这是因为protected/private继承下来的成员仅能在派生类内部使用,这种限制降低了代码的扩展性和维护性。因此,不提倡使用protected或private继承,以保持代码的灵活性和可维护性。
四、基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后
面再讲解,这里先了解一下)
其实就可以看成子类成员由父类成员+子类自我的成员组成,用父类的变量去接收子类成员的时候默认会将父类成员切出来。
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(){/*int i = 0;double d = i;double& d = i;const double& d = i;*/Person p;Student s;// 赋值兼容转换(切割,切片)//p = s;Person p1 = s;Person& rp = s;rp._name = "张三";Person* ptrp = &s;ptrp->_name = "李四";//s = (Student)p;return 0;}
五、继承中的作用域
概念解释
在继承体系中基类和派生类都有独立的作用域。子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。注意在实际中在继承体系里面最好不要定义同名的成员。
这里是想表达什么意思呢?
我们来看下面这个场景:
class Person{public:void fun(){cout << "Person::func()" << endl;}protected:string _name = "小李子"; // 姓名int _num = 111; // 身份证号};// 隐藏/重定义:子类和父类有同名成员,子类的成员隐藏了父类的成员class Student : public Person{public:void fun(){cout << "Student::func()" << endl;}void Print(){cout << " 姓名:" << _name << endl;cout << _num << endl;cout << Person::_num << endl;}protected:int _num = 999; // 学号};int main(){Student s;s.Print();s.fun();s.Person::fun();return 0;}
答案是会调用Student的成员,因为父类和子类是不同的作用域,编译器有就近原则,有现成的会先吃现成的,但是如果指定了父类作用域下,就会调父类的。
判断题目
接下来来看一下这道题选择什么?
class Person{public:void fun(){cout << "Person::func()" << endl;}protected:string _name = "小李子"; // 姓名int _num = 111; // 身份证号};// 隐藏/重定义:子类和父类有同名成员,子类的成员隐藏了父类的成员class Student : public Person{public:void fun(int i){cout << "Student::func()" << endl;}void Print(){cout << " 姓名:" << _name << endl;cout << _num << endl;cout << Person::_num << endl;}protected:int _num = 999; // 学号};int main(){Student s;s.Person::fun();s.fun(1);return 0;}
六、派生类的默认成员函数
行为总结
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。派生类的operator=必须要调用基类的operator=完成基类的复制。派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。派生类对象初始化先调用基类构造再调派生类构造。派生类对象析构清理先调用派生类析构再调基类的析构。因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
原理解释
我们先有一个人类,它写好了构造,拷贝构造,赋值重载,析构:
class Person{public:Person(const char* name = "peter") //假设先提供一下默认构造//Person(const char* 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 operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;delete _pstr;}protected:string _name; // 姓名//string* _pstr = new string("111111111");};
对于构造:
对于拷贝构造
情景三这里对于父类的成员,拷贝构造就相当于走了切片!
对于赋值重载
正常走就行,唯一注意的点就是父类与子类运算符重载构成了隐藏关系,所以需要指明作用域:
Student& operator=(const Student& s){if (this != &s){Person::operator=(s);_id = s._id;}return *this;}
对于析构函数
对于析构函数来说,我们需要写父类的析构吗?
Person::~Person();
小插曲:这里析构函数的名字没有构成隐藏关系为什么要指定作用域?
// 由于后面多态的原因(具体后面讲),析构函数的函数名被
// 特殊处理了,统一处理成destructor
我们来看这个的运行结果:
int main(){Student s1;Student s2(s1);Student s3("李四", 1);s1 = s3;return 0;}
明明是3个对象,却调用了6次析构,这很明显是不对的。
这里首先牵扯到一个问题:
我们说后定义的要先析构,也就是说,子类要先析构,如果我们显示写了父类的析构有可能会写成这样:
那么就无法保证后定义的先析构了。
第二个问题是这样的:
如果我们有操作,比如父类有个指针要在结束的时候打印一下:
这样就会导致_pstr被销毁了还在调用。
综上:
显示调用父类析构,无法保证先子后父
所以子类析构函数完成就,自定调用父类析构,这样就保证了先子后父,我们就不用显示写父类的析构了,编译器会在结尾自己调用!
七、继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
通俗点说就是父亲的朋友,不是儿子的朋友。
class Student;class Person{public: friend void Display(const Person& p, const Student& s);protected: string _name; // 姓名};class Student : public Person{protected: int _stuNum; // 学号};void Display(const Person& p, const Student& s){ cout << p._name << endl; cout << s._stuNum << endl;}void main(){ Person p; Student s; Display(p, s);}
八、继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 。
也就是说,子类虽然继承到了父类的static静态成员,但他们的本质还是一个值。
利用这一点我们可以统计这个父类延伸下去了多少个子类:
class Person{public:Person() { ++_count; }protected:string _name; // 姓名public:static int _count; // 统计人的个数。};int Person::_count = 0;class Student : public Person{protected:int _stuNum; // 学号};class Graduate : public Student{protected: string _seminarCourse; // 研究科目};void TestPerson(){Student s1;Student s2;Student s3;Graduate s4;cout << " 人数 :" << Person::_count << endl;Student::_count = 0;cout << " 人数 :" << Person::_count << endl;}int main(){TestPerson();}
总结
到这里,继承上篇就结束啦,
我们还剩一个菱形继承及继承模块的总结没有讲,将会放到继承(下篇)。
谢谢各位大佬的支持~?????(╯‵□′)╯︵┻━┻