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

C++抽象类构造函数的初始化逻辑

2024-06-251.6k 阅读

C++ 抽象类基础概念

在 C++ 中,抽象类是一种特殊的类,它至少包含一个纯虚函数。纯虚函数是在声明时被初始化为 0 的虚函数,其语法形式如下:

class AbstractClass {
public:
    virtual void pureVirtualFunction() = 0;
};

这里的 AbstractClass 就是一个抽象类,因为它包含了纯虚函数 pureVirtualFunction。抽象类不能被实例化,其存在的意义主要是为了提供一个通用的基类,让其他派生类去继承并实现其纯虚函数,从而实现多态性。

构造函数的作用

构造函数是类中的一种特殊成员函数,它的主要作用是在创建对象时对对象进行初始化。构造函数与类名相同,没有返回值(包括 void 也没有)。例如,对于一个简单的非抽象类 Point

class Point {
private:
    int x;
    int y;
public:
    Point(int a, int b) : x(a), y(b) {
        // 构造函数体,这里简单打印初始化信息
        std::cout << "Point object initialized with x = " << x << " and y = " << y << std::endl;
    }
};

在上述代码中,Point 类的构造函数接受两个整数参数 ab,并使用成员初始化列表将成员变量 xy 分别初始化为 ab。当我们创建 Point 对象时:

Point p(10, 20);

构造函数会被调用,完成对象的初始化工作,并打印出相应的初始化信息。

抽象类构造函数的特殊性

虽然抽象类不能被实例化,但它仍然可以拥有构造函数。这是因为当派生类创建对象时,会首先调用抽象基类的构造函数来初始化从抽象基类继承的成员。例如:

class Shape {
public:
    virtual double area() = 0;
    Shape() {
        std::cout << "Shape constructor called" << std::endl;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {
        std::cout << "Circle constructor called" << std::endl;
    }
    double area() override {
        return 3.14 * radius * radius;
    }
};

在上述代码中,Shape 是一个抽象类,它有一个构造函数。Circle 类继承自 Shape 类。当我们创建 Circle 对象时:

Circle c(5.0);

输出结果为:

Shape constructor called
Circle constructor called

可以看到,首先调用了 Shape 抽象类的构造函数,然后才调用 Circle 类的构造函数。这表明,即使 Shape 是抽象类不能被实例化,但在创建派生类 Circle 对象时,其构造函数会被调用以初始化从 Shape 继承的部分。

抽象类构造函数的初始化逻辑

成员初始化列表

与普通类一样,抽象类构造函数也可以使用成员初始化列表来初始化成员变量。例如,我们可以在抽象类 Shape 中添加一个成员变量 name 并在构造函数中初始化它:

class Shape {
private:
    std::string name;
public:
    virtual double area() = 0;
    Shape(const std::string& n) : name(n) {
        std::cout << "Shape constructor called with name: " << name << std::endl;
    }
};

class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(const std::string& n, double w, double h) : Shape(n), width(w), height(h) {
        std::cout << "Rectangle constructor called" << std::endl;
    }
    double area() override {
        return width * height;
    }
};

Shape 构造函数中,通过成员初始化列表将 name 初始化为传入的参数 n。在 Rectangle 构造函数中,首先调用 Shape 的构造函数来初始化 name,然后再初始化自身的成员变量 widthheight。当创建 Rectangle 对象时:

Rectangle r("MyRectangle", 10.0, 5.0);

输出结果为:

Shape constructor called with name: MyRectangle
Rectangle constructor called

初始化顺序

抽象类构造函数的初始化顺序遵循一定的规则。首先,按照成员变量在类中声明的顺序进行初始化,而不是按照成员初始化列表中的顺序。例如:

class AbstractExample {
private:
    int b;
    int a;
public:
    AbstractExample(int value) : a(value), b(a + 10) {
        // 这里虽然在初始化列表中先初始化 a 再初始化 b,但实际顺序是按照声明顺序
        std::cout << "a = " << a << ", b = " << b << std::endl;
    }
    virtual void doSomething() = 0;
};

class DerivedExample : public AbstractExample {
public:
    DerivedExample(int value) : AbstractExample(value) {
        std::cout << "DerivedExample constructor called" << std::endl;
    }
    void doSomething() override {
        std::cout << "DerivedExample doSomething called" << std::endl;
    }
};

AbstractExample 中,成员变量 b 先声明,a 后声明。尽管在初始化列表中先写了 a(value) 再写 b(a + 10),但实际初始化顺序是 b 先被初始化(此时 a 还未初始化,b 的值是未定义的),然后 a 被初始化。当创建 DerivedExample 对象时:

DerivedExample de(5);

输出结果可能是:

a = 5, b = -858993460
DerivedExample constructor called

