继承(Inheritance)
继承中的隐藏是编译器的名字查找规则导致的:当在派生类中查找某个名称时,编译器会先在派生类内部查找,找到后就停止向上查找基类的同名成员。这一机制可能导致意外屏蔽基类成员,因此实际开发中应尽量避免在派生类中定义与基类同名的成员(除非有意隐藏)。多继承的核心是让派生类同时拥有多个基类的功能,但也可能带来菱形继承(需用虚继承解决)和成员名冲突(需显式指定来源)等问题。实际开发中应谨慎使用多继承,优先考虑组
继承的基本概念
在面向对象编程(OOP)中,继承(Inheritance) 是一种允许一个类(称为派生类 / 子类)复用另一个类(称为基类 / 父类)的属性和方法,并可以在此基础上添加新特性或修改现有特性的机制。它是面向对象的三大核心特性(封装、继承、多态)之一,核心目的是代码复用和建立类之间的层次关系。
继承的核心概念
- 基类(Base Class):被继承的类,也称为父类或超类(Superclass),包含可复用的属性和方法。
- 派生类(Derived Class):继承基类的类,也称为子类(Subclass),可以继承基类的成员(成员变量和成员函数),并可新增自己的成员或重写基类的方法。
- 继承关系:派生类与基类之间形成 “is-a”(是一个)的逻辑关系。例如,“狗” 是 “动物” 的一种,因此
Dog类可以继承Animal类。
继承的基本语法
// 基类:动物
class Animal {
public:
void eat() { cout << "动物吃东西" << endl; }
void sleep() { cout << "动物睡觉" << endl; }
protected:
string name; // 受保护的成员,派生类可访问
};
// 派生类:狗(继承自动物)
class Dog : public Animal { // 公有继承
public:
void bark() { cout << "狗叫:汪汪" << endl; } // 新增方法
void setName(string n) { name = n; } // 访问基类的protected成员
};
在这个例子中:
Dog类通过public继承方式继承了Animal类的eat()、sleep()方法和name成员。Dog类新增了自己的方法bark(),并能访问基类的protected成员name。
继承的主要作用
-
代码复用
派生类无需重复编写基类中已有的代码,直接继承并使用基类的成员,减少冗余。例如,Dog、Cat等类都能复用Animal类的eat()和sleep()方法。 -
建立类层次
通过继承可以构建类的层次结构,清晰表达类之间的关系。例如:生物 → 动物 → 哺乳动物 → 狗
上层类更通用,下层类更具体。 -
支持多态
继承是多态的基础。派生类可以重写(override)基类的方法,使得不同派生类对同一方法有不同实现,通过基类指针 / 引用调用时能动态选择具体实现。
继承的类型
根据继承方式的不同,基类成员在派生类中的访问权限会发生变化(如前文 “访问权限交集” 规则),主要有 3 种继承方式:
- 公有继承(public):基类的
public成员在派生类中仍为public,protected成员仍为protected(最常用)。 - 保护继承(protected):基类的
public和protected成员在派生类中均变为protected。 - 私有继承(private):基类的
public和protected成员在派生类中均变为private。
| 基类成员原权限 | 公有继承(public) | 保护继承(protected) | 私有继承(private) |
|---|---|---|---|
| public | public(取更宽松) | protected | private |
| protected | protected | protected | private |
| private | 不可访问(基类私有成员任何继承方式都无法在派生类外访问) | 不可访问 | 不可访问 |
继承方式相当于 “权限上限”,基类成员原权限若高于继承方式的权限,则会被 “降级” 到继承方式的权限;若低于或等于,则保持原权限。最终权限是两者中更严格的那个(private > protected > public)。
总结:派生类的访问权限如下:
-
不管什么继承方式,派生类内部都不能访问基类的私有成员;
-
不管什么继承方式,派生类内部除了基类的私有成员不可以访问,其他的都可以访问;
-
不管什么继承方式,派生类对象在类外除了公有继承基类中的公有成员可以访问外,其他的都不能访问。
保护继承和私有继承之间有什么区别呢?
以三层继承为例,如果中间层采用保护继承的方式继承顶层基类,那么在底层派生类中也能访问到顶层基类的公有成员和保护成员。
如果中间层采用私有继承的方式继承顶层基类,那么底层派生类中对顶层基类的任何成员都无法访问了。
继承关系的局限性
创建、销毁的方式不能被继承 —— 构造、析构
复制控制的方式不能被继承 —— 拷贝构造、赋值运算符函数
空间分配的方式不能被继承 —— operator new 、 operator delete
友元不能被继承(友元破坏了封装性,为了降低影响,不允许继承)
单继承下派生类对象的创建和销毁
有这样一种说法:创建派生类对象时,先调用基类构造函数,再调用派生类构造函数,对吗?
错误,创建派生类对象,一定会先调用派生类的构造函数,在此过程中会先去调用基类的构造
来看下面这段代码
#include <iostream>
using std::cout;
using std::endl;
class Base{
public:
Base(){
cout << "Base()" << endl;
}
~Base(){
cout << "~Base()" << endl;
}
private:
int _a = 10;
int _b = 20;
};
class Derived : public Base{
public:
Derived(){
cout << "Derived()" << endl;
}
~Derived(){
cout << "~Derived()" << endl;
}
private:
int _c = 30;
int _d = 40;
};
void test(){
Derived d;
cout << sizeof(Base) << endl;//8
cout << sizeof(Derived) << endl;//16
Derived * p = &d;
int * pInt = (int *)p;
cout << *pInt << endl;//10
cout << *(pInt + 1) << endl;//20
cout << *(pInt + 2) << endl;//30
cout << *(pInt + 3) << endl;//40
}
int main()
{
test();
return 0;
}
这段代码直观地证明了:
- 继承时,派生类对象的内存包含基类成员(在前)和自身成员(在后)。派生类对象的内存空间是基类成员 + 自身成员的总和,基类成员在派生类对象中占据独立的内存空间。
- 构造 / 析构顺序严格遵循 “基类先构造,派生类后构造;派生类先析构,基类后析构”。创建派生类对象会马上调用派生类的构造函数,但在初始化列表的最开始调用基类的构造函数, 可以理解为派生对象中包含了一个基类对象
- 访问控制不改变成员的内存存在性,仅限制编译期的访问权限。
创建派生类对象时调用基类构造的机制
语法规则
1.当派生类中没有显式调用基类构造函数时,会自动调用基类的默认无参构造(或者所有参数都有默认值的有参构造);
2.此时如果基类中没有默认无参构造,Derived类的构造函数的初始化列表中也没有显式调用基类构造函数,编译器会报错
——不允许派生类对象的创建;
3.当派生类对象调用基类构造时,希望使用非默认的基类构造函数,必须显式地在初始化列表中写出。
#include <iostream>
using std::cout;
using std::endl;
class Base{
public:
Base(int baseNumber)
:_baseNumber(baseNumber)
{
cout << "Base(int)" << endl;
}
private:
int _baseNumber;
};
class ThirdObject{
public:
ThirdObject()
{
cout << "ThirdObject(int)" << endl;
}
};
class Derived :public Base{
public:
Derived(int derivedNumber, int thirdNumber, int baseNumber)
:Base(baseNumber)
,_derivedNumber(derivedNumber)
,_base(baseNumber)
{
cout << "Derived(int,int,int)" << endl;
}
private:
ThirdObject _thirdObj;
//持有基类对象成员
Base _base;
int _derivedNumber;
};
void test(){
Derived d(1, 2, 3);
}
int main()
{
test();
return 0;
}
output
Base(int) // 基类Base的构造(因Derived继承自Base)
ThirdObject(int) // 成员对象ThirdObject的构造
Base(int) // 成员对象Base(_base)的构造
Derived(int,int,int) // 派生类Derived自身的构造
这段代码的核心作用是演示对象构造的优先级顺序:
基类构造 → 成员对象构造(按声明顺序) → 派生类自身构造
派生类对象的销毁
当派生类析构函数执行完毕之后,会自动调用基类析构函数,完成基类部分所需要的销毁(回收数据成员申请的堆空间资源)。
记忆:创建一个对象,一定会马上调用自己的构造函数;一个对象被销毁,也一定会马上调用自己的析构函数。
当派生类对象中包含对象成员
如果此时派生类对象中包含对象成员,那么此时的调用顺序满足:
- 1.完成派生类对象所占内存空间的开辟
- 2.调用基类的构造函数,完成从基类吸收的数据成员的初始化
- 3.完成子对象、const数据成员、引用数据成员的初始化
- 4.执行派生类构造函数的函数体
对基类成员的隐藏
派生类中声明了和基类的数据成员同名的数据成员,就会对基类的这个数据成员形成隐藏。
派生类对象无法通过数据成员的名字直接访问基类的这个数据成员。
1. 成员变量的隐藏
当派生类定义了与基类同名的成员变量时,基类的同名变量会被隐藏。派生类中直接访问该变量时,默认访问的是派生类自己的变量;若要访问基类的同名变量,需用 基类名:: 限定。
示例:
#include <iostream>
using namespace std;
class Base {
public:
int x = 10; // 基类的x
};
class Derived : public Base {
public:
int x = 20; // 派生类的x,隐藏基类的x
};
int main() {
Derived d;
cout << d.x << endl; // 输出20(访问派生类的x)
cout << d.Base::x << endl; // 输出10(显式访问基类的x)
return 0;
}
如果一定要访问基类的这个数据成员,需要加上作用域,但是这种写法不符合面向对象的原则,不推荐实际使用。
2. 成员函数的隐藏
当派生类定义了与基类同名的成员函数时,无论参数列表是否相同,基类的同名函数都会被隐藏。
(1)函数名相同,参数列表不同(隐藏)
class Base {
public:
void func(int a) { // 基类的func(带int参数)
cout << "Base::func(int): " << a << endl;
}
};
class Derived : public Base {
public:
void func() { // 派生类的func(无参数),隐藏基类的func(int)
cout << "Derived::func()" << endl;
}
};
int main() {
Derived d;
d.func(); // 正确:调用派生类的func()
// d.func(10); // 错误:基类的func(int)被隐藏,无法直接调用
d.Base::func(10); // 正确:显式调用基类的func(int)
return 0;
}
派生类对基类的成员函数构成隐藏,只需要派生类中定义一个与基类中成员函数同名的函数即可(函数的返回类型、参数情况都可以不同,依然能隐藏)。
(2)函数名相同,参数列表相同(非虚函数时为隐藏,虚函数时为覆盖)
- 若基类函数不是虚函数:派生类同名同参数函数会隐藏基类函数。
- 若基类函数是虚函数:派生类同名同参数函数会重写(覆盖) 基类函数(多态的基础)。
class Base {
public:
void func() { cout << "Base::func()" << endl; } // 非虚函数
virtual void vfunc() { cout << "Base::vfunc()" << endl; } // 虚函数
};
class Derived : public Base {
public:
void func() { cout << "Derived::func()" << endl; } // 隐藏基类func()
void vfunc() { cout << "Derived::vfunc()" << endl; } // 重写基类vfunc()
};
int main() {
Derived d;
Base* b = &d;
b->func(); // 输出Base::func()(隐藏:非虚函数,按指针类型调用)
d.func(); // 输出Derived::func()(直接访问派生类)
b->vfunc(); // 输出Derived::vfunc()(重写:虚函数,按对象类型调用)
return 0;
}
隐藏的核心特点
- 触发条件:派生类成员与基类成员同名(与参数、返回值无关,虚函数重写是特殊情况)。
- 访问规则:派生类中直接访问同名成员时,默认指向派生类自己的;访问基类同名成员需用
基类名::限定。 - 与重写的区别:重写仅针对虚函数且要求签名完全一致,隐藏则对所有同名成员生效。
总结
继承中的隐藏是编译器的名字查找规则导致的:当在派生类中查找某个名称时,编译器会先在派生类内部查找,找到后就停止向上查找基类的同名成员。这一机制可能导致意外屏蔽基类成员,因此实际开发中应尽量避免在派生类中定义与基类同名的成员(除非有意隐藏)。
多继承
在 C++ 中,多继承(Multiple Inheritance) 是指一个派生类可以同时继承多个基类的特性(成员变量和成员函数)。与单一继承(一个类只继承自一个基类)相比,多继承能更灵活地组合不同类的功能,但也会带来一些复杂性。
多继承的基本语法
派生类声明时,在基类列表中用逗号分隔多个基类,语法如下:
class Derived : access-specifier Base1, access-specifier Base2, ... {
// 派生类成员
};
其中 access-specifier 是继承方式(public、protected、private),每个基类可以指定独立的继承方式。
多继承的示例
假设我们有两个基类 Student(学生)和 Worker(工人),可以通过多继承创建 StudentWorker(工读生)类,同时拥有两者的特性:
#include <iostream>
#include <string>
using namespace std;
// 基类1:学生
class Student {
protected:
string _school; // 学校
public:
Student(string school) : _school(school) {}
void study() { cout << "在" << _school << "学习" << endl; }
};
// 基类2:工人
class Worker {
protected:
string _company; // 公司
public:
Worker(string company) : _company(company) {}
void work() { cout << "在" << _company << "工作" << endl; }
};
// 派生类:工读生(同时继承学生和工人)
class StudentWorker : public Student, public Worker {
private:
string _name; // 姓名
public:
// 初始化列表需同时初始化所有基类
StudentWorker(string name, string school, string company)
: Student(school), Worker(company), _name(name) {}
void introduce() {
cout << "我是" << _name << "," << endl;
study(); // 调用Student的方法
work(); // 调用Worker的方法
}
};
int main() {
StudentWorker sw("小明", "清华大学", "字节跳动");
sw.introduce();
return 0;
}
输出结果:
我是小明,
在清华大学学习
在字节跳动工作
这个例子中,StudentWorker 同时继承了 Student 的 study() 方法和 Worker 的 work() 方法,实现了功能的组合。
多继承的问题与解决
多继承虽然灵活,但会引入一些特殊问题:
1. 菱形继承(钻石问题)

