MST

星途 面试题库

面试题:C++私有继承与组合的区别及应用场景

阐述C++中私有继承和组合两种方式的主要区别。在实际项目开发中,分别在哪些场景下更适合使用私有继承和组合,各举一个具体的例子说明。
39.9万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

私有继承和组合的主要区别

  1. 继承关系
    • 私有继承:子类以私有方式继承父类,父类的公有和保护成员在子类中变为私有成员。这意味着子类虽然可以访问父类的成员,但从外部看,这种继承关系更像是一种“实现复用”,而非“是一种”的关系。例如,一个Dog类私有继承Animal类,Dog类拥有Animal类的实现细节,但从外部接口角度,Dog不被看作是Animal类型。
    • 组合:是一种“有一个”的关系。一个类包含另一个类的对象作为成员变量。组合不涉及继承体系中的关系,被包含的对象的接口对于包含它的类来说是完全封装的,包含类只能通过调用被包含对象的公开接口来操作它。
  2. 访问权限
    • 私有继承:父类成员在子类中的访问权限降低为私有,子类可以直接访问父类的公有和保护成员,但外部代码不能通过子类对象访问父类原有的公有成员(除非子类提供相应的接口)。
    • 组合:包含类对被包含对象的成员访问完全取决于被包含对象的访问权限设置。如果被包含对象的成员是私有的,包含类只能通过被包含对象提供的公有接口来访问。
  3. 内存布局
    • 私有继承:子类对象的内存布局包含父类对象的所有成员,就像父类对象嵌入到子类对象内部一样。这种布局方式可能导致子类对象的内存空间相对较大,尤其是当父类有很多成员时。
    • 组合:包含类对象的内存布局中,被包含对象作为一个独立的成员变量存在。如果被包含对象是指针类型,只占用指针大小的空间,实际对象在堆上分配;如果是值类型,则直接在包含类对象的内存空间中分配。

适合场景及例子

  1. 私有继承适合场景及例子
    • 适合场景:当需要复用某个类的实现细节,但又不想让外部将当前类看作是基类类型时,适合使用私有继承。比如在实现一些内部工具类,这些类复用其他类的代码但对外提供完全不同的接口。
    • 例子:假设我们有一个String类用于字符串操作,现在要实现一个FileName类用于处理文件名。文件名本质上也是字符串,但文件名有自己特定的操作,如获取文件扩展名等,并且不希望外部将FileName当作普通的String来使用。
class String {
public:
    String(const char* str) { /* 字符串初始化逻辑 */ }
    void append(const char* str) { /* 追加字符串逻辑 */ }
    const char* c_str() const { /* 返回字符串指针 */ }
};

class FileName : private String {
public:
    FileName(const char* name) : String(name) {}
    const char* getExtension() const {
        const char* dot = strrchr(c_str(), '.');
        return dot? dot + 1 : "";
    }
};

在这个例子中,FileName私有继承String,复用了String的字符串操作实现,但对外只提供获取文件扩展名等特定接口,而不是String类的所有接口。

  1. 组合适合场景及例子
    • 适合场景:当一个类需要使用另一个类的功能,但两者之间没有“是一种”的关系,只是“有一个”的关系时,适合使用组合。例如一个Car类包含一个Engine类对象,Car使用Engine提供的动力,但Car不是Engine
    • 例子
class Engine {
public:
    void start() { std::cout << "Engine started." << std::endl; }
    void stop() { std::cout << "Engine stopped." << std::endl; }
};

class Car {
private:
    Engine engine;
public:
    void startCar() {
        engine.start();
        std::cout << "Car is starting." << std::endl;
    }
    void stopCar() {
        engine.stop();
        std::cout << "Car is stopped." << std::endl;
    }
};

在这个例子中,Car类通过组合Engine类对象来实现启动和停止功能,CarEngine是“有一个”的关系,各自有独立的接口和职责。