C++代码优化(二): 区分接口继承和实现继承
目录
1.引言
2.接口继承
3.实现继承
4.如何选择接口继承与实现继承
5.完整实例
6.总结
1.引言
在C++中,区分接口继承和实现继承是一种良好的编程实践,有助于提高代码的可维护性、可读性和可扩展性。接口继承通常指的是从基类继承纯虚函数(pure virtual functions),而实现继承则是从基类继承具体的实现。接口继承和实现继承之间的区别在于,它们分别用于不同的目的:前者用于定义行为,后者用于共享实现。
2.接口继承
接口继承通常用于定义一个抽象基类,其中只包含纯虚函数。这个基类不能被实例化,只能作为其他类的基类使用。
这种方式通常用于定义一组行为约定,保证所有派生类都实现相同的行为,但实现的细节可以各自不同。
特点:
-
基类只定义接口,不提供任何实现。
-
派生类必须实现基类中声明的所有纯虚函数。
-
主要目的是为了多态性,通过基类指针或引用调用派生类的方法。
// 纯虚类,用作接口
class Shape {
public:
virtual ~Shape() = default;
virtual void draw() const = 0; // 纯虚函数,派生类必须实现
};
class Circle : public Shape {
public:
void draw() const override {
// Circle 特有的实现
std::cout << "Drawing Circle" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
// Rectangle 特有的实现
std::cout << "Drawing Rectangle" << std::endl;
}
};
在这个例子中,Shape
类定义了一个纯虚函数 draw()
,用于描述绘制形状的行为,但没有提供实现。Circle
和 Rectangle
是具体实现类,负责实现接口中的 draw()
函数。客户端代码可以通过基类指针来调用具体类的实现,利用多态性实现灵活的代码设计。
优点:
-
派生类必须提供实现,保证了不同派生类实现同样的行为。
-
可以在接口层面定义抽象概念,解耦具体实现。
缺点:
-
如果没有具体实现,可能会产生重复代码,导致冗余实现。
3.实现继承
实现继承是指基类不仅定义接口,还提供某些功能的默认实现。派生类可以直接继承这些功能,或者根据需要选择覆盖(override)它们。
这种方式通常用于减少代码重复,提供通用的功能实现,派生类可以选择复用基类的实现,也可以根据具体情况进行覆盖。
特点:
-
基类既定义接口,也提供某些函数的实现。
-
派生类可以复用基类中的实现,也可以选择覆盖(override)基类的实现。
-
主要目的是代码复用,减少重复实现。
class Shape {
public:
virtual ~Shape() = default;
// 提供默认实现
virtual void draw() const {
std::cout << "Drawing a generic shape" << std::endl;
}
// 需要派生类实现的纯虚函数
virtual double area() const = 0;
};
class Circle : public Shape {
public:
void draw() const override {
// 调用基类实现,避免重复代码
Shape::draw();
std::cout << "Specifically drawing a circle" << std::endl;
}
double area() const override {
return 3.14 * radius * radius;
}
private:
double radius = 1.0;
};
在这个例子中,Shape
类不仅定义了接口,还提供了一个默认的 draw()
实现,派生类 Circle
可以调用基类的实现,同时扩展自己特有的行为。Circle
也可以选择覆盖 draw()
函数,改变绘制行为。
优点:
-
提供了代码复用机制,减少重复代码。
-
派生类可以在需要时覆盖基类中的实现,灵活性较高。
缺点:
-
基类的实现可能与派生类不完全匹配,派生类可能需要额外工作去适应。
-
如果不当使用,可能导致派生类对基类实现的过度依赖。
4.如何选择接口继承与实现继承
类设计时,接口继承与实现继承相互独立,代表着一定的设计意义,在二者之间进行选择时,我们需要考虑的因素:
1) 对于无法提供默认版本的函数接口选择函数接口继承,对于能够提供默认版本的函数接口,选择函数实现继承。
2) 如果你需要强制派生类实现某些行为,而基类不关心实现细节,使用接口继承;如果基类中有可以复用的代码实现,且派生类可能会依赖它,使用实现继承。
5.完整实例
#include <iostream>
#include <vector>
#include <memory>
// 接口继承
class IShape {
public:
virtual ~IShape() = default;
virtual void draw() const = 0;
virtual double area() const = 0;
};
// 实现继承
class Shape : public IShape {
public:
void draw() const override {
std::cout << "Drawing a generic shape" << std::endl;
}
// 具体形状必须提供自身的面积计算方式
virtual double area() const = 0;
};
class Circle : public Shape {
public:
Circle(double r) : radius(r) {}
// 覆盖并调用基类方法
void draw() const override {
Shape::draw(); // 调用基类的默认行为
std::cout << "Drawing a circle with radius " << radius << std::endl;
}
double area() const override {
return 3.14 * radius * radius;
}
private:
double radius;
};
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width(w), height(h) {}
void draw() const override {
Shape::draw();
std::cout << "Drawing a rectangle with width " << width << " and height " << height << std::endl;
}
double area() const override {
return width * height;
}
private:
double width, height;
};
int main() {
std::vector<std::shared_ptr<IShape>> shapes;
shapes.push_back(std::make_shared<Circle>(5.0));
shapes.push_back(std::make_shared<Rectangle>(3.0, 4.0));
for (const auto& shape : shapes) {
shape->draw();
std::cout << "Area: " << shape->area() << std::endl;
}
return 0;
}
6.总结
在面向对象编程中,接口继承和实现继承是两种不同的继承方式,它们的区别在于继承的成员的特性不同,分别对应了不同的编程需求。
接口继承是指派生类只继承了基类的接口(也就是纯虚函数),而没有继承基类的实现。这种方式使得派生类必须实现基类中的所有纯虚函数,从而使得派生类和基类的实现是分离的,实现了接口和实现的分离。这种继承方式常常用于实现抽象类和接口,强制要求派生类实现接口中的所有函数。
实现继承是指派生类继承了基类的接口和实现,包括数据成员和函数实现。这种方式使得派生类可以复用基类的代码,从而减少了代码的重复编写,同时也保证了派生类和基类的一致性。但是,这也意味着派生类和基类的实现是紧密耦合的,基类的修改可能会影响到派生类的行为。
因此,接口继承和实现继承各有其优缺点,需要根据具体的编程需求来选择合适的继承方式。如果需要实现接口或抽象类,或者需要避免实现的紧密耦合,那么应该选择接口继承;如果需要复用代码,并且基类的实现不会被修改,那么可以考虑使用实现继承。