C++ 继承(1):从代码复用、访问控制到对象切片与名字隐藏
C++ 继承(1):从代码复用、访问控制到对象切片与名字隐藏

文章目录
前言
继承是 C++ 面向对象部分非常重要的一块内容。
在学习继承之前,我们已经接触过函数复用、模板复用和 STL。它们解决的都是“相同代码不要重复写”的问题,而继承解决的是类层面的复用。
例如,学生和老师都具有姓名、年龄、电话和地址,也都需要进行身份认证。如果分别在 Student 和 Teacher 中定义这些成员,不但代码重复,后续修改起来也比较麻烦。
继承允许我们把公共部分抽取到一个基类中,再让不同的派生类在此基础上继续扩展。
不过,继承并不只是把一个类的代码复制给另一个类。它还涉及:
- 基类和派生类的关系
public、protected、private三种继承方式- 基类成员在派生类中的访问权限
- 派生类对象向基类的转换
- 对象切片
- 同名成员隐藏
- 类模板继承中的名字查找
接下来就让我们从对象模型和使用场景出发,一步一步理解。
一、为什么需要继承?
1.1 没有继承时的问题
假设我们需要设计学生类和教师类。
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
void identity()
{
cout << "学生身份认证:" << _name << endl;
}
void study()
{
cout << _name << " 正在学习" << endl;
}
protected:
string _name;
string _address;
string _telephone;
int _age = 18;
int _studentId = 0;
};
class Teacher
{
public:
void identity()
{
cout << "教师身份认证:" << _name << endl;
}
void teach()
{
cout << _name << " 正在授课" << endl;
}
protected:
string _name;
string _address;
string _telephone;
int _age = 30;
string _title;
};
这两个类中存在大量重复内容:
_name
_address
_telephone
_age
identity()
重复代码短期看起来没什么,但项目规模扩大以后,问题会慢慢出现。
例如,学校要求身份认证时同时打印电话号码,那么两个类都要修改。如果还有管理员、保安、实验员等类型,就需要在更多地方重复修改。
并且我们可以看到这里两个类很多地方显得很冗余,此时我们就可以用继承来对我们的代码进行一个复用,使得整个项目更加耦合

1.2 抽取公共基类
可以把公共成员放进 Person 类中:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
void identity() const
{
cout << "身份认证:" << _name << endl;
}
protected:
string _name;
string _address;
string _telephone;
int _age = 0;
};
class Student : public Person
{
public:
void study() const
{
cout << _name << " 正在学习" << endl;
}
protected:
int _studentId = 0;
};
class Teacher : public Person
{
public:
void teach() const
{
cout << _name << " 正在授课" << endl;
}
protected:
string _title;
};
此时:
Person
├── Student
└── Teacher
Person 是基类,也叫父类。
Student 和 Teacher 是派生类,也叫子类。
派生类会保留基类部分,同时增加自己独有的数据和行为。
1.3 继承表达的是 is-a 关系
公有继承通常表示一种 is-a 关系。
Student is a Person
Teacher is a Person
也就是:
学生是一种人
教师也是一种人
如果一个关系读成“某某拥有某某”更自然,它通常不是继承,而是组合。
例如:
Car has a Tire
汽车拥有轮胎
此时更适合把 Tire 作为 Car 的成员,而不是让 Car 继承 Tire。
二、继承的基本语法
2.1 定义格式
C++ 中继承的基本格式如下:
class 派生类名 : 继承方式 基类名
{
// 派生类自己的成员
};
例如:
class Student : public Person
{
public:
void study()
{
cout << "学习" << endl;
}
private:
int _studentId = 0;
};
其中:
Student:派生类
public:继承方式
Person:基类

2.2 三种继承方式
C++ 支持三种继承方式:
class A : public Base
{
};
class B : protected Base
{
};
class C : private Base
{
};
分别是:
public 继承
protected 继承
private 继承
实际开发中,最常见的是 public 继承。
protected 和 private 继承更多是在实现层面复用基类,并不公开表达“派生类也是一种基类”。
2.3 class 和 struct 的默认继承方式
如果没有明确写继承方式:
class Student : Person
{
};
class 默认使用 private 继承。
而:
struct Student : Person
{
};
struct 默认使用 public 继承。
虽然语法允许省略,但更推荐把继承方式写清楚:
class Student : public Person
{
};
读代码时不需要再回忆默认规则。
三、基类成员在派生类中的访问权限
这是继承中最容易混淆的一部分。
这里的主要规则就是二者相比取权限更小的那个。
需要同时区分两个概念:
基类成员原来的访问权限
派生类使用的继承方式

