Bootstrap

学习记录:C++基类构造函数的调用:理解和实践

在 C++ 中,当一个类继承自基类时,基类的构造函数通常会在派生类的构造函数执行之前被调用。根据 C++ 的构造函数调用规则,如果基类有默认构造函数,编译器会在派生类构造函数体执行前自动调用基类的默认构造函数;但如果基类没有默认构造函数,那么派生类构造函数的初始化列表中必须显式调用基类的构造函数。

让我们更深入地探讨这个话题。

1. 基类构造函数的自动调用

如果基类有默认构造函数(即没有参数的构造函数),那么派生类的构造函数会自动调用这个默认构造函数。这是由编译器隐式完成的,无需显式写出基类的构造函数调用。

class Base {
public:
    Base() {
        std::cout << "Base constructor called" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructor called" << std::endl;
    }
};

int main() {
    Derived obj;
}

在这个例子中,派生类 Derived 的构造函数首先调用基类 Base 的默认构造函数,然后才执行派生类的构造函数体。输出将是:

Base constructor called
Derived constructor called

2. 基类没有默认构造函数时的显式调用

如果基类没有默认构造函数(即没有无参数的构造函数),并且派生类没有在构造函数的初始化列表中显式地调用基类的构造函数,编译器将无法自动构造基类对象,并会报错。因此,在这种情况下,必须在派生类的构造函数初始化列表中显式调用基类的构造函数。

2.1 没有默认构造函数的基类

如果基类的构造函数没有默认构造函数,比如它需要参数,那么派生类必须在其构造函数的初始化列表中提供适当的参数来调用基类的构造函数。

class Base {
public:
    Base(int x) {
        std::cout << "Base constructor called with value: " << x << std::endl;
    }
};

class Derived : public Base {
public:
    Derived(int x) : Base(x) {  // 必须显式调用 Base 的构造函数
        std::cout << "Derived constructor called with value: " << x << std::endl;
    }
};

int main() {
    Derived obj(42);  // 传递参数给 Base 和 Derived 的构造函数
}

输出将是:

Base constructor called with value: 42
Derived constructor called with value: 42

在这种情况下,Derived 构造函数通过初始化列表显式调用了 Base 的构造函数,并传递了 x 参数。

2.2 基类构造函数无默认构造函数且未显式调用

如果我们没有在派生类的构造函数中显式调用基类的构造函数,编译器会报错,提示无法调用基类的构造函数。

class Base {
public:
    Base(int x) {
        std::cout << "Base constructor called with value: " << x << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        // 这里会报错,因为没有默认构造函数,且未显式调用 Base 的构造函数
        std::cout << "Derived constructor called" << std::endl;
    }
};

int main() {
    Derived obj;  // 错误:基类没有默认构造函数,且没有显式调用
}

此时编译器会报错:“没有可用的默认构造函数”。为了解决这个问题,必须在 Derived 构造函数的初始化列表中显式调用 Base 的构造函数,传递正确的参数。

3. 基类的多个构造函数

如果基类有多个构造函数,派生类可以根据需要选择合适的构造函数进行调用。假设基类有多个构造函数,其中一个是默认构造函数,另一个是带参数的构造函数,派生类可以选择调用任意一个。

class Base {
public:
    Base() {
        std::cout << "Base default constructor called" << std::endl;
    }
    
    Base(int x) {
        std::cout << "Base constructor called with value: " << x << std::endl;
    }
};

class Derived : public Base {
public:
    Derived(int x) : Base(x) {  // 显式调用带参数的 Base 构造函数
        std::cout << "Derived constructor called with value: " << x << std::endl;
    }
};

int main() {
    Derived obj(42);  // 调用带参数的 Base 构造函数
}

输出将是:

Base constructor called with value: 42
Derived constructor called with value: 42

在这个例子中,Derived 的构造函数显式调用了基类 Base 的带参数构造函数。

4. 基类的构造函数调用顺序

  • 初始化顺序:无论你在派生类构造函数的初始化列表中调用哪个基类构造函数,基类构造函数总是先于派生类构造函数体执行。
  • 多重继承:如果一个类有多个基类,基类构造函数的调用顺序取决于它们在类声明中出现的顺序(即 class Derived : public Base1, public Base2,所以在构造 Derived 对象时,Base1 的构造函数会先被调用,然后是 Base2,最后才是 Derived 的构造函数),而不是初始化列表中的顺序。
#include <iostream>

class Base1 {
public:
    Base1(int x) {
        std::cout << "Base1 constructor called with value: " << x << std::endl;
    }
};

class Base2 {
public:
    Base2(int x) {
        std::cout << "Base2 constructor called with value: " << x << std::endl;
    }
};

class Derived : public Base1, public Base2 {
public:
    Derived(int x) : Base1(x), Base2(x) {
        std::cout << "Derived constructor called with value: " << x << std::endl;
    }
};

int main() {
    Derived obj(42);  // 传递一个参数给 Derived 构造函数
    return 0;
}

输出将是:

Base1 constructor called
Base2 constructor called
Derived constructor called

5. 总结

  1. 基类构造函数自动调用:如果基类有默认构造函数,派生类会自动调用该构造函数。
  2. 显式调用基类构造函数:如果基类没有默认构造函数,派生类的构造函数必须显式调用基类的构造函数,且传递适当的参数。
  3. 基类构造函数调用顺序:基类构造函数总是在派生类构造函数体之前执行。
  4. 多重继承时的调用顺序:多重继承时,基类构造函数的调用顺序是按照基类声明的顺序执行的,而不是初始化列表中的顺序。

通过理解这些规则,我们可以更加准确地控制类和对象的初始化过程,避免构造函数中的潜在错误。

;