当两个基类继承自同一个间接基类,而派生类同时继承这两个基类时,会形成 “菱形” 结构,导致间接基类的成员在派生类中存在多份拷贝,引发二义性。
class A
{
public:
void print() const{
cout << "A::print()" << _a << endl;
}
double _a;
};
class B
: public A
{
public:
double _b;
};
class C
: public A
{
public:
double _c;
};
class D
: public B
, public C
{
public:
double _d;
};
菱形继承情况下,D类对象的创建会生成一个B类子对象,其中包含一个A类子对象;还会生成一个C类子对象,其中也包含一个A类子对象。所以D类对象的内存布局中有多个A类子对象,访问继承自A的成员时会发生二义性(无论是否涉及A类的数据成员,单纯访问A类的成员函数也会冲突)
因为编译器需要通过基类子对象去调用,但是不知道应该调用哪个基类子对象的成员函数。
解决方法:使用虚继承(Virtual Inheritance)
在基类继承间接基类时,用 virtual 关键字声明,确保间接基类在派生类中只存在一份拷贝:
class A
{
public:
void print() const{
cout << "A::print()" << endl;
}
double _a;
};
class B
: virtual public A
{
public:
double _b;
};
class C
: virtual public A
{
public:
double _c;
};
class D
: public B
, public C
{
public:
double _d;
};

