C ++初始化列表,类中的类(聚合)

时间:2012-06-10 22:34:14

标签: c++ class aggregation initialization-list

我正在写一个洗碗机程序,洗碗机有一个泵,电机和一个ID。泵,马达,日期,时间是洗碗机将使用的其他小类。我检查了调试器但是当我创建Dishwasher类时,我的所需值没有初始化。我想我做错了什么但是什么? :(

所以洗碗机课程如下:

class Dishwasher {
    Pump pump; // the pump inside the dishwasher
    Motor motor;// the motor inside the dishwasher

    char* washer_id;//011220001032 means first of December 2000 at 10:32h
    Time time_built;// Time variable, when the Dishwasher was built
    Date date_built;// Date variable, when the Dishwasher was built

    Time washing_time;      // a time object, like 1:15 h
public:
    Dishwasher(Pump, Motor, char*, float);
}; 

这是我初始化类的方法:

Dishwasher::Dishwasher(Pump p, Motor m, char *str, float f) 
    : pump(p), motor(m), max_load(f)
{
    washer_id = new char [(strlen(str)+1)];
    strcpy (washer_id,str);
    time_built.id2time(washer_id);
    date_built.id2date(washer_id);
}

这就是我创建类的方法:

Dishwasher siemens( 
        Pump(160, "011219991143"), 
        Motor(1300, "081220031201"), 
        "010720081032", 
        17.5);

如果您希望进一步了解,请参阅以下完整代码,因为我删除了一些未使用的内容以提高可读性:http://codepad.org/K4Bocuht

2 个答案:

答案 0 :(得分:4)

首先,我无法克服你使用char*来改变字符串的事实。大声喊叫,std::string。它是处理一串字符的标准化方法。它可以有效地摆脱你抛出的任何东西。无论是字符串文字,字符数组,字符*还是另一个字符串。

其次,你的教授需要专业的帮助。教导"较低层次的直觉"是一回事。有一些更原始的东西,但要对那些应该学习C ++的学生强制执行这一点是非常糟糕的做法。

现在,解决手头的问题。我只是采取最优秀的例子,即原始的,#34;裸体"指针位于您的洗碗机构造函数char* str中。如您所知,那是指针,即指向类型char的指针。指针存储变量的内存地址(任何类型的变量的第一个字节,它是内存中最低的可寻址单元)。

这种明显的差异非常重要。为什么?因为当您指定其他内容时,您不会复制实际对象,而只复制其第一个字节的地址。所以,实际上,你只需要指向同一个对象的两个指针。

就像你一样,毫无疑问,一个好记忆的公民,你可能会定义一个析构函数来处理这个问题:

washer_id = new char [(strlen(str)+1)];

你基本上在堆上分配strlen(str)+1个字节,这是系统无法管理的,并且仍然在你能干的手中。因此这个名字,一堆。一堆东​​西,失去对它的引用,你永远不会再找到它,你可以把它扔掉(当程序从main返回时所有泄漏实际发生的事情)。因此,您有责任告诉系统何时完成它。你做了,定义了一个析构函数,就是这样。

一个大而讨厌但......

但是......这个方案存在问题。你有一个构造函数。还有一个析构函数。其中管理资源分配和解除分配。但是复制呢?

Dishwasher siemens( 
        Pump(160, "011219991143"), 
        Motor(1300, "081220031201"), 
        "010720081032", 
        17.5);

您可能知道编译器将尝试隐式创建基本复制构造函数,复制赋值运算符(对于已构造的对象)和析构函数。由于隐式生成的析构函数无法手动释放(我们讨论了动态内存和我们的责任),因此它是空的。

因为我们使用动态内存并分配不同大小的字节块来存储我们的文本,所以我们有一个析构函数(如你的长代码所示)。这一切都很好,但我们仍然处理隐式生成的复制构造函数和复制直接值变量的复制赋值运算符。由于指针是一个值为内存地址的变量,隐式生成的复制构造函数或复制赋值操作符所做的只是复制该内存地址(这称为浅拷贝),它只创建另一个引用内存中唯一的单数字节(以及连续块的其余部分)。

我们想要的是一个深度拷贝,用于分配新内存并复制实际对象(或任何复合多字节类型,数组数据结构)存储在输入指针的存储器地址中。这样,它们将指向不同的对象,其生命周期与包络对象的生命周期相关,不是从中复制的对象的生命周期

在上面的例子中观察到你正在堆栈上创建临时对象,这些对象在构造器运行时是活动的,之后它们被释放并且它们的析构函数被调用。

Dishwasher::Dishwasher(Pump p, Motor m, char *str, float f) 
    : pump(p), motor(m), max_load(f)
{
    washer_id = new char [(strlen(str)+1)];
    strcpy (washer_id,str);
    time_built.id2time(washer_id);
    date_built.id2date(washer_id);
}

初始化列表还有一个好处,即不将对象初始化为默认值然后执行复制,因为您有幸直接调用复制构造函数(在这种情况下由编译器隐式生成):

pump(p)基本上是调用Pump :: Pump(const Pump&)并传入临时对象已初始化的值。 Pump类包含一个char *,它将第一个字节的地址保存到您推入临时对象的字符串文字中:Pump(160, "011219991143")

复制构造函数获取临时对象并复制显式可用的所有数据,这意味着它只获取char *指针中包含的地址,而不是整个字符串。因此,您最终会从两个地方指向同一个对象。

由于临时对象存在于堆栈中,一旦构造函数完成处理它们,它们将被释放并释放它们的析构函数。这些析构函数实际上会破坏字符串以及创建Dishwasher对象时放置的字符串。现在,Dishwasher对象中的Pump对象拥有一个悬空指针,指向在无尽记忆深渊中丢失的对象的内存地址。

解?

编写自己的复制构造函数和复制赋值运算符重载。以泵为例:

Pump(const Pump &pumpSrc) // copy constructor

Pump& operator=(const Pump &pumpSrc) // copy assignment operator overload

执行此foreach课程。

当然,除了你已经拥有的析构函数。这三个是经验法则的主角,称为“三个法则"它声明如果你必须明确地声明它们中的任何一个,你可能也需要明确地声明它们的其余部分。

规则的可能部分基本上是无责任的。当你使用C ++进一步发展时,对显式定义的需求实际上非常明显。例如,考虑你的班级做什么是确定你是否需要明确定义的所有内容的好方法。

示例:您的类依赖于指向一块内存的裸指针,而内存地址就是它所带来的全部内容。它是一个简单的类型变量,它只保存一个内存地址到相关对象的第一个字节。

为了防止在销毁对象时发生泄漏,您定义了一个析构函数来释放分配的内存。但是,你在对象之间复制数据吗?很可能,正如你所看到的那样。有时您将创建一个临时对象,该对象将分配数据并将内存地址存储到您将复制其值的指针数据成员中。过了一会儿,这个临时一定会在某个时候被破坏,你将失去它的数据。你唯一剩下的就是带有无用且危险的内存地址的悬空指针。

官方免责声明有一些简化措施可以阻止我写一本关于这个主题的书。此外,我总是试着专注于手头的问题,而不是OP的代码,这意味着我不会对所采用的做法发表评论。代码可能是可怕的,美丽的或介于两者之间的任何东西。但我并没有试图改变代码或OP,我只是试着回答这个问题,并且有时会提出一些我认为从长远来看对OP有益的建议。是的,我们可以准确地说是地狱并且限定一切......在我们大胆地试图说明2 + 3 = 3 + 2 = 5之前,我们也可以使用Peano公理来定义数字集和基本算术运算很有趣。

答案 1 :(得分:3)

您违反了Rule of Three。您可以通过以下两种方式解决问题:

  • 永远不要使用裸指针。在您的情况下,您可以通过将char*替换为每个实例中的std::string
  • 实现复制构造函数和复制赋值运算符,每个运算符都必须执行深层复制和析构函数。