24-4 派生类的构造函数与初始化(todo)

在过去的两节课中,我们探讨了C++中继承的基本概念以及派生类初始化的顺序。本节课我们将深入研究构造函数在派生类初始化过程中的作用。为此,我们将继续使用上一节课中创建的简单基类和派生类:

class Base
{
public:
    int m_id {};

    Base(int id=0)
        : m_id{ id }
    {
    }

    int getId() const { return m_id; }
};

class Derived: public Base
{
public:
    double m_cost {};

    Derived(double cost=0.0)
        : m_cost{ cost }
    {
    }

    double getCost() const { return m_cost; }
};

对于非派生类,构造函数只需处理自身的成员。例如,考虑基类 Base。我们可以这样创建一个 Base 对象:

int main()
{
    Base base{ 5 }; // use Base(int) constructor

    return 0;
}

当基类实例化时,实际发生的过程如下:

  1. 为基类预留内存
  2. 调用相应的基类构造函数
  3. 成员初始化列表初始化变量
  4. 构造函数主体执行
  5. 控制权返回给调用方

这相当直观。而对于派生类,情况则稍显复杂:

int main()
{
    Derived derived{ 1.3 }; // use Derived(double) constructor

    return 0;
}

当派生类实例化时,实际发生的过程如下:

  1. 为派生类预留内存(足够容纳基类和派生类部分)
  2. 调用相应的派生类构造函数
  3. 首先使用相应的基类构造函数构造基类对象。若未指定基类构造函数,则使用默认构造函数。
  4. 成员初始化列表完成变量初始化
  5. 构造函数主体执行完毕
  6. 控制权返回调用方

此过程与非继承情况的唯一实质差异在于:在派生构造函数执行实质操作前,基类构造函数会先行调用。基类构造函数完成对象基类部分的初始化后,控制权将返回派生构造函数,使其得以完成后续工作。


初始化基类成员

当前派生类的现有缺陷之一在于,创建派生对象时无法初始化 m_id。若需在创建派生对象时同时设置 m_cost(来自对象的派生部分)和 m_id(来自对象的基类部分),该如何实现?

新手程序员常尝试以下方式解决此问题:

class Derived: public Base
{
public:
    double m_cost {};

    Derived(double cost=0.0, int id=0)
        // does not work
        : m_cost{ cost }
        , m_id{ id }
    {
    }

    double getCost() const { return m_cost; }
};

这是一个不错的尝试,思路基本正确。我们确实需要在构造函数中添加另一个参数,否则C++将无法确定m_id的初始化值。

然而,C++禁止在构造函数的成员初始化列表中初始化继承的成员变量。换言之,成员变量的值只能在其所属类别的构造函数成员初始化列表中设置。

为何C++如此设计?答案与const和引用变量相关。试想若m_id为const类型会发生什么:由于const变量必须在创建时初始化,基类构造函数必须在创建时设定其值。但当基类构造函数执行完毕后,派生类的构造函数成员初始化列表将随即执行。此时每个派生类都有机会初始化该变量,从而可能改变其值!通过将变量初始化限制在其所属类的构造函数中,C++确保所有变量仅初始化一次。

最终结果是上述示例无法运行,因为 m_id 是从基类继承的,而成员初始化列表中只能初始化非继承变量。

但继承变量仍可在构造函数主体中通过赋值改变其值。因此新手程序员常尝试如下写法:

class Derived: public Base
{
public:
    double m_cost {};

    Derived(double cost=0.0, int id=0)
        : m_cost{ cost }
    {
        m_id = id;
    }

    double getCost() const { return m_cost; }
};

虽然在此情况下该方法可行,但若m_id为const或引用类型则会失效(因const值和引用必须在构造函数的成员初始化列表中初始化)。此方案还存在效率问题:m_id被赋值两次——一次在基类构造函数的成员初始化列表中,另一次在派生类构造函数主体中。最后,若基类在构造过程中需要访问该值该如何处理?由于该值要等到派生类构造函数执行时(通常是最后阶段)才会被设置,基类根本无法获取。

那么创建派生类对象时如何正确初始化 m_id?

在所有示例中,当实例化派生类对象时,基类部分始终通过基类的默认构造函数创建。为何总是默认调用基类构造函数?因为我们从未指定其他方式!

所幸C++允许我们显式选择调用哪个基类构造函数!只需在派生类的成员初始化列表中添加对基类构造函数的调用即可:

class Derived: public Base
{
public:
    double m_cost {};

    Derived(double cost=0.0, int id=0)
        : Base{ id } // Call Base(int) constructor with value id!
        , m_cost{ cost }
    {
    }

    double getCost() const { return m_cost; }
};

现在,当我们执行这段代码时:

#include <iostream>

