C++基础教程面向对象(学习笔记(46))

本文深入探讨C++中派生类构造函数的工作原理,包括如何初始化基类成员,构造函数调用顺序,以及如何正确初始化继承的成员变量。通过示例,解释了在构造函数中初始化私有成员的最佳实践。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

构造函数和派生类的初始化

在过去的两节课中,我们探讨了C ++中继承的一些基础知识以及派生类初始化的顺序。在本课中,我们将详细介绍构造函数在派生类初始化中的作用。为此,我们将继续使用我们在上一课中开发的简单Base和Derived类:

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); // 使用Base(int)构造函数
 
    return 0;
}

以下是实例化base时实际发生的情况:

Base内存被搁置
调用适当的Base构造函数
用初始化列表初始化变量
构造函数的主体执行
控制权返回给调用者

这非常简单。使用派生类,事情稍微复杂一些:

int main()
{
    Derived derived(1.3); // 用Derived(double) 构造函数
 
    return 0;
}

以下是实例化派生时实际发生的情况:

将派生的内存留出(足够用于Base和Derived部分)
调用适当的Derived构造函数
首先使用适当的Base构造函数构造Base对象。如果未指定基础构造函数,则将使用默认构造函数。
用初始化列表初始化变量
构造函数的主体执行
控制权返回给调用者
这种情况与非继承情况之间唯一真正的区别是,在Derived构造函数可以执行任何实际操作之前,首先调用Base构造函数。Base构造函数设置对象的Base部分,控制权返回到Derived构造函数,并允许Derived构造函数完成其作用。

初始化基类成员

我们编写的Derived类的当前缺点之一是在创建Derived对象时无法初始化m_id。如果我们要在创建Derived对象时同时设置m_cost(来自对象的Derived部分)和m_id(来自对象的Base部分),该怎么办?

新程序员经常尝试解决此问题,如下所示:

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; }
};

这是一个很好的尝试,几乎是正确的想法。我们肯定需要在构造函数中添加另一个参数,否则C ++将无法知道我们想要将m_id初始化为什么值。

但是,C ++阻止类在构造函数的初始化列表中初始化继承的成员变量。换句话说,变量的值只能在属于与变量相同的类的构造函数的初始化列表中设置。

为什么C ++会这样做?答案与const和引用变量有关。考虑如果m_id是const会发生什么。因为const变量必须在创建时使用值初始化,所以基类构造函数必须在创建变量时设置其值。但是,当基类构造函数完成时,然后执行派生类构造函数初始化列表。然后,每个派生类都有机会初始化该变量,可能会改变其值!通过将变量的初始化限制为这些变量所属的类的构造函数,C ++确保所有变量只初始化一次。

最终结果是上面的示例不起作用,因为m_id是从Base继承的,并且只能在初始化列表中更改非继承的变量。

但是,继承的变量仍然可以使用赋值在构造函数的主体中更改其值。因此,新程序员通常也会尝试这样做:

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被赋值两次:一次在Base类构造函数的初始化列表中,然后再在Derived类构造函数的主体中。最后,如果Base类在构造期间需要访问此值,该怎么办?它无法访问它,因为在Derived构造函数执行之前它不会被设置(这几乎发生在最后)。

那么在创建Derived类对象时如何正确初始化m_id呢?

在到目前为止的所有示例中,当我们实例化Derived类对象时,使用默认的Base构造函数创建了Base类部分。为什么它总是使用默认的Base构造函数?因为我们从来没有告诉它不这样做!

幸运的是,C ++使我们能够明确选择将调用哪个Base类构造函数!为此,只需在派生类的初始化列表中添加对基类Constructor的调用:

class Derived: public Base
{
public:
    double m_cost;
 
    Derived(double cost=0.0, int id=0)
        : Base(id), // 调用Base(int) 构造函数初始化值 id!
            m_cost(cost)
    {
    }
 
    double getCost() const { return m_cost; }
};

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

int main()
{
    Derived derived(1.3, 5); // 使用Derived(double,int)构造函数
    std::cout << "Id: " << derived.getId() << '\n';
    std::cout << "Cost: " << derived.getCost() << '\n';
 
    return 0;
}

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

因此,该程序将打印:

Id:5
Cost:1.3
更详细地说,这是发生的事情:

派生的内存被分配。
调用Derived(double,int)构造函数,其中cost = 1.3,id = 5
编译器会查看我们是否要求使用特定的Base类构造函数。我们有!所以它调用id = 5的Base(int)。
基类构造函数初始化列表将m_id设置为5
基类构造函数体执行,什么都不做
基类构造函数返回
派生类构造函数初始化列表将m_cost设置为1.3
派生类构造函数体执行,什么都不做
派生类构造函数返回
这看起来有点复杂,但实际上非常简单。所有发生的事情是Derived构造函数调用特定的Base构造函数来初始化对象的Base部分。因为m_id位于对象的Base部分,所以Base构造函数是唯一可以初始化该值的构造函数。

请注意,在Derived构造函数初始化列表中调用Base构造函数的位置并不重要,它将始终首先执行。

现在我们可以让我们的成员设置为private

既然您已经知道如何初始化基类成员,那么就不需要公开我们的成员变量了。我们应该将我们的成员变量再次设为private。

作为一个快速复习,public成员可以被任何函数访问。私有成员只能由同一类的成员函数访问。请注意,这意味着派生类无法直接访问基类的私有成员!派生类将需要使用访问函数来访问基类的私有成员。

考虑:

class Base
{
private: // 我们的成员现在是private
    int m_id;
 
public:
    Base(int id=0)
        : m_id(id)
    {
    }
 
