> 文章列表 > C++语法(15)---- 继承

C++语法(15)---- 继承

C++语法(15)---- 继承

C++语法(14)---- 模板进阶_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/130092939?spm=1001.2014.3001.5501

目录

1.继承概念和定义

1.概念

2.定义 

1.格式

2. 继承关系和访问限定符

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

3.继承中的作用域

针对成员对象

针对成员函数

注意

4.派生类的默认成员函数

默认生成的四个成员函数

派生类默认成员函数

1.构造函数

2.拷贝构造

3.赋值拷贝

4.析构函数

5.继承与友元

6.继承与静态成员

7.多继承

单继承形式

多继承形式

菱形继承

虚拟继承解决数据冗余/二义性问题

虚拟继承的原理

1.菱形继承存储模式

1.菱形虚拟继承存储模式

8.组合和继承辨析


1.继承概念和定义

1.概念

1.一个函数调用另有一个函数,那么重要直接写出其函数满足的要求即可,函数就被复用了

2.类中是不是也可以像函数那样复用呢,此时就有了继承的概念。

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

以前我们接触的复用都是函数复用,继承是类设计层次的复用。

2.定义 

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; // 姓名int _age = 18; // 年龄
};class Student : public Person
{
protected:int _stuid; // 学号
};class Teacher : public Person
{
protected:int _jobid; // 工号
};int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}

1.格式

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类

2. 继承关系和访问限定符

1.基类private:基类自己可以访问;继承到派生类里,派生类不可见private

2.权限:public > protect,派生类继承方式和基类的成员权限取小的权限

3.访问限定符前的" : "可以不写,class的默认是私有,struct默认是公有


4. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强

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

1.在public继承中,派生类可以直接转换为基类,不需要进行类型转换 -- 也就意味着中间不存在类型转换

2.引用的相关内容

①因为派生类转基类没有类型转换,那么可直接引用,引用的内容是派生类中基类的部分

②非继承关系的普通类型,其赋值其实是需要中间的临时变量进行转换,但是引用指向了临时变量,临时变量具有常性,所以编译器报错;固想要赋值正常,需要在前面加const修饰

 3.指针

 4.总结

即继承可以向上直接转换,前提是基类的成员是公有成员

3.继承中的作用域

针对成员对象

1. 在继承体系中基类和派生类都有独立的作用域,所以能两个结构体取同一个名字
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,若想使用,基类的同名变量,只要 基类::基类成员 显示访问就可以了)

针对成员函数

如果是成员函数的隐藏,只需要函数名相同就构成隐藏

注意

1.在实际中在继承体系里面最好不要定义同名的成员
2.重载需要在同一个作用域

3.重定义是继承时,派生类和基类有同名函数,构成函数名隐藏

4.派生类的默认成员函数

默认生成的四个成员函数

1.针对构造函数和析构函数

内置类型不处理,使得我们需要对内置类型手动处理

自定义类型调用对应的构造和析构

一般析构自动生成就够用了,特别的需要针对逐项释放可以自己写

2.针对拷贝构造和赋值拷贝

内置类型浅拷贝

自定义类型调用对应的拷贝构造和赋值拷贝

至于什么时候需要自己写,那就是需要深拷贝的情况,那么一个标志就是析构要自己写

派生类默认成员函数

1.构造函数

1.派生类不显示写会默认调用基类的默认构造

2.如果要主动构造,需要调用基类的构造函数,而不能直接定义基类的成员变量

3.先调用基类的构造函数,再调用派生类的构造函数

2.拷贝构造

1.派生类不显示写会默认调用基类的拷贝构造

2.如果要主动拷贝构造,需要调用基类的拷贝构造,而不能直接定义基类的成员变量

3.另外针对主动拷贝构造,基类要传入基类,不过由于派生类可以切割变成基类,所以不需要转换

3.赋值拷贝

1.派生类不显示写会默认调用基类的赋值拷贝

2.如果要主动拷贝构造,需要调用基类的赋值拷贝,而不能直接定义基类的成员变量

