继承(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;//8cout << sizeof(Derived) << endl;//16Derived * p = &d;int * pInt = (int *)p;cout << *pInt << endl;//10cout << *(pInt + 1) << endl;//20cout << *(pInt + 2) << endl;//30cout << *(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 Person {
protected:int _age;
public:Person(int age) : _age(age) {}
};// 基类1(继承自Person)
class Student : public Person {
public:Student(int age) : Person(age) {}
};// 基类2(继承自Person)
class Worker : public Person {
public:Worker(int age) : Person(age) {}
};// 派生类(同时继承Student和Worker)
class StudentWorker : public Student, public Worker {
public:// 需初始化两个基类,间接导致Person被初始化两次StudentWorker(int age1, int age2) : Student(age1), Worker(age2) {}void printAge() {// cout << _age << endl; // 错误:_age有两份,二义性cout << Student::_age << endl; // 需显式指定来自哪个基类cout << Worker::_age << endl;}
};
解决方法:使用虚继承(Virtual Inheritance)
在基类继承间接基类时,用 virtual
关键字声明,确保间接基类在派生类中只存在一份拷贝:
// 基类1:虚继承自Person
class Student : virtual public Person { ... };// 基类2:虚继承自Person
class Worker : virtual public Person { ... };
此时 StudentWorker
中 _age
只有一份,初始化时需直接初始化间接基类 Person
:
StudentWorker(int age) : Person(age), Student(age), Worker(age) {}
采用虚拟继承的方式处理菱形继承问题,实际上改变了派生类的内存布局。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 0Derived(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的情况(问题是由于派生类部分的浅拷贝导致的,基类的部分是没有问题的)