一、继承是什么?解决了什么问题?
在面向对象编程中,复用是永恒的追求。函数实现了代码块的复用,而继承将复用提升到了类设计的层次。
以设计 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 = "张三";
string _address;
string _tel;
int _age = 18;
string _title;
};
identity() 方法和 4 个属性完全重复,增加了代码量且埋下修改隐患。继承的核心思想是将共性抽离成基类(父类),让派生类(子类)继承特性并补充独有内容。
重构后的代码:
// 基类:抽离共性
class Person {
public:
void identity() { cout << "身份认证:" << _name << endl; }
protected:
string _name = "张三";
string _address;
string _tel;
int _age = 18;
};
// 派生类 Student:继承 Person
class Student : public Person {
public:
void study() {}
protected:
int _stuid;
};
// 派生类 Teacher:继承 Person
class Teacher : public Person {
public:
void teaching() {}
protected:
string _title;
};
int main() {
Student s;
Teacher t;
s.identity(); // 输出:身份认证:张三
t.identity(); // 输出:身份认证:张三
return 0;
}
二、继承的语法细节:三种继承方式与访问控制
基类成员在派生类中的访问权限,由基类的访问限定符和继承方式共同决定。
2.1 继承的基本格式
class 派生类名 : 继承方式 基类名 {
// 派生类的成员
};
例如 class Student : public Person 表示 Student 以 public 方式继承 Person。
2.2 继承方式如何影响访问权限?
| 基类成员类型 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public 成员 | 派生类 public | 派生类 protected | 派生类 private |
| protected 成员 | 派生类 protected | 派生类 protected | 派生类 private |
| private 成员 | 不可见 | 不可见 | 不可见 |
关键点:
- 不可见不是不继承:基类
private成员会被继承到对象中,但禁止访问。 - 保护成员(protected)的意义:不想被类外访问,但需让派生类访问时使用。
实例验证:
class Person {
public:
void Print() { cout << _name << endl; }
protected:
string _name = "张三";
private:
int _age = 18;
};
class Student : public Person {
public:
void Test() {
Print(); // 可以访问
_name = "李四"; // 可以访问
// _age = 20; // 编译报错
}
};
int main() {
Student s;
s.Print(); // 可以访问
// s._name = "王五"; // 编译报错
return 0;
}
2.3 实际开发中的继承方式选择
- 默认继承方式:
class默认为private,struct默认为public(建议显式写出)。 - 推荐用法:几乎只用
public继承。protected或private继承限制后续扩展,维护性差。
三、继承中的核心陷阱:作用域与隐藏规则
基类和派生类拥有独立的作用域,同名成员会触发'隐藏'规则。
3.1 什么是'隐藏'?
- 属性隐藏:派生类同名属性屏蔽基类属性直接访问。
- 方法隐藏:只要方法名相同即构成隐藏(不同于重载,重载要求同一作用域)。
class Person {
protected:
string _name = "小李子";
int _num = 111; // 身份证号
};
class Student : public Person {
public:
void Print() {
cout << "姓名:" << _name << endl; // 继承的_name
cout << "身份证号:" << Person::_num << endl; // 加作用域
cout << "学号:" << _num << endl; // 派生类的_num,隐藏基类
}
protected:
int _num = 999; // 学号
};
int main() {
Student s;
s.Print();
return 0;
}
3.2 区分重载与隐藏
class A {
public:
void func() { cout << "func()" << endl; }
};
class B : public A {
public:
void func(int i) { cout << "func(int i): " << i << endl; }
};
int main() {
B b;
b.func(10); // 运行:调用 B::func
b.func(); // 编译报错:B::func 隐藏了 A::func
b.A::func(); // 解决方案:显式加作用域
return 0;
}
四、派生类的默认成员函数:初始化与清理的顺序
4.1 核心规则
- 构造函数:派生类构造必须先调用基类构造。若基类无默认构造,必须在初始化列表中显式调用。
- 拷贝构造:先调用基类拷贝构造。
- 赋值重载:先调用基类赋值重载(注意显式加作用域避免隐藏)。
- 析构函数:执行完派生类逻辑后,自动调用基类析构。
4.2 实例演示
class Person {
public:
Person(const char* name = "peter") : _name(name) { cout << "Person 构造" << endl; }
Person(const Person& p) : _name(p._name) { cout << "Person 拷贝构造" << endl; }
Person& operator=(const Person& p) { if (this != &p) _name = p._name; cout << "Person 赋值重载" << endl; 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 拷贝构造" << endl; }
Student& operator=(const Student& s) {
if (this != &s) { Person::operator=(s); _num = s._num; }
cout << "Student 赋值重载" << endl; return *this;
}
~Student() { cout << "Student 析构" << endl; }
protected:
int _num;
};
int {
;
;
;
s1 = s3;
;
}
运行结果顺序:构造 Person→Student,析构 Student→Person。
4.3 实现不能被继承的类
- C++98 方式:将基类构造函数设为
private。 - C++11 方式:用
final关键字修饰基类。
class Base final {
public:
void func() { cout << "Base::func" << endl; }
};
// class Derive : public Base {}; // 编译报错
五、继承的特殊场景:友元、静态成员与多继承
5.1 友元不能继承
基类的友元无法访问派生类的 private/protected 成员。
class Student;
class Person {
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name = "张三";
};
class Student : public Person {
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum = 1001;
};
void Display(const Person& p, const Student& s) {
cout << p._name << endl;
// cout << s._stuNum << endl; // 编译报错
}
5.2 静态成员共享
基类的静态成员在整个继承体系中只有一份实例。
class Person {
public:
static int _count;
protected:
string _name;
};
int Person::_count = 0;
class Student : public Person {};
class Teacher : public Person {};
int main() {
Person p; Student s; Teacher t;
p._count++; s._count++; t._count++;
cout << Person::_count << endl; // 输出 3
return 0;
}
5.3 多继承与菱形问题
单继承:一个派生类只有一个直接基类。 多继承:一个派生类有多个直接基类,可能引发'菱形继承'问题。
5.3.1 菱形继承的坑
Assistant 同时继承 Student 和 Teacher,而两者都继承 Person,导致 Assistant 中有两份 Person 成员,造成数据冗余和二义性。
5.3.2 解决方案:虚继承
在间接基类继承处加 virtual 关键字。
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 _major; };
int main() {
Assistant a;
a._name = "peter"; // 正常访问,无二义性
return 0;
}
注意:虚继承增加性能开销,不建议设计菱形结构。
六、继承 vs 组合:代码复用的选择艺术
6.1 核心区别:is-a 与 has-a
- 继承(is-a):派生类是基类的一种(如'宝马是车')。白箱复用,耦合度高。
- 组合(has-a):一个类包含另一个类的对象(如'车有轮胎')。黑箱复用,耦合度低。
6.2 实例对比
// 场景 1:车和轮胎(has-a,用组合)
class Tire { protected: string _brand = "米其林"; int _size = 17; };
class Car { protected: string _color = "白色"; Tire _t1, _t2, _t3, _t4; };
// 场景 2:宝马和车(is-a,用继承)
class BMW : public Car { public: void Drive() { cout << "宝马:操控好" << endl; } };
// 场景 3:栈和 vector(优先组合)
class Stack2 {
private:
vector<int> _v;
public:
void push(int x) { _v.push_back(x); }
int top() { return _v.back(); }
};
6.3 最佳实践
当类之间明确是'is-a'关系且需要多态时,用继承。当类之间是'has-a'关系或关系模糊时,优先用组合。
七、总结:继承的核心要点
- 本质:类层次的代码复用。
- 访问控制:基类
private成员不可见,优先用public继承。 - 隐藏规则:同名成员触发隐藏,需显式加作用域。
- 默认成员函数:构造/拷贝/赋值需调用基类对应函数,析构自动调用基类析构。
- 特殊场景:友元不继承,静态成员共享,菱形继承用虚继承解决。
- 复用选择:优先组合(低耦合),适当继承(is-a 或多态)。
掌握继承的细节和陷阱,才能写出更优雅、更可维护的代码。