采用虚拟继承的方式处理菱形继承问题,实际上改变了派生类的内存布局。B类和C类对象的内存布局中多出一个虚基类指针,位于所占内存空间的起始位置,同时继承自A类的内容被放在了这片空间的最后位置。D类对象中只会有一份A类的基类子对象。 
2. 成员名冲突
当多个基类拥有同名成员时,派生类访问该成员会产生二义性,需显式指定来源。
class A {
public:
void func() { cout << "A::func()" << endl; }
};
class B {
public:
void func() { cout << "B::func()" << endl; }
};
class C : public A, public B {
public:
void callFunc() {
A::func(); // 显式调用A的func()
B::func(); // 显式调用B的func()
// func(); // 错误:二义性
}
};
多继承的适用场景
多继承适合 “组合多个独立功能” 的场景,例如:
- 一个类需要同时具备多个不相关类的特性(如 “工读生” 同时具备 “学生” 和 “工人” 的特性)。
- 实现 “接口组合”(在 C++ 中,接口通常是纯虚函数类,多继承多个接口可以组合不同的行为规范)。
总结
多继承的核心是让派生类同时拥有多个基类的功能,但也可能带来菱形继承(需用虚继承解决)和成员名冲突(需显式指定来源)等问题。实际开发中应谨慎使用多继承,优先考虑组合(将多个类作为成员对象)而非继承,以降低代码复杂度。
基类与派生类之间的转换
一般情况下,基类对象占据的空间小于派生类对象。
(空继承时,有可能相等)
1:可否把一个基类对象赋值给一个派生类对象?可否把一个派生类对象赋值给一个基类对象?

