0

I am writing a dish washer program, Dishwasher has a pump, motor, and an ID. Pump, motor, date, time are other small classes which Dishwasher will use. I checked with the debugger but when I create the Dishwasher class, my desired values aren't initialized. I think I am doing something wrong but what? :(

So the Dishwasher class is below :

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

This is how I initialize the Class :

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

This is how I create the class:

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

Here is the full code if you would like to see further, because I removed some unused things for better readability : http://codepad.org/K4Bocuht

4

2 回答 2

4

首先,我无法克服您使用 achar*来改变字符串的事实。因为大声哭泣,std::string. 这是处理字符串的标准化方式。它可以有效地摆脱你扔给它的任何东西。无论是字符串文字、char 数组、char* 还是另一个字符串。

其次,你的教授需要专业的帮助。用更原始的东西来教授“低级直觉”是一回事,但将这一点强加给应该学习 C++ 的学生显然是不好的做法。

现在,到手头的问题上。我将仅举顶级示例,即位于 Dishwasher 构造函数中的原始“裸”指针,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,同样乐趣。

于 2012-06-11T01:42:53.107 回答
3

你违反了三法则。您可以通过以下两种方式之一解决您的问题:

  • 永远不要使用裸指针。在您的情况下,您可以通过char*在每个实例中替换为std::string.
  • 实现一个复制构造函数和一个复制赋值运算符,每个都必须执行一个深拷贝和一个析构函数。
于 2012-06-10T22:50:22.460 回答