rdodC.doc

(222 KB) Pobierz
1









              Odpowiedzi              195

C

Odpowiedzi

 

Rozdział 1.

  1. Błąd polega na nieuwzględnieniu reguł pierwszeństwa operatorów — przedmiotowa instrukcja interpretowana jest przez kompilator jako:

while (ch = (getchar() != EOF))

Jest to więc próba przypisania wartości do zmiennej ch, którą to próbę kompilator traktuje jako błędne użycie operatora „=” zamiast „==”.

2a.              Najprostszym sposobem uchronienia się przed niespodziewanym użyciem liczb ósemkowych jest wprowadzenie opcjonalnego zakazu używania takich liczb za pomocą odpowiedniej opcji kompilatora.

2b.              Kompilator mógłby wychwytywać wszystkie przypadki użycia operatorów & oraz | w instrukcji if oraz złożonych porównaniach w sytuacji, gdy brak jest jawnego porównania z wartością 0. Tak więc instrukcja:

if (u & 1)   /* czy u jest nieparzyste? */

musiałaby zostać zmieniona na

if ((u & 1) != 0)  /* czy u jest nieparzyste? */

2c.  W przypadku poszukiwania niezamierzonych początków komentarza podejrzany jest każdy komentarz rozpoczynający się od znaku alfabetycznego albo nawiasu otwierającego. Jeśli nakażesz kompilatorowi wychwytywać wszystkie takie przypadki, łatwo pozbędziesz się ostrzeżeń z jego strony, wprowadzając co najmniej jedną spację pomiędzy gwiazdką w ograniczniku a wspomnianym alfabetykiem lub nawiasem:

/*Ten komentarz powodować będzie generowanie ostrzeżenia */

/* Ten nie, ze względu na spację rozdzielającą */

/*-----Ten komentarz także nie wzbudzi zastrzeżeń-------*/

Należy ponadto eliminować z kodu wątpliwe przypadki i jasno wyrażać swe intencje zapisem:

quot = numer/ *pdenom

albo

quot = numer/(*pdenom)

zamiast

quot = numer/*pdenom

2d. Kompilator mógłby na przykład kwestionować wszelkie przypadki łączenia operatorów bitowych z arytmetycznymi, jeżeli brak jest nawiasów wyznaczających explicite kolejność obliczeń. Instrukcja:

word = bHigh << 8 + bLow;

musiałaby wówczas zostać zapisana w jednej z poniższych postaci:

word = (bHigh << 8) + bLow;

word = bHigh << (8 + bLow);

zależnie od faktycznych intencji programisty.

Na podobnej zasadzie można by wykrywać łączenie dwóch operatorów o różniącym się priorytecie, bez użycia dodatkowych nawiasów. Zasada ta jest oczywiście zbyt prymitywna, by stosować ją w praktyce in extenso, jednakże sama idea jest warta uwagi i wzbogacenie jej o jakąś dodatkową heurystykę mogłoby przydać jej znaczenia praktycznego. Heurystyka ta nie powinna być oczywiście zbyt prymitywna — w szczególności nie powinny być kwestionowane konstrukcje takie, jak:

word = bHigh*256 + bLow;

if (ch == ' ' || ch == '\t' || ch == '\n')

  1. Kompilator mógłby generować ostrzeżenie w przypadku napotkania frazy else poprzedzonej dwiema instrukcjami if, jak w poniższych przykładach:

if (wyrażenie1)

  if (wyrażenie2)

      .
      .
      .
else

   .
   .
   .

if (wyrażenie1)

  if (wyrażenie2)

      .
      .
      .
  else

     .
     .
     .

Aby uniknąć niepotrzebnych ostrzeżeń, należałoby ujmować w nawiasy klamrowe każdą uwarunkowaną instrukcję:

if (wyrażenie1)

{

  if (wyrażenie2)

      .
      .
      .
}

else

     .
     .
     .

if (wyrażenie1)

{

  if (wyrażenie2)

      .
      .
      .
  else

      .
      .
      .

}

  1. Pożyteczna skądinąd zasada umieszczania stałych i wyrażeń po lewej stronie operatora porównania staje się bezużyteczna, jeżeli obydwa porównywane operandy są zmiennymi. Gdyby, za pomocą stosownej opcji, zmusić kompilator do kwestionowania każdej instrukcji przypisania mogącej stanowić wynik błędnego użycia operatora „=” zamiast „==”, po prostu otrzymywalibyśmy zazwyczaj zbyt wiele ostrzeżeń.
  2. Najprostszym sposobem wykrywania niezdefiniowanych makr jest wyposażenie kompilatora w przełącznik powodujący, iż preprocesor będzie kwestionował użycie takich makr. Domyślne utożsamianie niezdefiniowanych makr z zerem nie jest specjalnie użyteczne w kompilatorach honorujących zarówno starą dyrektywę #ifdef, jak i nowy operator jednoargumentowy defined w wyrażeniach #if. Przykładową sekwencję:

/* ustalenie platformy wynikowej */

 

#if  INTEL8080

 

 

#elif  INTEL80x86

 

 

#elif MC680x0

 

 

#endif

należałoby wówczas zmienić do następującej postaci:

/* ustalenie platformy wynikowej */

 

#if  defined(INTEL8080)

 

 

#elif  defined(INTEL80x86)

 

 

#elif defined(MC680x0)

 

 

#endif

Jednocześnie wspomniany przełącznik nie powinien powodować generowania ostrzeżenia w przypadku napotkania niezdefiniowanego makra w instrukcji #ifdef.

