MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++类不同继承方式的影响

2023-06-012.9k 阅读

C++类不同继承方式的基础概念

在C++ 中,类的继承允许我们依据已有的类创建新的类。继承的方式决定了基类成员在派生类中的访问属性。C++ 提供了三种继承方式:public(公有继承)、private(私有继承)和 protected(保护继承)。

公有继承(public inheritance)

当使用公有继承时,基类的公有成员在派生类中仍然是公有成员,基类的保护成员在派生类中仍然是保护成员,而基类的私有成员在派生类中不可访问。

下面是一个简单的代码示例:

#include <iostream>

// 基类
class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
public:
    Base() : publicData(10), protectedData(20), privateData(30) {}
    void printBase() {
        std::cout << "Public Data: " << publicData << std::endl;
        std::cout << "Protected Data: " << protectedData << std::endl;
        std::cout << "Private Data: " << privateData << std::endl;
    }
};

// 公有继承派生类
class PublicDerived : public Base {
public:
    void accessBaseMembers() {
        std::cout << "Accessing Public Data from Derived: " << publicData << std::endl;
        std::cout << "Accessing Protected Data from Derived: " << protectedData << std::endl;
        // 下面这行代码会报错,因为privateData在派生类中不可访问
        // std::cout << "Accessing Private Data from Derived: " << privateData << std::endl;
    }
};

int main() {
    PublicDerived pd;
    pd.accessBaseMembers();
    pd.publicData = 100;
    // 下面这行代码会报错,因为protectedData在外部不可访问
    // pd.protectedData = 200;
    pd.printBase();
    return 0;
}

在上述代码中,PublicDerived 类以公有继承的方式从 Base 类派生。PublicDerived 类的成员函数可以访问 Base 类的公有和保护成员,但不能访问私有成员。在 main 函数中,通过 PublicDerived 的对象可以访问 Base 类的公有成员 publicDataprintBase 函数,但不能直接访问 protectedData

私有继承(private inheritance)

当使用私有继承时,基类的公有成员和保护成员在派生类中都变成私有成员,而基类的私有成员在派生类中仍然不可访问。

以下是私有继承的代码示例:

#include <iostream>

// 基类
class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
public:
    Base() : publicData(10), protectedData(20), privateData(30) {}
    void printBase() {
        std::cout << "Public Data: " << publicData << std::endl;
        std::cout << "Protected Data: " << protectedData << std::endl;
        std::cout << "Private Data: " << privateData << std::endl;
    }
};

// 私有继承派生类
class PrivateDerived : private Base {
public:
    void accessBaseMembers() {
        std::cout << "Accessing Public Data from Derived: " << publicData << std::endl;
        std::cout << "Accessing Protected Data from Derived: " << protectedData << std::endl;
        // 下面这行代码会报错,因为privateData在派生类中不可访问
        // std::cout << "Accessing Private Data from Derived: " << privateData << std::endl;
    }
};

int main() {
    PrivateDerived pd;
    pd.accessBaseMembers();
    // 下面这两行代码都会报错,因为publicData和protectedData在外部不可访问
    // pd.publicData = 100;
    // pd.protectedData = 200;
    // 下面这行代码也会报错,因为printBase在外部不可访问
    // pd.printBase();
    return 0;
}

在这个例子中,PrivateDerived 类以私有继承的方式从 Base 类派生。虽然 PrivateDerived 类的成员函数可以访问 Base 类的公有和保护成员,但这些成员在 PrivateDerived 类外部不可访问,就如同它们是 PrivateDerived 类的私有成员一样。

保护继承(protected inheritance)

当使用保护继承时,基类的公有成员在派生类中变成保护成员,基类的保护成员在派生类中仍然是保护成员,而基类的私有成员在派生类中不可访问。

以下是保护继承的代码示例:

#include <iostream>

// 基类
class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
public:
    Base() : publicData(10), protectedData(20), privateData(30) {}
    void printBase() {
        std::cout << "Public Data: " << publicData << std::endl;
        std::cout << "Protected Data: " << protectedData << std::endl;
        std::cout << "Private Data: " << privateData << std::endl;
    }
};

