继承的概念与定义
继承的基本概念
继承允许我们在保留原有类特性的基础上进行扩展,增加新的成员变量和方法,从而创建新的派生类。
class Student
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 学习
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
int _stuid; // 学号
};
class Teacher
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 授课
void teaching()
{
//...
}
protected:
string _name = "张三"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
string _title; // 职称
};
int main()
{
return 0;
}
在示例中,我们看到Student和Teacher类有许多共同属性(如姓名、地址、电话、年龄)和方法(如身份认证)。通过继承,我们可以将这些公共部分提取到基类Person中,让Student和Teacher类继承Person类,从而避免代码冗余。
class Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" <<_name<< endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
class Student : public Person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
public:
// 授课
void teaching()
{
//...
}
protected:
string title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
继承的定义格式
继承的基本语法格式如下:
class 派生类名 : 继承方式 基类名 {
// 派生类新增成员
};
其中:
- 基类(父类):被继承的类
- 派生类(子类):继承基类的类
- 继承方式:public、protected或private
继承中的访问控制
访问权限变化规则
继承方式会影响基类成员在派生类中的访问权限,具体规则如下表:
基类成员访问权限 \ 继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
public成员 | public | protected | private |
protected成员 | protected | protected | private |
private成员 | 不可见 | 不可见 | 不可见 |
总结说明:
- 基类的private成员在任何继承方式下对派生类都不可见
- protected访问限定符是专为继承设计的,它允许成员在派生类中访问,但在类外不可访问
- 成员在派生类中的最终访问权限 = min(成员在基类的访问权限, 继承方式)
- class默认private继承,struct默认public继承(但建议显式指定)
实际应用建议
实践中绝大多数情况应使用public继承,protected和private继承会限制派生类成员的访问权限,降低代码的可维护性和扩展性,通常不建议使用。
继承中的特殊问题
基类与派生类的转换
public继承的派生类对象可以赋值给基类的指针或引用,这种现象称为"切片",即只保留派生类中基类部分的内容。
Student sobj;
Person* pp = &sobj; // 正确:派生类指针赋给基类指针
Person& rp = sobj; // 正确:派生类引用赋给基类引用
Person pobj = sobj; // 正确:调用基类的拷贝构造函数
sobj = pobj; // 错误:基类对象不能赋给派生类对象
作用域与名称隐藏
在继承体系中,基类和派生类有独立的作用域。如果派生类定义了与基类同名的成员,会隐藏基类的成员(即使参数列表不同)。要访问被隐藏的基类成员,需要使用作用域解析运算符::
。
class Person {
protected:
int _num = 111; // 身份证号
};
class Student : public Person {
public:
void Print() {
cout << Person::_num << endl; // 访问基类被隐藏的成员
cout << _num << endl; // 访问派生类成员
}
protected:
int _num = 999; // 学号
};
派生类的默认成员函数
构造与析构顺序
派生类对象的构造和析构遵循特定顺序:
- 构造顺序:基类构造 → 派生类构造
- 析构顺序:派生类析构 → 基类析构
默认成员函数的处理
派生类的默认成员函数需要特别注意基类部分的处理:
- 构造函数:必须调用基类构造函数初始化基类成员
- 拷贝构造:必须调用基类拷贝构造完成基类部分的拷贝
- operator=:必须调用基类operator=完成基类部分的赋值(注意名称隐藏问题)
- 析构函数:会自动调用基类析构函数清理基类成员
class Student : public Person {
public:
// 构造函数
Student(const char* name, int num)
: Person(name), _num(num) {} // 显式调用基类构造
// 拷贝构造
Student(const Student& s)
: Person(s), _num(s._num) {} // 调用基类拷贝构造
// 赋值运算符
Student& operator=(const Student& s) {
if(this != &s) {
Person::operator=(s); // 显式调用基类operator=
_num = s._num;
}
return *this;
}
// 析构函数会自动调用基类析构
~Student() {}
};
实现一个不能被继承的类
在某些设计场景中,我们希望禁止一个类被继承,C++提供了两种实现方式:
方法1:C++98风格——基类构造函数私有化
原理:派生类的构造函数必须调用基类的构造函数,如果将基类的构造函数设为私有,派生类就无法访问基类的构造函数,从而导致编译错误。
方法2:C++11风格——final
关键字
原理:C++11引入的final
关键字可以直接修饰类,明确禁止继承。
class NonInheritable final { // 使用final禁止继承
public:
void func() { cout << "Base function" << endl; }
};
class Derived : public NonInheritable {}; // 错误:无法继承final类
C++11的final
更简洁直观,是推荐做法。
继承与友元
友元关系的不可继承性
核心规则:基类的友元函数/类 不能 自动成为派生类的友元。
问题示例:
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; // 正确:访问Person的protected成员
cout << s._stuNum << endl; // 错误:无法访问Student的protected成员
}
解决方案:
若需要访问派生类的私有/保护成员,必须将函数同时声明为派生类的友元:
class Student : public Person {
public:
friend void Display(const Person&, const Student&); // 额外声明
protected:
int _stuNum;
};
关键点:
- 友元关系是单向的(基类友元 ≠ 派生类友元)。
- 多重继承时,需为每个派生类单独声明友元。
继承与静态成员
静态成员在继承体系中的特性
核心规则:基类中的静态成员会被整个继承树共享,无论派生出多少个子类,静态成员只有一份实例。
示例:
class Person {
public:
static int _count; // 静态成员
string _name;
};
int Person::_count = 0; // 初始化
class Student : public Person {
protected:
int _stuNum;
};
int main() {
Person p;
Student s;
// 验证静态成员共享
cout << &p._count << endl; // 0x1000
cout << &s._count << endl; // 0x1000(地址相同)
// 修改影响所有类
Person::_count = 10;
cout << Student::_count; // 输出10
}
关键行为:
- 存储唯一性:静态成员在内存中只有一份,所有派生类共享。
- 访问权限:继承方式影响静态成员的访问权限(与普通成员规则一致)。
- 初始化:静态成员仍需在类外单独初始化。
特殊场景:
若派生类定义了同名静态成员,会隐藏基类静态成员(需通过作用域解析访问):
class Student : public Person {
public:
static int _count; // 隐藏Person::_count
};
int Student::_count = 0;
cout << Person::_count; // 访问基类静态成员
cout << Student::_count; // 访问派生类静态成员
多继承与菱形继承
多继承的问题
多继承是指一个派生类有多个直接基类,这可能导致菱形继承问题:
class Person {};
class Student : public Person {};
class Teacher : public Person {};
class Assistant : public Student, public Teacher {}; // 菱形继承
菱形继承会导致:
- 数据冗余:基类Person的成员在Assistant中有两份
- 二义性:访问Person成员时需要指明路径
虚继承解决方案
使用虚继承可以解决菱形继承问题:
class Person {};
class Student : virtual public Person {}; // 虚继承
class Teacher : virtual public Person {}; // 虚继承
class Assistant : public Student, public Teacher {};
虚继承后:
- 消除了数据冗余
- 可以直接访问基类成员而无需指定路径
但虚继承会增加对象模型复杂度,降低性能,因此应尽量避免设计出菱形继承结构。
继承与组合的选择
继承与组合的比较
特性 | 继承(is-a) | 组合(has-a) |
---|---|---|
关系 | 派生类是基类的一种特例 | 类中包含另一个类的对象 |
复用方式 | 白箱复用(可见内部实现) | 黑箱复用(隐藏内部实现) |
耦合度 | 高 | 低 |
灵活性 | 较低(受基类约束) | 较高 |
- 优先使用组合:组合的耦合度低,更易维护
- 适合继承的情况:
- 类之间确实是is-a关系
- 需要实现多态特性
- 既适合继承又适合组合时,优先选择组合