好的,我们来系统地学习C++中继承的核心概念,特别是关于默认成员函数的行为。
一、 继承的基本概念
什么是继承?
- 继承是面向对象编程(OOP)的重要特性之一。
- 它允许我们定义一个新的类(称为派生类或子类),基于一个已经存在的类(称为基类或父类)。
- 派生类会“继承”基类的成员(数据成员和成员函数),并可以添加自己的新成员,或者重新定义(覆盖)继承来的成员函数。
为什么使用继承?
- 代码复用:避免重复编写基类中已有的代码。
- 层次化建模:反映现实世界中事物之间的“是一种(is-a)”关系(例如:
Student是一种Person)。 - 实现多态(通过虚函数):允许通过基类指针或引用来操作派生类对象。
继承类型
- 公有继承:
class Derived : public Base { ... };- 基类的
public成员在派生类中保持public。 - 基类的
protected成员在派生类中保持protected。 - 基类的
private成员在派生类中不可直接访问(需要通过基类的public/protected成员函数间接访问)。 - 体现了“是一种”关系。
- 基类的
- 保护继承:
class Derived : protected Base { ... };- 基类的
public和protected成员在派生类中都变为protected。 - 基类的
private成员不可直接访问。
- 基类的
- 私有继承:
class Derived : private Base { ... };- 基类的
public和protected成员在派生类中都变为private。 - 基类的
private成员不可直接访问。 - 体现了“由...实现”的关系,较少使用。
- 基类的
- 公有继承:
二、 访问控制与继承
- 基类成员的访问权限(
public,protected,private) 决定了它们在基类内部的可见性。 - 继承方式(
public,protected,private) 决定了基类成员在派生类内部的最低访问级别。- 基类的
public成员在派生类中的访问权限 <= 继承方式指定的权限。 - 基类的
protected成员在派生类中的访问权限 <= 继承方式指定的权限。 - 基类的
private成员永远不能在派生类内部直接访问(无论何种继承方式)。
- 基类的
- 派生类对象对基类成员的访问:
- 只能访问派生类内部可见的基类成员(即经过继承方式调整后,在派生类中是
public的成员)。 - 派生类对象不能直接访问基类的
protected或private成员(即使它们在派生类内部是可见的protected)。
- 只能访问派生类内部可见的基类成员(即经过继承方式调整后,在派生类中是
三、 继承中的默认成员函数
当派生类被创建、销毁、赋值时,其默认的构造函数、析构函数、拷贝构造函数和赋值运算符的行为与基类紧密相关。
构造函数
- 派生类对象的构造先构造其直接基类部分(如果有多个直接基类,按照声明顺序构造)。
- 然后按照声明顺序构造派生类自己的数据成员。
- 规则:
- 派生类的构造函数默认会调用基类的默认构造函数(无参构造函数)。
- 如果基类没有默认构造函数,或者你想调用基类的特定构造函数(带参数的),必须在派生类构造函数的初始化列表中显式调用基类构造函数。
class Base { public: Base(int x) { /* ... */ } // 基类没有默认构造函数 }; class Derived : public Base { public: // 错误!默认会调用 Base(),但 Base 没有默认构造函数 // Derived() { ... } // 正确:在初始化列表中显式调用 Base(int) Derived(int y) : Base(y * 2) { ... } };析构函数
- 派生类对象的析构顺序与构造顺序相反:
- 首先执行派生类自己的析构函数体。
- 然后按照声明相反的顺序销毁派生类自己的数据成员。
- 最后按照继承树从底向上的顺序(即派生类析构后)调用其直接基类的析构函数。
- 规则:
- 基类的析构函数会被自动调用。
- 如果基类的析构函数是虚函数,那么通过基类指针删除派生类对象时,会正确调用派生类的析构函数(这是良好实践!通常将基类析构函数声明为
virtual)。 - 如果基类析构函数不是虚函数,通过基类指针删除派生类对象会导致未定义行为(通常只调用了基类的析构函数,派生类部分未被正确销毁)。
- 派生类对象的析构顺序与构造顺序相反:
拷贝构造函数
- 当用一个派生类对象初始化另一个派生类对象时(如
Derived d2 = d1;)。 - 默认行为:派生类的默认拷贝构造函数会:
- 调用基类的拷贝构造函数来初始化派生类对象中的基类子对象。
- 然后对派生类自己的成员进行值拷贝(如果成员是类类型,调用其拷贝构造函数)。
- 自定义:如果你为派生类定义了拷贝构造函数,必须在初始化列表中显式调用基类的拷贝构造函数(或其它合适的基类构造函数),否则基类部分会调用默认构造函数。
class Derived : public Base { public: // 默认拷贝构造函数大致等价于: // Derived(const Derived& other) : Base(other) { /* 拷贝成员 */ } // 自定义拷贝构造函数 Derived(const Derived& other) : Base(other) // 显式调用基类拷贝构造,传递派生类引用给基类引用 , /* 初始化派生类成员 */ { // ... } };- 当用一个派生类对象初始化另一个派生类对象时(如
拷贝赋值运算符
- 当将一个派生类对象赋值给另一个派生类对象时(如
d2 = d1;)。 - 默认行为:派生类的默认拷贝赋值运算符会:
- 调用基类的拷贝赋值运算符来赋值派生类对象中的基类子对象。
- 然后对派生类自己的成员进行赋值(如果成员是类类型,调用其拷贝赋值运算符)。
- 自定义:如果你为派生类定义了拷贝赋值运算符,必须显式调用基类的拷贝赋值运算符(通常通过作用域解析运算符
::)。
class Derived : public Base { public: // 默认拷贝赋值运算符大致等价于: // Derived& operator=(const Derived& other) { // Base::operator=(other); // 调用基类的赋值 // /* 赋值派生类成员 */ // return *this; // } // 自定义拷贝赋值运算符 Derived& operator=(const Derived& other) { if (this != &other) { Base::operator=(other); // 显式调用基类赋值 // ... 赋值派生类自己的成员,处理自赋值 ... } return *this; } };- 当将一个派生类对象赋值给另一个派生类对象时(如
四、 总结:类复用的核心逻辑
- 继承是复用基类代码和接口的主要机制。
- 理解访问控制和继承方式是正确使用继承的基础。
- 默认成员函数在继承体系中的行为规则确保了对象构造、析构、拷贝和赋值的正确性:
- 构造:先基类,后派生类成员。
- 析构:先派生类成员和析构函数体,后基类(与构造相反)。
- 拷贝构造/赋值:需要显式或隐式处理基类子对象的拷贝/赋值。
- 为基类声明虚析构函数是安全使用多态(通过基类指针操作派生类)的关键。
通过掌握这些基础概念和默认成员函数的行为,你就能更好地利用C++的继承机制来实现代码的复用和扩展。