int main()
{
    Derived derived{ 1.3, 5 }; // use Derived(double, int) constructor
    std::cout << "Id: " << derived.getId() << '\n';
    std::cout << "Cost: " << derived.getCost() << '\n';

    return 0;
}

基类构造函数 Base(int) 将用于将 m_id 初始化为 5,而派生类构造函数将用于将 m_cost 初始化为 1.3!

因此,程序将输出:

image

具体过程如下:

  1. 为派生类分配内存。
  2. 调用派生类构造函数 Derived(double, int),此时 cost = 1.3,id = 5。
  3. 编译器检查是否指定了基类构造函数。已指定!因此调用基类构造函数 Base(int),传入 id = 5。
  4. 基类构造函数的成员初始化列表将m_id设为5。
  5. 基类构造函数主体执行(无操作)。
  6. 基类构造函数返回。
  7. 派生类构造函数的成员初始化列表将m_cost设为1.3。
  8. 派生类构造函数主体执行(无操作)。
  9. 派生类构造函数返回。

这看似复杂,实则简单明了。整个过程仅是派生构造函数调用特定基类构造函数来初始化对象的基类部分。由于m_id位于对象的基类部分,基类构造函数是唯一能初始化该值的构造函数。

需注意:无论在派生构造函数成员初始化列表的何处调用基类构造函数——它始终会优先执行。


现在我们可以将成员变量设为私有

既然你已经掌握了初始化基类成员的方法,就没有必要继续将成员变量设为公有。我们将成员变量重新设为私有,这才是它们应有的状态。

快速回顾:public成员可被任何人访问,private成员仅限同类成员函数访问。请注意,这意味着派生类无法直接访问基类的private成员!派生类必须通过访问函数才能获取基类的private成员。

请看:

#include <iostream>

class Base
{
private: // our member is now private
    int m_id {};

public:
    Base(int id=0)
        : m_id{ id }
    {
    }

    int getId() const { return m_id; }
};

class Derived: public Base
{
private: // our member is now private
    double m_cost;

public:
    Derived(double cost=0.0, int id=0)
        : Base{ id } // Call Base(int) constructor with value id!
        , m_cost{ cost }
    {
    }

    double getCost() const { return m_cost; }
};

int main()
{
    Derived derived{ 1.3, 5 }; // use Derived(double, int) constructor
    std::cout << "Id: " << derived.getId() << '\n';
    std::cout << "Cost: " << derived.getCost() << '\n';

    return 0;
}

在上面的代码中,我们将 m_id 和 m_cost 设为私有成员。这样处理是合理的,因为我们通过相应的构造函数初始化它们,并使用公有的访问器获取其值。

这段代码的输出结果如预期所示:

image


另一个例子

让我们再看看之前用过的一对类:

#include <string>
#include <string_view>

class Person
{
public:
    std::string m_name;
    int m_age {};

    Person(std::string_view name = "", int age = 0)
        : m_name{ name }, m_age{ age }
    {
    }

    const std::string& getName() const { return m_name; }
    int getAge() const { return m_age; }
};

// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
public:
    double m_battingAverage {};
    int m_homeRuns {};

    BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
       : m_battingAverage{ battingAverage },
         m_homeRuns{ homeRuns }
    {
    }
};

正如我们之前所写,BaseballPlayer 类仅初始化其自身的成员变量,并未指定要使用的 Person 构造函数。这意味着创建的每个 BaseballPlayer 对象都会使用 Person 的默认构造函数,导致姓名初始化为空、年龄初始化为 0。由于创建 BaseballPlayer 时赋予姓名和年龄更符合逻辑,我们应修改构造函数以添加这些参数。

以下是采用私有成员的更新类结构:BaseballPlayer类通过调用相应的Person构造函数来初始化继承的Person成员变量:

#include <iostream>
#include <string>
#include <string_view>

class Person
{
private:
    std::string m_name;
    int m_age {};

public:
    Person(std::string_view name = "", int age = 0)
        : m_name{ name }, m_age{ age }
    {
    }

    const std::string& getName() const { return m_name; }
    int getAge() const { return m_age; }

};
// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
private:
    double m_battingAverage {};
    int m_homeRuns {};

public:
    BaseballPlayer(std::string_view name = "", int age = 0,
        double battingAverage = 0.0, int homeRuns = 0)
        : Person{ name, age } // call Person(std::string_view, int) to initialize these fields
        , m_battingAverage{ battingAverage }, m_homeRuns{ homeRuns }
    {
    }

    double getBattingAverage() const { return m_battingAverage; }
    int getHomeRuns() const { return m_homeRuns; }
};

现在我们可以这样创建棒球运动员:

#include <iostream>

