【C++】继承
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
目录
一、继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段它允许程序员在保持原有类特性的基础上进行扩展增加功能这样产生新的类称子类派生类。以前我们接触的复用都是函数复用继承是类设计层次的复用。
现在有学生类和老师类类中均有年龄、性别等相同的属性这些相同的属性在每个类中都写一份就会出现代码的冗余。可以使用一个父类包含这些共有的成员变量和成员函数让学生类、老师类作为子类对父类进行继承即可。
二、被继承成员访问方式的变化
public继承 | protected继承 | private继承 | |
父类的public成员 | public | protected | private |
父类的protected成员 | protected | protected | private |
父类的private成员 | 子类不可见 | 子类不可见 | 子类不可见 |
1、父类的private成员被子类继承后是不可见的。不可见指的是父类的private成员会被继承到子类但是子类无论是在类里面还是类外面都无法对这些被继承的私有成员进行访问。但是可以使用继承的“获取成员变量的偷家函数”对这些不可见的成员变量进行修改、访问
2、除了父类中的private成员其他成员被子类继承后最终的访问方式取决于该成员在父类中的权限和继承权限两者权限较小的那个。
3、可以看出protected限定符是因为继承才出现的。保护和私有在当前类中没有区别但子类继承时父类中的私有成员子类是不可见的而保护成员是可见的。
4、在实际中一般使用都是public继承因为protetced/private继承下来的成员都只能在派生类的类里面使用实际中扩展维护性不强。父类一般是公有和保护不使用私有。
5、class默认私有继承struct默认公有继承。但好习惯是写明继承方式。
struct Student : public person
{
};
struct Teacher :person//不提倡最好写明继承方式
{
};
三、赋值兼容规则切片
class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
int _num;//学号
};
int main()
{
Person p;
Student s;
//父类=子类 赋值兼容/切割/切片
p = s;//父类对象
Person* ptr = &s;//父类的指针
Person& ref = s;//父类的引用
return 0;
}
1、子类对象可以赋值给父类对象、父类的指针、父类的引用。这种操作叫做赋值兼容/切割/切片意为将子类对象中继承于父类的成员切割下来赋值给父类对象。这不是类型转换是天然的赋值行为。(切片仅限公有继承。举例父类为公有子类保护或私有继承后成员变为保护和私有子类再切片给父类那么被继承的成员权限会变所以切片仅限公有继承)
2、只能将子类对象赋值给父类父类对象不能给子类赋值。但是指针和引用却可以不过存在越界风险。
int main()
{
Person p;
Student s;
//s = (Student)p;//父类对象无法赋值给子类强转也不行
Student* ptr = (Student*)& p;//类型强转有越界风险
Student& ref = (Student&)p;//类型强转有越界风险
return 0;
}
3、基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。
四、继承中的作用域
1、在继承中父类和子类都有自己独立的类域。
2、当子类和父类中存在同名成员时子类将会屏蔽继承于父类的同名成员这种情况被称为隐藏或重定义。子类内部优先使用自己类域的同名成员外部可使用stu.Person::_name进行显示访问
3、子类和父类中的同名成员函数并不构成函数重载因为它们所处于不同的类域子类会隐藏父类同名函数。
4、父类和子类尽量不要使用同名成员。
五、子类的默认成员函数
1、父、子类中各自的成员处理方式
子类中有两部分成员一类是子类原生的成员另一类是继承于父类的成员。
对于原生成员按照普通类调用默认成员函数的规则进行处理对于继承于父类的成员将会调用父类中的默认成员函数进行处理。各管各的
2、需要自己写默认成员函数的情况
1、父类没有默认构造函数需要自己显式写构造。
2、子类存在浅拷贝问题需要自己显式写拷贝构造和赋值。
3、子类有资源需要释放需要自己写显式析构。
3、子类中默认成员函数的写法
3.1父类没有默认构造函数需要在子类的构造函数里补充
class Person
{
public:
Person(const char* name)
: _name(name)
{}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const char* name = "张三", int num = 10)
: Person(name)//必须调用父类的构造函数进行初始化
, _num(num)
{}
protected:
int _num; //学号
};
父类有提供默认构造函数就可以不用在子类写了。
3.2在子类中显式写拷贝构造
class Person
{
public:
Person(const Person& p)//形参引用切片对象
: _name(p._name)
{}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const Student& s)
:Person(s)//切片
,_num(s._num)
{}
protected:
int _num; //学号
};
利用切片传入父类对象构造父类。Student s1(s2),在初始化列表中利用s2中的父类成员去拷贝构造s1中的父类成员。
3.3在子类中显式写赋值运算符重载
class Person
{
public:
Person& operator=(const Person& p)
{
if (this != &p)
_name = p._name;
return *this;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student& operator=(const Student& s)
{
if (this != &s)//防止自己给自己赋值
{
Person::operator=(s);//切片传入父类赋值运算符重载中
//根据子类成员进行深浅拷贝
_num = s._num;
}
return *this;
}
protected:
int _num; //学号
};
在子类赋值运算符重载中调用父类赋值运算符重载通过切片完成父类成员的赋值。
3.4不需要显式调用析构函数
错误代码
~Student()
{
Person::~Person();
}
//子类析构函数结束后会调用一次父类的析构函数
~Person前必须加类域Person。因为析构函数的名字会被编译器统一处理为destructor()子类的析构函数和父类的析构函数之间构成隐藏所以这里需要写明类域。
但是我们并不需要显式调用父类的析构函数因为出了子类析构函数的作用域编译器会自动调用父类的析构。手动调用父类析构将会造成重复析构。
六、继承和友元、静态成员的关系
1、友元关系不能被继承
2、父类中的静态成员也会被继承但是整个继承关系中共用这个静态成员
七、菱形继承和菱形的虚拟继承
1、菱形继承
从上图可以看出Assistant中会有两份Person成员调用时存在二义性和数据冗余。
2、二义性和数据冗余
int main()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
//a._name = "peter";//不能这么写因为a中有两个_name成员需要指定类域
// 需要显示指定访问哪个父类的成员可以解决二义性问题但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
由于Assistant中有两个_name成员直接调用存在二义性需要在成员之前指定类域。
_name这个成员变量有两个问题不大毕竟一个人可以叫蒋灵瑜在其他场合也可以叫小蒋。但如果这个成员变量是一个int _arr[50000]的数组呢一个类中同时有两份这么大的数组将会导致数据冗余。
3、虚拟继承解决二义性和数据冗余
产生二义性和数据冗余的本质就是子类继承了多份相同成员。
解决方法是在“腰部”类增加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; // 主修课程
};
4、virtual关键字解决二义性和数据冗余的方法
先来一段菱形继承的代码_a、_b、_c、_d分别是类A、类B、类C、类D中的原生成员。
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._b = 2;
d.C::_a = 3;
d._c = 4;
d._d = 5;
return 0;
}
4.1未使用virtual关键字
通过调用内存会发现对象d中存在两份的_a存在二义性和数据冗余。
4.2使用virtual关键字
当使用了虚拟继承通过调用内存发现对象d中仅有一份_a但是继承于B类和C类的_b和_c上方多了一串地址再次要通过内存查找这串地址发现这串地址之后的位置存放一个数字0x14这个数字就是继承于B的成员到_a的偏移量通过这个偏移量对象d便能到d.B::_a。这样就解决了菱形继承成员冗余的问题。
这里的A叫做虚基类在对象d中将虚基类的成员放到一个公共的位置继承的B、C类需要找到A的成员通过虚基表中的偏移量进行计算。
实际使用时尽量不要使用用菱形继承因为它本质就是C++设计的一个坑
八、继承和组合的区别
组合也是一种类复用的手段。
1、组合的使用场景
适用组合的代码轮胎和车的关系
class Tire
{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 18; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "xxxxx"; // 车牌号
Tire _t; // 轮胎
};
2、继承和组合的区别
public继承是一种is-a的关系每个子类对象都是一个父类对象例如“学生”是“人”子类学生父类人
组合是一种has-a的关系B组合了A每个B对象中都有一个A例如“车”包含“轮胎”
如果两个类既可以是is-a又可以是has-a的关系那么优先使用组合。
继承是一种白盒复用父类内部的细节对子类可见破坏了封装。子类将会继承父类的公有和保护成员一旦父类修改了这些成员的实现将会影响子类的功能。子类和父类之间的依赖关系强耦合度高。
组合是一种黑盒复用父类内部的细节对子类不可见子类仅可使用父类的公有成员只要父类的公有成员的实现细节不变子类影响较小。父子之间没有很强的依赖关系耦合度较低。