3.1 public 继承
class Derived : public Base
{
};
权限变化如下:
| 基类成员权限 | 在派生类中的权限 |
|---|---|
public | public |
protected | protected |
private | 不可直接访问 |
例如:
class Person
{
public:
void identity()
{
}
protected:
string _name;
private:
int _age = 0;
};
class Student : public Person
{
public:
void print()
{
identity(); // 可以访问基类 public 成员
cout << _name; // 可以访问基类 protected 成员
// cout << _age; // 错误:不能直接访问基类 private 成员
}
};
3.2 protected 继承
class Derived : protected Base
{
};
权限变化如下:
| 基类成员权限 | 在派生类中的权限 |
|---|---|
public | protected |
protected | protected |
private | 不可直接访问 |
基类原来的公有接口被收紧成了保护接口,只能在派生类及其后续派生类中使用。
3.3 private 继承
class Derived : private Base
{
};
权限变化如下:
| 基类成员权限 | 在派生类中的权限 |
|---|---|
public | private |
protected | private |
private | 不可直接访问 |
基类中的公有成员和保护成员,都会变成派生类的私有成员。
3.4 一张表记住三种继承方式
| 基类成员 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | 不可直接访问 | 不可直接访问 | 不可直接访问 |
可以把规则简单理解为:
继承方式决定了基类可访问成员在派生类中的最高公开程度。
3.5 private 成员到底有没有被继承?
基类的 private 成员依然存在于派生类对象的基类子对象中,只是派生类不能直接通过名字访问它。
例如:
class Person
{
public:
void setAge(int age)
{
_age = age;
}
int getAge() const
{
return _age;
}
private:
int _age = 0;
};
class Student : public Person
{
};
虽然 Student 不能直接写:
_age = 20;
但它仍然可以通过基类提供的公有接口操作这部分数据:
Student student;
student.setAge(20);
cout << student.getAge() << endl;
因此,“不可见”不等于“对象中不存在”。

四、public、protected 和 private 应该怎么选?
4.1 类外需要访问:public
对外提供的稳定接口通常放在 public 区域。
class Person
{
public:
void setName(const string& name)
{
_name = name;
}
const string& getName() const
{
return _name;
}
private:
string _name;
};
4.2 派生类需要访问:protected
有些数据不希望类外直接修改,但确实需要让派生类使用,可以放在 protected 区域。
class Person
{
protected:
string _name;
};
不过 protected 也不要使用过多。
派生类直接依赖基类的数据布局,会增加两个类之间的耦合。很多时候,基类提供受控的 protected 成员函数,会比直接暴露成员变量更稳妥。
class Person
{
protected:
const string& name() const
{
return _name;
}
private:
string _name;
};
4.3 只允许本类使用:private
普通成员变量通常优先放在 private 区域。
class Person
{
private:
string _name;
int _age = 0;
};
这样可以减少外部代码对内部实现的依赖。不过一般在实际工程中我们都不会这样使用
五、派生类对象向基类转换
5.1 派生类对象包含基类子对象
假设有下面两个类:
class Person
{
public:
string _name;
int _age = 0;
};
class Student : public Person
{
public:
int _studentId = 0;
};
一个 Student 对象可以粗略理解为:
Student 对象
├── Person 基类部分
│ ├── _name
│ └── _age
└── Student 自己的部分
└── _studentId
因此,在可访问的 public 继承中,派生类对象可以隐式转换成基类指针或基类引用。

5.2 转换成基类指针
Student student;
Person* personPtr = &student;
personPtr 指向的是 student 对象中的 Person 基类子对象。
此时可以访问 Person 提供的成员:
personPtr->_name = "小明";
personPtr->_age = 18;
但不能通过 Person* 访问学生独有的成员:
// personPtr->_studentId = 1001; // 错误
5.3 转换成基类引用
Student student;
Person& personRef = student;
引用同样绑定到 Student 对象中的基类部分。
这种从派生类型向基类型的转换,通常称为向上转换:
upcasting
在 public 继承中,向上转换是自然且安全的。
5.4 基类对象不能自动变成派生类对象
下面的写法是错误的:
Person person;
// Student student = person; // 编译错误
原因是 Person 对象中只有基类数据,并没有学生的学号等派生类数据。
编译器无法凭空补齐这些内容。
六、对象切片到底是什么?
“派生类转基类”经常和“对象切片”放在一起讲,但这两个概念不能完全画等号。

6.1 指针和引用转换不会切片
Student student;
Person* ptr = &student;
Person& ref = student;
这里没有创建新的 Person 对象。
ptr 和 ref 仍然指向原来的 Student 对象,只是通过基类视角访问它。
因此,这种情况不发生对象切片。
6.2 按值赋给基类对象会切片
Student student;
student._name = "小明";
student._studentId = 1001;
Person person = student;
这里会新建一个独立的 Person 对象,只复制 Student 中的基类部分。
学生独有的 _studentId 不会进入 person。
可以理解为:
Student
├── Person 部分 ──────复制──────> Person 对象
└── Student 独有部分 被舍弃
这才是真正的对象切片。
6.3 为什么函数参数常用引用?
假设函数写成按值传递:
void printPerson(Person person)
{
}
传入 Student 时会发生一次切片:
Student student;
printPerson(student);
如果只是读取,更适合写成:
void printPerson(const Person& person)
{
}
这样既避免拷贝,也不会切掉派生类部分。
七、继承体系中的作用域和名字隐藏
7.1 基类和派生类有独立作用域
基类和派生类不是同一个作用域。
如果派生类定义了和基类同名的成员,派生类成员会隐藏基类成员。
#include <iostream>
using namespace std;
class Person
{
protected:
int _number = 111;
};
class Student : public Person
{
public:
void print() const
{
cout << "基类编号:" << Person::_number << endl;
cout << "学生学号:" << _number << endl;
}
protected:
int _number = 999;
};
在 Student 的作用域中:
_number
默认找到的是:
Student::_number
想访问基类版本,需要明确指定类域:
Person::_number

