Obiektowość na AVR od podstaw 5 z n...

Tu poruszamy tematy związane z pisaniem programów w języku C++ dla AVR.
Awatar użytkownika
mokrowski
User
User
Posty: 190
Rejestracja: czwartek 08 paź 2015, 20:50
Lokalizacja: Tam gdzie Centymetro

Obiektowość na AVR od podstaw 5 z n...

Postautor: mokrowski » sobota 24 cze 2017, 04:30

Dziś czeka nas trochę zaskoczeń :-) Będziemy chcieli obsłużyć nasze obiekty dynamicznie :-P

Zanim jednak zaczniemy, przypomnę komplet plików które są wyjściem do tej części. Dziś będziemy stosowali new i delete, stąd pojawią się także odpowiednie pliki. Dodamy także konstruktory i destruktory obiektów aby obserwować jak wygląda ich cykl życia.

main.cpp:

Kod: Zaznacz cały

#include <avr/io.h>
#include "MyClass.hpp"

int main(void) {
   // PortC out dir...
   DDRC = 0xFF;

   MigaczSzybki myMigacz = MigaczSzybki();
   Migacz myMigacz2 = myMigacz;

   myMigacz.migaj();

}

MyClass.hpp:

Kod: Zaznacz cały

#ifndef MYCLASS_HPP_
#define MYCLASS_HPP_

#include <avr/io.h>
#include "util/delay.h"

class Migacz {
public:
   Migacz() { }

   void migaj() const {
      while(true) {
         PORTC ^= (1 << PIN7);
         _delay_ms(500);
      }
   }
   ~Migacz() {}
};

class MigaczSzybki : public Migacz {
public:
   MigaczSzybki() { }

   void migaj() const {
      while(true) {
         PORTC ^= (1 << PIN6);
         _delay_ms(500);
      }
   }
   void migajSzybko() const {
      while(true) {
         PORTC ^= (1 << PIN6);
         _delay_ms(100);
      }
   }
   ~MigaczSzybki() { }
};

#endif /* MYCLASS_HPP_ */


new.cpp:

Kod: Zaznacz cały

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#include "new.hpp"


// XXX: Implementacja osługi wyjątków? Na MCU chyba nie jest potrzebna :)

// new i new[]
void * operator new(size_t size) {
    return malloc(size);
}

void * operator new[](size_t size) {
    return malloc(size);
}

// Naiwna implementacja new w miejscu
void * operator new(size_t, void * ptr) {
   return ptr;
}

void * operator new[](size_t, void * ptr) {
    return ptr;
}

// delete i delete[]
void operator delete(void* ptr) {
    free(ptr);
}

void operator delete[](void* ptr) {
    free(ptr);
}

// Naiwna implementacja delete w miejscu.

void operator delete(void *, void * ptr) {
   free(ptr);
}

void operator delete[](void *, void *ptr) {
   free(ptr);
}

namespace {

inline char& flag_part(__guard *g) {
   return *(reinterpret_cast<char*>(g));
}

inline uint8_t& sreg_part(__guard *g) {
   return *(reinterpret_cast<uint8_t*>(g) + sizeof(char));
}

} // anonymous namespace

int __cxa_guard_acquire(__guard *g) {
    uint8_t oldSREG = SREG;
    cli();
    // Inicjalizacja zmiennej statycznej powinna być bezpieczna wielowątkowo.
    // Jeśli chcesz zrezygnować z tego mechanizmu, kompiluj z:
    // -fno-threadsafe-statics
    if (flag_part(g)) {
        SREG = oldSREG;
        return false;
    } else {
        sreg_part(g) = oldSREG;
        return true;
    }
//   return !*(char *)(g);
}
void __cxa_guard_release(__guard *g) {
   flag_part(g) = 1;
   SREG = sreg_part(g);
   //   *(char *)g = 1;
}
void __cxa_guard_abort (__guard *g) {
   SREG = sreg_part(g);
}

void __cxa_pure_virtual(void) {
   // Zgodnie ze standardem powinno być std::terminate()
   abort();
}
void __cxa_deleted_virtual(void) {
   // Zgodnie ze standardem powinno być std::terminate()
   abort();
}


