📝前言:
这篇文章我们来讲讲面向对象三大特性之一——继承
🎬个人简介:努力学习ing
📋个人专栏:C++学习笔记
🎀CSDN主页 愚润求学
🌄其他专栏:C语言入门基础,python入门基础,python刷题专栏,Linux
文章目录
- 一,面相对象三大特性
- 二,继承
- 1 大白话讲继承
- 2 继承定义格式
- 2.1 继承方式的作用
- 2.2 继承类模板
- 2.2.2 需指定类域
- 2.2.1 按需实例化
- 3 基类和派生类间的转换
- 4 继承中的作用域
- 4.1 隐藏规则
- 5 派生类的默认成员函数
- 6 实现⼀个不能被继承的类
- 7 友元关系不能继承
- 8 静态成员的继承
- 9 多继承
- 10 继承与组合
一,面相对象三大特性
面相对象编程具有三大特性,分别是封装、继承和多态:
- 封装
- 概念:将数据和操作数据的方法绑定在一起,组成一个不可分割的整体,即对象。同时,对外部隐藏对象的内部实现细节,只对外提供有限的访问接口。迭代器就是一种封装,底层不一样,但是却能用相似的方法访问
- 作用:通过封装,可以提高代码的安全性和可维护性。避免外部代码直接访问和修改对象的内部数据,防止数据被意外篡改,同时也使得代码的结构更加清晰,各个模块的职责更加明确。
- 继承
- 概念:允许创建一个新的类(子类),它基于现有的类(父类)进行扩展,子类可以继承父类的属性和方法,并且可以在子类中添加自己特有的属性和方法,或者重写父类的方法。
- 作用:继承实现了代码的复用,避免了重复编写相似的代码。同时,它也体现了面向对象编程中的“is - a”关系,即子类是父类的一种特殊类型,有助于建立清晰的类层次结构,便于对问题域进行建模。
- 多态
- 概念:指同一个方法或操作在不同的对象上可以有不同的表现形式。也就是说,不同的子类对象在调用相同的方法时,可能会执行不同的代码逻辑,产生不同的结果。
- 作用:多态提高了代码的灵活性和可扩展性。当需要添加新的功能或修改现有功能时,不需要大量修改客户端代码,只需要在相应的子类中进行修改或扩展即可。它使得代码更加易于维护和升级,同时也增强了代码的可读性和可理解性。
二,继承
1 大白话讲继承
简单来说,子类继承父类就是指:子类可以使用父类的成员,并且也可以自己加自己的成员。我们也把父类称为基类,子类称为派⽣类。
示例(下面这个程序是没有问题的):
class Person
{
public:string name;int age;char sex;
};class Student : public Person
{
public:int st_number;
};int main()
{Student st1;st1.sex = 'b';st1.st_number = 23;return 0;
}
在这里,Person
是父类,Student
是子类
通过监视窗口我们可以看到,st1里面继承了父类Person的三个成员变量。
2 继承定义格式
2.1 继承方式的作用
我们都知道,访问限定符有,private
,public
,和protected
。protected
就是专门为继承设置的。
继承方式对应也有:private
,public
,和protected
继承类成员访问方式的变化:
- 基类
private
成员在派⽣类中是不可见的。不可见是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。(但是其实还是被继承了过去) protected
成员在类外不能直接访问,在子类中可以被访问。- 基类的其他成员在派⽣类的访问⽅式 == Min(成员在基类的访问限定符,继承方式),public > protected >private
class
默认的继承⽅式是private
,struct
默认的是public
,但是建议显式写出继承方式,且一般用public
如,上述代码中父类改成:
class Person
{
public:string name;private:int age;char sex;
};
这时候子类继承后,类外执行st1.sex = 'b';
就会报错,因为sex
是父类的私有成员
2.2 继承类模板
继承类模板需要注意的是:在子类中使用父类类模板的方法时,如果参数是不确定的,要指定一下父类的类域(才能实例化)
2.2.2 需指定类域
namespace tr
{template<class T>class stack: vector<T>{public:void push(T x){push_back(x);}};
}int main()
{tr::stack<int> st;st.push(3);return 0;
}
报错:
原因是:
stack<int>
实例化时,也实例化vector<int>
了,但是不代表push_back
实例化了,因为模板是按需实例化的- 到了
push
操作,编译器要对其实例化,但是因为编译器不知道push_back
是vector<T>
里的成员,从而找不到push_back
这个标识符
正确写法:
void push(T x)
{vector<T>::push_back(x);
}
2.2.1 按需实例化
示例:
namespace tr
{template<class T>class stack: public vector<T>{public:void push(T x){push_back(x);}void print(){cout << "push_back" << endl;}};
}int main()
{tr::stack<int> st;st.print();return 0;
}
上面代码是能正常运行的,原因是实例化stack
的时候,并不会把所有成员都实例化了,后面调用谁,才实例化谁。
3 基类和派生类间的转换
对象传递:
当派生类对象传递给基类对象的时候,会进行切片,即:派生类对象中基类部分的数据会被复制到基类对象中,而派生类特有的成员则被 “切掉”(会丢失派生类成员的所有信息)。这时候基类对象不能访问派生类的成员,调用父类的成员时,结果也是父类对象的。
指针/引用传递:
public
继承的派生类对象的指针/引用 可以赋值给 基类的指针 / 基类的引用,叫向上传型,也类似做切片。传递后,基类的指针/引用指向派生类,但是只能调用派生类中的基类成员那一部分。
如:
class Person
{
public:string name;int age;char sex;
};class Student : public Person
{
public:int st_number;
};int main()
{Student st1;// 派生类对象赋值给基类的指针/引用Person* p1 = &st1;Person& p2 = st1;// 派生类对象赋值给基类对象(实际上调用的是拷贝构造)Person p3 = st1;return 0;
}
注意:基类的不能赋值给子类(基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time TypeInformation)的dynamic_cast
来进⾏识别后进⾏安全转换。)
4 继承中的作用域
基类和派生类都有独立的作用域
4.1 隐藏规则
隐藏:派⽣类和基类中有同名成员(即同名变量或者函数,函数只要同名就算),派⽣类成员将屏蔽基类对同名成员的直接访问。(也叫做重定义)
如果要访问被隐藏的父类成员,可以指定域。基类名::成员
示例:
class Person
{
public:void print(){cout << "Person" << endl;}string name;int age = 10;char sex;
};class Student : public Person
{
public:void print(){cout << "Student" << endl;}int age = 18;int st_number;};int main()
{Student st;cout << "st.age: " << st.age << endl; // 父类的被隐藏st.Person::print(); // 指定父类的域return 0;
}
只要函数同名就会隐藏,如下也是隐藏:
5 派生类的默认成员函数
我们可以派生类中的变量看出三种类型:内置类型,自定义类型,来自父类
**在子类继承父类的时候,父类的成员相当于是最先被声明的,然后才到子类自己的成员。**所以调用构造的时候也是,先父类的,再子类的
如果继承多个父类,则先继承的先声明。
- 派⽣类中基类的成员,必须调用基类的构造函数来初始化(派生类的构造函数会自动调用基类的默认构造,所以,通常,派生类中的成员又有资源申请(需要深拷贝)的时候,我们才需要自己实现构造,拷贝构造和析构也同理)。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显式调⽤(基类的构造函数)
示例(子类构造自动调用基类默认构造完成对基类成员的初始化):
class Person
{
public:Person(int age = 10) // 基类有默认构造:name("小红"),age(age),sex('b'){}void print(){cout << "Person" << endl;}string name;int age;char sex;
};class Student : public Person
{
public:Student(int number) // 子类构造自动调用父类默认构造{st_number = number;}void print(int i){cout << "Student" << endl;}int st_number;};int main()
{Student st(23);return 0;
}
当父类没有默认构造:
class Person
{
public:Person(int a) // 父类没有默认构造:name("小红"),age(a),sex('b'){}void print(){cout << "Person" << endl;}string name;int age;char sex;
};class Student : public Person
{
public:Student(int number){st_number = number;}void print(int i){cout << "Student" << endl;}int st_number;
};int main()
{Student st(23);return 0;
}
报错:
正确写法:
Student(int number, int a):Person(a) // 在初始化列表显示调用父类的默认构造
{st_number = number;
}
- 派⽣类的拷贝构造函数必须调用基类的拷贝构造完成对基类成员的拷贝初始化(如果这个拷贝构造不是缺省的,即不是默认构造函数,也要放在初始化列表)
- 派⽣类的operator=必须要调用基类的operator完成基类的复制。需要注意的是派⽣类的
operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域 - 派⽣类的析构函数会在被调⽤完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员(后定义的先清理)的顺序
- 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同。那么编译器会对析构函数名进⾏特殊处理,处理成
destructor()
,所以基类析构函数不加virtual
的情况下,派生类析构函数和基类析构函数构成隐藏关系。如果要显示调用就要指定域。
示例:
class Person
{
public:Person(int a):name("小红"),age(a){}Person(const Person& p){name = p.name;age = p.age;}Person& operator=(const Person& p){if (this != &p){name = p.name;age = p.age;}return *this;}~Person(){cout << "~Person()" << endl;}string name;int age;
};class Student : public Person
{
public:Student(int number, int a):Person(a) // 在初始化列表显示调用父类的默认构造{cout << "Student(int number, int a)" << endl;st_number = number;}// 拷贝构造错误写法:/*Student(const Student& s){Person(s);st_number = s.st_number;cout << "Student(const Student& s)" << endl;}*/// 正确写法:Student(const Student& s): Person(s){st_number = s.st_number;cout << "Student(const Student& s)" << endl;}Student& operator=(const Student& s){cout << "Student& operator=(const Student& s)" << endl;if (this != &s){Person::operator=(s); // 显式调用父类的=重载来初始化父类的成员st_number = s.st_number;}return *this;}~Student(){cout << "~Student()" << endl;}int st_number;
};int main()
{Student st1(23, 18);Student st2(st1);Student st3(25, 35);st1 = st3;return 0;
}
运行结果:
6 实现⼀个不能被继承的类
- ⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不见就不能调用了,那么派生类就⽆法实例化出对象。
- ⽅法2:C++11新增了⼀个final关键字,final修改基类(表示最终类),派⽣类就不能继承了。
示例:class Person final
7 友元关系不能继承
也就是说基类友元只对基类起作用,而不能访问派⽣类私有和保护成员。
8 静态成员的继承
基类定义了static
静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static
成员实例,用的是用一块内存空间的static
成员
9 多继承
-
单继承:⼀个派⽣类只有⼀个直接基类
-
多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
-
菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。
虚拟菱形继承:
在菱形继承结构中,如果存在多层继承关系,使得一个派生类从两个或多个基类中继承了相同的成员,就会产生数据冗余和二义性问题。
虚拟继承就是为了解决这种问题而引入的,通过在继承关系中使用virtual关键字,使得在最终的派生类中只保留一份共享的基类子对象。
比如,在上述Student
好Teacher
继承时使用虚拟继承,在可能产生数据冗余和二义性的地方添加virtual
,就不会产生两个name
成员,在通过Assistant
访问成员name
时,就只有一个,不会产生二义性问题:
如果不添加:
添加以后:
class Person
{
public:string _name; // 姓名
// 使⽤虚继承Person类
class Student : virtual public Person
{
protected:int _num; //学号
};
// 使⽤虚继承Person类
class Teacher : virtual public Person
{
protected:int _id; // 职⼯编号
};// 教授助理
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
int main()
{
// 使⽤虚继承,可以解决数据冗余和⼆义性Assistant a;a._name = "tr";return 0;
}
10 继承与组合
public
继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。- 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
- 继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤。“白箱”:即相对可见。在继承中,基类的内部细节对派⽣类可见,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。
- 对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),因为对象的内部细节是不可⻅的。组合类之间没有很强的依赖关系,耦合度低。
- 优先使⽤对象组合有助于你保持每个类被封装,优先使⽤组合(has - a),⽽不是继承(is - 1)
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!