3.由于其operator=派生类和基类同名,构成隐藏,如果不指定基类显示调用,会出现死递归随后栈溢出;所以赋值拷贝调用基类需要显示调用operator=()函数

4.析构函数

1.派生类不显示写会默认调用基类的析构函数

2.手动调用,要加作用域修饰基类析构函数

3.之所以要加作用域,因为派生类的析构和基类的构造函数构成隐藏关系(由于多态关系需求,所有析构函数都会特殊处理成统一的destructor()函数名,由此构成隐藏关系)

4.主动写析构函数,会调用多次析构函数,可能会有危险,因此不需要显示写析构函数

5.派生类析构完成再析构基类的析构

5.继承与友元

###友元不会被继承

6.继承与静态成员

class Person
{
protected:string _name; // 姓名
public:static int _count; // 统计人的个数。
};
int Person::_count = 0;class Student : public Person
{
protected:int _stuNum; // 学号
};int main()
{Person p;Student s;p._count++;s._count++;cout << Person::_count << endl;Student::_count = 0;cout << Student::_count << endl;
}

基类的静态成员和派生类继承的静态成员是同一个

特殊的:

class Person
{
public:string _name; // 姓名void Print(){cout << 0 << endl;}
public:static int _count; // 统计人的个数。
};
int Person::_count = 0;int main()
{Person* pp = nullptr;cout << pp->_count << endl; //可以运行cout <<(*PP)._count<< endl; //会优化,与上面一样pp->Print();                //可以运行(*PP)->Print();             //会优化,与上面一样cout << pp->_name<< endl;   //不可以运行
}

函数并没有在类型中,函数在代码段中

静态变量没有存在类中,而在静态区

而类中的变量,由于指针是空,无法访问到

7.多继承

单继承形式

多继承形式

多继承本身没什么问题,但是多继承后可能会生成菱形继承,这样就有数据冗余和二义性的问题。

菱形继承

在空间角度:数据冗余

在逻辑角度:二义性

class Person
{
public :string _name ; // 姓名
};class Student : public Person
{
protected :int _num ; //学号
};class Teacher : public Person
{
protected :int _id ; // 职工编号
};class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a ;
a._name = "peter";// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}

当然,能都指定哪个类的数据命名解决二义性的问题,但是数据依然冗余

虚拟继承解决数据冗余/二义性问题

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。关键之 --- virtual

在腰部使用virtual进行虚拟继承

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; // 主修课程
};

问题解决,三个东西其实是同一个的

虚拟继承的原理

1.菱形继承存储模式

class A
{
public:int _a;
};class B : public A
{
public:int _b;
};class C : public A
{
public:int _c;
};class D : public B, public C
{
public:int _d;
};

菱形继承模型

1.菱形虚拟继承存储模式

class A
{
public:int _a;
};class B :virtual public A
{
public:int _b;
};class C :virtual public A
{
public:int _c;
};class D : public B, public C
{
public:int _d;
};

那么B,C的开头又是什么?

这些其实是指针,它指向一虚基表,该虚基表的第二个数据值存储了偏移量

为了让B和C找到A,使得B,C,D的_a都是一个共同的数据;

访问A的内容比较麻烦,访问B的数据比较方便;

如果切牌你,形式其实依然是先有一个指针,随后跟着数据;只是偏移量变小了

这样设计如果A的大小足够大,那就节省了空间,因为由指针替换了整体。

8.组合和继承辨析

//继承
class X
{
public:int _x;
};class Y:public X
{
public:int _y;
};//组合
class M
{
public:int _m;
};class N
{M m;int _n;
};

1.继承和组合都完成了复用

2.protect能继承下来;继承为白箱复用(看得见内部),组合为黑箱复用(不知道内部)

3.继承更多逻辑关系是is-a;组合是一种has-a的关系

4.如果都能用,选择组合,组合更满足低耦合(依赖性不高)

耦合的解释:假设100个成员,80个保护,20个公有

继承:由于关联度很强,任何一个修改都会影响到派生类,因为保护的也被继承了,派生类看待基类是信息透明的(白箱)

组合:该类只有20个公有修改才可能影响到组合的,组合看待类的内容是不完全公开的(黑箱)