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个公有修改才可能影响到组合的,组合看待类的内容是不完全公开的(黑箱)