OOP: Bilpekarexemplet från föreläsning 8

Här är programmet bilpekarna-1.cpp, som är (en lite omflyttad variant av) det som jag skrev på tavlan på föreläsningen:
#include <iostream>
#include <string>

using namespace std;

// -------------- Klassen "Person" ---------------

class Person {
private:
  string namn;
  int alder;
public:
  Person(string namn, int alder);
};

Person::Person(string namn, int alder) {
  this->namn = namn;
  this->alder = alder;
}

// ---------------- Klassen "Bil" ----------------

class Motor {
private:
  double effekt;
public:
  Motor(double effekt);
};

Motor::Motor(double effekt) {
  this->effekt = effekt;
}

// --------------- Klassen "Motor" ---------------

class Bil {
private:
  string bilnr;
  Person* owner;
  Motor* motor;
public:
  Bil(string bilnr, Person* owner, Motor* motor);
  ~Bil();
};

Bil::Bil(string bilnr, Person* owner, Motor* motor) {
  this->bilnr = bilnr;
  this->motor = motor;
  this->owner = owner;
}

Bil::~Bil() {
  delete motor;
}

// -------------- Funktionen "main" --------------

int main() {
  Person goran("Göran", 58);
  Person* gudrunp = new Person("Gudrun", 57);
  Motor m1(110.0);
  Motor* m2p = new Motor(120.0);
  Bil bil1("RFN 540", gudrunp, m2p);
  Bil* bil2p = new Bil("BOS 118", &goran, &m1);

  delete bil2p;
  delete m2p;
  delete gudrunp;
}
Här i programmet bilpekarna-2.cpp har jag lagt till destruktorer i klasserna Person och Motor (inte bara i Bil), och lagt till utskrifter i alla destruktorerna, så man ser vad som händer:
#include <iostream>
#include <string>

using namespace std;

// -------------- Klassen "Person" ---------------

class Person {
private:
  string namn;
  int alder;
public:
  Person(string namn, int alder);
  ~Person();
};

Person::Person(string namn, int alder) {
  this->namn = namn;
  this->alder = alder;
}

Person::~Person() {
  cout << "Person-destruktorn körs. Personen " << namn << " tas bort.\n";
}

// ---------------- Klassen "Bil" ----------------

class Motor {
private:
  double effekt;
public:
  Motor(double effekt);
  ~Motor();
};

Motor::Motor(double effekt) {
  this->effekt = effekt;
}

Motor::~Motor() {
  cout << "Motor-destruktorn körs. Motorn på " << effekt << " hk tas bort.\n";
}

// --------------- Klassen "Motor" ---------------

class Bil {
private:
  string bilnr;
  Person* owner;
  Motor* motor;
public:
  Bil(string bilnr, Person* owner, Motor* motor);
  ~Bil();
};

Bil::Bil(string bilnr, Person* owner, Motor* motor) {
  this->bilnr = bilnr;
  this->motor = motor;
  this->owner = owner;
}

Bil::~Bil() {
  cout << "Bil-destruktorn körs. Bilen " << bilnr << " tas bort.\n";
  delete motor;
  cout << "Bil-destruktorn klar. Bilen " << bilnr << " har tagits bort.\n";
}

// -------------- Funktionen "main" --------------

int main() {
  Person goran("Göran", 58);
  Person* gudrunp = new Person("Gudrun", 57);
  Motor m1(110.0);
  Motor* m2p = new Motor(120.0);
  Bil bil1("RFN 540", gudrunp, m2p);
  Bil* bil2p = new Bil("BOS 118", &goran, &m1);

  cout << "Ska börja städa upp i main.\n";

  delete bil2p;
  delete m2p;
  delete gudrunp;

  cout << "Klar med main.\n";
}
Här är bilden från föreläsningen på hur datastrukturerna ser ut, precis före första delete-anropet:

Datastrukturerna

Bill Gates har varit snäll, så Visual Studio kollar faktiskt vad man gör, så programmet gör inte bara fel i tysthet, utan avslutas med ett felmeddelande:

Kraschmeddelande från Windows

Även GCC under Linux ger ett felmeddelande:

Ska börja städa upp i main.
Bil-destruktorn körs. Bilen BOS 118 tas bort.
Motor-destruktorn körs. Motorn på 110 hk tas bort.
*** glibc detected *** double free or corruption (out): 0xbfe2c5b0 ***
Abort (core dumped)
Felen i programmet är flera:
  1. Motorn m1 är en vanlig variabel, och inte en pekarvariabel som innehåller en pekare till en new-allokerad motor. Vi använder operatorn & för att göra en pekare till den, och skickar med den pekaren till konstruktorn när vi skapar bilen med bilnumret BOS 118. När Bil-klassens destruktor sen städar upp bilen BOS 118, försöker den ta bort den motorn med delete, men den motorn är ju inte allokerad med new, så man kan inte använda delete på den!
  2. Pekaren m2p pekar visserligen på en new-allokerad motor, och vi skickar den pekaren till konstruktorn när vi skapar bilen RFN 540. När Bil-klassens destruktor sen städar upp bilen RFN 540, så tar den bort den motorn med delete. Men, ajajaj, vi tar ju dessutom bort m2p-motorn med delete direkt i main-funktionen.
