Diskuze problematických příkladů v rámci vnitrosemestrální písemky z PB161 (2013)

Příklad 1 - práva pro zapouzdření (skupina 14 hod)

Myšlenka příkladu: Nastavit správně práva pro atributy a metody tak, aby kód dodržoval pravidla zpouzdření.

Obecná pravidla:

  • Atributy třídy by neměly být public. Typicky jsou private. V případě, že chceme umožnit přístup z potomků (použito jen výjimečně), nastavíme na protected.
  • Metody třídy, které chceme zveřejnit pro okolí (stává se součástí rozhranní třídy) jsou public. Jako private nastavujeme metody, které třída využívá pro svoji vnitřní potřebu. Jako protected označujeme metody, které jsou přístupné našim potomkům, ale ne světu.
class A {
_PRAVO1_
  A(int value) : m_value(value) {}
public:
  virtual int GetValue() const = 0;
  virtual void SetValue(int value) {
    m_value = value;
  }
_PRAVO2_
  int m_value;
};
 
class B : _PRAVO3_ A {
_PRAVO4_
    B(int value) : A(value) {}
    int GetValue() const { return m_value; }
};
 
int main() {
  B test(10);
  test.SetValue(11);
  int value = test.GetValue();
  return 0;
}

Problém: Jako korektní byly označeny v odpovědníku tyto odpovědi:

  • A) _PRAVO1_ = private:, _PRAVO2_ = protected:, _PRAVO3_ = public, _PRAVO4_ = public:
    • Chybná možnost. Znamená, že třída A má privátní konstruktor a ten tedy nelze volat (ani z okolí, ani z potomků). Třída B se jej snaží zavolat ve svém konstruktoru (B(int value) : A(value) {}), díky čemuž nepůjde uvedený kód vůbec přeložit ( error: 'A::A(int)' is private).
  • B) _PRAVO1_ = protected:, _PRAVO2_ = protected:, _PRAVO3_ = public, _PRAVO4_ = public:
    • Korektní možnost. Konstruktor třídy A je dostupný pouze pro potomky, stejně tak atribut m_value.

Příklad 2 - práva pro zapouzdření (skupina 15 hod)

Myšlenka příkladu i obecná pravidla: stejné jako pro Příklad 1.

class A {
_PRAVO1_
  A(int value) : m_value(value) {}
public:
  int GetValue() const { return m_value; }
  virtual void SetValue(int value) {
    m_value = value;
  }
_PRAVO2_
  int m_value;
};
 
class B : _PRAVO3_ A {
_PRAVO4_
  B(int value) : A(value) {}
  int GetValue() const {
    return A::GetValue();
  }
};
 
int main() {
  B test(10);
  test.SetValue(11);
  int value = test.GetValue();
  return 0;
}

Problém: Jako korektní byly označeny v odpovědníku tyto odpovědi:

  • A) _PRAVO1_ = private:, _PRAVO2_ = private:, _PRAVO3_ = public, _PRAVO4_ = public:
    • Chybná možnost. Znamená, že třída A má privátní konstruktor a ten tedy nelze volat (ani z okolí, ani z potomků). Třída B se jej snaží zavolat ve svém konstruktoru (B(int value) : A(value) {}), díky čemuž nepůjde uvedený kód vůbec přeložit ( error: 'A::A(int)' is private).
  • B) _PRAVO1_ = protected:, _PRAVO2_ = private:, _PRAVO3_ = public, _PRAVO4_ = public:
    • Korektní možnost. Konstruktor třídy A je dostupný pouze pro potomky. Atribut m_value nemusí být ani dostupný pro potomky, pro přístup se používá A::GetValue().

Příklad 3 - virtuální destruktor a memory leaks (skupina 14 hod)

Myšlenka příkladu: Pokud vytváříme třídu A, která je určená pro dědění, tak je vhodné udělat destruktor virtuální. Potomek B tak má možnost ve svém destruktoru uklidit jím alokované dynamické struktury i v případě, že je nejprve přetypován na A a nad tímto typem je pak voláno delete (A* obj = new B; delete obj;).

class A {
protected:
  void release() {}
public:
  virtual ~A() { release(); }
};
 
class B : public A {
  int* m_array;
public:
  B(int length) {m_array = new int[length];}
  void release() {
    if (m_array)delete[] m_array;
  }
};
 
int main() {
    A* object1 = new B(10);
    B* object2 = new B(10);
 
    delete object1;
    delete object2;
 
    return 0;
}

