十、继承
C++中的继承是一种面向对象编程(OOP)的特性,它允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的属性和方法。通过继承,派生类可以重用基类的代码,同时添加新的属性和方法或修改继承来的方法。这种机制促进了代码的复用、扩展和维护。
继承的基本概念
- 基类(Base Class):被继承的类,也称为父类或超类。它包含了一些基本的属性和方法,这些属性和方法可以被派生类继承。
- 派生类(Derived Class):继承自基类的类,也称为子类或继承类。派生类可以包含基类的所有成员(除非它们被声明为私有并且没有被友元关系访问),并且还可以添加新的成员或重写继承来的成员。
- 继承性(Inheritance):类之间的一种关系,其中一个类(派生类)继承另一个类(基类)的属性和方法。
使用继承的原因
在面向对象编程(OOP)中,继承是一种非常重要的特性,它提供了多种理由和优势来支持其使用。
代码复用:
继承允许我们重用基类中已经定义好的属性和方法,而不必在派生类中重新定义它们。这避免了代码的重复,使得代码更加简洁和易于维护。例如,如果我们有多个类表示不同类型的动物,它们可能都需要实现一些共同的行为(如吃、睡、移动等),那么我们可以将这些共同的行为定义在一个基类中,并让各个动物类继承这个基类。扩展性:
通过继承,我们可以在不修改基类代码的情况下,为派生类添加新的属性和方法。这种扩展性使得我们可以轻松地根据需求对类进行扩展,而不需要担心会破坏现有的代码或功能。多态性:
多态性是面向对象编程的一个重要概念,它允许我们以统一的接口来处理不同类型的对象。继承是实现多态性的一种手段。通过继承,我们可以定义基类的指针或引用来指向派生类的对象,并在运行时根据对象的实际类型调用相应的方法。这种能力使得我们的代码更加灵活和强大。表达类之间的“是一个”关系:
在现实世界中,很多事物之间都存在着“是一个”的关系。例如,猫是一个动物,汽车是一个交通工具等。通过继承,我们可以在代码中表达这种关系。基类表示更一般的概念(如动物、交通工具),而派生类则表示更具体的概念(如猫、汽车)。这种表达方式使得我们的代码更加符合现实世界的逻辑。框架和库的设计:
在设计和实现软件框架或库时,继承是不可或缺的一部分。通过定义一系列的基类和接口,我们可以为开发者提供一个可扩展的、可重用的代码基础。开发者可以通过继承这些基类和接口来创建自己的类,从而实现特定的功能或需求。简化设计和实现:
继承可以简化类的设计和实现过程。通过将共通的属性和方法抽象到基类中,我们可以将注意力集中在派生类特有的属性和方法上。这种分而治之的策略使得类的设计和实现变得更加清晰和简单。
然而,也需要注意的是,过度使用继承可能会导致类层次结构变得复杂和难以维护。因此,在使用继承时应该谨慎考虑,并确保它确实是解决问题的最佳方案。在某些情况下,组合(composition)可能是比继承更好的选择。
继承的用途
- 代码复用:通过继承,派生类可以重用基类的代码,避免重复编写相同的代码。
- 多态性:继承是实现多态性的基础。通过继承,可以定义基类的指针或引用来指向派生类的对象,并在运行时根据对象的实际类型调用相应的方法。
- 扩展性:派生类可以在继承基类的基础上添加新的属性和方法,从而扩展类的功能。
继承的注意事项
- 避免过度继承:过度使用继承会使类层次结构变得复杂,难以理解和维护。应该优先考虑组合(composition)而不是继承。
- 注意访问权限:在继承时,要注意基类成员的访问权限,确保派生类能够访问到需要的成员。
- 构造函数和析构函数:派生类的构造函数需要调用基类的构造函数来初始化继承来的成员。同样,如果基类有析构函数(特别是如果它管理了资源),派生类也应该有一个析构函数来确保资源的正确释放。
- 虚函数和纯虚函数:通过声明虚函数或纯虚函数,可以在基类中定义接口,让派生类来实现这些接口的具体行为。这是实现多态性的关键。
继承的基本使用
C++中的继承是一种面向对象编程的特性,它允许我们定义一个新的类(派生类或子类)来继承另一个类(基类或父类)的属性和方法。继承的基本使用包括定义基类、定义派生类、以及通过派生类的对象来访问基类的成员。以下是C++继承的基本用法示例:
定义基类
首先,我们需要定义一个基类,这个类包含了派生类将要继承的属性和方法。
class Base {
public:
void display() {
cout << "Displaying base class" << endl;
}
};
定义派生类
接着,我们定义一个派生类,这个类通过继承关键字(:
)来继承基类的成员。
class Derived : public Base {
public:
void show() {
cout << "Showing derived class" << endl;
}
};
注意,在上面的例子中,public
关键字指定了继承的访问级别。C++ 支持三种继承方式:public
、protected
和 private
。public
继承使得基类的 public
和 protected
成员在派生类中保持原有的访问级别。
使用派生类的对象
最后,我们可以通过创建派生类的对象并使用它来访问基类和派生类的成员。
#include <iostream>
using namespace std;
// 基类和派生类的定义(如上所示)
int main() {
Derived d;
// 访问派生类自己的方法
d.show();
// 访问继承自基类的方法
d.display();
return 0;
}
注意事项
- 当派生类对象访问继承的成员时,如果派生类中有与基类同名的成员(包括成员变量和成员函数),则派生类成员会隐藏基类的同名成员。这被称为“名称隐藏”或“名称覆盖”。
- 如果需要访问被隐藏的基类成员,可以使用作用域解析运算符(
::
)来明确指定访问的类。 - 构造函数和析构函数不能被继承,但派生类可以定义自己的构造函数和析构函数来执行必要的初始化或清理工作。在派生类构造函数中,可以通过成员初始化列表显式地调用基类的构造函数。
- 派生类可以覆盖(Override)基类的虚函数,以提供特定于派生类的实现。这是多态性的基础之一。
示例:构造函数和析构函数的调用
class Base {
public:
Base() { cout << "Base Constructor" << endl; }
~Base() { cout << "Base Destructor" << endl; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived Constructor" << endl; }
~Derived() { cout << "Derived Destructor" << endl; }
};
int main() {
Derived d;
// 输出顺序:Base Constructor, Derived Constructor
// 当d离开作用域时,析构函数按相反顺序调用
// 输出顺序:Derived Destructor, Base Destructor
return 0;
}
继承基本规则
继承类型:
public
继承:基类的public
和protected
成员在派生类中保持其访问级别。protected
继承:基类的public
和protected
成员在派生类中变为protected
成员。private
继承:基类的所有成员(public
、protected
和private
)在派生类中均为private
成员。注意,private
继承并不常用,因为它基本上隐藏了基类接口。
构造函数和析构函数:
- 构造函数和析构函数不能继承,但派生类可以定义自己的构造函数和析构函数来执行必要的初始化或清理工作。
- 派生类构造函数可以通过成员初始化列表显式地调用基类的构造函数。
- 派生类对象的析构函数在派生类析构之后、基类析构之前被调用。
静态成员:
- 基类的静态成员被所有的派生类共享,不论派生类对象的数量。
- 派生类不能定义与基类同名的静态成员(除非它们在不同的作用域内,例如通过嵌套类)。
方法重写(覆盖):
- 派生类可以重写基类中的虚函数(通过相同的函数签名和
virtual
关键字)。 - 如果基类中的函数不是虚函数,派生类中的同名函数将隐藏基类中的函数,而不是重写它。
- 派生类可以重写基类中的虚函数(通过相同的函数签名和
访问权限:
- 派生类不能增加基类成员的访问权限(例如,基类中的
protected
成员在派生类中不能变为public
)。 - 但派生类可以进一步限制基类成员的访问权限(例如,通过私有继承)。
- 派生类不能增加基类成员的访问权限(例如,基类中的
继承方式
在C++中,继承过来的权限主要取决于继承方式(public、protected、private)以及基类成员的原始访问权限(public、protected、private)
public继承
- 当子类从父类以public方式继承时:
- 父类的public成员在子类中保持为public成员,允许类以外的代码访问这些成员。
- 父类的protected成员在子类中保持为protected成员,只允许子类及其派生类的成员访问。
- 父类的private成员在子类中仍然不可访问,但它们在子类对象中是存在的(仅从内存布局角度)。
- 当子类从父类以public方式继承时:
protected继承
- 当子类从父类以protected方式继承时:
- 父类的public成员和protected成员在子类中均变为protected成员,只允许子类及其派生类的成员访问。
- 父类的private成员在子类中仍然不可访问。
- 当子类从父类以protected方式继承时:
private继承
- 当子类从父类以private方式继承时:
- 父类的public成员和protected成员在子类中均变为private成员,只允许子类自身的成员访问。
- 父类的private成员在子类中仍然不可访问。
- 当子类从父类以private方式继承时:
注意事项
- 基类中的private成员在派生类中无论以何种方式继承都是不可见的,但这并不意味着它们不被继承。从内存布局的角度看,派生类对象中确实包含了这些私有成员。
- 访问权限的变更仅影响继承后的成员的访问方式,不会改变基类中成员本身的访问权限。
- 使用
class
关键字定义类时,默认的继承方式是private;而使用struct
关键字时,默认的继承方式是public。然而,为了代码的清晰性和可维护性,建议显式指定继承方式。
示例
假设有以下基类Base
和派生类Derived
:
class Base {
public:
int publicMember;
protected:
int protectedMember;
private:
int privateMember;
};
class DerivedPublic : public Base {
public:
void accessMembers() {
publicMember = 10; // 可以访问
protectedMember = 20; // 可以访问
// privateMember = 30; // 编译错误,不可访问
}
};
class DerivedProtected : protected Base {
public:
void accessMembers() {
publicMember = 10; // 可以在类内部访问,但外部不可访问
protectedMember = 20; // 可以在类内部访问,但外部不可访问
// privateMember = 30; // 编译错误,不可访问
}
};
class DerivedPrivate : private Base {
public:
void accessMembers() {
publicMember = 10; // 只能在类内部访问
protectedMember = 20; // 只能在类内部访问
// privateMember = 30; // 编译错误,不可访问
}
};
在上述示例中,不同的继承方式导致了基类成员在派生类中具有不同的访问权限。
赋值兼容原则
在C++中,赋值兼容原则主要涉及指针和引用的赋值。基本规则是,派生类对象的指针或引用可以安全地赋值给基类类型的指针或引用,但反之则不然。这是因为基类指针或引用只能访问基类定义的接口,而派生类可能添加了额外的成员。
- 基类指针/引用指向派生类对象:这是多态性的基础。通过基类指针或引用,我们可以调用派生类重写的虚函数,实现运行时多态。
- 派生类指针/引用不能隐式转换为基类指针/引用:这是因为派生类可能添加了额外的成员,而基类指针或引用无法访问这些成员。如果确实需要将派生类指针或引用赋值给基类类型的变量,通常需要进行显式类型转换(如静态转换
static_cast
或动态转换dynamic_cast
)。
总之,C++的继承机制提供了一种强大的方式来复用代码、表达类之间的层次关系,并实现多态性。然而,它也伴随着一些复杂的规则和限制,需要开发者仔细理解和遵守。
继承中的同名成员
在C++中,当子类(派生类)和父类(基类)中存在同名成员时,这些成员实际上是在不同的作用域中定义的。这种情况主要涉及到成员函数(方法)和成员变量(属性)。处理这些同名成员时,需要特别注意作用域解析运算符(::
)的使用以及成员函数重写(Overriding)和隐藏(Hiding)的概念。
成员变量
对于成员变量,如果子类定义了一个与父类同名的成员变量,那么这两个变量实际上是在不同的作用域中。子类中的同名成员变量会隐藏父类中的同名成员变量。此时,如果子类中的成员函数想要访问父类中被隐藏的成员变量,需要使用作用域解析运算符(::
)来明确指定要访问的变量属于哪个类。
class Base {
public:
int x;
};
class Derived : public Base {
public:
int x; // 隐藏了Base类中的x
void show() {
std::cout << "Derived::x = " << x << std::endl; // 访问Derived中的x
std::cout << "Base::x = " << Base::x << std::endl; // 访问Base中的x
}
};
成员函数
对于成员函数,情况稍微复杂一些。成员函数存在重写(Overriding)和隐藏(Hiding)两种可能性。
重写(Overriding):当子类定义了一个与父类中具有相同签名(函数名、参数列表、返回类型、const属性、volatile属性、引用属性等)的虚函数时,子类中的这个函数会重写(Override)父类中的虚函数。此时,通过基类指针或引用来调用该函数时,会调用到子类中的版本。
隐藏(Hiding):如果子类中的函数与父类中的函数同名,但参数列表不同(或者函数不是虚函数),那么子类中的函数会隐藏父类中的同名函数。这种情况下,通过基类指针或引用来调用该函数时,不会调用到子类中的版本,除非使用子类类型的指针或引用来调用。
class Base {
public:
virtual void func() { std::cout << "Base::func()" << std::endl; }
void anotherFunc(int) { std::cout << "Base::anotherFunc(int)" << std::endl; }
};
class Derived : public Base {
public:
void func() override { std::cout << "Derived::func()" << std::endl; } // 重写
void anotherFunc(double) { std::cout << "Derived::anotherFunc(double)" << std::endl; } // 隐藏Base::anotherFunc(int)
};
// 使用
Base* basePtr = new Derived();
basePtr->func(); // 输出Derived::func(),因为func()被重写了
basePtr->anotherFunc(1); // 输出Base::anotherFunc(int),因为Derived::anotherFunc(double)隐藏了Base::anotherFunc(int)
Derived* derivedPtr = new Derived();
derivedPtr->anotherFunc(1.0); // 输出Derived::anotherFunc(double),直接调用Derived的方法
总结
在C++中处理父子类中的同名成员时,需要注意成员变量会被隐藏,而成员函数则可能涉及重写或隐藏。了解这些概念对于编写清晰、可维护的C++代码非常重要。
继承中的静态成员,构造函数与析构函数
在C++中,父子类(派生类与基类)之间的静态成员、构造函数和析构函数的行为有其特定的规则和特性。
静态成员
静态成员(包括静态变量和静态成员函数)属于类本身,而不是类的某个对象。因此,无论创建了多少个类的对象,静态成员都只有一份拷贝。当静态成员被定义在基类中时,这些成员也会被子类继承,但它们仍然是属于基类的,而不是子类的一个独立拷贝。
静态变量:所有派生类的对象共享基类中定义的静态变量。如果派生类定义了同名的静态变量,则它会隐藏基类中的同名静态变量,而不是覆盖它。
静态成员函数:可以通过基类或派生类的对象(以及类名本身,如果它们是可访问的)来访问基类的静态成员函数。如果派生类定义了同名的静态成员函数,那么通过派生类的对象或类名调用该函数时,将调用派生类中的函数,这类似于非静态成员函数的隐藏行为。
构造函数
构造函数是特殊的成员函数,用于在对象创建时初始化对象。
基类的构造函数:在创建派生类对象时,首先会调用基类的构造函数(如果有的话)。如果基类有多个构造函数,派生类构造函数可以通过初始化列表来指定使用哪一个。
派生类的构造函数:派生类的构造函数可以初始化派生类特有的成员变量,并且可以通过初始化列表来调用基类的构造函数。
析构函数
析构函数也是特殊的成员函数,用于在对象销毁前进行清理工作。
- 析构函数的调用顺序:与构造函数的调用顺序相反,当派生类对象被销毁时,首先会调用派生类的析构函数,然后是基类的析构函数。这确保了任何由派生类构造函数分配的资源都能被正确地释放,然后才是基类资源的释放。
示例
#include <iostream>
class Base {
public:
static int staticVar;
Base() { std::cout << "Base Constructor\n"; }
~Base() { std::cout << "Base Destructor\n"; }
static void staticFunc() { std::cout << "Base Static Function\n"; }
};
int Base::staticVar = 0;
class Derived : public Base {
public:
// 注意:这里没有定义staticVar或staticFunc,所以它们从Base继承
Derived() { std::cout << "Derived Constructor\n"; }
~Derived() { std::cout << "Derived Destructor\n"; }
// 可以在这里定义同名的静态成员,但这将隐藏Base中的同名成员
};
int main() {
Derived d;
// 调用静态函数,将输出Base Static Function
Base::staticFunc();
// 访问静态变量,修改后通过Base和Derived访问都会看到修改结果
std::cout << "Before: " << Base::staticVar << std::endl;
Base::staticVar = 5;
std::cout << "After: " << Derived::staticVar << std::endl; // 注意:这里通过Derived访问也是合法的
return 0; // 析构顺序:Derived Destructor, Base Destructor
}
在这个示例中,你可以看到静态成员(staticVar
和staticFunc
)是如何被基类和派生类共享的,以及构造函数和析构函数的调用顺序。注意,由于Derived
类没有定义staticVar
或staticFunc
,所以它们是从Base
类继承的。如果Derived
类定义了同名的静态成员,那么这些成员将隐藏基类中的同名成员。