跳到主要内容C++ 继承详解:派生类函数、虚继承原理与菱形继承案例 | 极客日志C++算法
C++ 继承详解:派生类函数、虚继承原理与菱形继承案例
综述由AI生成深入讲解了 C++ 继承的核心机制。内容涵盖继承的概念与定义,详细分析了 public、protected、private 三种继承方式对访问权限的影响。探讨了派生类默认成员函数的生成与调用顺序,特别是构造函数与析构函数的执行流程。重点剖析了多继承中的菱形继承问题,解释了数据冗余和二义性的成因,并展示了虚继承的解决方案及 STL IO 库中的应用实例。最后对比了继承与组合的区别,强调在满足 is-a 关系或多态需求时使用继承,而在其他场景优先选择组合以降低耦合度。
KernelLab6 浏览 继承的概念与定义
继承(Inheritance)是面向对象程序设计中实现代码复用的核心机制。它允许我们在保留基类特性的基础上进行扩展,增加新的成员函数或属性,从而生成新的派生类。这种机制体现了由简单到复杂的认知过程,将类设计层次的复用提升到了新的高度。
概念解析
假设我们有两个类 Student 和 Teacher,它们都有公共的成员如姓名、电话、地址等,以及身份认证的功能。如果分别定义这些成员,会造成冗余。更好的做法是将公共部分封装到一个基类 Person 中,让 Student 和 Teacher 通过继承来复用这些成员。
class Person {
public:
void Identify() {
cout << "void Identify()" << _name << endl;
}
protected:
string _name;
string _tel;
string _address;
int _age;
};
class Student : public Person {
public:
void study() { }
protected:
int _stuid;
};
class Teacher : public Person {
public:
void teaching() { }
protected:
string _title;
};
继承的定义与方式
在 C++ 中,基类也称为父类,派生类也称为子类。继承方式主要有三种:public、protected 和 private。
- 访问权限控制:子类继承后能否访问基类成员,取决于基类的访问限定符和子类的继承方式。
- Private 成员:基类的
private 成员在派生类中无论以什么方式继承都是不可见的。虽然它们被继承到了对象内存中,但语法上限制派生类无法直接访问。
- Protected 成员:如果基类成员不想在类外直接被访问,但需要在派生类中访问,应定义为
protected。这是因继承而出现的特殊限定符。
- 总结规则:基类其他成员在派生类的访问方式遵循
Min(基类访问限定符,继承方式) 的原则,即 public > protected > private。
- 默认继承方式:使用关键字
class 时默认继承方式是 private,使用 struct 时默认是 public。实际开发中建议显式写出继承方式,且通常使用 public 继承。
实例演示
class Person {
public:
void Print() { cout << _name << endl; }
protected:
string _name = "1";
private:
int _age;
};
class Student : public Person {
protected:
int _stunum;
};
int main() {
Student s;
s.Print();
return 0;
}
在此模式下,_name 最终继承为 protected,可在类内访问。
class Student : protected Person {
};
此时 Print 函数在子类中变为 protected,类外不可访问。
class Student : private Person {
};
所有从基类继承的成员在子类中都变为 private,类内外均不可访问。
继承类模板
当基类是模板类时,子类访问其成员需要指定作用域,因为模板类是按需实例化的。
template<class T>
class Stack : public vector<T> {
public:
void push(const T& x) {
vector<T>::push_back(x);
}
void pop() {
vector<T>::pop_back();
}
};
基类和派生类间的转换
向上转换(Upcasting):public 继承的派生类对象可以赋值给基类的指针或引用。这被称为'切片'(Slicing),意指只保留了派生类中属于基类的那部分数据。
class Person {
protected:
string _name;
int _age;
};
class Student : public Person {
public:
int _No;
};
int main() {
Student s;
Person* ptr = &s;
Person& a = s;
Person per = s;
return 0;
}
向下转换(Downcasting):基类指针指向派生类对象时,可以通过强制类型转换赋值给派生类指针。若基类是多态类型,建议使用 dynamic_cast 进行安全转换。
继承中的作用域
在继承体系中,基类和派生类拥有独立的作用域。如果派生类中存在与基类同名的成员,派生类成员将屏蔽基类同名成员的直接访问,这种现象称为'隐藏'。
class Person {
protected:
string _name = "小李";
int _num = 111;
};
class Student : public Person {
public:
void Print() {
cout << "姓名:" << _name << endl;
cout << "身份证号:" << Person::_num << endl;
cout << "学号:" << _num << endl;
}
protected:
int _num = 999;
};
int main() {
Student s1;
s1.Print();
return 0;
}
注意:如果是成员函数的隐藏,只需函数名相同即可构成隐藏。在实际开发中,尽量避免在继承体系内定义同名成员,以免混淆。
派生类的默认成员函数
派生类会生成 6 个默认成员函数,它们的调用规则如下:
- 构造函数:派生类构造函数必须调用基类构造函数初始化基类部分。如果基类没有默认构造函数,必须在初始化列表中显式调用。
- 拷贝构造函数:派生类拷贝构造需调用基类拷贝构造。
- 赋值运算符:派生类
operator= 会隐藏基类的 operator=,因此显式调用时需指定基类作用域。
- 析构函数:派生类析构函数调用完成后会自动调用基类析构函数。顺序是'先定义的类后析构',即先清理派生类成员,再清理基类成员。
示例代码
class Person {
public:
Person(const char* name = "peter") : _name(name) {
cout << "Person()" << endl;
}
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() {
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person {
public:
Student(const char* name, int num) : Person(name), _num(num) {
cout << "Student()" << endl;
}
Student(const Student& s) : Person(s), _num(s._num) {
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s) {
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s) {
Person::operator=(s);
_num = s._num;
}
return *this;
}
~Student() {
cout << "~Student()" << endl;
}
protected:
int _num;
};
int main() {
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
return 0;
}
实现不能被继承的类
方法一:将基类构造函数私有化,派生类无法访问。
方法二:C++11 引入 final 关键字修饰基类。
class Base final {
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
};
class Derive : public Base {
void func4() { cout << "Derive::func4" << endl; }
};
继承与友元
友元关系不能继承。基类的友元不能访问派生类的私有和保护成员,但可以访问基类的私有和保护成员。
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;
}
int main() {
Person p;
Student s;
Display(p, s);
return 0;
}
解决方案:将 Display 也声明为 Student 的友元。
继承与静态成员
基类定义的 static 静态成员在整个继承体系中只有一个实例。无论派生出多少个派生类,都共享同一个静态成员。
多继承及其菱形继承问题
继承模型
- 单继承:一个派生类只有一个直接基类。
- 多继承:一个派生类有两个或以上直接基类。内存布局通常是先继承的基类在前,后继承的在后,派生类成员在最后。
- 菱形继承:多继承的特殊情况,存在数据冗余和二义性问题。
例如,Assistant 同时继承 Student 和 Teacher,而两者都继承自 Person。如果不处理,Assistant 对象中将包含两份 Person 的数据。
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;
};
int main() {
Assistant a;
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
虚继承
使用虚继承可以解决菱形继承的数据冗余和访问二义性问题。
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;
};
int main() {
Assistant a;
a._name = "peter";
return 0;
}
在实际开发中,虽然支持多继承,但应尽量避免设计菱形继承模型。C++ IO 库中的 basic_ostream 和 basic_istream 就使用了虚继承来解决 basic_ios 的重复问题。
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits> {};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits> {};
继承和组合
- 继承:是一种
is-a 的关系。派生类对象是一个基类对象。属于白箱复用,耦合度高,基类改变会影响派生类。
- 组合:是一种
has-a 的关系。B 对象中包含 A 对象。属于黑箱复用,耦合度低,接口良好定义。
程序设计应追求高内聚、低耦合。优先使用对象组合有助于保持类的封装性。当然,如果类之间确实是 is-a 关系或需要实现多态,则应使用继承。
结语
掌握 C++ 继承的关键点,包括访问权限的 Min 规则、菱形继承的虚继承解决方案,以及继承与组合的权衡,是进阶面向对象编程的重要一步。理解这些机制背后的内存布局和调用顺序,能帮助你在实际开发中写出更稳健的代码。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online