❤️欢迎来到我的博客❤️ |
继承的概念
想象你要设计一家公司的员工管理系统:
1️⃣ 重复劳动问题
所有员工都有 基础信息(姓名、工号),但程序员需要额外记录技术栈,销售员需要记录业绩指标。若每个岗位单独写一个类,会导致重复编写基础信息代码。
2️⃣ 继承的解决方案
基类(员工档案柜):创建一个 Employee 基类,存放所有员工的公共属性(姓名、工号)
派生类(分类文件夹):程序员类 Programmer 和销售类 Sales 继承基类,自动获得基础信息,只需添加自己的特有属性(技术栈/业绩)
// ❌ 无继承:重复定义基础字段
class Programmer {
string name; // 重复
int id; // 重复
string techStack; // 特有
};
class Sales {
string name; // 重复
int id; // 重复
double salesData; // 特有
};
// ✅ 用继承:消除冗余
class Employee { /* 基础字段 */ };
class Programmer : public Employee { /* +技术栈 */ };
class Sales : public Employee { /* +业绩 */ };
继承关系和访问限定符
继承方式:
public继承
protected继承
private继承
访问限定符:
public访问
protected访问
private访问
继承基类成员访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
基类的私有成员 - private 在派生类中都是不可见的(不可见是在语法上限制访问(类里面和类外面均不可使用),跟private不一样(private类外面不可用,类里面可用)
父类的私有成员无论以什么方式继承子类都用不了
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
总结:
1、私有成员在派生类怎么继承都是不可见。
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; // 学号
};
int main()
{
//产生了临时变量,发生了转换
int i = 0;
double d = i;
Person p;
Student s;
//这不会产生临时变量
//赋值兼容转换(切割/切片)
p = s;
s = p; //这个转换不允许,因为父类不能给子类
return 0;
}
一个子类对象一定是一个特殊的父类对象(子类对象中一定会有父类的一些成员)
p = s - 会把子类当中属于父类的一部分切割出来,然后拷贝给父类,中间不会产生临时变量
int main()
{
Person p;
Student s;
Person& rp = s;
return 0;
}
这里的rp是子类对象中父类的那一部分的别名
向上转换都是可以的 - 父类对象/引用/指针
继承中的作用域
子类和父类是可以定义同名的变量的
class Person
{
protected:
string _name = "小李";
int _num = 111;
};
//隐藏/重定义:子类和父类有同名成员,子类的成员隐藏了父类的成员
class Student : public Person
{
public:
void Print()
{
cout << _num << endl;
}
protected:
int _num = 999;
};
int main()
{
Student s;
s.Print(); //打印结果为999,因为就近原则编译器会找子类的数据
return 0;
}
查找规则:先在局部域中找 ->当前类域 -> 成员变量 -> 父类 ->全局 ->报错,只要在一个地方找到了就不会进入下一个区域查找
如果想要父类的数据那么可以指定访问:cout << Person::_num << endl;
打印结果就为111
父子类域中,成员函数名相同就构成隐藏而不是重载
派生类的默认成员
6个默认成员函数,默认指的是在我们不写的情况下编译器会自动生成
派生类必须调用父类的构造函数初始化父类的成员
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
- 派生类的operator=必须要调用基类的operator=完成基类的复制
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
- 派生类对象初始化先调用基类构造再调派生类构造
- 派生类对象析构清理先调用派生类析构再调基类的析构
- 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
补充:初始化顺序和初始化链表出现的顺序没有关系,主要和声明的顺序有关
显示调用父类析构,无法保证先父后子,所以子类析构函数完成就会自动调用父类析构,这样就保证了先子后父(为什么要先子后父:符合对象在栈里面的顺序,子类当中是有可能用到父类成员的,而父类则没有可能用到子类成员)
继承和友元
友元关系不能继承
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person
{
protected:
int _stuNum;
};
//父类的友元
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;//这里会报错
}
void main()
{
Person p;
Student s;
Display(p, s);
}
如果我们想要使用只能在子类中再定义一个友元代码就能正常通过
class Student : public Person
{
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum;
};
继承与静态成员
静态成员属于父类和派生类,在派生类中不会单独拷贝一份,继承的是使用权
复杂的继承
有没有可能一个类同时具有两个类的特征,答案是有的,那么就出现了多继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况
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;
};
菱形继承会导致数据冗余和二义性,如果去访问_name就会产生二义性,不知道要去访问谁,但是可以通过指定访问解决,但是有点违背常理,比如一个人不可能有2个性别
虚继承
为了解决以上问题C++出现了虚继承
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;
};
那么来看这样的一个类
class A
{
public:
int _a;
};
// class B : public A
class B : public A
{
public:
int _b;
};
// class C : public A
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
//d里面有两个_a
//一个是B继承的_a
d.B::_a = 1;
//一个是C继承的_a
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
可以看到菱形继承会产生数据冗余
菱形虚拟继承
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;
d._a = 0;
return 0;
}
菱形虚拟继承在内存当中的布局发生了非常大的变化
_a既没有放在B里面也没有放在C里面,而是被单独拿出来了,那就不会有数据冗余和二义性了
但是B和C里面多了一行东西,看起来像是一个指针,我们查找这个指针会发现
他们指向的位置存放的都是0,但是下一个位置存放了一个有效值,分别是20和12
我们再回到第一张图
可以发现B到A的距离是20,而C到A的距离是12
那么这个指针下一个位置存的数据就是距离A的偏移量(相对距离)
那么为什么这个值不直接存在对象里面,而是单独存储
假设我们有很多个D对象,D d1 D d2 D d3那么d1 d2 d3都可以直接指向这个地址,而不用单独存储数据
int main()
{
D d;
B b;
B* ptr = &b;
ptr->_a++;
ptr = &d;
ptr->_a++;
return 0;
}
在这个场景中,编译器做的操作都是,取到偏移量,计算_a的地址,再进行访问
总结
有了多继承(谨慎使用,避免菱形继承),就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承,否则在复杂度及性能上都有问题
以上就是本篇文章的全部内容了,希望大家看完能有所收获
❤️创作不易,点个赞吧❤️ |