作者前言
🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂
🎂 作者介绍: 🎂🎂
🎂 🎉🎉🎉🎉🎉🎉🎉 🎂
🎂作者id:老秦包你会, 🎂
简单介绍:🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂
喜欢学习C语言、C++和python等编程语言,是一位爱分享的博主,有兴趣的小可爱可以来互讨 🎂🎂🎂🎂🎂🎂🎂🎂
🎂个人主页::小小页面🎂
🎂gitee页面:秦大大🎂
🎂🎂🎂🎂🎂🎂🎂🎂
🎂 一个爱分享的小博主 欢迎小可爱们前来借鉴🎂
继承
继承介绍
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
写法:
Person是父类,也称作基类。Student是子类,也称作派生类
继承方式的介绍
我们可以看到,类的访问限定符和继承方式是一样的,这就需要我们进一步了解,继承方式的不同,派生类可以获取到的基类的成员或者成员函数,在派生类中属于啥访问形式,
形式:
下面一一介绍
需要注意的是基类的私有成员不能被派生类使用
public继承
#include<iostream>
#include<vector>
#include<string>
using namespace std;
namespace bit
{
class Person
{
public:
void print()
{
cout << _id << endl;
cout << _name << endl;
}
int _id;
string _name;
};
class Stude : public Person
{
protected:
string home;
};
}
int main()
{
bit::Stude student1;
student1._id;
student1.print();
return 0;
}
这样是不会报错的
protected继承
namespace bit
{
class Person
{
public:
void print()
{
cout << _id << endl;
cout << _name << endl;
}
int _id;
string _name;
};
class Stude : protected Person
{
protected:
string _home;
};
}
int main()
{
bit::Stude student1;
student1._id;
student1.print();
return 0;
}
结果如下:
基类的公用成员,变成了派生类的保护成员,
private继承
同理使用private继承的话,和protected继承的结果类似,但是需要注意的是
继承基类的成员,在派生类中都变成了私有
总结
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它。 - 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强
基类和派生类对象赋值转换
这里只适用于public继承
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
namespace bit
{
class Person
{
public:
void print()
{
cout << _id << endl;
cout << _name << endl;
}
int _id;
string _name;
};
struct Stude : Person
{
protected:
string _home;
};
}
int main()
{
bit::Stude student1;
bit::Person a;
a = student1;
return 0;
}
需要注意的是 ,这个赋值过程不会产生临时变量,我们知道,类型转换会产生临时变量,该变量具有常性,需要用const修饰
如理:
图中没有报错,所以可以说明,不会派生类对象赋值给基类对象,不会产生临时变量,
还有一些情况:
继承中的作用域
前面我们知道,相同的作用域不能有相同的变量名,不同的作用域可以有,
- 而在继承中,在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问),一般默认访问的是派生类的
namespace bit
{
class Person
{
public:
void print()
{
cout << _id << endl;
cout << _name << endl;
}
int _id;
string _name;
};
struct Stude : Person
{
void print()
{
cout << _id << endl;
cout << Person::_id;
}
protected:
string _home;
int _id = 1;
};
}
int main()
{
bit::Stude student1;
student1.print();
return 0;
}
需要注意的是,函数重载是要在相同作用域,派生类和基类有各自的作用域
代码中的基类和派生类的成员函数有相同的名字,这种情况是隐藏,不是重载
如果要调用基类的成员函数,可以这样
student1.Person::print();
个人建议: 不要定义同名的成员,这样就不会出现很多问题
派生类和基类的析构、拷贝析构和析构的顺序
还有一些情况,比如默认生成的构造函数:
派生类: 内置类型和自定义类型分别处理, 自定义类型调用自己的默认构造函数,内置类型,随机值
基类的成员:调用基类的构造函数
namespace bit
{
class Person
{
public:
Person(int id, const string& name)
{
_id = id;
_name = name;
}
void print()
{
cout << _id << endl;
cout << _name << endl;
}
int _id;
string _name;
};
struct Stude : Person
{
public:
Stude(int id, const string& name, const string& home)
:Person(id, name)//调用基类的构造函数
,_home(home)
{
}
void print()
{
Person::print();
cout << _home << endl;
}
protected:
string _home;
};
}
派生类的里面的基类成员,在默认构造函数要调用对应的基类默认构造函数,不能乱定义,在初始化列表中,先初始化基类, 我们需要记住,先声明先初始化,不要片面的理解成员的顺序决定初始化顺序,
如果是拷贝构造的话,我们主要记得派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用,还是要调用基类的拷贝构造函数
如图:
注意:析构函数、默认构造、拷贝构造也是要调用基类的
调用析构函数的问题:
这里的原因是:派生类的析构和基类的析构构成隐藏关系,后面的多态的问题,析构函数被特殊处理,函数名都被处理成了destructor()
还有一个情况,析构顺序,是先析构派生类,再析构基类
如果先释放基类,派生类的成员访问到释放的地址就是野指针,防止有这个问题出现,就有这个顺序了
总结:
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类
中,这几个成员函数是如何生成的呢?
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。 - 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。 - 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
- 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
继承与友元
我们来复习一下友元
- 友元分为:友元函数和友元类
- 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在
- 类的内部声明,声明时需要加friend关键字。
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
- 友元函数没有this指针。
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<vector>
#include<string>
using namespace std;
namespace bit
{
class Person
{
public:
friend void fired(const Person& funtion);
Person(int id, const string& name)
{
_id = id;
_name = name;
}
Person(const Person& n)
{
_id = n._id;
_name = n._name;
}
~Person()
{
cout << "~Person()" << endl;
fired(*this);
}
void print()
{
cout << _id << endl;
cout << _name << endl;
}
int _id;
string _name;
};
struct Stude : Person
{
public:
Stude(int id, const string& name, const string& home)
:Person(id, name)//调用基类的构造函数
,_home(home)
{
}
void print()
{
Person::print();
cout << _home << endl;
}
Stude(const Stude& n)
:Person(n)
{
_home = n._home;
}
~Stude()
{
cout << "~Stude()" << endl;
}
protected:
string _home;
};
void fired(const Person& funtion)
{
cout << funtion._id;
cout << funtion._name;
}
}
int main()
{
bit::Stude student1(21, "李四", "广西");
//bit::Stude student2 = student1;
bit::Person a(20, "张三");
a.print();
a = student1;//派生类赋值给基类
a.print();
student1.print();
bit::Person& b = student1;
bit::Person* c = &student1;
return 0;
}
当我们再里面调用基类的友元的时候,该友元是不存在的,而基类析构里面的友元正常调用,
所以说,要想有和基类一样的友元,需要声明在派生类中。
继承与静态成员
复习一些静态成员:
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用
static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
这个静态成员是在静态区
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例,也就是说,派生类会继承,但是只会有一份
单继承
一个派生类只有一个基类
这个意思就是 Person的派生类是Student , Student的派生类是PostGraduate
多继承
一个子类有两个或以上直接父类时称这个继承关系为多继承
会导致许多的菱形继承,这个就很复杂,下面我们来具体介绍
复杂的菱形继承及菱形虚拟继承
菱形继承指的是当一个类继承自两个直接父类,而这两个父类又继承自同一个父类时所形成的继承结构。这种结构导致子类通过两个不同的路径继承了相同的属性和方法,从而引发潜在的冲突和歧义
从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份
我们想要的结果是:
可以看出菱形继承的缺点很大,如果想要解决可以有下面方法:
#include<iostream>
#include<vector>
#include<string>
using namespace std;
namespace school
{
class Person
{
public:
string _name;
};
class Student : public Person
{
protected:
int _id;
};
class Teacher : public Person
{
protected:
string _school_number;
};
class school_table : public Student,public Teacher
{
protected:
string _maj;
};
}
我们定义一个A1对象
school::school_table A1;
这是一个菱形继承的样例, 如果我们直接给A1._name直接赋值,这样会有二义性无法明确知道访问的是哪一个,就会有下面报错:
如果想要解决这个问题,可以指向对应的基类的成员进行赋值,可以如下:
int main()
{
school::school_table A1;
A1.Teacher::_name = "张大";
A1.Student::_name = "小张";
return 0;
}
这样的话,但是数据冗余问题无法解决,只是一个治标不治本的方法,不太可靠,
所以在c++后面就引入了虚拟继承
虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和
Teacher的继承Person时使用虚拟继承,即可解决问题。
namespace school
{
class Person
{
public:
string _name;
};
class Student :virtual public Person
{
protected:
int _id;
};
class Teacher : virtual public Person
{
protected:
string _school_number;
};
class school_table : public Student,public Teacher
{
protected:
string _maj;
};
}
virtual的使用方法
virtual 关键字,这个使用的话,是有规则的,不是简简单单的在多继承的派生类的基类的继承的时候加,而是在最开始有数据出现冗余的时候,如图,
G是多继承,开始产生数据冗余的是B和C,所以在B和C继承A的时候要加vritual,而DEF不需要,
具体是怎么解决二义性和数据冗余的话,我们需要进一步理解一下其原理,
这个图的意思就是, Student和Teacher的相同的数据冗余被拿出来,统一放_majorCourse在最后面,就变成了我们理想状态下的了,
我们以下面的代码为例子:
namespace school
{
class Person
{
public:
string _name;
};
class Student :public Person
{
public:
int _id;
};
class Teacher : public Person
{
public:
string _school_number;
};
class school_table : public Student,public Teacher
{
protected:
string _maj;
};
}
int main()
{
int size = sizeof(string);
int ailgin = offsetof(school::Person, _name);
school::school_table A1;
A1.Student::_name = "1234";
A1.Teacher::_name = "4321";
A1._id = 1111;
A1._school_number = "5678";
return 0;
}
这里我没有使用virtual,我们可以观察到内存窗口如下:
可以看到A1的大小,(内存对齐),数据冗余等问题可以看得很清楚,
当我们使用virtual之后就会发现:
数据冗余的字段,放在最后面了,并且后面赋值过来的值都会覆盖了,
可以简单的说明, Person只有一个位置,所以我们可以大致清楚最开始的图的意思是啥情况
为了我们进一步的了解,上面的代码我们不使用,我们使用一份新的代码
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
```powershell
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
如图:
这个是没有使用virtual的
使用virtual后:
在原来的_a的地方,存储的不再是值了,而是一个地址,通过地址,找到对应的位置,然后我们可以看到,这个位置下存储的值,(需要用到8个字节, 前四个自己有其他用途,后面四个字节存储的是偏移量)
然后使用0x00D7FCB8加上指针所指向的值(20),和0x00D7FCC0加上指针所指向的值(12),最终两个获取到的地址就是0x007FCCC
也就是_a存储的位置,由此可见,原本旧的_a存储的地址指向的地方,存储的是旧_a到公共_a的偏移量,
虽然有虚拟继承可以预防菱形继承,但是也并不是百分百的好,
当使用了虚拟继承后,赋值给一个基类对象,该基类对象也会像这个派生类对象一样,通过去寻找偏移量,找到对应的那个公共地址去进行访问
图中的d切片赋值给bb,当我们去查看bb的内存的时候,就会发现
在0x00AFFCC0中存储的是偏移量的地址,通过偏移量去找到公共地址,
因为虚继承没有直接存储_a,而是把值存放在一个公共的区域
总结:
所以一般不建议设计出多继承,如果设计了多继承一定不要设计出菱形继承。否则在复杂度及性能上都有问题
组合
前面我们知道继承可以很好的让我们更好的使用类,但是也引出了很大的问题,除了继承,还有一个方式,就是组合.
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
组合:低耦合,一个对象的改变对另外一个对象影响不大
继承: 高耦合, 基类的改变会很大程度影响派生类
namespace bit
{
template<class T>
class A
{
public:
A()
{
_aa = nullptr;
}
T _a;
protected:
T* _aa;
};
//白箱复用(继承)
template<class T>
class B : public A<T>
{
public:
B()
::A<T>::A()
{
}
protected:
T _bb;
};
//黑箱复用(组合)
template<class T>
class C
{
protected:
T _cc;
A<T> _c;
};
}
int main()
{
bit::B<int> bb;
bit::C<int> cc;
}
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
为白箱复用(white-box reuse)。
新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),
总结:
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。