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

C++抽象类构造函数的访问权限设置

2023-06-081.6k 阅读

C++ 抽象类构造函数访问权限的基础概念

抽象类的定义与特点

在 C++ 中,抽象类是一种特殊的类,它至少包含一个纯虚函数。纯虚函数是通过在声明时赋值为 0 来定义的,例如:

class Shape {
public:
    virtual double area() const = 0;
};

这里的 Shape 类就是一个抽象类,因为它包含了纯虚函数 area。抽象类不能被实例化,它主要用作其他具体类的基类,为这些子类提供一个通用的接口和部分实现。

构造函数的作用与常规访问权限

构造函数是类中的特殊成员函数,用于在创建对象时初始化对象的成员变量。在一般的类中,构造函数通常具有 public 访问权限,这样外部代码就可以通过构造函数创建该类的对象。例如:

class Rectangle {
private:
    double width;
    double height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() const {
        return width * height;
    }
};

在上述 Rectangle 类中,构造函数 Rectangle(double w, double h)public 的,所以可以在外部代码中这样创建对象:

Rectangle rect(5.0, 3.0);

构造函数也可以有其他访问权限,比如 privateprotected。当构造函数是 private 时,外部代码无法直接创建该类的对象,这常用于实现单例模式等特殊设计。例如:

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;

在这个单例模式的示例中,Singleton 类的构造函数是 private 的,外部代码只能通过 getInstance 静态成员函数来获取唯一的实例。

C++ 抽象类构造函数访问权限的特殊性

为什么抽象类需要构造函数

虽然抽象类不能被实例化,但它仍然需要构造函数。这是因为抽象类通常作为基类,为子类提供初始化的基础。当子类继承自抽象类时,子类的构造函数会首先调用抽象类的构造函数,以确保抽象类部分的成员变量得到正确初始化。例如:

class Shape {
protected:
    std::string name;
public:
    Shape(const std::string& n) : name(n) {}
    virtual double area() const = 0;
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(const std::string& n, double r) : Shape(n), radius(r) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

在这个例子中,Shape 抽象类有一个构造函数 Shape(const std::string& n),用于初始化 name 成员变量。Circle 子类继承自 Shape,在 Circle 的构造函数中,首先调用 Shape(n) 来初始化 Shape 部分的成员变量。

抽象类构造函数常见访问权限

  1. protected 访问权限:这是抽象类构造函数最常见的访问权限设置。将抽象类的构造函数设为 protected,意味着只有子类的构造函数可以调用它,外部代码无法直接调用。这符合抽象类不能被实例化,但需要为子类提供初始化的设计原则。例如上面的 Shape 类构造函数就是 protected 的,这样外部代码不能 Shape s("SomeShape"); 这样创建 Shape 对象,但子类 Circle 可以在其构造函数中调用 Shape 的构造函数。
  2. public 访问权限:虽然抽象类不能被实例化,但理论上可以将其构造函数设为 public。不过这样做会破坏抽象类不能直接实例化的约定,容易让开发者产生困惑,所以在实际应用中很少这样设置。例如:
class AbstractClass {
public:
    AbstractClass() {}
    virtual void pureVirtualFunction() = 0;
};

虽然这里 AbstractClass 的构造函数是 public,但试图 AbstractClass obj; 创建对象时会报错,因为它是抽象类。