Problém: Jako korektní byly označeny v odpovědníku tyto odpovědi:

  • A) Uvedený kód způsobí memory leak jednoho pole o velikosti new int[10].
    • Chybná možnost. Kód způsobí ve skutečnosti dva memory leaky, každý o velikosti new int[10]. Jde o pole alokované v konstruktoru třídy B.
    • A* object1 = new B(10); způsobí memory leak, protože A má sice virtuální destruktor, ale B neposkytuje vlastní destruktor se zavoláním úklidové metody release(). Volání metody release() v destruktoru A nepomůže, neboť se zavolá A::release(), nikoli B::release(), protože release() není virtuální. Ale ani deklarace metody A::release() jako virtuální by nepomohlo (viz. druhá možnost).
    • B* object2 = new B(10); způsobí memory leak ze stejného důvodu, jako předchozí možnost.
  • B) Pokud bychom deklarovali metodu release() třídy B jako virtuální, tak by uvedený kód nezpůsobil žádný memory leak.
    • Chybná možnost. Deklarace metody B::release() jako virtuální neudělá metodu v předkovi A::release() virtuální. I kdybychom deklarovali A::release() jako virtuální, tak by řešení nefungovalo - v době volání destruktoru třídy A již třída B neexistuje a nemůže se tak volat jeho metoda. Více informací: http://www.artima.com/cppsource/nevercall.html

Příklad 4 - virtuální destruktor a memory leaks (skupina 15 hod)

Myšlenka příkladu: stejná jako pro Příklad 3.

Poučení: Z konstruktoru ani destruktoru nevolejme virtuální metody - potomci, kteří danou metodu překrývají, již v době volání destruktoru neexistují. Více informací: http://www.artima.com/cppsource/nevercall.html

class A {
protected:
  virtual void release() {}
public:
  ~A() { release(); }
};
 
class B : public A {
  int* m_array;
public:
  B(int length) { m_array = new int[length]; }
  void release() {
    if (m_array) delete[] m_array;
  }
  ~B() { release(); }
};
 
int main() {
    A* object1 = new B(10);
    B* object2 = new B(10);
 
    delete object1;
    delete object2;
 
    return 0;
}

Problém: Jako korektní byly označeny v odpovědníku tyto odpovědi:

  • A) Uvedený kód způsobí memory leak jednoho pole alokovaného jako new int[10].
    • Korektní možnost. Jde o pole alokované v konstruktoru třídy B příkazem A* object1 = new B(10);. A nemá virtuální destruktor a proto se nezavolá destruktor třídy B, pouze destruktor třídy A. Ten sice volá virtuální metodu release(), ale ta se přeloží na volání A::release(), neboť část objektu odpovídající třídě B je již v době volání ~A zrušena.
  • B) Pokud bychom deklarovali destruktor třídy A jako virtuální, tak by uvedený kód nezpůsobil žádný memory leak.
    • Korektní možnost. Protože je destruktor virtuální, zavolá se nejprve destruktor třídy B, který zavolá B::release() a korektně uklidí.

Příklad 5 & 6 - vztahy dědičnosti (skupina 14 i 15 hod)

Myšlenka příkladu: Potomek jde přetypovat na předka, předek nelze přetypovat na potomka.

int main() {
    B* obj1 = new A;
    C* obj2 = new C;
    C* obj3 = new A;
    return 0;
}

Problém: Text otázky není logicky dobře položen.

  • Původní formulace byla „Který z uvedených vztahů dědičnosti mezi třídami A, B, C platí v případě, že uvedený kód lze zkompilovat?“. Pro některé odpovědi kód zkompilovat nelze kvůli přetypování předka na potomka. Pro některé zkompilovat lze, ale díky nevhodné formulaci otázky může existovat ještě jiný (opačný) vztah, pro který půjde kód také zkompilovat.
  • Odpověď „A je potomek B, A je potomek C, B není potomek C“ je označena jako správná a pro tyto popsané vztahy kód opravdu zkompilovat lze. Pokud by ale platilo, že B bude potomek C, tak kód půjde stále zkompilovat. Takže kód může jít zkompilovat, i když „A je potomek B, A je potomek C, B není potomek C“ neplatí.
  • Vhodnější formulace by byla např. „Pro které z uvedených možností jde kód zkompilovat?“.
QR Code
QR Code public:pb161_vnitro_2013 (generated for current page)