Egzamin - funkcje wirtualne, konstruktory

0

Witam, mam do rozkminienia takie zadanie. "Co zostanie wypisane w jakiej kolejności i dlaczego?". Co to funkcje wirtualne mniej więcej wiem, nie miałem okazji korzystać z nich w swoich programach (chyba że testowo do nauki) a konstruktory/destruktory to oczywista oczywistość. Jednak nie bardzo wiem czym się kierować w tego typu zadaniach, jak to działa

#include <iostream>
#include <list>
#include <string>
 
class A
{
  public: 
  virtual void f() 
  {
  printf("A.f ");
  }
  A()
  {
  f();
  }
};
 
class B : public A
{
public:
  void f()
  {
  printf("B.f ");
  }
  B()
  {
 f();
  }
  ~B
  {
  printf("~B.f ");
  }
};

B b; //uwaga!

int main()
{
  A*a = new B();
  printf("M ");
  delete a;
  return 0;
}

wypisane zostaje

A.f B.f A.f B.f M ~B.f 

Wiem tylko dlaczego pierwsze 2 się wyświetlają bo przy tworzeniu obiektu w ten sposób

B b;

jest wywoływany i konstruktor i destruktor a przy new tylko konstruktor.

Pozdrawiam

0

Najpierw jest tworzony globalny obiekt klasy B (przed wywołaniem main()). Tworzenie B to:

  1. Wywołanie konstruktora klasy bazowej,
  2. Uruchomienie własnego konstruktora.
    Konstruktor klasy bazowej wywołuje wirtualną funkcję f(). Jest ona wirtualna i przeciążona w klasie B, co sugerowałoby uruchomienie B::f(), ale w tym momencie jest wywoływana z kontekstu obiektu klasy A, bo to jej konstruktor jest wykonywany. Dlatego też wypisywane jest "A.f".

Po zakończeniu działania konstruktora A wykonywany jest konstruktor B, który wypisuje (tu bez zaskoczeń) "B.f ". I na tym kończy się tworzenie obiektu globalnego b. Rozpoczyna się wywołanie main().

Pierwszą operacją w main() jest utworzenie na stercie dynamicznego obiektu klasy B. Jego tworzenie przebiega dokładnie tak samo, jak poprzednio (konstruktor klasy bazowej -> konstruktor własny).

Później printf() wypisuje stringa "M ".

Na koniec obiekt dynamicznie utworzony jest usuwany, za pomocą delete, co powoduje wywołanie jego destruktora, co wypisuje "~B.f ".

Obiekt globalny nie ma wywołanego destruktora dlatego, że zmienne globalne są usuwane po zakończeniu wykonania całego programu (po main()). Dlatego wywołano tyko jeden destruktor – po obiekcie globalnym sprząta już OS.</del>
Na koniec wywołany jest destruktor obiektu globalnego b. Destruktor a nie wywołuje się, bo delete wywołany jest na wskaźniku na klasę bazową, która destruktora nie ma. Co innego, gdyby destruktor A był wirtualny (choćby pusty).

0

Dzięki za odp. Ja trochę zedytowałem ten post, mam nadzieję że odpisałeś do aktualnego ;]

Ok czyli na początek taka zasada:

  1. Konstruktor klasy bazowej.
  2. Konstruktor klasy pochodnej.

Tylko jak się to ma do funkcji wirtualnych i tworzena obiektu na podstawie szablonu innej klasy (tzn. jaka tu jest reguła?). Kiedy tak się dzieje jak powyżej?

A*a = new B()
0

Ok dzięki. Inaczej teraz bo trochę zamotałem:

  1. Wiemy że B dziedziczy z A. Dlaczego więc tworząc obiekt klasy B
B b;

odpalany jest konstruktor klasy A? To przecież bardzo niebezpieczne. Ja tworząc obiekt klasy B nie chcę aby mi się wywoływały konstruktory klas z których dziedziczę.
2. Jak się mają do tego wszystkiego funkcje wirtualne, czy zmieniają priorytet wykonywania się funkcji/konstruktorów? Jak usunąłem słówko [i]virtual[/i] to nic się nie zmieniło.

1

Nic się nie zmieniło, ponieważ funkcje wirtualne wywołane z konstruktora zachowują się jak funkcji normalne.

0

A skoro usuwam obiekt klasy B, która ma destruktor czemu 2 razy nie pokazało się ~B.f ? Usuwam dynamiczny delete'm a potem OS usuwa na końcu ten z B b;

1

@rincewind: to z destruktorem obiektu globalnego to jest nieprawda.