// 保护继承派生类
class ProtectedDerived : protected Base {
public:
    void accessBaseMembers() {
        std::cout << "Accessing Public Data from Derived: " << publicData << std::endl;
        std::cout << "Accessing Protected Data from Derived: " << protectedData << std::endl;
        // 下面这行代码会报错,因为privateData在派生类中不可访问
        // std::cout << "Accessing Private Data from Derived: " << privateData << std::endl;
    }
};

// 从ProtectedDerived类再派生的类
class FurtherDerived : public ProtectedDerived {
public:
    void accessBaseMembers() {
        std::cout << "Accessing Public Data from Further Derived: " << publicData << std::endl;
        std::cout << "Accessing Protected Data from Further Derived: " << protectedData << std::endl;
    }
};

int main() {
    ProtectedDerived pd;
    pd.accessBaseMembers();
    // 下面这两行代码都会报错,因为publicData和protectedData在外部不可访问
    // pd.publicData = 100;
    // pd.protectedData = 200;
    // 下面这行代码也会报错,因为printBase在外部不可访问
    // pd.printBase();

    FurtherDerived fd;
    fd.accessBaseMembers();
    return 0;
}

在上述代码中,ProtectedDerived 类以保护继承的方式从 Base 类派生。Base 类的公有成员 publicDataProtectedDerived 类中变成保护成员。因此,ProtectedDerived 类的成员函数可以访问这些成员,但在 ProtectedDerived 类外部不可访问。而从 ProtectedDerived 类再派生的 FurtherDerived 类,其成员函数可以访问 ProtectedDerived 类从 Base 类继承过来的保护成员。

不同继承方式对类型兼容性的影响

公有继承下的类型兼容性

在公有继承中,派生类对象可以被当作基类对象来使用,这是因为派生类对象包含了基类对象的所有成员。这种特性被称为 is - a 关系,即派生类对象 “是一种” 基类对象。

例如:

#include <iostream>

class Animal {
public:
    void speak() {
        std::cout << "Animal makes a sound." << std::endl;
    }
};

class Dog : public Animal {
public:
    void bark() {
        std::cout << "Dog barks." << std::endl;
    }
};

void makeSound(Animal& a) {
    a.speak();
}

int main() {
    Dog d;
    makeSound(d);
    return 0;
}

在这个例子中,Dog 类公有继承自 Animal 类。makeSound 函数接受一个 Animal 类的引用,而我们可以将 Dog 类的对象传递给它,因为 Dog “是一种” Animal。这种类型兼容性使得我们可以编写更通用的代码,提高代码的复用性。

私有继承下的类型兼容性

在私有继承中,派生类对象不能被当作基类对象来使用。因为私有继承破坏了 is - a 关系,虽然派生类包含了基类的所有成员,但这些成员在派生类中都变成了私有成员,从外部看,派生类和基类之间不存在自然的类型兼容性。

例如:

#include <iostream>

class Animal {
public:
    void speak() {
        std::cout << "Animal makes a sound." << std::endl;
    }
};

class Dog : private Animal {
public:
    void bark() {
        std::cout << "Dog barks." << std::endl;
    }
};

void makeSound(Animal& a) {
    a.speak();
}

int main() {
    Dog d;
    // 下面这行代码会报错,因为Dog类私有继承自Animal类,不能将Dog对象当作Animal对象传递
    // makeSound(d);
    return 0;
}

在这个例子中,Dog 类私有继承自 Animal 类,当我们尝试将 Dog 对象传递给接受 Animal 引用的 makeSound 函数时,编译器会报错,因为私有继承破坏了类型兼容性。

保护继承下的类型兼容性

在保护继承中,派生类对象同样不能被当作基类对象在外部使用,原因与私有继承类似。虽然保护继承下基类的公有成员在派生类中变为保护成员,保留了一定的继承关系,但从外部看,这种关系并不足以支持将派生类对象当作基类对象使用。

例如:

#include <iostream>

class Animal {
public:
    void speak() {
        std::cout << "Animal makes a sound." << std::endl;
    }
};

class Dog : protected Animal {
public:
    void bark() {
        std::cout << "Dog barks." << std::endl;
    }
};

void makeSound(Animal& a) {
    a.speak();
}

int main() {
    Dog d;
    // 下面这行代码会报错,因为Dog类保护继承自Animal类,不能将Dog对象当作Animal对象传递
    // makeSound(d);
    return 0;
}