new.hpp:

Kod: Zaznacz cały

#ifndef NEW_HPP_
#define NEW_HPP_

//
// avr-libc nie dostarcza operatorów new i delete.
// Ma także problemy z definicją obsługi dziedziczenia i dziedziczenia
// wirtualnego. Plik poprawia te problemy.
// Źródło:
//  http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=59453
//

#include <stdlib.h>

#ifdef __cplusplus

// Operatory new, new[]
extern void * operator new(size_t size);
extern void * operator new[](size_t size);

// Operatory new i new[] alokujące w miejscu
extern void * operator new(size_t size, void *ptr);
extern void * operator new[](size_t size, void *ptr);

// Operatory delete i delete[]
extern void operator delete(void *ptr);
extern void operator delete[](void *ptr);

// Operatory delete i delete[] alokujące w miejscu
extern void operator delete(void *srcPtr, void *dstPtr);
extern void operator delete[](void *srcPtr, void *dstPtr);

// Obsługa dziedziczeń i dziedziczeń wirtualnych...
__extension__ typedef int __guard __attribute__((mode (__DI__)));

extern "C" {
   int __cxa_guard_acquire(__guard *);
   void __cxa_guard_release (__guard *);
   void __cxa_guard_abort (__guard *);

   void __cxa_pure_virtual(void) __attribute__ ((__noreturn__));
   void __cxa_deleted_virtual(void) __attribute__ ((__noreturn__));
}

#endif // __cplusplus

#endif /* NEW_HPP_ */

Zaczynamy...

Aby tworzyć dynamicznie obiekt, jak zapewne wiesz (bo o tym wspominałem :-) ), używasz new. Od razu przejdziemy do kodu w main.cpp aby to pokazać. new zwraca wskaźnik więc także wywołanie metody się zmieni z myMigacz.migaj() na myMigacz->migaj().

main.cpp:

Kod: Zaznacz cały

#include <avr/io.h>
#include "MyClass.hpp"

int main(void) {
   // PortC out dir...
   DDRC = 0xFF;

   MigaczSzybki * myMigacz = new MigaczSzybki();
   Migacz * myMigacz2 = myMigacz;

   myMigacz->migaj();

}

Sprawdźmy jak będzie wyglądała konstrukcja i destrukcja takiego obiektu. W tym celu dla klasy Migacz zapalę w trakcie destrukcji diodę 0 a dla MigaczSzybki diodę 1 a przy konstrukcji Migacz diodę 4 a dla MigaczSzybki diodę 5. To stosunkowo prosty zabieg w kodzie, tu go umieszczę w całości abyś mógł/mogła sprawnie to zaimplementować.

MyClass.hpp:

Kod: Zaznacz cały

#ifndef MYCLASS_HPP_
#define MYCLASS_HPP_

#include <avr/io.h>
#include "util/delay.h"

class Migacz {
public:
   Migacz() {
      PORTC |= ( 1 << PIN4);
   }
   void migaj() const {
      while(true) {
         PORTC ^= (1 << PIN7);
         _delay_ms(500);
      }
   }
   ~Migacz() {
      PORTC |= (1 << PIN0);
   }
};

class MigaczSzybki : public Migacz {
public:
   MigaczSzybki() {
      PORTC |= ( 1 << PIN5);
   }
   void migaj() const {
      while(true) {
         PORTC ^= (1 << PIN6);
         _delay_ms(500);
      }
   }
   void migajSzybko() const {
      while(true) {
         PORTC ^= (1 << PIN6);
         _delay_ms(100);
      }
   }
   ~MigaczSzybki() {
      PORTC |= (1 << PIN1);
   }
};

#endif /* MYCLASS_HPP_ */

No to sama destrukcja stosunkowo prosta. Proszę sprawdź 2 scenariusze które widzisz w komentarzu:

main.cpp:

Kod: Zaznacz cały

#include <avr/io.h>
#include "MyClass.hpp"

int main(void) {
   // PortC out dir...
   DDRC = 0xFF;

   MigaczSzybki * myMigacz = new MigaczSzybki();
   Migacz * myMigacz2 = myMigacz;

   // Zamień myMigacz2 na myMigacz
   delete myMigacz2;

   // Pętla nieskończona...
   while(true);
}

