继承 (inheritance) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称 派生类 。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
继承的主要功能如下:
代码复用 :子类可以继承父类的属性和方法,避免重复编写相同的代码。
继承属性和行为 :子类可以获得父类的属性和方法,从而具备相同的行为和特性。
扩展功能 :子类可以在继承基础上添加新的属性和方法,以实现特定的功能需求。
代码组织和层次化 :通过继承,可以将类组织成层次结构,便于理解和管理。
其中,访问限定符:用于定义类中成员(属性和方法)的可访问性。
分为以下几类:
继承方式 :指的是一个类从另一个类继承属性和方法时的行为。
可以理解为下图
类成员 / 继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
- 基类private成员在派生类中都是完全不可见的(不论继承方式)。不可见是指基类的私有成员被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2. 基类private成员在派生类中不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。- 由上面的表格可以看出:基类的私有成员在子类都是不可见 。基类的其他成员在子类的访问方式 == min(成员在基类的访问限定符,继承方式),
public > protected > private
。- 使用关键字 class时默认的继承方式 是
private
,使用struct时默认的继承方式是public
,但最好显式的写出继承方式。- 在实际运用中一般使用
public继承
,很少使用也不提倡使用protetced/private继承
,因为protetced/private继承
下来的成员都 只能在派生类的类 里面使用,扩展维护性不强。
// Student的_num和Person的_num构成隐藏关系
// 可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected :
string _name = "李田所"; // 姓名
int _num = 1145141919810; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 身份证号:" << Person::_num << endl;
cout << " 学号:" << _num << endl;
}
protected:
int _num = 123456; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" <<i<<endl;
}
};
void Test()
{
B b;
b.fun(10);
};
operator=
必须要调用基类的operator=
完成基类的复制 。destrutor()
,父类析构函数不加 virtual 的情况下,子类析构函数和父类析构函数构成隐藏关系。基类友元不能被继承。,尽管派生类可以访问基类的公有和保护成员,但对于基类中的友元函数,派生类并没有同样的访问权限。
class Base {
protected:
int num;
public:
Base(int n) : num(n) {}
friend void Display(const Base& obj); // 友元函数
};
class Derived : public Base {
public:
Derived(int n) : Base(n) {}
};
void Display(const Base& obj) {
std::cout << "Number: " << obj.num << std::endl;
}
int m ain() {
Derived d(42);
Display(d); // 尝试在派生类中调用友元函数
return 0;
}
在上面的代码中,我们尝试通过 Display(d)
在派生类 Derived
中调用友元函数。然而,这会导致编译错误,提示找不到匹配的函数。因为 友元函数 Display 不会被派生类继承,它只能访问声明它为友元的类的私有成员。
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。
因为 静态成员属于类而不是类的实例,它在内存中只有一个副本。无论派生多少个子类,这些子类都共享同一个静态成员实例。
class Base {
public:
static int count;
};
int Base::count = 0; // 初始化静态成员
class Derived : public Base {
public:
Derived() {
count++; // 在派生类中访问和修改静态成员
}
};
int main() {
Derived d1;
Derived d2;
Derived d3;
std::cout << "Count: " << Base::count << std::endl; // 输出静态成员的值
return 0;
}
在上面的代码中:由于静态成员在整个继承体系中只有一个副本,因此输出的结果将是所有派生类实例共享的 count 的值。
单继承 :一个子类只有一个直接父类 时称这个继承关系为单继承。
多继承 :一个子类有两个或以上直接父类 时称这个继承关系为多继承。
菱形继承 :一个子类有两个或以上直接父类 时称这个继承关系为多继承。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有 数据冗余 和 二义性 的问题。 在Assistant
的对象中Person
成员会有两份。
将上面的模型图 实现为代码:
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stu_id; //学号
};
class Teacher : public Person
{
protected:
int _emp_id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
对于上面的类,当我们使用
Assistant a ;
a._name = "阿杰"; // _name不明确,编译错误
这种访问方式的话,程序会因为二义性(_name不明确),导致编译错误。
我们可以指定 该成员变量的父类解决该问题:
a.Student::_name = "阿伟";
a.Teacher::_name = "淑慧";
但依然没有解决数据冗余的问题,为了解决该问题,我们引用虚拟继承的概念:
虚拟继承 可以解决菱形继承的 二义性 和 数据冗余 的问题。
如上面的继承关系,在 Student
和 Teacher
的继承 Person
时使用虚拟继承,即可解决问题。另外需要注意:虚拟继承不要在其他地方去使用。
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _stu_num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _e_id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse; // 主修课程
};
void Test ()
{
Assistant a;
a._name = "阿伟";
}
上面的代码执行后,不会出错,程序会将Assistant a的_name 变量 改变。
虚拟继承 解决 二义性 和 数据冗余的原理
虚拟继承的原理是通过 在派生类对象中只保留一份共享基类子对象 的方式来消除二义性和数据冗余。
具体来说,虚拟继承使得虚基类子对象被单独存储,并通过指针访问,而不是嵌入到每个派生类对象中,因此不会在每个派生类之间产生冗余数据。
当多个类从同一个虚基类派生时,它们共享同一个虚基类子对象。这个虚基类子对象只被构造一次,而不是被每个派生类分别构造,因此可以消除数据冗余和二义性。
虚拟继承中的 虚基类子对象的构造顺序 比较特殊,需要按照 “最远派生类优先”(Most Derived Class First)的顺序进行构造,也就是从最后一个派生类开始构造虚基类子对象,依次向上构造。
需要注意的是,虚拟继承有一些性能上的开销,因为每次访问虚基类时需要间接寻址。此外,虚拟继承可能会导致代码的可读性降低,因为虚基类的存在并不直观。因此,在使用虚拟继承时需要权衡其优缺点,并根据具体情况决定是否使用。