继承
继承是使得代码实现最大程度上的复用,是类设计层次的复用
设计一个基础类,将大多数相同属性的放到一个父类里面
继承方式:总共9种关系.父子各自有3种:public protected private
父类中私有的在子类中不可见不代表没有继承,在子类里面的类外面都不可用.
子类继承父类的权限,取访问方式和访问限定符小的那个.
访问限定符不写时,class 默认是私有,struct默认是共有.
切片(赋值兼容规则)
同类型的可以互相赋值,那么不同类型之间如何赋值?相关的可以进行隐士类型转换.
- 那么父子类如何进行赋值呢?限于公有继承–>切片
拿子类中继承的一部分切割出来赋值给父类,还可以给给父类的指针和引用,指向子类父类公有的部分,子类的私有父类其实还是看不见的.
-
注意:不存在类型转换,要不然无法实现引用,是语法天然支持的。类型转换是会产生临时变量,临时变量具有常性,引用临时变量前面得加const.
-
基类指针可以通过强转直接指向子类对象,子类的指针和引用不能调用基类的
student肯定是一个Person(基类可以调用子类的对象)
Person不非得是student(子类并不能完全代表Person基类) -
为甚么私有继承不支持切割切片呢?
私有继承父类的公有和保护,在子类中是私有的,如果切片之后,和父类中会存在权限矛盾.将派生类切片为其私有基类没有任何意义。想一想“私人”是什么意思。这意味着外界不应该关心它。允许对私人基地进行切片(铸造)意味着外界会关心。
-
父类不能给子类(少的给多的给不了)
-
对象不行,但是指针和引用可以。反正就是一个指针指向,但是会存在会越界的风险,子类指针指向了父类的私有就出现了越界的问题。
#include<iostream>
#include<cstdio>
using namespace std;
class Person
{
public:
string _name; // 姓名
string _sex; // 性别
private:
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
void Test2()
{
//子类对象给给父类对象-切片
Student s;
Person p = s;
Person* p = &s;//指针
Person& p = s;//引用
Person p1;
Student s;
//s = p1;//父类对象不可以给给子类对象
Person* ptr = &p1;//父类对象的指针可以通过强转的方式赋值给子类指针
Student* sptr = (Student*)ptr;
//子类指针可修改父类公有无法修改私有(越界风险,都看不见)
sptr->_name = "12345";
}
继承-作用域
基类和派生类都有独立的作用域.
当有同名成员时,局部优先原则,就近原则.
- 怎样先访问外面的?指定作用域。
同理在子类当中存在同名的变量时,先访问自己的。
如果想要访问父类的,指定父类的就行。
隐藏
子类会隐藏父类的同名成员(成员变量,成员函数).
父类和子类中相同名字的函数是不构成函数重载的,因为在同一作用域才构函数重载,成员函数名相同就构成隐藏,和参数的区别无关。
隐藏之后就无法直接调用父类继承来的内个同名函数,被隐藏的,想调得指定基类作用域
例如:反向迭代器是对正向迭代器的一层封装,在lsit类中typedef 之后会就近找同名,造成bug.可以指定作用域名或者根据向上寻找的特点调换位置.
继承-成员函数
派生类的默认成员函数(总共6个,有两个不重要):构造函数,析构函数,拷贝构造函数,赋值函数operator=
(4个重要的)
- 我们不写,默认生成的派生的构造和析构?
父类继承的(调用父类的默认构造和析构处理) 和自己的(内置类型和自定义类型成员)
- 我们不写,默认生成的派生的拷贝构造和赋值
从父类继承的(调用父类的拷贝构造和赋值) 和自己的(拷贝构造和赋值)
- 小结:继承来的调用父类处理,自己的按照普通类基本规则处理.
- 什么时候需要自己写?
- 父类没有默认构造时,需要我们自己显示写构造
- 如果子类有开辟的资源需要释放,就需要自己显示写析构函数.
- 如果子类存在浅拷贝,就需要自己实现拷贝构造和赋值解决浅拷贝的问题
- 怎么写?
- 父类的成员调用父类的拷贝构造和赋值
析构函数的名字会统一为destructer,名字相同,
子类的析构函数和父类的就构成隐藏,把父类的给隐藏了。
class Person
{
public:
Person(const string name)
{
_name = name;
}
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person& operator=(const Person& p)" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{}
public:
string _name=""; // 姓名
};
class Student : public Person
{
public:
//当父类没有默认的拷贝构造时:
Student(const char* name="张三", int No=10)
:Person(name)
, _No(No)
{}
Student(const Student& s)
:Person(s)//切片
,_No(s._No)
{}
//当父类没有默认的赋值构造时:
Student& operator=(const Student& s)
{
if (this != &s)
{
//不指定会因为就近原则无线递归栈溢出
Person::operator=(s);//切片
_No = s._No;
}
return *this;
}
~Student()
{
//只能指定作用域才能调用析构函数?
//析构函数的名字会统一为destructer,名字相同(为什么?--多态)
//子类的析构函数和父类的就构成隐藏,把父类的给隐藏了。
Person::~Person();
//delete[] _ptr;
}
public:
int _No; // 学号
//int* _ptr=new int[10];
};
void Test()
{
Student s;
Student s1(s);
Student s2 = s;
}
- 但是会自动调用两次析构函数(如果存在空间释放两次造成崩溃)为啥呢?
子类的析构函数不需要显示调用一下父类的。子类析构函数结束时,会自动调用父类的析构函数。
栈里面的变量是先进后出,后初始化构造的先析构。也就是子类先析构
如果自己显示调用,会先析构父类的,不符合先后关系。
继承和友元
友元关系是不能够继承的,不能够访问子类的私有成员。
继承和静态成员
统计一个父类到底派生了多少个子类,采用静态变量加加的方式。无论派生多少个子类,这些类指向的静态变量是同一个。
class Person
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
cout << &s4._count << endl;
cout << &s3._count << endl;
cout << &s2._count << endl;
}
菱形继承
单继承:一个子类只有一个直接父类时的继承关系
多继承:有两个或者以上的直接父类的继承关系,会出现数据的冗余二义性
菱形继承是多继承的一种特殊情况:
- 指定类还能确定对象。但是开空间就是二倍开,那如何解决这种数据冗余呢?-虚继承
class Person
{
public:
string _name; // 姓名
int a[10000];
};
class Student : public Person
{
public:
int _num; //学号
};
class Teacher : public Person
{
public:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
public:
string _majorCourse; // 主修课程
};
void Test()
{
Assistant a;
//a._name = "张三";//"Assistant::_name" 不明确
a.Student::_name = "张三";
a.Teacher::_name = "李四";
//如果存在开辟一个大数组,就会存在数据冗余
}
虚继承使用:
虚继承 virtual public
虚继承设置在距离二义性和数据冗余的基类的第一级子类上.
- 不是虚继承时
在内存的存储中,先继承的类的成员放在前面储存。
class D : public B, public C//先B后C
- 是虚继承时:
- 将冗余的变量放到一个公共的地方,也就是最后的地方
但是会多出现不认识的东西:在内存中输入之后得到16进制数字
就是B和C公共A成员的偏移量也及时相对距离。(小端存储,低地址存小值)
A叫虚基类,找虚继类的表叫虚基表.在D中,A放在一个公共的位置,那么B要找A,就需要通过虚基表中的偏移量进行计算.
- 场景
B b=d;
C c=d;
//发生切片时,切片的过程中,_a已经被切除,但是如果要在子类中通过B找到A,就需要这个偏移量的表,虚基表找到_a.
B* pb=&d;
pb->_b=20;//b类找到,很容易
pb->_a=10;//通过b找到就需要相对a偏移量找到a
继承-组合
- 继承
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用
(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。
继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关
系很强,耦合度高。
// Car和BMW Car和Benz构成is-a的关系
class Car
{
protected:
string _colour = "白色"; // 颜色
string _num = "ABIT00"; // 车牌号
};
class BMW : public Car {
public:
void Drive() { cout << "好开" << endl; }
};
class Benz : public Car {
public:
void Drive() { cout << "舒适" << endl; }
};
- 组合
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对
象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),
因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,
耦合度低。优先使用对象组合有助于你保持每个类被封装。
// Tire和Car构成has-a的关系
class Tire{
protected:
string _brand = "Miche"; // 品牌
size_t _size = 17; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "ABIT00"; // 车牌号
Tire _t; // 轮胎
};
优先使用对象组合,而不是类继承 。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
类之间/模块之间的关系最好是低耦合高内聚.
要点陈列:
-
继承的功能(代码复用的体现):继承可以使用现有类的所有功能,并在无需重新编写原来类的情况下对这些功能进行扩展。
-
子类不一定要比基类大,有可能子类只是改写父类的方法而已,并没有增加其自身的数据成员,则大小一样.
-
狗是一种动物,体现了继承的思想,属于is-a的关系。
-
访问权限在类内部,继承权限在类外部。
-
派生类在继承基类时,可以不指定继承方式,默认是private.
-
class 声明的类默认是private
-
菱形继承(相似就行,都会出现数据冗余和二义性)接继承冗余类的地方,是内个虚继承的地方。
-
C++缺陷:没有垃圾回收器,多继承
-
继承可以访问基类的public 和protected.组合只可以访问基类的public
继承的关联关系更强,组合的自由性更高。低耦合高内聚。
多态是建立在继承的基础之上的。 -
当构成子类对于父类函数的重定义(隐藏)后,调用函数必须按照子类的来,要不然就会编译报错
-
子类构造函数的定义又是需要参考基类构造函数
-
当父类没有默认构造函数时,在派生构造函数初始化列表的位置必须显式调用基类构造函数.
-
子类成员变量中不包含静态变量,静态变量不属于任何类,属于整个继承体系
-
基类指针可以直接指向子类对象
-
基类不能直接给给子类对象,因为类型并不完全
-
p1 p2都是父类,但是在子类的内存模型当中位置不同,所以p1p2指向的子类的位置不相同,p1!=p由于p1对象是第一个被继承的父类类型,所有的地址与子类对象的地址.先继承谁的,谁就在内存中排在前面。
-
子类实例化对象,由于继承的有父类。所以会先构造父类,然后在构造子类,析构顺序完全按照构造的相反顺序进行析构.
-
菱形继承中子类对象不能直接访问最顶层基类B中继承下来的b成员,因为在D对象中,b有两份,一份是从C1中继承的,一份是从C2中继承的,直接通过D的对象访问b会存在二义性问题,在访问时候,可以加类名::b,来告诉编译器想要访问C1还是C2中继承下来的b。
-
子类对象可以传给父类,切片切割。各种子类的返回值类型都能够赋值给父类的类型.运算符重载不能够改变内置类型。
-
如果是虚继承,公有的部分存储时候放在最后,但是调用声明的是确实最前面的.因为他属于公共部分,既不属于B也不属于C.
象 -
基类不能直接给给子类对象,因为类型并不完全
-
p1 p2都是父类,但是在子类的内存模型当中位置不同,所以p1p2指向的子类的位置不相同,p1!=p由于p1对象是第一个被继承的父类类型,所有的地址与子类对象的地址.先继承谁的,谁就在内存中排在前面。
-
子类实例化对象,由于继承的有父类。所以会先构造父类,然后在构造子类,析构顺序完全按照构造的相反顺序进行析构.
-
菱形继承中子类对象不能直接访问最顶层基类B中继承下来的b成员,因为在D对象中,b有两份,一份是从C1中继承的,一份是从C2中继承的,直接通过D的对象访问b会存在二义性问题,在访问时候,可以加类名::b,来告诉编译器想要访问C1还是C2中继承下来的b。
-
子类对象可以传给父类,切片切割。各种子类的返回值类型都能够赋值给父类的类型.运算符重载不能够改变内置类型。
-
如果是虚继承,公有的部分存储时候放在最后,但是调用声明的是确实最前面的.因为他属于公共部分,既不属于B也不属于C.