  1. private 访问权限:将抽象类构造函数设为 private 会导致子类无法调用它的构造函数,这会破坏继承体系中的初始化逻辑。因为子类构造函数在初始化自身成员变量之前,需要先调用基类(抽象类)的构造函数。例如:
class AbstractWithPrivateCtor {
private:
    AbstractWithPrivateCtor() {}
    virtual void pureVirtual() = 0;
};

class SubClass : public AbstractWithPrivateCtor {
public:
    SubClass() {
        // 这里会报错,无法调用基类的 private 构造函数
    }
};

在这个例子中,SubClass 的构造函数试图调用 AbstractWithPrivateCtor 的构造函数,但由于其构造函数是 private 的,所以会编译失败。

深入理解抽象类构造函数访问权限对继承体系的影响

对继承关系中初始化顺序的影响

当抽象类构造函数的访问权限设置正确时,继承体系中的初始化顺序是清晰的。首先调用抽象类的构造函数,然后再调用子类的构造函数。例如:

class BaseAbstract {
protected:
    int baseValue;
    BaseAbstract(int v) : baseValue(v) {
        std::cout << "BaseAbstract constructor" << std::endl;
    }
public:
    virtual void print() const = 0;
};

class Derived : public BaseAbstract {
private:
    int derivedValue;
public:
    Derived(int bv, int dv) : BaseAbstract(bv), derivedValue(dv) {
        std::cout << "Derived constructor" << std::endl;
    }
    void print() const override {
        std::cout << "Base value: " << baseValue << ", Derived value: " << derivedValue << std::endl;
    }
};

在创建 Derived 对象时,会先调用 BaseAbstract 的构造函数,输出 BaseAbstract constructor,然后调用 Derived 的构造函数,输出 Derived constructor。如果抽象类构造函数的访问权限设置不当,比如设为 private,那么这个初始化顺序就会被破坏,导致编译错误。

对多态性和接口一致性的影响

正确设置抽象类构造函数的访问权限对于维护多态性和接口一致性非常重要。抽象类定义了一组子类必须实现的接口(通过纯虚函数),同时为子类提供了公共的初始化逻辑(通过构造函数)。例如,在图形绘制的例子中,不同的图形类(如 CircleRectangle 等)都继承自 Shape 抽象类。Shape 抽象类的构造函数可以初始化一些通用的属性,如名称等。子类通过继承 Shape 并实现其纯虚函数 area,保证了接口的一致性。如果抽象类构造函数访问权限设置错误,可能会导致子类无法正确初始化,进而影响多态性的实现。比如,若 Shape 构造函数为 privateCircle 无法初始化 Shape 部分的成员变量,那么在使用多态时,如通过 Shape* 指针调用 area 函数,可能会因为未初始化的数据而出现错误。

实际应用场景中抽象类构造函数访问权限的选择

框架设计中的应用

在大型框架设计中,抽象类常用于定义核心的接口和通用的行为。例如,在一个游戏开发框架中,可能有一个抽象类 GameObject,它定义了游戏对象的基本属性和行为,如位置、更新等。GameObject 的构造函数会初始化一些基本属性,如初始位置等。由于 GameObject 本身是抽象的,不能直接创建实例,所以其构造函数通常设为 protected。具体的游戏对象,如 PlayerEnemy 等类继承自 GameObject,在它们的构造函数中调用 GameObject 的构造函数来初始化基本属性。例如:

class GameObject {
protected:
    float x;
    float y;
    GameObject(float _x, float _y) : x(_x), y(_y) {}
public:
    virtual void update() = 0;
};

class Player : public GameObject {
public:
    Player(float _x, float _y) : GameObject(_x, _y) {}
    void update() override {
        // 玩家更新逻辑
        std::cout << "Player is updating at (" << x << ", " << y << ")" << std::endl;
    }
};

代码复用与扩展中的应用

在代码复用和扩展的场景下,抽象类构造函数访问权限的合理设置也至关重要。假设我们有一个图形处理库,其中有一个抽象类 GraphicObject 用于表示各种图形对象。GraphicObject 的构造函数初始化一些公共属性,如颜色等。当开发者想要扩展这个库,创建新的图形对象类时,通过继承 GraphicObject 并调用其构造函数来复用这些初始化逻辑。如果 GraphicObject 的构造函数是 protected,那么开发者只能通过继承的方式来使用它,保证了库的结构清晰和扩展性。例如:

class GraphicObject {
protected:
    std::string color;
    GraphicObject(const std::string& c) : color(c) {}
public:
    virtual void draw() = 0;
};

class Triangle : public GraphicObject {
private:
    float sideLength;
public:
    Triangle(const std::string& c, float sl) : GraphicObject(c), sideLength(sl) {}
    void draw() override {
        std::cout << "Drawing a " << color << " triangle with side length " << sideLength << std::endl;
    }
};

关于抽象类构造函数访问权限设置的常见错误与避免方法

错误设置为 private

如前面提到的,将抽象类构造函数设为 private 是一个常见错误。这会导致子类无法调用基类构造函数,破坏继承体系的初始化逻辑。要避免这个错误,开发者需要清楚抽象类的作用是为子类提供初始化基础,所以构造函数一般应设为 protected。在编写代码时,如果遇到子类构造函数无法调用基类构造函数的编译错误,首先要检查基类(抽象类)构造函数的访问权限。

意外设置为 public

虽然将抽象类构造函数设为 public 不会导致编译错误,但这违反了抽象类不能直接实例化的约定,容易引起混淆。为避免这种情况,在定义抽象类时,开发者应养成将构造函数设为 protected 的习惯,除非有特殊的设计需求(这种情况非常少见)。同时,代码审查过程中也应重点检查抽象类构造函数的访问权限是否合理。

忘记在子类构造函数中调用抽象类构造函数

即使抽象类构造函数访问权限设置正确,如果子类构造函数忘记调用抽象类构造函数,也会导致抽象类部分的成员变量未初始化。例如:

class AbstractBase {
protected:
    int data;
    AbstractBase(int d) : data(d) {}
public:
    virtual void print() = 0;
};

class SubClass : public AbstractBase {
public:
    SubClass() {
        // 忘记调用 AbstractBase 的构造函数,data 未初始化
    }
    void print() override {
        std::cout << "Data: " << data << std::endl;
    }
};

在这个例子中,SubClass 的构造函数没有调用 AbstractBase 的构造函数,当调用 print 函数时,data 是未初始化的,可能会导致运行时错误。为避免这种错误,在编写子类构造函数时,要确保首先调用抽象类的构造函数,并且传递正确的参数。

总结抽象类构造函数访问权限设置要点

  1. 基本原则:抽象类构造函数通常应设为 protected,这样既保证了抽象类不能被外部代码直接实例化,又允许子类在其构造函数中调用抽象类构造函数来进行初始化。
  2. 特殊情况:将抽象类构造函数设为 public 虽然可行,但违反了抽象类的设计初衷,应尽量避免。而将其设为 private 会破坏继承体系中的初始化逻辑,是错误的做法。
  3. 实际应用考虑:在框架设计、代码复用和扩展等实际场景中,要根据具体需求合理设置抽象类构造函数的访问权限,以确保代码的可维护性、扩展性和正确性。
  4. 避免错误:要注意避免常见错误,如错误设置为 privatepublic,以及忘记在子类构造函数中调用抽象类构造函数。通过良好的编程习惯和代码审查机制可以有效避免这些错误。

总之,正确设置 C++ 抽象类构造函数的访问权限是构建健壮、可维护的面向对象程序的重要环节,开发者需要深入理解其原理和应用场景,以编写出高质量的代码。