这里 b 的值是未定义的,因为它在 a 之前被初始化,而使用了未初始化的 a 来计算其值。为了避免这种问题,应该确保成员变量的初始化不依赖于未初始化的其他成员变量,并且尽量按照声明顺序在初始化列表中进行初始化。

初始化基类部分

当抽象类作为基类时,派生类构造函数在初始化自身成员之前,会先调用抽象基类的构造函数来初始化从基类继承的部分。这是 C++ 继承机制的一部分。例如,我们有一个更复杂的继承结构:

class BaseAbstract {
private:
    int baseValue;
public:
    BaseAbstract(int value) : baseValue(value) {
        std::cout << "BaseAbstract constructor called with baseValue = " << baseValue << std::endl;
    }
    virtual void baseFunction() = 0;
};

class IntermediateAbstract : public BaseAbstract {
private:
    int intermediateValue;
public:
    IntermediateAbstract(int base, int intermediate) : BaseAbstract(base), intermediateValue(intermediate) {
        std::cout << "IntermediateAbstract constructor called with intermediateValue = " << intermediateValue << std::endl;
    }
    virtual void intermediateFunction() = 0;
};

class FinalDerived : public IntermediateAbstract {
public:
    FinalDerived(int base, int intermediate) : IntermediateAbstract(base, intermediate) {
        std::cout << "FinalDerived constructor called" << std::endl;
    }
    void baseFunction() override {
        std::cout << "FinalDerived baseFunction called" << std::endl;
    }
    void intermediateFunction() override {
        std::cout << "FinalDerived intermediateFunction called" << std::endl;
    }
};

在这个例子中,BaseAbstract 是一个抽象类,IntermediateAbstract 继承自 BaseAbstract 也是抽象类,FinalDerived 继承自 IntermediateAbstract 是具体类。当创建 FinalDerived 对象时:

FinalDerived fd(10, 20);

输出结果为:

BaseAbstract constructor called with baseValue = 10
IntermediateAbstract constructor called with intermediateValue = 20
FinalDerived constructor called

可以看到,构造函数的调用顺序是从最顶层的抽象基类 BaseAbstract 开始,依次向下调用到派生类 FinalDerived。这确保了对象从基类继承的部分能够正确初始化。

初始化 const 和引用成员

抽象类中也可以包含 const 成员变量和引用成员变量,它们同样需要在构造函数的初始化列表中进行初始化。例如:

class AbstractWithConstAndRef {
private:
    const int constValue;
    int& refValue;
public:
    AbstractWithConstAndRef(int& value) : constValue(10), refValue(value) {
        std::cout << "AbstractWithConstAndRef constructor called, constValue = " << constValue << ", refValue = " << refValue << std::endl;
    }
    virtual void abstractFunction() = 0;
};

class DerivedFromAbstractWithConstAndRef : public AbstractWithConstAndRef {
public:
    DerivedFromAbstractWithConstAndRef(int& value) : AbstractWithConstAndRef(value) {
        std::cout << "DerivedFromAbstractWithConstAndRef constructor called" << std::endl;
    }
    void abstractFunction() override {
        std::cout << "DerivedFromAbstractWithConstAndRef abstractFunction called" << std::endl;
    }
};

AbstractWithConstAndRef 类中,constValueconst 类型,refValue 是引用类型,它们都在构造函数的初始化列表中被初始化。当创建 DerivedFromAbstractWithConstAndRef 对象时:

int num = 5;
DerivedFromAbstractWithConstAndRef dfc(num);

输出结果为:

AbstractWithConstAndRef constructor called, constValue = 10, refValue = 5
DerivedFromAbstractWithConstAndRef constructor called

需要注意的是,const 成员变量一旦初始化后就不能再修改,而引用成员变量必须在构造函数初始化列表中绑定到一个有效的对象。

抽象类构造函数与多态性

虽然抽象类不能被实例化,但它在多态性中起着重要作用。通过抽象类的指针或引用,我们可以调用派生类中重写的虚函数。而抽象类构造函数在这个过程中,为对象的初始化提供了必要的基础。例如:

class Animal {
public:
    virtual void speak() = 0;
    Animal() {
        std::cout << "Animal constructor called" << std::endl;
    }
};

class Dog : public Animal {
public:
    Dog() {
        std::cout << "Dog constructor called" << std::endl;
    }
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    Cat() {
        std::cout << "Cat constructor called" << std::endl;
    }
    void speak() override {
        std::cout << "Meow!" << std::endl;
    }
};

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

在上述代码中,Animal 是抽象类,DogCat 是派生类。makeSound 函数接受一个 Animal 引用,通过这个引用可以调用不同派生类中重写的 speak 函数。当我们这样使用时:

Dog dog;
Cat cat;
makeSound(dog);
makeSound(cat);

输出结果为:

Animal constructor called
Dog constructor called
Animal constructor called
Cat constructor called
Woof!
Meow!