在这个例子中,Dog 类保护继承自 Animal 类,同样不能将 Dog 对象传递给接受 Animal 引用的 makeSound 函数,因为保护继承也破坏了外部的类型兼容性。

不同继承方式在多态中的应用

公有继承与多态

多态是面向对象编程的重要特性之一,它允许通过基类指针或引用调用派生类的重写函数。在C++ 中,实现多态需要满足以下条件:基类的函数必须是虚函数,并且通过公有继承创建派生类。

例如:

#include <iostream>

class Shape {
public:
    virtual void draw() {
        std::cout << "Drawing a shape." << std::endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a rectangle." << std::endl;
    }
};

void drawShape(Shape& s) {
    s.draw();
}

int main() {
    Circle c;
    Rectangle r;
    drawShape(c);
    drawShape(r);
    return 0;
}

在这个例子中,Shape 类定义了一个虚函数 drawCircleRectangle 类通过公有继承从 Shape 类派生,并各自重写了 draw 函数。drawShape 函数接受一个 Shape 类的引用,当传递 CircleRectangle 对象时,会根据对象的实际类型调用相应的 draw 函数,从而实现多态。

私有继承与多态

私有继承不能直接用于实现多态,因为私有继承破坏了类型兼容性,不能将派生类对象当作基类对象使用。虽然派生类中包含了基类的虚函数,但由于外部无法将派生类对象转换为基类对象,多态无法通过常规方式实现。

例如:

#include <iostream>

class Shape {
public:
    virtual void draw() {
        std::cout << "Drawing a shape." << std::endl;
    }
};

class Circle : private Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

class Rectangle : private Shape {
public:
    void draw() override {
        std::cout << "Drawing a rectangle." << std::endl;
    }
};

void drawShape(Shape& s) {
    s.draw();
}

int main() {
    Circle c;
    Rectangle r;
    // 下面这两行代码都会报错,因为Circle和Rectangle类私有继承自Shape类,不能将它们的对象当作Shape对象传递
    // drawShape(c);
    // drawShape(r);
    return 0;
}

在这个例子中,尽管 CircleRectangle 类重写了 Shape 类的虚函数 draw,但由于私有继承,无法将它们的对象传递给 drawShape 函数,从而无法实现多态。

保护继承与多态

保护继承同样不能直接用于实现多态,原因与私有继承类似,因为保护继承破坏了外部的类型兼容性。虽然派生类内部可以调用基类的虚函数,但从外部无法通过基类指针或引用访问到派生类重写的函数。

例如:

#include <iostream>

class Shape {
public:
    virtual void draw() {
        std::cout << "Drawing a shape." << std::endl;
    }
};

class Circle : protected Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

class Rectangle : protected Shape {
public:
    void draw() override {
        std::cout << "Drawing a rectangle." << std::endl;
    }
};

void drawShape(Shape& s) {
    s.draw();
}

int main() {
    Circle c;
    Rectangle r;
    // 下面这两行代码都会报错,因为Circle和Rectangle类保护继承自Shape类,不能将它们的对象当作Shape对象传递
    // drawShape(c);
    // drawShape(r);
    return 0;
}

在这个例子中,CircleRectangle 类保护继承自 Shape 类,虽然它们重写了 draw 函数,但由于保护继承破坏了外部的类型兼容性,无法通过 drawShape 函数实现多态。

不同继承方式对代码维护和扩展性的影响

公有继承对代码维护和扩展性的影响

公有继承有助于保持代码的清晰和可维护性,因为它遵循 is - a 关系,使得代码结构符合自然的逻辑。在公有继承中,派生类可以添加新的功能,同时保留基类的接口,这使得代码具有良好的扩展性。

例如,在一个图形绘制的程序中,如果我们有一个 Shape 基类和多个公有继承自 Shape 的派生类(如 CircleRectangle 等),当需要添加新的图形类型(如 Triangle)时,只需要创建一个新的公有继承自 ShapeTriangle 类,并实现相应的绘制函数即可,而不会影响到现有的代码逻辑。

私有继承对代码维护和扩展性的影响

私有继承使得派生类与基类之间的关系更加紧密,这种紧密关系在一定程度上增加了代码维护的难度。因为基类的任何改变都可能影响到派生类,而派生类的实现细节又依赖于基类。在扩展性方面,私有继承不利于添加新的功能,因为外部无法将派生类对象当作基类对象使用,限制了代码的复用和扩展。

