【c++面向对象编程】第11篇:继承(一):基本语法——公有继承、保护继承、私有继承
目录
四、保护继承(protected)——“is-a”但对外隐藏
五、私有继承(private)——“is-implemented-in-terms-of”
一、为什么需要继承?
假设你要写三个类:Student(学生)、Teacher(老师)、Staff(行政人员)。
他们都有人类共有的属性:姓名、年龄、身份证号。如果没有继承,你得在三个类里各写一遍:
cpp
class Student {
string name;
int age;
string id;
string studentId; // 学号
void study();
};
class Teacher {
string name;
int age;
string id;
string teacherId; // 工号
void teach();
};
class Staff {
string name;
int age;
string id;
string staffId;
void work();
};
三个类有大量重复代码。更糟糕的是:如果要加一个“性别”字段,三个类都要改。
用继承,把这些公共部分提取到一个基类(Person)中,然后让Student、Teacher、Staff继承它:
cpp
class Person {
public:
string name;
int age;
string id;
};
class Student : public Person { // 公有继承
public:
string studentId;
void study();
};
class Teacher : public Person {
public:
string teacherId;
void teach();
};
现在Student自动拥有了name、age、id,不需要重复声明。这就是继承最朴素的价值——复用代码。
二、三种继承方式的语法
C++的继承语法:
cpp
class Derived : access-specifier Base {
// 派生类成员
};
access-specifier可以是:
-
public:公有继承 -
protected:保护继承 -
private:私有继承
基类的准备
为了演示可见性变化,定义一个包含三种成员的基类:
cpp
class Base {
public:
int pub;
protected:
int prot;
private:
int priv;
};
不同继承方式的效果
| 基类成员类型 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| public成员 | 在派生类中是 public | 在派生类中是 protected | 在派生类中是 private |
| protected成员 | 在派生类中是 protected | 在派生类中是 protected | 在派生类中是 private |
| private成员 | 派生类不可访问 | 派生类不可访问 | 派生类不可访问 |
| 外部通过派生类访问public成员 | ✅ 可以 | ❌ 不可以 | ❌ 不可以 |
核心规律:继承方式不改变基类private成员的“不可访问性”,只改变public和protected成员在派生类中的访问级别。且继承方式只会收紧访问权限,不会放宽。
三、公有继承(public)——真正的“is-a”
语法与效果
cpp
class Derived : public Base {
void func() {
pub = 10; // ✅ 可以访问(仍然是public)
prot = 20; // ✅ 可以访问(仍然是protected)
// priv = 30; // ❌ 不可访问(基类private)
}
};
int main() {
Derived d;
d.pub = 100; // ✅ 外部可以访问(因为是public继承)
// d.prot = 200; // ❌ 外部不能访问(prot现在还是protected)
}
公有继承的含义
公有继承表达的是“is-a”关系(是一种):
-
Student是一种Person -
Dog是一种Animal -
Circle是一种Shape
这意味着:任何可以使用Base的地方,都可以使用Derived。
cpp
void printPerson(Person& p) {
cout << p.name << endl;
}
Student s;
printPerson(s); // Student可以当作Person来用
这是面向对象多态的基础(后面会详细讲)。95%以上的继承场景都用公有继承。
四、保护继承(protected)——“is-a”但对外隐藏
语法与效果
cpp
class Derived : protected Base {
void func() {
pub = 10; // ✅ 在派生类内部可以访问(变成protected)
prot = 20; // ✅ 可以访问(还是protected)
// priv = 30; // ❌ 不可访问
}
};
int main() {
Derived d;
// d.pub = 100; // ❌ 外部不能访问(pub变成了protected)
}
保护继承的含义
保护继承仍然表达“is-a”关系,但对外部代码隐藏了基类的public接口。只有在派生类及其子类中才能访问。
使用场景极其罕见。一般只在以下情况出现:
-
基类的public接口过于宽泛,不想暴露给外部
-
作为实现继承的一种手段(而不是接口继承)
现实项目中,保护继承几乎不用。
五、私有继承(private)——“is-implemented-in-terms-of”
语法与效果
cpp
class Derived : private Base {
void func() {
pub = 10; // ✅ 在派生类内部可以访问(变成private)
prot = 20; // ✅ 可以访问(变成private)
// priv = 30; // ❌ 不可访问
}
};
class GrandChild : public Derived {
void func2() {
// pub = 10; // ❌ 不可访问!在Derived中pub已变成private
// prot = 20; // ❌ 也不可访问
}
};
int main() {
Derived d;
// d.pub = 100; // ❌ 外部不能访问
}
私有继承的含义
私有继承不表达“is-a”关系。它表达的是一种“根据某物实现”(is-implemented-in-terms-of)的关系。
比如:你要实现一个Queue(队列),可以用list来实现。但Queue不是一种list,不应该继承list的public接口。这时可以用私有继承。
cpp
class Queue : private list<int> {
public:
void push(int x) { push_back(x); }
void pop() { pop_front(); }
int front() { return list<int>::front(); }
bool empty() { return list<int>::empty(); }
};
外部只能通过Queue的public接口操作,不能直接使用list的方法。
现实中的选择:私有继承的功能几乎都可以用组合(has-a)替代。上面的队列例子,用组合写会更清晰:
cpp
class Queue {
private:
list<int> data; // 组合,不是继承
public:
void push(int x) { data.push_back(x); }
// ...
};
除非需要访问基类的protected成员,或需要重写虚函数,否则优先用组合。
六、一张表总结三种继承
| 继承方式 | 基类public在派生类中变为 | 基类protected在派生类中变为 | 外部能否通过派生类访问基类public | 表达的关系 |
|---|---|---|---|---|
public |
public | protected | ✅ 能 | is-a(是一种) |
protected |
protected | protected | ❌ 不能 | 受限的is-a |
private |
private | private | ❌ 不能 | is-implemented-in-terms-of |
记忆口诀:
-
public:最小限制,基类public出去还是public -
protected:收紧一层,public变protected -
private:全部收紧为private
七、完整例子:不同继承方式的效果对比
cpp
#include <iostream>
using namespace std;
class Base {
public:
int pub = 1;
protected:
int prot = 2;
private:
int priv = 3;
};
// 公有继承
class PubDerived : public Base {
public:
void show() {
cout << "PubDerived内部: pub=" << pub
<< ", prot=" << prot << endl;
// cout << priv; // 错误:不能访问基类private
}
};
// 保护继承
class ProtDerived : protected Base {
public:
void show() {
cout << "ProtDerived内部: pub=" << pub
<< ", prot=" << prot << endl;
}
};
// 私有继承
class PrivDerived : private Base {
public:
void show() {
cout << "PrivDerived内部: pub=" << pub
<< ", prot=" << prot << endl;
}
};
// 二级派生类
class GrandChild : public PubDerived {
public:
void showGrand() {
cout << "GrandChild内部: pub=" << pub
<< ", prot=" << prot << endl; // ✅ 仍然可以访问
}
};
class GrandChildProt : public ProtDerived {
public:
void showGrand() {
// cout << pub; // ❌ 错误!pub在ProtDerived中是protected
// 但通过ProtDerived访问Base的pub?实际上不能直接访问
// 需要调用ProtDerived的接口
}
};
int main() {
cout << "=== 外部访问测试 ===" << endl;
PubDerived pubObj;
pubObj.pub = 100; // ✅ 可以
// pubObj.prot = 200; // ❌ 不能
cout << "pubObj.pub = " << pubObj.pub << endl;
ProtDerived protObj;
// protObj.pub = 100; // ❌ 不能,pub变成了protected
PrivDerived privObj;
// privObj.pub = 100; // ❌ 不能,pub变成了private
cout << "\n=== 内部访问测试 ===" << endl;
pubObj.show();
protObj.show();
privObj.show();
cout << "\n=== 二级派生类测试 ===" << endl;
GrandChild gc;
gc.showGrand();
return 0;
}
输出:
text
=== 外部访问测试 === pubObj.pub = 100 === 内部访问测试 === PubDerived内部: pub=100, prot=2 ProtDerived内部: pub=1, prot=2 PrivDerived内部: pub=1, prot=2 === 二级派生类测试 === GrandChild内部: pub=100, prot=2
八、三个常见错误
1. 搞混继承方式和成员访问级别
cpp
class Base {
int x; // 默认private
};
class Derived : public Base {
void func() {
// x = 10; // ❌ 错误!基类的private成员在任何情况下都不可访问
}
};
基类的private成员永远不可访问,无论用什么方式继承。
2. 忘记写继承方式(默认为private)
cpp
class Derived : Base { // 忘了写public
// 这是private继承!
};
C++中class的继承默认是private,struct的继承默认是public。这是一个常见的坑。
cpp
class A {};
struct B : A {}; // public继承
class C : A {}; // private继承(!)
3. 滥用私有继承
需要实现“根据某物实现”时,先考虑组合。私有继承只在以下少数情况优于组合:
-
需要访问基类的protected成员
-
需要重写基类的虚函数
-
对空间有极致要求(组合会增加一个对象的大小,私有继承可能利用空基类优化)
九、这一篇的收获
你现在应该能:
-
区分三种继承方式的语法和可见性变化
-
理解公有继承表达“is-a”关系,是最常用的继承方式
-
知道保护继承和私有继承很少用,私有继承可用组合替代
-
记住基类的private成员永远不可直接访问
💡 小作业:设计三个类:
Shape(形状)、Circle(圆形)、Rectangle(矩形)。Shape有color属性和getArea()虚函数。用公有继承实现Circle和Rectangle,并写一个函数打印任意形状的面积。
下一篇预告:第12篇《继承(二):构造与析构顺序,继承中的构造函数》——派生类对象构造时,先构造基类还是先构造派生类?派生类的构造函数如何向基类构造函数传参?下篇讲清楚继承中的生命周期管理。
更多推荐
所有评论(0)