可以看到,在创建 DogCat 对象时,首先调用了 Animal 抽象类的构造函数。而通过 makeSound 函数,实现了多态调用,根据对象的实际类型(DogCat)调用相应的 speak 函数。这体现了抽象类构造函数在多态实现中的基础作用,即正确初始化对象,使得后续的多态调用能够正确执行。

抽象类构造函数的注意事项

避免在构造函数中调用虚函数

在抽象类构造函数中调用虚函数是一个容易出错的行为。因为在构造函数执行期间,对象的类型还没有完全确定,此时调用虚函数不会产生多态行为,而是调用当前类的函数版本。例如:

class BaseAbstractWithVirtualInCtor {
public:
    virtual void virtualFunction() {
        std::cout << "BaseAbstractWithVirtualInCtor virtualFunction" << std::endl;
    }
    BaseAbstractWithVirtualInCtor() {
        virtualFunction();
    }
};

class DerivedFromAbstractWithVirtualInCtor : public BaseAbstractWithVirtualInCtor {
public:
    void virtualFunction() override {
        std::cout << "DerivedFromAbstractWithVirtualInCtor virtualFunction" << std::endl;
    }
};

当创建 DerivedFromAbstractWithVirtualInCtor 对象时:

DerivedFromAbstractWithVirtualInCtor dva;

输出结果为:

BaseAbstractWithVirtualInCtor virtualFunction

可以看到,在 BaseAbstractWithVirtualInCtor 构造函数中调用 virtualFunction 时,并没有调用 DerivedFromAbstractWithVirtualInCtor 中重写的版本,而是调用了 BaseAbstractWithVirtualInCtor 自身的版本。这可能会导致逻辑错误,因此应避免在构造函数中调用虚函数。

抽象类构造函数不能是虚函数

C++ 不允许将抽象类构造函数声明为虚函数。这是因为虚函数调用依赖于虚函数表指针(vptr),而在构造函数执行期间,对象的 vptr 还没有正确设置。如果构造函数是虚函数,就无法确定应该调用哪个版本的构造函数,会导致编译错误。例如,尝试将抽象类构造函数声明为虚函数:

class AbstractWithVirtualCtor {
public:
    virtual AbstractWithVirtualCtor() {
        // 这会导致编译错误
    }
    virtual void abstractFunction() = 0;
};

上述代码在编译时会报错,提示构造函数不能是虚函数。

与析构函数的配合

虽然抽象类构造函数主要负责对象的初始化,但它与析构函数密切相关。当对象被销毁时,析构函数的调用顺序与构造函数相反。对于抽象类及其派生类,先调用派生类的析构函数,然后再调用抽象基类的析构函数。例如:

class AbstractWithDestructor {
public:
    AbstractWithDestructor() {
        std::cout << "AbstractWithDestructor constructor called" << std::endl;
    }
    virtual ~AbstractWithDestructor() {
        std::cout << "AbstractWithDestructor destructor called" << std::endl;
    }
    virtual void abstractFunction() = 0;
};

class DerivedFromAbstractWithDestructor : public AbstractWithDestructor {
public:
    DerivedFromAbstractWithDestructor() {
        std::cout << "DerivedFromAbstractWithDestructor constructor called" << std::endl;
    }
    ~DerivedFromAbstractWithDestructor() {
        std::cout << "DerivedFromAbstractWithDestructor destructor called" << std::endl;
    }
    void abstractFunction() override {
        std::cout << "DerivedFromAbstractWithDestructor abstractFunction called" << std::endl;
    }
};

当创建并销毁 DerivedFromAbstractWithDestructor 对象时:

{
    DerivedFromAbstractWithDestructor dfd;
}

输出结果为:

AbstractWithDestructor constructor called
DerivedFromAbstractWithDestructor constructor called
DerivedFromAbstractWithDestructor destructor called
AbstractWithDestructor destructor called

可以看到,析构函数的调用顺序与构造函数相反,这确保了对象在销毁时,其资源能够按照正确的顺序被释放。

总结

C++ 抽象类构造函数在对象初始化过程中扮演着重要角色。尽管抽象类不能被实例化,但它为派生类对象的初始化提供了基础。通过成员初始化列表、遵循正确的初始化顺序以及与派生类构造函数的配合,抽象类构造函数能够正确初始化对象的各个部分,包括从基类继承的成员、自身的成员变量以及 const 和引用成员。同时,在使用抽象类构造函数时,需要注意避免在构造函数中调用虚函数、不能将构造函数声明为虚函数以及与析构函数的正确配合等问题。深入理解抽象类构造函数的初始化逻辑,有助于编写高效、正确且可维护的 C++ 代码,尤其是在涉及到复杂继承结构和多态性的场景中。掌握这些知识对于 C++ 开发者来说是至关重要的,能够帮助他们更好地设计和实现面向对象的程序。