C++ 继承
一、继承的基本概念
C++ 继承(Inheritance)是面向对象编程中的一个重要特性,它允许 一个类(称为派生类或子类)继承另一个类(称为基类或父类)的属性和行为(成员变量和成员函数),从而实现代码的复用和扩展性。继承的主要目的是实现代码重用,以及建立一种类型之间的层次关系。
1.1 继承的特点
- 代码重用:子类继承了父类的属性和方法,减少了代码的重复编写。
- 扩展性:子类可以扩展父类的功能,添加新的属性和方法,或者重写(覆盖)现有的方法。
- 多态性:通过继承和虚函数,C++支持多态,允许在运行时决定调用哪个函数。
1.2 继承的基本语法
class 父类名 {
public:
// 公有成员
protected:
// 受保护成员
private:
// 私有成员
};
class 子类名 : 继承方式 父类名 {
// 子类可以新增自己的成员
};
1.3 继承方式(访问控制)
继承方式 | 父类 public 成员在子类中 | 父类 protected 成员在子类中 | 父类 private 成员在子类中 |
---|---|---|---|
public | public | protected | 不可访问 |
protected | protected | protected | 不可访问 |
private | private | private | 不可访问 |
说明:
- public 继承:最常用,表示“是一种”关系,如学生是人(
Student : public Person
)。 - protected/private 继承:一般用于类的内部实现或限制对外接口访问。
1.4 继承简单示例
#include <iostream>
using namespace std;
/* 基类/父类 */
class Car{ // 车辆类,抽象的概念
public:
string type;
string name;
string color;
double price;
int numOfWhell;
void runCar(){
cout << "车跑起来了" << endl;
}
void stopCar();
};
/* 派生类/子类 */
class Sportscar : public Car{ // 跑车类,抽象的概念
public:
void openTopped(); // 开跑车成员方法
void pdrifting(); // 玩漂移成员方法
};
/* 派生类/子类 */
class Electricvehicle : public Car{ // 电动车类,抽象的概念
};
int main()
{
Sportscar ftype; // 定义捷豹ftype对象
ftype.type = "捷豹ftype";
cout << ftype.type << endl;
ftype.runCar();
Electricvehicle Yadi; // 定义雅迪对象
Yadi.type = "雅迪";
cout << Yadi.type << endl;
return 0;
}
二、继承分文件实现
想象我们这个程序中,我们有一个基类 Animal
,它定义了所有动物共有的特性和行为。然后,我们可以创建几个派生类,如 Lion
、 Cat
,这些类继承自 Animal
类,并添加或修改特定于它们自己的特性和行为。
基类:Animal
:
/* Animal.h */
#ifndef ANIMAL_H
#define ANIMAL_H
#include <iostream>
using namespace std;
class Animal
{
public:
string name; // 动物名字
string age; // 动物年龄
Animal();
void makeSound(); // 发出声音
void eatFood(); // 吃食物
};
#endif // ANIMAL_H
/* Animal.cpp */
#include "animal.h"
Animal::Animal()
{
}
void Animal::makeSound()
{
cout << "动物发出声音" << endl;
}
void Animal::eatFood()
{
cout << "动物吃食物" << endl;
}
派生类:Lion
/* Lion.h */
#ifndef LION_H
#define LION_H
#include "animal.h"
class Lion : public Animal
{
public:
int sleepingTime; // 狮子一天睡多久
Lion();
void hunting(); // 狮子捕猎
};
#endif // LION_H
/* Lion.cpp */
#include "lion.h"
Lion::Lion()
{
}
void Lion::hunting()
{
cout << "狮子捕杀羚羊" << endl;
}
派生类:Cat
/* Cat.h */
#ifndef CAT_H
#define CAT_H
#include "animal.h"
class Cat : public Animal
{
public:
Cat();
void eatFish(); // 猫吃鱼
void catchTheMouse(); // 猫抓老鼠
};
#endif // CAT_H
/* Cat.cpp */
#include "cat.h"
Cat::Cat()
{
}
void Cat::eatFish()
{
cout << "猫吃鱼" << endl;
}
void Cat::catchTheMouse()
{
cout << "猫抓老鼠" << endl;
}
使用这些类:
/* main.cpp */
#include <iostream>
#include "animal.h"
#include "lion.h"
#include "cat.h"
using namespace std;
int main()
{
Animal a; // 定义动物a对象
a.makeSound();
Lion sangbiao; // 定义狮子“丧彪”对象
sangbiao.makeSound();
sangbiao.hunting();
Cat fluffyCat; // 定义狸花猫对象
fluffyCat.catchTheMouse();
fluffyCat.eatFish();
fluffyCat.eatFood();
return 0;
}
在这个例子中:
Animal
是基类,定义了所有动物共有的属性(如name
和age
)和方法(如makeSound
和eatFood
)。Lion
、Cat
是派生类,它们继承了Animal
的特性,并创建了它们自己的行为方法。- 在
main
函数中,创建了各种动物的实例,并展示了它们的行为。
这个例子展示了继承如何使代码更有组织、更易于管理,并且如何通过重写基类方法来实现多态性。
三、权限对继承的影响
3.1 权限的两种来源
- 父类中成员的访问权限(
public
、protected
、private
) - 子类对父类的继承方式(
public
、protected
、private
继承)
这两者 共同决定了子类是否能访问父类的成员、以及通过子类对象访问时的权限表现。
3.2 父类成员访问权限说明
访问权限 | 类内部 | 同一个类的对象 | 派生类(子类) | 类外部 |
---|---|---|---|---|
public | ✔️ 可访问 | ✔️ 可访问 | ✔️ 可访问 | ✔️ 可访问 |
private | ✔️ 可访问 | ❌ 不可访问 | ❌ 不可访问 | ❌ 不可访问 |
protected | ✔️ 可访问 | ❌ 不可访问 | ✔️ 可访问 | ❌ 不可访问 |
3.3 权限对继承的影响
在C++中,访问控制符对继承的影响可以通过下表来清晰地展示。这个表格展示了不同类型的继承( public
、 protected
、 private
)如何影响基类的不同类型成员( public
、 protected
、private
)在派生类中的访问级别。
基类成员类型 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | 不可访问 | 不可访问 | 不可访问 |
解释:
public
继承:基类的public
成员在派生类中仍然是public
的,protected
成员仍然是protected
的。基类的private
成员在派生类中不可访问。protected
继承:基类的public
和protected
成员在派生类中都变成protected
的。基类的private
成员在派生类中不可访问。private
继承:基类的public
和protected
成员在派生类中都变成private
的。基类的private
成员在派生类中不可访问。
这个表格提供了一个快速参考,帮助理解在不同类型的继承中基类成员的访问级别是如何变化的。记住,无论继承类型如何,基类的 private
成员始终不可直接在派生类中访问。
3.4 权限对继承的影响示例
class Base {
public:
int pub = 1;
protected:
int pro = 2;
private:
int pri = 3;
};
class A : public Base {
// pub 仍是 public
// pro 仍是 protected
// pri 不可访问
};
class B : protected Base {
// pub → protected
// pro → protected
// pri 不可访问
};
class C : private Base {
// pub → private
// pro → private
// pri 不可访问
};
void test() {
A a;
cout << a.pub << endl; // ✅ OK
// cout << a.pro << endl; // ❌ 不可访问(protected)
B b;
// cout << b.pub << endl; // ❌ 现在是 protected
}
四、基类构造函数
在C++中,派生类可以通过其构造函数的初始化列表来调用基类的构造函数。这是在构造派生类对象时初始化基类部分的标准做法。
当创建派生类的对象时,基类的构造函数总是在派生类的构造函数之前被调用。如果没有明确指定,将调用基类的默认构造函数。如果基类没有默认构造函数,或者你需要调用一个特定的基类构造函数,就需要在派生类构造函数的初始化列表中明确指定。
4.1 基类构造函数示例一
假设我们有一个基类 Base
和一个派生自 Base
的类 Derived
:
#include <iostream>
using namespace std;
class Base {
public:
int data;
Base(int x) {
cout << "Base constructor with x = " << x << std::endl;
}
};
class Derived : public Base {
public:
double ydata;
Derived(int x, double y) : Base(x) { // 调用 Base 类的构造函数
std::cout << "Derived constructor with y = " << y << std::endl;
}
};
int main()
{
Derived obj(10, 3.14); // 首先调用 Base(10),然后调用 Derived 的构造函数
return 0;
}
在这个例子中:
Base
类有一个接受一个整数参数的构造函数。Derived
类继承自Base
,它的构造函数接受一个整数和一个双精度浮点数。在其初始化列表中,它调用Base
类的构造函数,并传递整数参数。- 当
Derived
类的对象被创建时,首先调用Base
类的构造函数,然后调用Derived
类的构造函数。
4.2 基类构造函数示例二
#include <iostream>
using namespace std;
/* 基类 汽车类 */
class Car{
public:
string state; // 国家
string brand; // 品牌
Car(string state, string brand){
this->state = state;
this->brand = brand;
cout << "基类构造函数被调用" << endl;
}
void runCar();
void stopCar();
};
/* 派生类 宝马类 */
class BMW : public Car{
public:
string seriesBMW; //系列
BMW(string state, string brand, string series) : Car(state, brand){
cout << "派生类构造函数被调用" << endl;
seriesBMW = series;
};
};
int main()
{
BMW BMW3("德国", "宝马", "三系");
cout << "汽车国家: " << BMW3.state << " 汽车品牌: " << BMW3.brand << " 汽车系列: " << BMW3.seriesBMW << endl;
return 0;
}
这段代码通过定义基类 Car
和派生类 BMW
,演示了 C++ 中派生类如何通过构造函数初始化列表显式调用基类构造函数。在创建 BMW
对象时,先调用 Car
的构造函数初始化国家和品牌属性,再执行 BMW
的构造函数设置系列信息,并输出相关提示信息,最终通过继承的方式实现了对国家、品牌和系列属性的完整封装与输出,体现了类的继承与构造顺序。
五、虚函数
在C++中, virtual
和 override
关键字用于支持多态,尤其是在涉及类继承和方法重写的情况下。正确地理解和使用这两个关键字对于编写可维护和易于理解的面向对象代码至关重要。
5.1 virtual关键字
-
使用场景:在基类中声明虚函数。
-
目的:允许派生类重写该函数,实现多态。
-
行为:当通过基类的指针或引用调用一个虚函数时,调用的是对象实际类型的函数版本。
-
示例:
class Base { public: virtual void func() { std::cout << "Function in Base" << std::endl; } };
5.2 override关键字
-
使用场景:在派生类中重写虚函数。
-
目的:明确指示函数意图重写基类的虚函数。
-
行为:确保派生类的函数确实重写了基类中的一个虚函数。如果没有匹配的虚函数,编译器会报错。
-
示例:
class Derived : public Base { public: void func() override { std::cout << "Function in Derived" << std::endl; } };
5.3 虚函数注意事项
- 只在派生类中使用 override:
override
应仅用于派生类中重写基类的虚函数。 - 虚析构函数:如果类中有析构函数,通常应该将析构函数也声明为虚的。
- 默认情况下,成员函数不是虚的:在C++中,成员函数默认不是虚函数。只有显式地使用
virtual
关键字才会成为虚函数。 - 继承中的虚函数:一旦在基类中声明为虚函数,该函数在所有派生类中自动成为虚函数,无论是否使用
virtual
关键字。
正确使用 virtual
和 override
关键字有助于清晰地表达程序员的意图,并利用编译器检查来避免常见的错误,如签名不匹配导致的非预期的函数重写。
六、多重继承
6.1 多重继承基本概念
在C++中,多重继承是一种允许一个类同时继承多个基类的特性。这意味着派生类可以继承多个基类的属性和方法。多重继承增加了语言的灵活性,但同时也引入了额外的复杂性,特别是当多个基类具有相同的成员时。
在多重继承中,派生类继承了所有基类的特性。这包括成员变量和成员函数。如果不同的基类有相同名称的成员,则必须明确指出所引用的是哪个基类的成员。
6.2 多重继承基本语法
class Base1 {
public:
void show1() {
cout << "Base1" << endl;
}
};
class Base2 {
public:
void show2() {
cout << "Base2" << endl;
}
};
// 派生类同时继承 Base1 和 Base2
class Derived : public Base1, public Base2 {
public:
void showDerived() {
cout << "Derived" << endl;
}
};
6.3 多重继承示例
假设有两个基类 ClassA
和 ClassB
,以及一个同时从这两个类继承的派生类 Derived
:
#include <iostream>
using namespace std;
/* 基类 ClassA */
class ClassA{
public:
void displayA(){
cout << "displayA" << endl;
}
void testFunc(){
cout << "testFunc ClassA" << endl;
}
};
/* 基类 ClassB */
class ClassB{
public:
void displayB(){
cout << "displayB" << endl;
}
void testFunc(){
cout << "testFunc ClassB" << endl;
}
};
/* 派生类 Derived */
class Derived : public ClassA, public ClassB{ // 派生类同时继承了 ClassA 和 ClassB
public:
void display() {
displayA(); // 调用ClassA里面的displayA成员函数
displayB(); // 调用ClassB里面的displayB成员函数
ClassA::testFunc();
}
};
int main()
{
Derived obj;
obj.displayA(); // 调用 ClassA 里面的 displayA 成员函数
obj.displayB(); // 调用 ClassB 里面的 displayB 成员函数
obj.display(); // 调用 Derived 里面的 display 成员函数
return 0;
}
在这个示例中, Derived
类同时继承了 ClassA
和 ClassB
。因此,它可以使用这两个类中定义的方法。
6.4 多重继承注意事项
- 菱形继承问题:如果两个基类继承自同一个更高层的基类,这可能导致派生类中存在两份基类的副本,称为菱形继承(或钻石继承)问题。这可以通过虚继承来解决。
- 复杂性:多重继承可能会使类的结构变得复杂,尤其是当继承层次较深或类中有多个基类时。
- 设计考虑:虽然多重继承提供了很大的灵活性,但过度使用可能导致代码难以理解和维护。在一些情况下,使用组合或接口(纯虚类)可能是更好的设计选择。
多重继承是C++的一个强大特性,但应谨慎使用。合理地应用多重继承可以使代码更加灵活和强大,但不当的使用可能导致设计上的问题和维护困难。
七、菱形继承和虚继承
7.1 虚继承基本概念
C++ 中的 虚继承(Virtual Inheritance) 是为了解决 多重继承中“菱形继承” 引发的问题——即同一个基类被间接继承多次,导致成员重复、访问冲突和资源浪费。
虚继承是C++中一种特殊的继承方式,主要用来解决多重继承中的菱形继承问题。在菱形继承结构中,一个类继承自两个具有共同基类的类时,会导致共同基类的成员在派生类中存在两份拷贝,这不仅会导致资源浪费,还可能引起数据不一致的问题。虚继承通过确保共同基类的单一实例存在于继承层次中,来解决这一问题。
7.2 什么是菱形继承问题?
class Base{
public:
int data;
};
class Derived1 : public Base{
public:
// 继承自Base
};
class Derived2 : public Base{
public:
// 继承自Base
};
class FinalDerived : public Derived1, public Derived2{
public:
// 继承自 Derived1 和 Derived2
};
在这个例子中, FinalDerived
类通过 Derived1
和 Derived2
间接地继承自 Base
类两次。因此,它包含了两份 Base
的成员拷贝。
此时 FinalDerived
中有两份 Base
的副本,访问 data
时编译器报错:
int main()
{
FinalDerived final;
final.data = 10; // ❌ 错误,二义性
return 0;
}
必须明确访问路径:
int main()
{
FinalDerived final;
final.Derived1::data = 10; // ✅
final.Derived2::data = 20; // ✅
return 0;
}
但这不是我们期望的,因为 data
是同一个基类的成员,不应存在两份。
7.3 使用虚继承解决菱形继承问题
要解决这个问题,应使用虚继承:
#include <iostream>
using namespace std;
class Base{
public:
int data;
};
class Derived1 : virtual public Base{
public:
// 虚继承 Base
};
class Derived2 : virtual public Base{
public:
// 虚继承 Base
};
class FinalDerived : public Derived1, public Derived2{
public:
// 继承自 Derived1 和 Derived2
};
int main()
{
FinalDerived final;
final.data = 10;
cout << "data = " << final.data << endl;
return 0;
}
通过将 Derived1
和 Derived2
对 Base
的继承声明为虚继承( virtual public Base
),FinalDerived
类中只会有一份 Base
类的成员。无论通过 Derived1
还是 Derived2
的路径,访问的都是同一个 Base
类的成员。
7.4 特点和注意事项
- 初始化虚基类:在使用虚继承时,虚基类(如上例中的
Base
类)只能由最派生的类(如FinalDerived
)初始化。 - 内存布局:虚继承可能会改变类的内存布局,通常会增加额外的开销,比如虚基类指针。
- 设计考虑:虚继承应谨慎使用,因为它增加了复杂性。在实际应用中,如果可以通过其他设计(如组合或接口)避免菱形继承,那通常是更好的选择。
虚继承是C++语言中处理复杂继承关系的一种重要机制,但它也带来了一定的复杂性和性能考虑。正确地使用虚继承可以帮助你建立清晰、有效的类层次结构。
八、C++继承总结
C++ 继承相关的学习内容整理成表格的形式:
学习内容 | 描述 |
---|---|
继承的基础 | 理解基类和派生类的概念,以及如何通过继承扩展类功能。了解不同继承类型(公有、私有、保护)及其影响。 |
构造函数和析构函数在继承中的行为 | 学习派生类如何调用基类的构造函数和析构函数,以及它们的调用顺序。 |
访问控制和继承 | 理解公有、私有和保护继承对成员访问权限的影响。掌握继承中的访问修饰符(public, protected, private)。 |
函数重写和多态 | 学习多态和如何通过虚函数实现它,了解如何重写基类方法,以及纯虚函数和抽象类的概念。 |
虚继承和解决菱形问题 | 理解菱形继承问题及其解决方式,学习如何使用虚继承。 |
C++11 新特性中的继承相关内容 | 理解和应用 override 和 final 关键字,了解移动语义在继承中的应用。 |
设计原则与最佳实践 | 学习正确使用继承的方法,区分何时使用继承,何时使用组合,以及面向对象设计原则的应用。 |
实际案例分析 | 通过分析和编写实际代码示例加深理解,研究设计模式中继承的应用。 |
这个表格概述了学习 C++ 继承的关键方面和内容,有助于系统地理解和应用继承的概念。