【C++】继承

在这里插入图片描述

write in front
所属专栏: C++学习
️博客主页:睿睿的博客主页
️代码仓库:VS2022_C语言仓库
您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我你们将会看到更多的优质内容!!


文章目录

  • 前言
  • 一.继承基类成员访问方式的变化:
  • 二.基类和派生类对象赋值转换:
  • 三.继承中的作用域:
  • 四.继承的默认成员函数:
    • 1.构造函数:
    • 2.析构函数:
    • 3.拷贝构造函数:
    • 4.赋值重载函数:
  • 五.基类的静态变量:
  • 六.菱形继承:
    • 菱形继承的原理:
    • 菱形虚拟继承的优点:
    • 菱形继承的构造与析构:
  • 继承总结:
    • 继承与组合
  • 总结

前言

  继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

一.继承基类成员访问方式的变化:

【C++】继承_第1张图片

  事实上,在平时我们用public继承最多,protect这个私有继承就是为了继承而生的。

二.基类和派生类对象赋值转换:

  在继承里面,派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用
  这里有个形象的说法叫赋值兼容(切片或者切割)。寓意把派生类中父类那部分切来赋值过去,这就与平时内置类型的转换不一样。

int a=10;
double b=a;//这里会生成临时变量
double &c=a;//所以这里就会报错
//正确的方式:
const double &c=a;

  在内置类型中用不同的类型赋值,会先产生一个临时变量,用这个临时变量来赋值,但是继承类就是直接切片:

class A
{
	public:
		int a;
};

class B:public A
{
	public:
	int b;
}

int main()
{
	B tb;
	A& ta=tb;
	ta.a=10;
}

  在这里我们会发现,继承类不产生临时变量,引用的话直接就指向那个类的切片,指针就直接指向那个切片的地址,赋值就直接用切片赋值就行了。
【C++】继承_第2张图片

三.继承中的作用域:

  在继承里面,如果父类和子类出现同样名字的成员,那么在子类访问该成员的时候就会直接将父类的成员隐藏/重定义掉,因为子类和父类都有自己的独立的作用域。如果在子类里面要访问就用 基类::基类成员 来访问。
  在下面的代码中,Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆

class Person
{
protected :
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
cout<<" 身份证号:"<<Person::_num<< endl;
cout<<" 学号:"<<_num<<endl;
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};

大家可以看一下下面这段代码的两个fun函数构成什么关系?
a、隐藏/重定义 b、重载 c、重写/覆盖 d、编译报错

class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" <<i<<endl;
}
};
void Test({
B b;
b.fun(10);
};

答案:a (父子类域中,成员函数名相同就构成隐藏)

四.继承的默认成员函数:

1.构造函数:

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  • 派生类对象初始化先调用基类构造再调派生类构造。
  • 多继承会根据继承顺序(声明顺序)来先后调用构造函数
    我们来看一道题目:
    【C++】继承_第3张图片
      我们来看看这道题目,答案是c
    【C++】继承_第4张图片

  其实这就是切片的一道题目,根据类型的不同,父类的指针指向不同的切片,所以会导致指向不同,但是根据继承顺序可以得知派生类的组成顺序,所以p1==p3;

2.析构函数:

  由于构造函数的顺序是先父后子,所以在调用析构函数的时候是顺序就是先子后父,这样也保证了在析构的时候如果要对父类进行处理的特殊情况。
【C++】继承_第5张图片
  所以为了保证析构顺序,编译器在子类析构函数完成后,会自动调用父类的析构函数,这样就保证了先子后父。

3.拷贝构造函数:

派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化

4.赋值重载函数:

派生类的operator=必须要调用基类的operator=完成基类的复制

五.基类的静态变量:

  基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。

六.菱形继承:

【C++】继承_第6张图片
  在多继承的时候就会出现菱形继承,就会有数据冗余和二义性的问题:
【C++】继承_第7张图片
  祖师爷为了解决二义性的问题就想出了虚拟继承的方式。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承。

class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}

菱形继承的原理:

我们以这段代码为例子:

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;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}

通过调试我们发现,经过虚拟继承,其子类和正常菱形继承的子类已经产生了不同:

【C++】继承_第8张图片
我们发现菱形继承的父类A两个_a是不同的值。

【C++】继承_第9张图片
  但是虚拟继承下,我们发现A的位置存了不同的地址,并且在最下面重新存了一个新的A。通过查看B和C里的A的两个地址,通过二进制转十进制发现了两个数字,而这两个数字就分别是从B里面的A和C里面的A到虚拟继承D里面真正的A的偏移量。
  在虚拟继承以后,就避免了二义性的存在,并且我们在定义B和C的时候,其地址也会发生变化:
【C++】继承_第10张图片
  以前是直接存A这个类,在虚拟之后也是存到A这个类的偏移量了。

这就是为了让切片的时候符合要求!!!!不然我们切片肯定会出错!

以这个例子,我们用子类给父类赋值,如果要访问A的_a,其过程就是找到偏移量,找到A的新地址,也就是说我们寻找重复父类的方式要统一

菱形虚拟继承的优点:

  其实虚拟继承还是有一些优点的,他把两个重复的父类变成了一个地址,通过这个地址找到偏移量找到新创建的重复父类,而且这个时候就只有一个重复父类了,大大的节省了空间。
  说是这样说,但是大家还是尽量少用菱形继承。

菱形继承的构造与析构:

class A {
public:
	A(const char* s) { cout << s << endl; }
	~A() {}
};

class B :virtual public A
{
public:
	B(const char* sa, const char* sb) :A(sa) { cout << sb << endl; }
};

class C :virtual public A
{
public:
	C(const char* sa, const char* sb) :A(sa) { cout << sb << endl; }
};

class D :public B, public C
{
public:
	D(const char* sa, const char* sb, const char* sc, const char* sd) 
		:B(sa, sb), C(sa, sc), A(sa)
	{
		cout << sd << endl;
	}
};

int main() {
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

  由上面的菱形继承的结构,我们知道派生类里面只有一个重复父类。所以其调用顺序就是先对重复父类调用构造函数,然后根据继承顺序来调用构造函数初始化。析构就是以前的规律反着来,先构造后析构。

继承总结:

很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java

继承与组合

继承:
class D : public C
{};
组合:
class E
{
private:
	C _cc;
};

【C++】继承_第11张图片
  简单的说,和继承相比,组合的耦合度更低,也就是说其关联性更低。因为继承是将一个类的保护成员和公有成员继承到一个新类,而组合只是将一个类的公有成员放到一个新类。所以组合的耦合度更低,在改动父类的时候对新类影响更小。
  一般来说,对于何时继承何时组合只要满足以下规则就可以了:

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象

总结

  更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

专栏订阅:
每日一题
C语言学习
算法
智力题
初阶数据结构
Linux学习
C++学习
更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

你可能感兴趣的:(c++,c++,ajax,java)