7.2 函数名相同也会构成隐藏
看下面的代码:
class Base
{
public:
void func()
{
cout << "Base::func()" << endl;
}
};
class Derived : public Base
{
public:
void func(int value)
{
cout << "Derived::func(int): " << value << endl;
}
};
调用:
Derived derived;
derived.func(10);
derived.func();
第二行会编译失败。
虽然两个函数的参数不同,但它们不在同一个作用域中,因此不构成重载。
Derived::func(int) 会隐藏基类中所有名为 func 的成员。
7.3 使用类域访问基类函数
可以明确指定基类作用域:
derived.Base::func();
也可以在派生类中使用 using,把基类重载引入派生类作用域:
class Derived : public Base
{
public:
using Base::func;
void func(int value)
{
cout << "Derived::func(int): " << value << endl;
}
};
此时:
Derived derived;
derived.func();
derived.func(10);
两种调用都可以正常工作。
7.4 隐藏、重载和重写不要混淆
| 概念 | 所处作用域 | 主要条件 |
|---|---|---|
| 重载 | 同一作用域 | 函数名相同,参数列表不同 |
| 隐藏 | 基类与派生类不同作用域 | 派生类出现同名成员 |
| 重写 | 基类与派生类 | 基类虚函数与派生类函数满足重写条件 |
当前只需要先分清重载和隐藏。
重写与动态多态有关,可以放到后面的多态章节继续学习。
八、继承类模板时为什么经常要写 this->?
继承类模板时,会遇到一个比较特别的名字查找问题。
8.1 一个简单例子
#include <vector>
using namespace std;
template<class T>
class Buffer
{
protected:
void append(const T& value)
{
_data.push_back(value);
}
private:
vector<T> _data;
};
template<class T>
class LoggingBuffer : public Buffer<T>
{
public:
void push(const T& value)
{
append(value);
}
};
在某些编译器和标准规则下:
append(value);
可能无法被直接找到。
原因是:
Buffer<T>
属于依赖模板参数的基类。编译器第一次解析 LoggingBuffer 模板时,还不能确定基类中一定存在 append。
8.2 使用 this->
可以写成:
template<class T>
class LoggingBuffer : public Buffer<T>
{
public:
void push(const T& value)
{
this->append(value);
}
};
this-> 告诉编译器:
这个名字属于当前对象,请等模板真正实例化时再查找。
8.3 使用基类类域
也可以明确指定基类:
template<class T>
class LoggingBuffer : public Buffer<T>
{
public:
void push(const T& value)
{
Buffer<T>::append(value);
}
};
实际代码中,this-> 通常更自然。
九、public 继承并不等于单纯复用代码
继承最容易被误用的地方,是只看到“能少写代码”,却忽略了类型关系。
假设为了复用 vector 的尾插接口,写出:
template<class T>
class Stack : public vector<T>
{
};
这样确实复用了 vector,但也产生了一个问题:
Stack is a vector?
栈是一种可以随机访问、任意插入和遍历的 vector?
显然并不准确。
栈只允许访问栈顶,而 vector 暴露了大量不符合栈语义的接口:
Stack<int> stack;
stack.push_back(1);
stack.insert(stack.begin(), 2);
stack[0] = 100;
这会破坏栈本应有的接口约束。
更合适的方式是组合:
template<class T>
class Stack
{
public:
void push(const T& value)
{
_data.push_back(value);
}
void pop()
{
_data.pop_back();
}
T& top()
{
return _data.back();
}
bool empty() const
{
return _data.empty();
}
private:
vector<T> _data;
};
继承不只是实现复用,它还会建立类型关系。
只有当派生类确实可以当作基类使用时,public 继承才比较合适。

十、常见问题总结
10.1 派生类能直接访问基类 private 成员吗?
不能。
private 成员存在于基类子对象中,但派生类不能直接通过名字访问,只能通过基类提供的接口操作。
10.2 protected 和 private 有什么区别?
private 只能由当前类及其友元访问。
protected 除了当前类及其友元,还可以由派生类访问。
10.3 为什么实际中最常见的是 public 继承?
因为公有继承能够保留基类的公开接口,并表达稳定的 is-a 类型关系。
10.4 class 不写继承方式时默认是什么?
默认是 private 继承。
10.5 struct 不写继承方式时默认是什么?
默认是 public 继承。
10.6 派生类对象赋给基类引用会切片吗?
不会。
引用仍然绑定原来的派生类对象,只是以基类视角访问。
10.7 派生类对象赋给基类对象会切片吗?
会。
新基类对象只保留派生对象中的基类部分。
10.8 基类和派生类中的同名函数一定是重载吗?
不是。
重载要求函数处在同一作用域。基类和派生类中的同名函数通常构成隐藏关系。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)