2:可否将一个基类指针指向一个派生类对象?可否将一个派生类指针指向一个基类对象?

3:可否将一个基类引用绑定一个派生类对象?可否将一个派生类引用绑定一个基类对象?
引用和指针同理
派生类对象间的复制控制(重点)
复制控制函数就是 拷贝构造函数、赋值运算符函数
原则:基类部分与派生类部分要单独处理
(1)当派生类中没有显式定义复制控制函数时,就会自动完成基类部分的复制控制操作;
(2)当派生类中有显式定义复制控制函数时,不会再自动完成基类部分的复制控制操作,需要显式地调用;
如果Derived类的数据成员申请了堆空间,那么必须手动写出Derived类的复制控制函数,此时就要考虑到基类的复制控制函数的显式调用。
(如果只是Base类的数据成员申请了堆空间,那么Base类的复制控制函数必须显式定义,Derived类自身的数据成员如果没有申请堆空间,不用显式定义复制控制函数)
#include <string.h>
#include <iostream>
using std::ostream;
using std::cout;
using std::endl;
//继承体系下的复制控制函数
//这部分代码也需要会写,和之前学习的赋值运算符拷贝构造函数
//同等的地位
class Base{
public:
Base(const char * p)
:_pbase(new char[strlen(p) + 1]())
{
cout << "Base()" << endl;
strcpy(_pbase, p);
}
~Base(){
cout << "~Base()" << endl;
if(_pbase){
delete [] _pbase;
_pbase = nullptr;
}
}
//拷贝构造函数
Base(const Base & rhs)
:_pbase(new char[strlen(rhs._pbase) + 1]())
{
cout << "Base(const Base &)" << endl;
strcpy(_pbase, rhs._pbase);
}
//赋值运算符函数
Base & operator=(const Base & rhs){
cout << "Base::operator=" << endl;
if(this != &rhs){
delete [] _pbase;
_pbase = new char[strlen(rhs._pbase) + 1]();
strcpy(_pbase, rhs._pbase);
}
return *this;
}
//private:
protected:
//采取深拷贝
char * _pbase;
};
//第二阶段:派生类中定义一个指针数据成员
class Derived : public Base{
public:
Derived(const char * base, const char * derived)
:Base(base)
,_pderived(new char[strlen(derived) + 1]())
{
strcpy(_pderived, derived);
cout << "Derived(const char *)" << endl;
}
~Derived(){
cout << "~Derived" << endl;
if(_pderived){
delete [] _pderived;
_pderived = nullptr;
}
}
//需要显示写出派生类的拷贝构造函数和赋值运算符函数
//为什么在派生类的拷贝构造函数中,会去调用基类的无参构造函数呢?
//在没有显示写出派生类的拷贝构造函数时,编译器会自动帮助
//我们调用基类的拷贝构造函数完成对应的操作
//但是一旦我们显示写出派生类的拷贝构造函数,但是却没有主动调用基类的
//拷贝构造函数时,那么只能够调用基类的无参构造函数来进行初始化
#if 0
Derived(const Derived & rhs)
:Base(rhs)//调用基类的拷贝构造函数,向上转型
,_pderived(new char[strlen(rhs._pderived) + 1]())
{
strcpy(_pderived, rhs._pderived);
cout << "Derived(const Derived &)" << endl;
}
//赋值运算符函数
//一定需要显式调用基类的赋值运算符函数
Derived & operator=(const Derived & rhs){
if(this != &rhs){
//调用基类的赋值运算符函数
Base::operator=(rhs);
delete [] _pderived;
_pderived = new char[strlen(rhs._pderived) + 1]();
strcpy(_pderived, rhs._pderived);
}
return *this;
}
#endif
friend ostream & operator<<(ostream & os, const Derived & rhs);
private:
char * _pderived;
};
//输出流运算符一般需要加上const
//1.不会在函数中被修改数据成员
//2.可以绑定右值,可以输出右值
//如果需要去访问函数,那么只能够访问const成员函数
ostream & operator<<(ostream & os, const Derived & rhs){
if(rhs._pbase){
os << rhs._pbase;
}
if(rhs._pderived){
os << "," << rhs._pderived;
}
return os;
}
void test(){
Derived d1("hello", "world");
cout << d1 << endl;
Derived d2 = d1;
cout << d2 << endl;
}
void test2(){
Derived d1("hello", "world");
Derived d2("c++", "python");
d1 = d2;
cout << d1 << endl;
}
int main()
{
test();
return 0;
}
如果派生类没有定义自己的拷贝构造函数,那么对应的内存图示如上;此时派生类部分会采取浅拷贝的方式,但是随后会自动调用基类的拷贝构造函数完成基类部分的处理

如果派生类拥有自己的指针数据成员,但是同时没有定义赋值运算符函数,那么便会发生上述现象,会存在内存泄漏以及double free的情况(问题是由于派生类部分的浅拷贝导致的,基类的部分是没有问题的)

更多推荐



所有评论(0)