int main()
{
    BaseballPlayer pedro{ "Pedro Cerrano", 32, 0.342, 42 };

    std::cout << pedro.getName() << '\n';
    std::cout << pedro.getAge() << '\n';
    std::cout << pedro.getBattingAverage() << '\n';
    std::cout << pedro.getHomeRuns() << '\n';

    return 0;
}

这将输出:

image

如您所见,基类的姓名和年龄已正确初始化,派生类的本垒打数和打击率也已正确初始化。


继承链

继承链中的类完全以相同的方式运作。

#include <iostream>

class A
{
public:
    A(int a)
    {
        std::cout << "A: " << a << '\n';
    }
};

class B: public A
{
public:
    B(int a, double b)
    : A{ a }
    {
        std::cout << "B: " << b << '\n';
    }
};

class C: public B
{
public:
    C(int a, double b, char c)
    : B{ a, b }
    {
        std::cout << "C: " << c << '\n';
    }
};

int main()
{
    C c{ 5, 4.3, 'R' };

    return 0;
}

在此示例中,类 C 继承自类 B,而类 B 继承自类 A。那么当我们实例化类 C 的对象时会发生什么?

首先,main() 调用 C(int, double, char)。C 的构造函数调用 B(int, double)。B的构造函数调用A(int)。由于A未继承自任何类,这是首个被构造的类。A完成构造后输出值5,并将控制权交还给B。B完成构造后输出值4.3,并将控制权交还给C。C完成构造后输出字符串'R',并将控制权交还给main()。至此完成!

因此该程序输出:

image

值得一提的是,构造函数只能调用其直接父类/基类的构造函数。因此,C构造函数无法直接调用或向A构造函数传递参数。C构造函数只能调用B构造函数(该构造函数负责调用A构造函数)。


析构函数

当派生类被销毁时,每个析构函数将按构造时的逆序依次调用。在上例中,当销毁对象c时,将依次调用C构造函数、B构造函数、A构造函数。

警告
若基类包含虚函数,则构造函数也应声明为虚函数,否则在特定情况下会导致未定义行为。本主题将在第25.4节“虚构造函数、虚赋值与虚函数重写”中详细阐述。


总结

构造派生类时,由派生类构造函数决定调用哪个基类构造函数。若未指定基类构造函数,则使用基类的默认构造函数。若无法找到(或默认创建)基类默认构造函数,编译器将报错。此时类将按从最基类到最派生类的顺序依次构造。

至此,您已掌握足够的C++继承知识来创建自己的继承类!


测验时间!

让我们实现继承导入部分提到的水果示例。创建Fruit基类,包含两个私有成员:名称(std::string)和颜色(std::string)。创建继承自Fruit的Apple类,该类应额外拥有私有成员纤维(double)。再创建同样继承自Fruit的Banana类,该类不添加额外成员。

以下程序应能运行:

#include <iostream>

int main()
{
	const Apple a{ "Red delicious", "red", 4.2 };
	std::cout << a << '\n';

	const Banana b{ "Cavendish", "yellow" };
	std::cout << b << '\n';

	return 0;
}

并打印以下内容:

Apple(Red delicious, red, 4.2)
Banana(Cavendish, yellow)

提示:由于 a 和 b 是 const 类型,你需要注意 const 约束。确保参数和函数都正确使用 const 声明。

显示解决方案

#include <iostream>
#include <string>
#include <string_view>

class Fruit
{
private:
	std::string m_name;
	std::string m_color;

public:
	Fruit(std::string_view name, std::string_view color)
		: m_name{ name }, m_color{ color }
	{
	}

	const std::string& getName() const { return m_name; }
	const std::string& getColor() const { return m_color; }

};

class Apple : public Fruit
{
private:
	double m_fiber;

public:
	Apple(std::string_view name, std::string_view color, double fiber)
		:Fruit{ name, color },
		m_fiber{ fiber }
	{
	}

	double getFiber() const { return m_fiber; }

};

std::ostream& operator<<(std::ostream& out, const Apple& a)
{
	out << "Apple(" << a.getName() << ", " << a.getColor() << ", " << a.getFiber() << ')';
	return out;
}

class Banana : public Fruit
{
public:
	Banana(std::string_view name, std::string_view color)
		:Fruit{ name, color }
	{
	}
};

std::ostream& operator<<(std::ostream& out, const Banana& b)
{
	out << "Banana(" << b.getName() << ", " << b.getColor() << ')';
	return out;
}

int main()
{
	const Apple a{ "Red delicious", "red", 4.2 };
	std::cout << a << '\n';

	const Banana b{ "Cavendish", "yellow" };
	std::cout << b << '\n';

	return 0;
}
posted @ 2026-01-31 21:44  游翔  阅读(0)  评论(0)    收藏  举报