例如,如果在一个私有继承的场景中,基类的接口发生了变化,那么派生类可能需要进行大量的修改,因为基类的公有成员在派生类中变成了私有成员,可能需要在派生类中重新实现对这些成员的访问逻辑。

保护继承对代码维护和扩展性的影响

保护继承在代码维护和扩展性方面的表现介于公有继承和私有继承之间。保护继承保留了一定的继承关系,使得派生类之间可以共享一些保护成员,但外部无法直接访问这些成员。这在一定程度上增加了代码维护的复杂性,因为需要考虑派生类之间的保护成员访问问题。在扩展性方面,由于外部无法将派生类对象当作基类对象使用,扩展性也受到一定的限制。

例如,如果在一个保护继承的体系中,需要在外部使用派生类对象的某些功能,就需要额外的设计来暴露这些功能,这增加了代码的复杂性和维护成本。

不同继承方式在实际项目中的应用场景

公有继承的应用场景

  1. 实现多态和代码复用:在图形绘制、游戏开发等领域,经常使用公有继承来实现多态。例如,一个游戏中有多种角色(如战士、法师、刺客等),这些角色可以公有继承自一个基类 Character,通过多态实现不同角色的不同行为。
  2. 构建类层次结构:在面向对象的设计中,公有继承常用于构建类的层次结构。例如,在一个电商系统中,Product 类可以作为基类,BookClothing 等类可以公有继承自 Product 类,以继承和扩展 Product 类的属性和方法。

私有继承的应用场景

  1. 代码复用但不暴露接口:当我们希望复用基类的代码,但又不希望外部将派生类对象当作基类对象使用时,可以使用私有继承。例如,在一个文件处理类中,可能会私有继承一个底层的文件操作类,只在文件处理类内部使用底层文件操作类的功能,而不向外部暴露这些功能。
  2. 封装实现细节:私有继承可以将基类的实现细节封装在派生类内部。例如,在一个加密算法类中,可能会私有继承一些数学运算类,将这些数学运算作为加密算法的实现细节,不对外暴露。

保护继承的应用场景

  1. 派生类之间的代码复用:当我们希望在派生类之间共享一些保护成员,同时又不希望外部访问这些成员时,可以使用保护继承。例如,在一个图形处理库中,Shape 类可以通过保护继承派生出 Polygon 类和 Curve 类,Polygon 类和 Curve 类可以共享 Shape 类的一些保护成员,而外部无法直接访问这些成员。
  2. 内部框架的构建:在构建内部框架时,保护继承可以用于限制某些功能的访问范围。例如,在一个公司内部的开发框架中,某些基础类可能通过保护继承派生出一些业务相关的类,这些业务类可以访问基础类的保护成员,但外部的应用程序无法直接访问这些成员。

总结不同继承方式的选择要点

  1. 考虑类型兼容性:如果需要将派生类对象当作基类对象使用,实现多态和代码复用,应选择公有继承。如果不希望外部将派生类对象当作基类对象使用,应选择私有继承或保护继承。
  2. 考虑访问控制:根据对基类成员在派生类中的访问需求来选择继承方式。如果希望基类的公有成员在派生类中仍然是公有成员,应选择公有继承;如果希望将基类的公有和保护成员都变成私有成员,应选择私有继承;如果希望基类的公有成员在派生类中变成保护成员,应选择保护继承。
  3. 考虑代码维护和扩展性:如果希望代码具有良好的可维护性和扩展性,遵循自然的 is - a 关系,应选择公有继承。如果需要封装实现细节,减少对外部的暴露,可选择私有继承或保护继承,但要注意可能带来的维护和扩展的复杂性。

在实际编程中,应根据具体的需求和设计目标,综合考虑以上因素,选择合适的继承方式,以编写高效、可维护和可扩展的代码。

通过对C++ 类不同继承方式的深入分析,我们了解了它们在访问控制、类型兼容性、多态、代码维护和扩展性以及实际应用场景等方面的特点和影响。在实际项目中,正确选择继承方式对于构建健壮、高效的软件系统至关重要。希望通过本文的介绍,读者能够更加深入地理解和运用C++ 类的继承机制,编写出更加优秀的C++ 代码。