    int getId() const { return m_id; }
};
class Derived: public Base
{
private: // 我们的会员现在是 private
    double m_cost;
 
public:
    Derived(double cost=0.0, int id=0)
        : Base(id), // 使用值id调用Base(int)构造函数!
            m_cost(cost)
    {
    }
 
    double getCost() const { return m_cost; }
};
 
int main()
{
    Derived derived(1.3, 5); // 使用Derived(double,int)构造函数
    std::cout << "Id: " << derived.getId() << '\n';
    std::cout << "Cost: " << derived.getCost() << '\n';
 
    return 0;
}

在上面的代码中,我们将m_id和m_cost设为private。这很好,因为我们使用相关的构造函数来初始化它们,并使用public访问器来获取值。

按预期打印:

Id:5
Cost:1.3
我们将在下一课中详细讨论访问说明符。

另一个例子

让我们来看看我们之前使用过的另一对类:

#include <string>
class Person
{
public:
    std::string m_name;
    int m_age;
 
    Person(std::string name = "", int age = 0)
        : m_name(name), m_age(age )
    {
    }
 
    std::string getName() const { return m_name; }
    int getAge() const { return m_age; }
};
 
// BaseballPlayer公开继承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构造函数,它将名称初始化为空并且age为0.因为在创建它们时给我们的BaseballPlayer一个名称和年龄是有意义的,我们应该修改这个构造函数添加这些参数。

这是我们使用private成员的更新类,BaseballPlayer类调用相应的Person构造函数来初始化继承的Person成员变量:

#include <string>
class Person
{
private:
    std::string m_name;
    int m_age;
 
public:
    Person(std::string name = "", int age = 0)
        : m_name(name), m_age(age )
    {
    }
 
    std::string getName() const { return m_name; }
    int getAge() const { return m_age; }
 
};
// BaseballPlayer公开继承Person
class BaseballPlayer : public Person
{
private:
    double m_battingAverage;
    int m_homeRuns;
 
public:
    BaseballPlayer(std::string name = "", int age = 0,
        double battingAverage = 0.0, int homeRuns = 0)
        : Person(name, age), // 调用Person(std :: string,int)来初始化这些字段
            m_battingAverage(battingAverage), m_homeRuns(homeRuns)
    {
    }
 
    double getBattingAverage() const { return m_battingAverage; }
    int getHomeRuns() const { return m_homeRuns; }
};

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

int main()
{
    BaseballPlayer pedro("Pedro Cerrano", 32, 0.342, 42);
 
    std::cout << pedro.getName() << '\n';
    std::cout << pedro.getAge() << '\n';
    std::cout << pedro.getHomeRuns() << '\n';
 
    return 0;
}

这输出:

Pedro Cerrano
32
42
如您所见,基类的名称和年龄已正确初始化,派生类的本垒打数也是如此。

继承链

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

#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)。C构造函数调用B(int,double)。B构造函数调用A(int)。因为A不会从任何类继承,所以这是我们要构建的第一个类。A构造,打印值5,并将控制权返回到B.构造B,打印值4.3,并将控制权返回到C.构造C,打印值’R’,并将控制权返回到main()。我们所有的都完成了!

因此,该程序打印:

A: 5
B: 4.3
C: R

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

析构函数

当派生类被销毁时,每个析构函数都以相反的构造顺序被调用。在上面的例子中,当c被销毁时,首先调用C析构函数,然后调用B析构函数,然后调用A析构函数。

Summary:

构造派生类时,派生类构造函数负责确定调用哪个基类构造函数。如果未指定基类构造函数,则将使用默认基类构造函数。在这种情况下,如果找不到(或默认创建)默认基类构造函数,编译器将显示错误。然后按照从大多数到大多数派生的顺序构造类。

此时,您现在已经足够了解C ++继承以创建自己的继承类!

Quiz Time!

1)让我们实现我们在继承介绍中谈到的Fruit示例。创建一个包含两个私有成员的Fruit基类:名称(std :: string)和颜色(std :: string)。创建一个继承Fruit的Apple类。Apple应该有一个额外的private成员:fiber(double)。创建一个也继承Fruit的Banana类。香蕉没有其他成员。

应运行以下程序:

int main()
{
	const Apple a("Red delicious", "red", 4.2);
	std::cout << a;
 
	const Banana b("Cavendish", "yellow");
	std::cout << b;
 
	return 0;
}

并打印以下内容:

Apple(Red delicious,red,4.2)
Banana(Cavendish,yellow)
**提示:**因为a和b是const,所以你需要注意你的const。确保您的参数和函数是适当的常量。

解决方案

#include <string>
#include <iostream>
class Fruit
{
private:
	std::string m_name;
	std::string m_color;
 
public:
	Fruit(std::string name, std::string color)
		: m_name(name), m_color(color)
	{
	}
 
	std::string getName() const { return m_name; }
	std::string getColor() const { return m_color; }
 
};
 
class Apple : public Fruit
{
private:
	double m_fiber;
 
public:
	Apple(std::string name, std::string color, double fiber)
		:Fruit(name, color), m_fiber(fiber)
	{
	}
 
	double getFiber() const { return m_fiber; }
 
	friend std::ostream& operator<<(std::ostream &out, const Apple &a)
	{
		out << "Apple (" << a.getName() << ", " << a.getColor() << ", " << a.getFiber() << ")\n";
		return out;
	}
};
 
class Banana : public Fruit
{
public:
	Banana(std::string name, std::string color)
		:Fruit(name, color)
	{
	}
 
	friend std::ostream& operator<<(std::ostream &out, const Banana &b)
	{
		out << "Banana (" << b.getName() << ", " << b.getColor() << ")\n";
		return out;
	}
 
};
 
int main()
{
	const Apple a("Red delicious", "red", 4.2);
	std::cout << a;
 
	const Banana b("Cavendish", "yellow");
	std::cout << b;
 
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值