引言
继承是面向对象编程三大基本特性(封装、继承、多态)之一,它允许我们基于已有的类创建新类,实现代码的重用和层次化设计。就像人类社会中的"基因传承",子女会继承父母的特征,同时发展出自己独特的特点。这篇博客将结合实际生活例子,深入浅出地讲解C++继承的核心概念和使用方法。
一、继承的基本概念
继承建立了类之间的父子关系(基类-派生类关系)。派生类自动获得基类的所有成员(成员变量和成员函数),同时可以添加新的成员或者重新定义基类的成员。
生活中的继承例子
想象一下汽车的设计过程:首先有一个基本的"交通工具"类,它具有基本的属性如"速度"、"重量"、"载客量"等,以及基本的方法如"启动"、"停止"、"转向"等。然后我们可以派生出"汽车"类,它继承了"交通工具"的所有特性,同时添加了特有的属性如"燃油类型"、"排量"等,以及特有的方法如"换挡"、"加油"等。
// 基类:交通工具
class Vehicle {
protected:
double speed;
double weight;
int passengerCapacity;
public:
Vehicle(double s, double w, int pc) : speed(s), weight(w), passengerCapacity(pc) {}
void start() { std::cout << "交通工具启动" << std::endl; }
void stop() { std::cout << "交通工具停止" << std::endl; }
void turn(const std::string& direction) {
std::cout << "交通工具向" << direction << "转向" << std::endl;
}
};
// 派生类:汽车
class Car : public Vehicle {
private:
std::string fuelType;
double engineVolume;
public:
Car(double s, double w, int pc, const std::string& ft, double ev)
: Vehicle(s, w, pc), fuelType(ft), engineVolume(ev) {}
void changeGear(int gear) {
std::cout << "汽车换到" << gear << "档" << std::endl;
}
void refuel() {
std::cout << "给汽车加" << fuelType << "油" << std::endl;
}
};
二、继承的类型
C++支持三种继承方式:公有继承(public)、保护继承(protected)和私有继承(private)。
公有继承(public inheritance)
公有继承是最常用的继承方式,它保持基类成员的访问权限不变:基类的public成员在派生类中仍为public,protected成员仍为protected。这种继承方式表达了"是一个"(is-a)的关系。
例如:一只猫"是一个"动物,一辆轿车"是一个"汽车。
class Animal {
public:
void eat() { std::cout << "动物在进食" << std::endl; }
void sleep() { std::cout << "动物在睡觉" << std::endl; }
};
class Cat : public Animal {
public:
void meow() { std::cout << "猫咪喵喵叫" << std::endl; }
};
// 使用示例
Cat fluffy;
fluffy.eat(); // 继承自基类
fluffy.sleep(); // 继承自基类
fluffy.meow(); // 派生类自己的方法
保护继承(protected inheritance)
保护继承将基类的public成员变为派生类的protected成员,基类的protected成员在派生类中仍为protected。这种继承方式不常用,表达了一种内部实现关系。
私有继承(private inheritance)
私有继承将基类的所有成员(public和protected)在派生类中都变为private。这种继承表达了"使用一个"(使用其实现)的关系,而非"是一个"的关系。
例如:一个引擎是汽车的组成部分,但我们通常不会说一个引擎"是一个"汽车。
class Engine {
public:
void start() { std::cout << "引擎启动" << std::endl; }
void stop() { std::cout << "引擎停止" << std::endl; }
};
class Car : private Engine {
public:
void drive() {
start(); // 可以访问基类的方法
std::cout << "汽车行驶中" << std::endl;
}
void park() {
std::cout << "汽车停车" << std::endl;
stop(); // 可以访问基类的方法
}
};
// 使用示例
Car myCar;
myCar.drive();
myCar.park();
// myCar.start(); // 错误!基类的方法变成了私有的,外部不能访问
三、继承中的构造和析构
在继承关系中,当创建派生类对象时,会先调用基类的构造函数,再调用派生类的构造函数;析构时则相反,先调用派生类的析构函数,再调用基类的析构函数。
构造函数的调用顺序
想象建造一栋房子,必须先建好地基(基类),然后才能建墙和屋顶(派生类)。
class Base {
public:
Base() { std::cout << "基类构造函数" << std::endl; }
~Base() { std::cout << "基类析构函数" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "派生类构造函数" << std::endl; }
~Derived() { std::cout << "派生类析构函数" << std::endl; }
};
// 使用示例
Derived obj;
// 输出:
// 基类构造函数
// 派生类构造函数
// 派生类析构函数
// 基类析构函数
初始化列表中调用基类构造函数
派生类可以在其初始化列表中显式调用基类的构造函数,传递必要的参数。
class Person {
protected:
std::string name;
int age;
public:
Person(const std::string& n, int a) : name(n), age(a) {
std::cout << "创建了一个人:" << name << ",年龄:" << age << std::endl;
}
};
class Student : public Person {
private:
std::string school;
int grade;
public:
Student(const std::string& n, int a, const std::string& s, int g)
: Person(n, a), school(s), grade(g) {
std::cout << name << "是" << school << "的学生," << grade << "年级" << std::endl;
}
};
// 使用示例
Student s("张三", 15, "第一中学", 9);
四、虚函数与多态
继承最强大的特性之一是支持多态,允许我们通过基类指针或引用调用派生类的方法。这通过虚函数来实现。
虚函数基础
虚函数使用virtual关键字声明,告诉编译器该函数可能会被派生类重写(override)。
就像不同的动物都会发出声音,但具体的声音不同。
class Animal {
public:
virtual void makeSound() {
std::cout << "动物发出声音" << std::endl;
}
};
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "汪汪汪!" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "喵喵喵!" << std::endl;
}
};
// 多态示例
void letAnimalSpeak(Animal& animal) {
animal.makeSound(); // 调用实际对象的方法
}
Dog dog;
Cat cat;
letAnimalSpeak(dog); // 输出:汪汪汪!
letAnimalSpeak(cat); // 输出:喵喵喵!
纯虚函数与抽象类
纯虚函数是没有实现的虚函数,使用= 0声明。包含纯虚函数的类称为抽象类,不能直接实例化。
比如我们可以谈论"交通工具"这个概念,但不能制造一个"纯粹的交通工具",我们只能制造具体的交通工具,如汽车、自行车等。
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
virtual double perimeter() const = 0; // 纯虚函数
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
double perimeter() const override {
return 2 * 3.14159 * radius;
}
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
double perimeter() const override {
return 2 * (width + height);
}
};
// 使用示例
// Shape shape; // 错误!抽象类不能实例化
Circle circle(5.0);
Rectangle rectangle(4.0, 6.0);
std::cout << "圆的面积:" << circle.area() << std::endl;
std::cout << "矩形的面积:" << rectangle.area() << std::endl;
五、虚析构函数的重要性
当通过基类指针删除派生类对象时,如果基类的析构函数不是虚函数,则只会调用基类的析构函数,而不会调用派生类的析构函数,导致资源泄漏。
就好像拆除一栋多层建筑时,如果只拆除基础(基类)而忽略了上层结构(派生类),会留下悬空的危险结构。
class Base {
public:
Base() { std::cout << "基类构造" << std::endl; }
// 错误示范:非虚析构函数
~Base() { std::cout << "基类析构" << std::endl; }
// 正确做法:虚析构函数
// virtual ~Base() { std::cout << "基类析构" << std::endl; }
};
class Derived : public Base {
private:
int* data;
public:
Derived() {
std::cout << "派生类构造" << std::endl;
data = new int[100]; // 分配资源
}
~Derived() {
std::cout << "派生类析构" << std::endl;
delete[] data; // 释放资源
}
};
// 问题示例
Base* ptr = new Derived();
delete ptr; // 如果Base的析构函数不是虚函数,这里会导致内存泄漏
六、多重继承
C++支持多重继承,一个类可以同时继承多个基类。尽管强大,但多重继承也容易引起一些问题,如菱形继承问题。
多重继承基础
比如一个人既可以是学生,又可以是员工(兼职学生)。
class Student {
protected:
std::string school;
int studentId;
public:
Student(const std::string& s, int id) : school(s), studentId(id) {}
void study() { std::cout << "学习中..." << std::endl; }
};
class Employee {
protected:
std::string company;
int employeeId;
public:
Employee(const std::string& c, int id) : company(c), employeeId(id) {}
void work() { std::cout << "工作中..." << std::endl; }
};
class PartTimeStudent : public Student, public Employee {
public:
PartTimeStudent(const std::string& s, int sId, const std::string& c, int eId)
: Student(s, sId), Employee(c, eId) {}
void showInfo() {
std::cout << "我是" << school << "的学生,学号" << studentId << std::endl;
std::cout << "同时也是" << company << "的员工,工号" << employeeId << std::endl;
}
};
// 使用示例
PartTimeStudent pts("北京大学", 12345, "腾讯", 67890);
pts.study(); // 来自Student类
pts.work(); // 来自Employee类
pts.showInfo();
菱形继承与虚继承
菱形继承是指一个派生类通过多条继承路径继承了同一个基类,导致基类成员在派生类中出现多次。
比如一个人继承了父亲和母亲的基因,而父亲和母亲又都继承了祖父母的基因,这会导致这个人从两条路径继承了相同的基因。
class Animal {
protected:
std::string name;
public:
Animal(const std::string& n) : name(n) {}
void eat() { std::cout << name << "正在吃东西" << std::endl; }
};
// 不使用虚继承
class Mammal : public Animal {
public:
Mammal(const std::string& n) : Animal(n) {}
void giveMilk() { std::cout << name << "哺乳中" << std::endl; }
};
class Bird : public Animal {
public:
Bird(const std::string& n) : Animal(n) {}
void fly() { std::cout << name << "飞行中" << std::endl; }
};
// 使用虚继承可以解决菱形继承问题
// class Mammal : virtual public Animal { ... };
// class Bird : virtual public Animal { ... };
class Bat : public Mammal, public Bird {
public:
// 不使用虚继承时,需要显式指定调用哪个基类的成员
Bat(const std::string& n) : Mammal(n), Bird(n) {}
// 调用哪个eat方法?这里产生了二义性
// void doSomething() { eat(); } // 错误:二义性
// 需要显式指定
void doSomething() {
Mammal::eat(); // 或者 Bird::eat();
}
};
虚继承(使用virtual关键字)可以解决菱形继承问题,确保共同基类在派生类中只有一个实例。
七、总结与最佳实践
继承的优点
- 代码重用:避免重复编写相同的代码
- 建立类层次结构:反映现实世界中的关系
- 支持多态:提高代码的灵活性和扩展性
使用继承的注意事项
- 遵循"是一个"原则:公有继承应表达"是一个"的关系
- 基类析构函数应为虚函数:防止资源泄漏
- 慎用多重继承:可能引起复杂性和二义性问题
- 考虑组合代替继承:如果关系更像"有一个"而非"是一个"
实际应用场景
继承在许多实际应用中都很有用,例如:
- 图形用户界面(GUI)框架:按钮、文本框等都继承自通用组件类
- 游戏开发:不同类型的游戏角色继承自基本角色类
- 数据库访问层:不同数据库连接类继承自通用数据库接口
// GUI框架示例
class Widget {
protected:
int x, y;
int width, height;
public:
Widget(int x, int y, int w, int h)
: x(x), y(y), width(w), height(h) {}
virtual void draw() = 0;
virtual void handleEvent(const Event& e) = 0;
};
class Button : public Widget {
private:
std::string label;
std::function<void()> clickHandler;
public:
Button(int x, int y, int w, int h, const std::string& l, std::function<void()> handler)
: Widget(x, y, w, h), label(l), clickHandler(handler) {}
void draw() override {
std::cout << "绘制按钮:" << label << std::endl;
}
void handleEvent(const Event& e) override {
if (e.type == EventType::CLICK && isInside(e.x, e.y)) {
clickHandler();
}
}
bool isInside(int mouseX, int mouseY) {
return mouseX >= x && mouseX <= x + width &&
mouseY >= y && mouseY <= y + height;
}
};
结语
继承是面向对象编程的核心特性之一,它通过建立类之间的层次关系,促进了代码重用和系统扩展。理解继承的原理和正确使用方法,对于设计高效、可维护的C++程序至关重要。通过将继承与实际生活场景联系起来,我们可以更加直观地理解和应用这一强大的编程概念。