Jak pisać by nie powodować problemów z kodem.

Każdy kto chce pisać oprogramowanie powiniem napisać sobie wpierw wytyczne do pisania kodu. Takie założenia pozwolą uniknąć wielu pułapek wynikłych ze sposobu zapisu i powstających błędów wynikłych z przeoczeń jak i niezamierzonych literówek. Chciałbym zaproponować w tym artykule rozwiązania, które uchronią od poszukiwania wielu błędów.

  1. Nazewnictwo stałych definicji.

    Proponuję stałe zapisywać wyłącznie dużymi literami. pozwoli to od razu zauważyć w kodzie co jest stałą wartością, a co zdefiniowaną zmienną;

  2. Definiowanie zmiennych globalnych.

    Nie definiować zmiennych globalnych a globalne zmienne strukturalne o nazwie modułu i zmiennych globalnych umieszczonych w strukturze. Uchroni to przed trudnym do uchwycenia problemem użycia tej samej nazwy dla zmiennych globalnych w różnych modułach programu. Dodatkowo nazwa struktury od razy wskaże z którego modułu pobieramy zmienną globalną;

  3. Jeżeli używany makrodefinicji funkcji (#define) to parametry w miejscu użycia

    otaczajmy nawiasami. uchroni nas to przed wieloma kłopotliwymi do uchwycenia błędami.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// niepoprawna definicja
#define pow(_x) (x*x)

//wywołanie nie powodujące problemu
z=pow(6);
// rozwinięcie
z=(6*6);

//wywołanie powodujące problem
int a=6;
z=pow(a+4);
// rozwinięcie
z=(a+4*a+4);
z=(a+24+4);
z=34; // uzyskujemy 34 zamiast spodziewanej wartości 100

//poprawna definicja
#define pow(_x) ((x)*(x))
  1. Jeśli istnieje taka możliwość zastępujmy makrodefinicje funkcją inline.

    Takie rozwiązanie również zostanie rozwinięte w miejscu użycia i zoptymalizowqane w sposób identyczny do makrodefinicji lecz funkcje inline nie powodują bliżej nieokreślonych błędów i dziwnych ostrzeżeń ze strony kompilatora;

  2. Pisanie kodu bez zbędnych spacji.

    Ładniej wygląda kod z przerwami między operatorami lecz czasem niehcący możemy opuścić znak spacji. efektem jest problem ze znajdowaniem ściśle określonego ciągu. Pisanie bez spacji idzie łatwiej, gdyż jak się przyzwyczai programista to nie ma opcji że przypadkiem wstawi spację, która utrudni wyszukiwanie.

  3. Nazewnictwo typów danych.

    Nie należy używać podstawowych typów zdefiniowanych w C/C++. Problem stanowią szczególnie typi „int”. wynika to z faktu, iż w niektórych kompilatorach mają one różny rozmiar. Wskazane jest zdefiniować następujące podstawowe typy:

    • u8_t

    • s8_t

    • u16_t

    • s16_t

    • u32_t

    • s32_t

    • u64_t

    • s64_t

    Te definicje od razu nam powiedzą co przekazujemy, lub jakiego typu oczekujemy dla danej zmiennej. Wszystkie definiowane typy powinny kończyć się ciągiem „_t” Doprzym zwyczajem jest również oznaczanie typu strukturalnego na końcu ciągiem „_st_t” Zaś zaznaczanie typu unii suffixem „_u_t” w przypadku deklaracji zmiennej typu strukturalnego po nazwie zmiennej pozostawić suffix „_st” a po nazwie zmiennej typu unia suffixu „_u” Dodatkowo wskaźniki do typu poprzedzać warto prefixem „p _” Przykład:

1
2
flags_u_t   znaczniki_u;
flags_u_t* p_znaczniki_u=&znaczniki_u;
  1. Dostęp do części zmiennej.

    Nie realizujmy dostępu do części zmiennej poprzez rzutowania i przesunięcia. Powoduje to nieładny i mniej czytelny kod. korzystniej jest takie zmienne deklarować jako unie. Przykłady takich unii:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
typedef union {
    u8_t ub[2];
    s8_t sb[2];
    u16_t uw;
    s16_t sw;
} un16_t;

typedef union {
    u8_t ub[4];
    s8_t sb[4];
    u16_t uw[2];
    s16_t sw[2];
    u32_t ud;
    s32_t sd;
/*    f32_t     f;
 *
 */
} un32_t;

typedef union {
    u8_t ub[8];
    s8_t sb[8];

    u16_t uw[4];
    s16_t sw[4];

    u32_t ud[2];
    s32_t sd[2];

    u64_t uq;
    s64_t sq;
    /*    f64_t     f;
     *
     */
} un64_t;
  1. Na czytelność kodu wpływa dodatkowo nagłówek z komentarzem,

    w którym umieszczamy opis pliku i funkcji wraz ze znacznikami Doxygen-a zapewniającymi generację dokumentacji kodu. Takie nagłówki pozwalają wygenerować dokumentację, która pozwala prześledzić zależności funkcji dzięki czemu można łatwiej np. zrefaktoryzować kod programu. Takie tabelki nie pozwalają się równocześnie zlać w jeden ciąg kodu różnych funkcji.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/*=========================================================================*/
/*!\file text.c
 * \brief funkcje obsługi łańcuchów tekstowych
 *
 *
 *
 * \par Compiler:
 *          arm-none-eabi-gcc 5.4.1
 * \author
 *      Author:     Arkadiusz Krysiak
 *
 * \todo*/
/*=========================================================================*/
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/*=========================================================================*/
/*!\brief funkcja dopasowuje wzorzec do tekstu w pamięci FLASH
 *
 * funkcja dopasowuje wzorzec do tekstu w pamięci flash i jeśli wzorzec
 * mieści się całkowicie w tekście to zwraca true
 * jeśli wzorzec nie pokrywa się z tekstem zwraca false
 *
 * \author  : Arkadiusz Krysiak
 *
 * \param   : char *pattern wskaźnik do wzorca NULL TERMINATED
 *
 * \param   : char text     adres tekstu w pamięci FLASH zakończonego NULL TERMINATED
 *
 * \retval  true    wzorzec dopasowano do wskazanego tekstu
 *                  false   wzorzec nie pokrywa się z tekstem
 *
 * \callgraph
 * \callergraph*/
/*==========================================================================*/
  1. Znaczniki kończące blok warunkowy.

    Same znaczniki są nieczytelne. przy większej liczbie warunkowych bloków kompilacji lub bloków warunkowego wykonania nie bardzo widać które zamknięcie przynależy do którego otwarcia. dobry zwyczaj polega na właściwym opisaniu pokazującym powiązane otwarcie z zamknięciem. Przykład:

1
} /* while(0!=*((place))) */
1
#endif /* #ifndef __TEXT_H__ */
  1. Zapis złożonych warunków. Pierwszym ważnym czynnikiem ograniczającym przypadkowe błędy logiczne jest nie zdawanie się na kolejność działań operatorów. otaczajmy działania nawiasami według kolejności, w której powinny siuę wykonać. Drugim jest sposób zapisu bardzo złożonych warunków. Aby były czytelne dla następnego programisty rozbijajmy je na mniejsze i stopniujmy jak na poniższym przykładzie. od razu po zapisie widać kolejność w której się one wykonają.