I zaskoczenie! Przy konstrukcji obiektu MigaczSzybki, uruchamiane są dwa konstruktory. Rodzica oraz Dziecka. Przy destrukcji zaś jedynie destruktor rodzica. Tak dzieje się dla wywołania: delete myMigacz2;

Dla wywołania: delete myMigacz; konstrukcja i destrukcja obejmuje tak rodzica jak i dziecko.

W tym drugim przypadku (delete myMigacz) zgadzam się jest ok. Zmienna myMigacz wskazuje na obiekt typu MigaczSzybki a on wie że należy wywołać destrukcję w rodzicu i dopiero „na samym sobie”. Ale ten pierwszy przypadek! Przecież to potencjalna utrata zasobu! Jeśli w klasie Migacz będzie jakieś alokowanie pamięci a będziemy niszczyli MigaczSzybki, to brak destrukcji spowoduje jej utratę! Kilka wywołań konstrukcja/destrukcja i nie mamy RAM!

Trzeba więc jakoś poinformować destruktor Migacz że może być wywoływany na rzecz dziecka żeby „sprawdził” czy nie należy wywołać destrukcji takiej jak w obiekcie MigaczSzybki.

Do takich zadań stosujemy metody wirtualne. Tu znów standard języka C++ nie narzuca sposobu implmenetacji. Niemniej jednak większość kompilatorów obsługuje to tak jak przedstawię.

Wraz z każdą metodą która jest wirtualna, przechowywana jest tablica vtable w której zapisane są wskaźniki do metod dzieci. Powoduje to oczywiście narzut pamięci ale jeśli programujesz w C/C++ już dostatecznie długo, uwierz że jest na pewno szybsze niż np. techniki switch/case z C a i o wiele wygodniejsze.

Dodajmy więc do destruktora w Migacz, słowo kluczowe virtual. Do kodu...

MyClass.hpp:

Kod: Zaznacz cały

...
class Migacz {
public:
   Migacz() {
      PORTC |= ( 1 << PIN4);
   }
   void migaj() const {
      while(true) {
         PORTC ^= (1 << PIN7);
         _delay_ms(500);
      }
   }
   virtual ~Migacz() {
      PORTC |= (1 << PIN0);
   }
};
...

Teraz wywołanie w main.cpp takie:

Kod: Zaznacz cały

...
delete myMigacz2;
...

Kończy się poprawnie bo destrukcją wywołaną dla obydwu klas. Uff...

A jak byśmy chcieli z MigaczSzybki dziedziczyć? A tu jest ciekawostka :-) Nie trzeba (formalnie) już takiej metody/konstruktora określać jako wirtualnej ale do dobrego zwyczaju należy napisanie słowa virtual ze względu na czytanie kodu :-)

Słowo kluczowe virtual określa którą metodę należy uruchomić na rzecz obiektu obsługiwanego przez wskaźnik lub referencję. Jeśli ma to być metoda właściwa typowi wskaźnika lub referencji, nie stosujemy virtual. Jeśli chcesz mieć metodę właściwą obiektowi stosuj virtual.

W praktyce stosuje się zasadzę (w programowaniu na systemach głównego nurtu) że każdą metodę która ma być przesłonięta w klasach dziedziczących, deklaruje się w klasie bazowej jako virtual. Najczęściej zainteresowana/y jesteś takim działaniem. Programując jednak na MCU musisz znać także i „obiektowość statyczną” bo w niej nie płacisz pamięcią RAM na tablicę vtable.

Aha, no tak... czyli jeśli chcę aby migaj() było właściwe dla obiektu w zależności od tego kim jest to powinienem/powinnam mieć w klasie Migacz metodę migaj() wirtualną? Tak! Sprawdzimy to.

Tak to ma wyglądać w MyClass.hpp:

Kod: Zaznacz cały

...
class Migacz {
public:
   Migacz() {
      PORTC |= ( 1 << PIN4);
   }
   virtual void migaj() const {
      while(true) {
         PORTC ^= (1 << PIN7);
         _delay_ms(500);
      }
   }
   virtual ~Migacz() {
      PORTC |= (1 << PIN0);
   }
};
...

