探索C++三大特性--C++ 继承详解:从概念到高级用法
一、继承的概念
继承(Inheritance)机制是面向对象程序设计中最重要的手段之一。通过继承,我们可以在已有类的基础上进行扩展,增加新的方法和属性,从而创建新的类,这些新类被称为派生类(Derived Class)【14†source】。继承的主要目的是实现代码复用,使得相似功能代码的编写更加简洁高效,并使系统具有更好的可维护性。
1.1 没有继承时的问题
在没有继承的情况下,我们可能会设计多个功能相似的类,这些类中含有相同的成员变量和成员函数。例如,我们设计了两个类:——Student
和Teacher
,它们都拥有姓名、地址、电话等成员变量,以及身份认证的成员函数。这导致了代码的冗余和维护困难。
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; // 职称
};
如上代码中,Student
和Teacher
类中存在大量重复成员变量和函数,这种冗余不仅增加了代码量,也使得代码维护变得复杂。
1.2 使用继承的改进
通过继承,我们可以将公共的成员变量和函数提取到一个基类(Base Class)中,例如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; // 职称
};
通过上述改进,Student
和Teacher
类继承了Person
类,公共成员被提取到基类中,这样可以减少重复代码,提高代码的可维护性。
二、继承的定义与访问控制
2.1 继承的定义方式
在C++中,继承是通过指定继承方式来实现的,常见的继承方式有三种:
-
public
继承:基类的public
和protected
成员在派生类中保持相同的访问控制。 -
protected
继承:基类的public
成员在派生类中变为protected
,而protected
成员保持不变。 -
private
继承:基类的所有非private
成员在派生类中都变为private
。
class Person {
public:
void Print() {
cout << _name << endl;
}
protected:
string _name; // 姓名
};
// public 继承
class Student : public Person {
protected:
int _stunum; // 学号
};
在实际使用中,public
继承是最常见的方式,因为它可以保持基类成员的可访问性。而protected
和private
继承较少使用,因为它们限制了派生类对基类成员的访问,从而降低了代码的扩展性和维护性。
2.2 基类成员的访问控制
在继承体系中,基类的private
成员在派生类中是不可见的,这意味着派生类对象无法访问基类中的私有成员。而基类的protected
成员则可以在派生类中访问,这使得派生类能够继承和使用这些成员。
class Person {
protected:
string _name; // 姓名
private:
int _age; // 年龄
};
class Student : public Person {
public:
void Print() {
cout << _name << endl; // 可以访问基类的受保护成员
// cout << _age; // 无法访问基类的私有成员
}
protected:
int _stunum; // 学号
};
三、继承中的作用域与隐藏规则
3.1 成员隐藏
在继承体系中,如果派生类和基类中存在同名的成员,那么基类的成员会被隐藏。此时,派生类只能访问自己的成员,而不能直接访问基类的同名成员。
class Person {
protected:
int _num = 111; // 身份证号
};
class Student : public Person {
public:
void Print() {
cout << "身份证号: " << Person::_num << endl;
cout << "学号: " << _num << endl;
}
protected:
int _num = 999; // 学号
};
在上面的例子中,Student
类中的_num
隐藏了Person
类中的同名成员,因此如果想访问基类中的_num
,需要使用Person::_num
来显式指定。
四、派生类的默认成员函数
在派生类中,默认的成员函数(如构造函数、析构函数、拷贝构造函数、赋值运算符等)会自动生成,并且派生类会调用基类的相应成员函数来初始化基类部分。
class Person {
public:
Person(const char* name = "peter") : _name(name) {
cout << "Person()" << endl;
}
~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() {
cout << "~Student()" << endl;
}
protected:
int _num; // 学号
};
int main() {
Student s1("jack", 18);
return 0;
}
在上面的代码中,Student
类的构造函数和析构函数在执行时会首先调用基类Person
的构造函数和析构函数,这样能够确保基类部分正确初始化和清理。
五、多继承与菱形继承问题
5.1 多继承
C++ 支持多继承,即一个类可以继承自多个基类。然而,多继承可能会导致菱形继承问题,即多个基类中有相同的成员,导致派生类中有两份相同的成员拷贝。
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; // 主修课程
};
在上述代码中,Assistant
类同时继承自Student
和Teacher
,由于这两个基类都继承自Person
,因此Assistant
类中存在两份_name
成员,这就产生了数据冗余和访问二义性的问题。
5.2 虚继承解决菱形继承问题
为了避免菱形继承带来的问题,我们可以使用虚继承。通过虚继承,派生类只会保留一份基类的成员。
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;
}
通过虚继承,Assistant
类中只会有一份Person
类的成员,避免了数据冗余和访问的二义性。
六、继承与组合的选择
在面向对象设计中,继承表示一种is-a
的关系,而组合表示一种has-a
的关系。优先使用组合而不是继承可以降低类之间的耦合度,提高代码的灵活性和可维护性。
class Tire {
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car {
protected:
string _colour = "白色"; // 颜色
Tire _t1, _t2, _t3, _t4; // 四个轮胎
};
class BMW : public Car {
public:
void Drive() { cout << "驾驶宝马车" << endl; }
};
在上述代码中,Car
类通过组合方式包含了四个轮胎对象Tire
,这种设计更符合逻辑上的has-a
关系。在实际设计中,优先使用组合可以减少代码的耦合性,增强代码的复用性和灵活性。
总结
继承是C++中实现代码复用和扩展的重要手段,它可以简化代码结构,减少冗余代码,增强代码的可维护性。然而,在使用继承时,我们需要遵循一些设计原则,避免滥用继承导致代码复杂化。同时,理解继承与组合之间的差异,合理选择使用继承或组合,对于编写高质量的面向对象代码非常重要。
希望这篇博客能够帮助大家深入理解C++继承的概念和使用方法。欢迎大家在实际项目中应用这些知识,编写出更加优雅和高效的代码。