1
2
3
4
5
6
7
8
if(
    (znak>('0'-1))
    &&
    (znak<('9'+1))
  )
{
v_temp=znak-'0';
}
  1. Operatory unarne.

    Operatory onarne tj,:

    • predekrementacja - - a,

    • preinkrementacja + + a,

    • postdekrementacja a - -,

    • postinkrementacja a + +.

    Powodują bardzo duże niebezpieczeństwo. Nie wolno ich używać w wyrażeniu w którym zmienna której dotyczą te operatory jest używana więcej niż jeden raz. W zależności od zapisu a również i w zależności od rodzaju kompilatora wyrażenie po obliczeniu może dać niespodziewany wynik. Najprostszy przykład niepewności jaką daje taki operator w obliczanym wyrażeniu przedstawiono poniżej. Nie mamy pojęcia jaką wartość będzie miało drugie użycie zmiennej a w wyrażeniu. Z przed dekrementacji czy z po dekrementacji? Przykład:

1
y=--a*b+a;
  1. Dynamiczne przydzielanie pamięci. W systemach embedded jeśli nie ma takiej potrzeby należy unikać dynamicznego przydziału pamięci. W przypadku systemów klasy safe w zasadzie nie powinno się brać tego nawet pod uwagę. Statyczny przydział zapewnia nam świadomość co się dziejee z pamnięcią, eliminuje problem fragmentacji pamięci i zapobiega sytuacji gdy aplikacji w trakcie pracy może zabraknąć wolnej pamięci. Przydzielenie odpowiednich rozmiarów pamięci jest wtedy już możliwe do policzenia przez linker w trakcie łączenia bloków programu i dowiadujemy się o problemie w tzw. czasie CT (Compile Time) a nie w trakcie czasu RT (Run Time).

  2. Zapis warunku zmiennej ze stałą. Wszyscy warunek typu if zapisują odruchowo w postaci która, w przypadku częstego błędu zapisu spowodowanego omyłkowym wbiciem jednego znaku = nie powoduje błędu kompilacji ale błędne działanie:

1
2
3
4
5
#define EIGHT 10
if(zmienna==EIGHT)

// częsty błąd który nie spowoduje błędu kompilacji
if(zmienna=EIGHT)
1
2
3
4
5
#define EIGHT 10
if(EIGHT==zmienna)

// częsty błąd spowoduje błąd kompilacji
if(EIGHT==zmienna)
  1. Wyrównanie pamięci. O ile w kontrolerach 8-io bitowych wyrównanie danych w pamięci kontrolera nie ma znaczenia o tyle w 16..32..64-ro bitowych może to stanowić znaczny problem odbijający się negatywnie na szybkości działania programu i powodujący wzrost rozmiaru kodu. Jednostki oparte na core ARM-a wymagają w zależności od wersji core wyrównania do 2 lub 4-rech bajtów. W przypadku niewyrównania dana jest pobierana jako pojedyncze bajty i składama za pomocą odpowiednich rozkazów w rejestrach w zawartość 16 lub 32 bitową. więc zaczytanie zmiennej 4-ro bajtowej może wymagać zamiast jednego odczytu 2 lub 4 odczyty + rozkazy konieczne do złożenia zmiennej z dwóch półsłów lub 4 ćwierćsłów. Przy zapisie również tak się dzieje. W przypadku gdy chcemy zaoszczędzić miejsca w pamięci a dane trzymamy w postaci struktury, to wskazane jest takie układanie danych w strukturze by obok siebie leżały definicje 2 pól 16 bitowych lub 4-rech pól 8-mio bitowych. przy właściwym ustawieniu parametr packed nie spowoduje braku wyrównania w strukturze a struktura będzie zwarta i bez pustych niewykorzystanych dziur w pamięci.