Rozdział 2.

  1. Jednym z rozwiązań jest makro ASSERTMSG posiadające dwa parametry: testowane wyrażenie oraz komunikat wyświetlany w sytuacji, gdy wyrażenie to ma wartość FALSE. W przypadku wykrycia, iż kopiowane bloki nakładają się, wywołanie makra mogłoby być następujące:

ASSERTMSG(pbTo >= pbFrom+size || pbFrom >= pbTo+size,

             "memcpy: bloki nakładają się");

Prezentowana na następnej stronie implementacja makra ASSERTMSG powinna znaleźć się w dołączonym pliku nagłówkowym:

#ifdef DEBUG

 

  void _AssertMsg(char *strMessage); /* prototyp */

 

  #define ASSERTMSG(f,str)             \

          if (f)                       \

              {}                       \

          else                         \

            _AssertMsg(str)

 

#else

 

  #define ASSERTMSG(f,str)

 

#endif 

Wykorzystywana funkcja _AssertMsg ma następującą definicję — powinna ona znaleźć się w konwencjonalnym pliku źródłowym:

#ifdef DEBUG

 

  void _AssertMsg(char *strMessage)

  {

    fflush(NULL);

    fprintf(stderr, "\n\nNiespełniona asercja - %s\n",

                    strMessage );

 

    fflush(stderr);

    abort()

  }

 

#endif

  1. Niektóre kompilatory (być może opcjonalnie) optymalizują kod w taki sposób, iż określony łańcuch, używany w wielu miejscach kodu, tworzony jest tylko w jednym egzemplarzu — wszystkie odwołania do niego współdzielą tę samą kopię. Wówczas niezależnie od liczby asercji używających nazwy pliku, nazwa ta zapamiętana zostanie w kodzie tylko jednokrotnie. Problem w tym, że taka „oszczędnościowa” polityka dotyczyć będzie wszystkich łańcuchów używanych w programie, czego programista mógłby sobie z pewnych względów nie życzyć.

Alternatywne rozwiązanie polega na takim zaimplementowaniu makra ASSERT, by wszystkie jego wywołania w danym pliku posługiwały się w zamierzony sposób tą samą kopią łańcucha zawierającego nazwę pliku. Należy w związku z tym zdefiniować jeszcze jedno makro — ASSERTFILE — i wywoływać je jednokrotnie na początku każdego pliku źródłowego:

#include <stdio.h>

.

.

.

#include <debug.h>

 

ASSERTFILE(__FILE__)

.

.

.

 

void *memcpy(void *pvTo, void *pvFrom, size_t size)

{

  byte *pbTo   = (byte *)pvTo;

  byte *pbFrom = (byte *)pvFrom;

 

  ASSERT(pvTo != NULL && pvFrom != NULL);

   .

   .

   .

Jak widać, sposób wywołania makra ASSERT nie zmienił się, zmieniła się natomiast jego definicja:

#ifdef DEBUG

 

   #define ASSERTFILE(str)     \

             static char strAssertFile[] = str;

 

   #define ASSERT(f)           \

           if (f)              \

               {}              \

           else                \

               _Assert(strAssertFile, __LINE__)

#else

   #define ASSERTFILE(str)    

   #define ASSERT(f)

#endif

Gdy używa się makra ASSERT w tej postaci, można niekiedy zaoszczędzić sporo pamięci — podczas testowania przykładowego kodu niniejszej książki udało mi się zaoszczędzić 3 kB danych.

Problem z użyciem asercji w prezentowanym przykładzie polega na tym, iż testowane wyrażenie musi być wartościowane także w handlowej wersji programu. Obecna wersja, przy niezdefiniowanym symbolu DEBUG wpadnie w długotrwałą pętlę kończącą się w sposób losowy, gdy bajt wskazywany przez pch zawierać będzie znak nowej linii. By uniknąć tej przykrej pułapki, należałoby przepisać funkcję getline w sposób następujący:

void getline(char *pch)

{

  int ch;     /* ch musi być zadeklarowane jako int */

 

  do

  {

    ch = getchar();

    ASSERT(ch != EOF);

  }

  while ((*pch++ = ch) != '\n');

}

Jednym ze sposobów jest umieszczenie „fałszywej” asercji w domyślnej części instrukcji switch, do której sterowanie nie ma prawa trafić — o ile oczywiście każdy możliwy wariant selektora obsługiwany jest w sposób jawny:

.

.

.

default:

   ASSERT(FALSE);  /* bezwarunkowe załamanie */

   break;

 

}

Wzorzec dla każdej pozycji tablicy powinien zawierać się w pewnej stowarzyszonej z nią masce; jeżeli przykładowo maska ma postać 0´FF00, w młodszym bajcie wzorca wszystkie bity muszą być zerowe, w przeciwnym razie dopasowanie wzorca dla dowolnej instrukcji nie będzie możliwe. W związku z tym funkcja CheckIdInst musi zostać uzupełniona o stosowną weryfikację:

void CheckIdInst(void)

{

  indenfity *pid, *pidEarlier;

  instruction inst;

 

  for (pid = &idInst[0]; pid->mask != 0; pid++)

  {

    /* upewnij się, że wzorzec jest zgodny z maską */

    ASSERT((pid->pat & pid->mask) == pid->pat);

    .

    .

    .

Należy zweryfikować, za pomocą odpowiednich asercji, poprawność ustawień związanych z instrukcją reprezentowaną przez inst:

instruction *pcDecodeEOR(instruction inst, instruction *pc,

                         opcode *popc)

{

  /* czy przypadkowo nie jest to instrukcja CMPM lub CMPA.L ? */

  ASSERT(eamode(inst) != 1 && mode(inst) != 3);

...

Zgłoś jeśli naruszono regulamin