@MateuszS: dzieje się tak zawsze, tak samo z destruktorami, tyle, że one są odpalane w przeciwnej kolejności(najpierw pochodnej, potem bazowej).

Podsumowując:

  1. pierwsze dwa to konstruktory wywoływane przy tworzeniu tego globalnego obiektu b (najpierw klasy bazowej, potem pochodnej)
  2. następne dwa to to dynamiczne tworzenie obiektu klasy B
  3. M to ten printf przed delete
  4. ostatnie to odpalenie destruktora obiektu b! Tak, tego globalnego (najpierw pochodnej, potem bazowej[którego nie ma])

WAŻNE!: delete w tym wypadku nie wywoła destruktora B ponieważ wskaźnik jest typu A, a w klasie A nie ma destruktora! Dużym błędem tutaj jest brak wirtualnego destruktora w klasie A. Wystarczy, że dopiszesz w niej virutal ~A(){} wtedy przy wywołaniu tego delete a; wywoła się destruktor B, ponieważ kompilator będzie o istnieniu takiego wiedział(vtables).

0

Masz tutaj inny przykład:

#include <iostream>
#include <list>
#include <string>

class A
{
public: 
	virtual void foo() { printf("A.foo()\n"); }
	void bar() { printf("A.bar()\n"); }
	
	A() { printf("A()\n"); }
	virtual ~A() { printf("~A()\n"); }
};

class B : public A
{
public:
	void foo() { printf("B.foo()\n"); }
	void bar() { printf("B.bar()\n"); }
	B() { printf("B()\n"); }
	~B() { printf("~B()\n"); }
};

int main()
{
	A*a = new B();
	a->foo();
	a->bar();
	delete a;
	return 0;
}

Zauważ, że metoda foo() jest wirtualna, a bar() nie. Zobacz, co dzieje się przy wywołaniu tych metod w main():

A()
B()
B.foo()
A.bar()
~B()
~A()

Nowy obiekt klasy B został przypisany do wskaźnika na klasę bazową. I tu pojawia się różnica między metodami wirtualnymi i nie. Przy wywołaniu foo() (wirtualna) przez wskaźnik na klasę bazową została użyta wersja z klasy pochodnej, czyli takiej, jaka została stworzona – B::foo(). Natomiast metoda niewirtualna nie ma takiej właściwości: została wywołana ze wskaźnika na A, więc wywołała A::bar() – mimo, że wskaźnik pokazuje na obiekt klasy B.

Zauważ też, że A ma tutaj wirtualny destruktor – dzięki temu przy usuwaniu poprzez wskaźnik na klasę bazową wywoła się właściwy destruktor, a nie tylko ten dla klasy bazowej. Dlatego też „minąłem się z prawdą” przy tłumaczeniu, co zauważył @byku_guzio, dlaczego w twoim przykładzie tylko raz wywołał się destruktor: tylko raz dlatego, że delete było na wskaźniku na klasę bazową, która destruktora nie ma.

0

No teraz juz zaczynam powoli kminić, tylko jeszcze jedno: dlaczego gdy zamieniam funkcję main do takiej postaci:

        B b;
        b.foo();
        b.bar();

Ok, najpierw wywołało konstruktor klasy bazowej, potem pochodnej, ale potem już funkcja wirtualna nie zadziałała i zamiast A.bar() mam B.bar() wywołane. Rozumiem to tylko dla interpretacji wskaznikowej jest tak jak napisales?

1

Wirtualność polega właśnie na tym, że mimo wskaźnika typu klasy bazowej wywoływana jest metoda dla właściwego typu. Nie ma co oczekiwać, że w tym przykładzie wywoła się A::bar(), bo przecież jest on przeciążony w klasie pochodnej. Specjalne działanie metod wirtualnych odnosi się tylko do wskaźników i referencji.

Edit W krótkich słowach. Jeśli metoda nie jest wirtualna, to przy wywołaniu jej jest brany pod uwagę tylko typ wskaźnika/referencji. Natomiast przy wywołaniu metody wirtualnej brany jest pod uwagę typ obiektu na który jest dany wskaźnik/referencja.

1

Warto by jeszcze wspomnieć o czymś pozornie prostym (a często zapominanym): gdy tworzony jest obiekt klasy pochodnej to na etapie wywołania konstruktora klasy bazowej obiekt pochodny jeszcze nie istnieje! Wszystkie funkcje wirtualne wywołane z konstruktora klasy bazowej zostaną wywołane co najwyżej z tego poziomu.

1 użytkowników online, w tym zalogowanych: 0, gości: 1