Dodano virtual do metody migaj() w Migacz i uruchamiam taki kod w main.cpp:

Kod: Zaznacz cały

....
int main(void) {
   // PortC out dir...
   DDRC = 0xFF;

   Migacz * myMigacz = new MigaczSzybki();

   myMigacz->migaj();
}

Jak sprawdzisz działa metoda z klasy MigaczSzybki. Teraz usuń virtual z klasy Migacz który jest przy metodzie migaj() w pliku MyClass.hpp. I co? No działa metoda z Migacz.

Czyli potwierdza się ta prosta zasada którą tu podam jeszcze raz (masz ją wyłuszczoną wcześniej).

Rodzaj wskaźnika określa która metoda w obiekcie jest uruchamiana. Jeśli chcesz aby to typ obiektu określał jaka metoda będzie uruchamiana, dodaj virtual do metody.

Na marginesie, zerknij jak wygląda apetyt programu na pamięć Flash jeśli stosujesz wywołania wirtualne.

Jak to się mówi: „Nie ma darmowych obiadów”.....

Dobrze, ale do czego się przydaje jeszcze virtual?

Jeśli chcesz „zmusić” klasy dziedziczące do implementacji określonych metod w kodzie, stosujesz tzw. metody abstrakcyjne. Wyglądają one dość nietypowo w języku C++ :-)

Załóżmy że klasa Migacz koniecznie ma mieć metodę migaj. Nie ważne czy wirtualną czy nie. Koniecznie ta metoda ma być bezargumentowa. Możesz wtedy zdefiniować klasę Lampka (a taki przykład.... ) która posiada metodę migaj() abstrakcyjną. Nie będzie miała ona ciała a jedynie będzie podstawą do dziedziczenia. Ja zaprezentuję to w kodzie, ale nie będziemy jeszcze go używali.

Kod: Zaznacz cały

....
class Lampka {
public:
   virtual void migaj() const = 0;
};

class Migacz : public Lampka {
public:
...
   virtual void migaj() const {
      ...
   }
   ...
};

Metoda abstrakcyjna jest opatrzona virutal i ma „przypisanie” 0. No tak się to notuje :-) Specjaliści od innych języków: to jest sposób na implementację interfejsów w C++ :-)

Oczywiście to zobowiązanie rozciąga się także na klasy dziedziczące od Migacz. Każda musi mieć metodę migaj()!

Super... znamy virtual, wiadomo jak działa dziedziczenie ale... jak działa wielodziedziczenie? Haa.... A Java i C# nie ma wielodziedziczenia :-) Dobrze... nie będę się nabijał bo wiem dlaczego nie ma i czego nie ma C++. Każdy z tych języków przyjął jakieś założenia na etapie rozwoju. Założenia także co do osób używających tego języka...

Wielodziedziczenie nazywane jest poprawniej dziedziczeniem wielobazowym. To bardzo interesująca technika i wymaga wyjaśnienia.

Załóżmy że klasa MigaczSzybki będzie dziedziczyła z 2 klas. Pierwszą będzie Migacz a drugą Swiatlo. Aby było zabawniej w Swiatlo także jest metoda migaj(). Powstanie pytanie. Która z tych metod będzie uruchomiona (nie, to nie jest podchwytliwe, wiesz już coś o „obiektowości statycznej” i virtual)? Zanim jednak do tego przejdziemy w następnej części, chcę pokazać jak to zapisać. Od razu dodam także miganie diodą (już trochę ich brakuje... :-)) na pinie 2.

Kod: Zaznacz cały

class Swiatlo {
public:
   void migaj() {
      while(true) {
         PORTC ^= ( 1<< PIN2);
         _delay_ms(500);
      }
   }
};
class MigaczSzybki : public Migacz, public Swiatlo {
public:
...
};

Pozostaje jeszcze tylko przedstawienie tego wielodziedziczenia na diagramie UML. Zapis jest bardzo podobny do poprzedniego.
Obrazek
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek

Wróć do „Programowanie AVR w C++”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 5 gości