Saker som allokerats med new ska tas bort med delete en gång. Inte noll gånger, för då ligger de kvar och skräpar (i alla fall tills programmet avslutas), och inte flera gånger, för då kraschar programmet. Och om man har otur kanske programmet inte ens kraschar, utan ger någon annan sorts fel.

Några lärdomar som man bör ha med sig från den här övningen:

  1. Om man har krångliga datastrukturer, blir det lättare att förstå vad som händer om man ritar upp dem på ett papper.
  2. Om man har ett krångligt program, blir det lättare att se vad som händer om man lägger in spårutskrifter på lämpliga ställen. Det går också att stega sig fram i debuggern, men jag tycker att det ofta syns bättre vad som händer om man samtidigt kan titta på en hel sekvens av utskrifter.
  3. Om man skriver en destruktor i en klass så körs den vid delete-anrop, men också automatiskt när en vanlig variabel (t ex en lokal variabel i en funktion) försvinner.
  4. Det är krångligt och farligt att blanda new-allokerade objekt med pekare till vanliga variabler. Kompilatorn hittar inga fel, för i båda fallen är det pekare som pekar på objekt av en och samma klass, men det ena objektet är dynamiskt allokerat och det andra inte, och därför blir det fel om man tar bort dem med delete.
  5. Man måste hålla reda på vem som äger ett objekt, så man vet vem som ansvarar för att ta bort det med delete. I det här fallet har vi sagt att bilen äger sin motor, och när vi i main använder en motor (m2p) när vi bygger en bil (bil1), så är det ju den bilen (och inte main-funktionen) som i fortsättningen äger den motorn.
    (En annan lösning, som kan vara bättre, är att inte överföra ägarskapet, utan i stället låta bilen ha en kopia av motorn.)
  6. Automatisk skräpsamling, som i C++/CLI, Java, C# med flera språk, är inte så dumt.
Om vi ska få programmet att fungera kan vi skapa bägge motorerna med new, och sen förstås låta bli att ta bort dem en extra gång. Ur programmet bilpekarna-3.cpp:
// -------------- Funktionen "main" --------------

int main() {
  Person goran("Göran", 58);
  Person* gudrunp = new Person("Gudrun", 57);
  Motor* m1p = new Motor(110.0);
  Motor* m2p = new Motor(120.0);
  Bil bil1("RFN 540", gudrunp, m2p);
  Bil* bil2p = new Bil("BOS 118", &goran, m1p);

  cout << "Ska börja städa upp i main.\n";

  delete bil2p;
  // delete m1p; -- Nej! "Ägandet" har överförs till bilen.
  // delete m2p; -- Nej! "Ägandet" har överförs till bilen.
  delete gudrunp;

  cout << "Klar med main.\n";
}
Utskrifter från bilpekarna-3:
Ska börja städa upp i main.
Bil-destruktorn körs. Bilen BOS 118 tas bort.
Motor-destruktorn körs. Motorn på 110 hk tas bort.
Bil-destruktorn klar. Bilen BOS 118 har tagits bort.
Person-destruktorn körs. Personen Gudrun tas bort.
Klar med main.
Bil-destruktorn körs. Bilen RFN 540 tas bort.
Motor-destruktorn körs. Motorn på 120 hk tas bort.
Bil-destruktorn klar. Bilen RFN 540 har tagits bort.
Person-destruktorn körs. Personen Göran tas bort.
(Notera att de lokala variablerna i main, i det här fallet goran och bil1, städas bort efter att sista satsen i main har körts.)

Kanske blir det lättast att inte alls blanda vanliga och new-allokerade variabler. Ur programmet bilpekarna-4.cpp:

// -------------- Funktionen "main" --------------

int main() {
  Person* goranp = new Person("Göran", 58);
  Person* gudrunp = new Person("Gudrun", 57);
  Motor* m1p = new Motor(110.0);
  Motor* m2p = new Motor(120.0);
  Bil* bil1p = new Bil("RFN 540", gudrunp, m2p);
  Bil* bil2p = new Bil("BOS 118", goranp, m1p);

  cout << "Ska börja städa upp i main.\n";

  delete bil1p;
  delete bil2p;
  // delete m1p; -- Nej! "Ägandet" har överförs till bilen.
  // delete m2p; -- Nej! "Ägandet" har överförs till bilen.
  delete gudrunp;
  delete goranp;

  cout << "Klar med main.\n";
}
Utskrifter från bilpekarna-4:
Ska börja städa upp i main.
Bil-destruktorn körs. Bilen RFN 540 tas bort.
Motor-destruktorn körs. Motorn på 120 hk tas bort.
Bil-destruktorn klar. Bilen RFN 540 har tagits bort.
Bil-destruktorn körs. Bilen BOS 118 tas bort.
Motor-destruktorn körs. Motorn på 110 hk tas bort.
Bil-destruktorn klar. Bilen BOS 118 har tagits bort.
Person-destruktorn körs. Personen Gudrun tas bort.
Person-destruktorn körs. Personen Göran tas bort.
Klar med main.


Thomas Padron-McCarthy (Thomas.Padron-McCarthy@tech.oru.se), 11 oktober 2007