Tabela przeglądowa ze stałą tablicą

Zaimplementowałem następujące ćwiczenie:

Zaimplementuj tabela przeglądowa z operacjami takimi jak find(struct table*, const char*), insert(struct table*, const char*,int) i remove(struct table*, const char*). Tabela może być tablicą składającą się z struct pary lub par tablic (const char*[] i int*); wybierz, wybierz także typy zwracania dla swoich funkcji.

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #define ARR_SIZE 10 struct Pair { const char* word; int val; }; struct Table { struct Pair* pairs[ARR_SIZE]; size_t sz; }; struct Pair* make_pair(const char* word, int val) { assert(word); struct Pair* pair = (struct Pair*)malloc(sizeof(struct Pair)); pair->val = val; pair->word = word; return pair; } void table_empty(struct Table* tbl) { assert(tbl); size_t i = 0; for (i = 0; i < tbl->sz; ++i) { free(tbl->pairs[i]); tbl->pairs[i] = NULL; } tbl->sz = 0; } int search_word(struct Table* tbl, const char* word) { assert(tbl); assert(word); size_t i = 0; for (i = 0; i < tbl->sz; ++i) { //printf("%s %i\n", tbl->pairs[i]->word,i); if (strcmp(tbl->pairs[i]->word, word) == 0) { return i; } } return -1; // error } void table_insert(struct Table* tbl, const char* word, int val) { assert(tbl); assert(word); int i = search_word(tbl, word); if (i != -1) { // replace val tbl->pairs[i]->val = val; } else { // add new pair struct Pair* pair = make_pair(word, val); tbl->pairs[tbl->sz] = pair; // add pair at the last position ++tbl->sz; } } int table_find(struct Table* tbl, const char* word, int* return_val) { assert(tbl); assert(word); assert(return_val); int i = search_word(tbl, word); if (i != -1) { *return_val = tbl->pairs[i]->val; return 0; } return -1; // error not found } int table_remove(struct Table* tbl, const char* word) { assert(tbl); assert(word); int i = search_word(tbl, word); if (i == -1) return -1; free(tbl->pairs[i]); // free value at current pos tbl->pairs[i] = tbl->pairs[tbl->sz - 1]; // put address of last word at the pos of the current --tbl->sz; // "erase" last word return 0; } void table_print(struct Table* tbl) { assert(tbl); printf("\n"); printf("table size = %i\n", tbl->sz); for (int i = 0; i < tbl->sz; ++i) printf("%s %i\n", tbl->pairs[i]->word, tbl->pairs[i]->val); fflush(stdout); } void print_search_result(struct Table* tbl, const char* word) { assert(tbl); assert(word); int val = 0; if (table_find(tbl, word, &val) == 0) printf("%s %i\n",word, val); else printf("%s not found in table\n", word); printf("\n"); fflush(stdout); } void print_remove_result(struct Table* tbl, const char* word) { assert(tbl); assert(word); if (table_remove(tbl, word) == -1) printf("%s not deleted\n", word); else printf("%s deleted\n", word); printf("\n"); fflush(stdout); } int main() { struct Table table = { 0 }; int val = 0; table_insert(&table, "Hello", 10); table_insert(&table, "Test", 15); table_insert(&table, "Hello", 18); // testing overrite val table_insert(&table, "What", 5); table_insert(&table, "is", 3); table_insert(&table, "going", 4); table_insert(&table, "on", 77); table_print(&table); print_search_result(&table, "Hello"); print_search_result(&table, "Test"); print_search_result(&table, "keyword"); print_remove_result(&table, "Hello"); print_remove_result(&table, "Hello"); // double delete == false print_remove_result(&table, "What"); print_remove_result(&table, "going"); print_remove_result(&table, "is"); print_remove_result(&table, "on"); print_remove_result(&table, "Test"); table_print(&table); table_insert(&table, "Hello", 10); table_insert(&table, "Test", 15); table_insert(&table, "Hello", 18); // testing overrite val table_insert(&table, "What", 5); table_insert(&table, "is", 3); table_insert(&table, "going", 4); table_insert(&table, "on", 77); table_print(&table); table_empty(&table); table_print(&table); getchar(); } 

Zapraszam do komentowania wszelkich ulepszeń. Czy jest lepszy sposób na zrobienie tego?

Mam również jedno konkretne pytanie: czy moje zastosowania assert są odpowiednie?

Komentarze

  • Jako stylistyczny chwytak, ' d polecam * we wskaźniku wpisz obok nazwy zmiennej (np. const char *word). Składnia typu C została zaprojektowana tak, aby naśladować użycie, gdzie na przykład ' d wpisz *word, aby uzyskać dostęp do const char element. Zwróć również uwagę, że dzięki temu jest bardziej jasne, jaki typ b znajduje się w int *a, b;.

Odpowiedz

Używając struct

Osobiście uważam, że dokonałeś właściwego wyboru, używając struct z char* i int zamiast 2 tablicami char* i int. Pair to pojęciowa jednostka danych w Twojej aplikacji, więc warto je połączyć. Gdybyś miał 2 oddzielne tablice, łatwo byłoby im stracić synchronizację i trudno byłoby ustalić, dlaczego tak się stało. Dobra robota!

Preferuj const i funkcje zamiast makr

Zdefiniowałeś rozmiar tablicy za pomocą makra. W ten sposób w niekorzystnej sytuacji podczas programowania. Usunąłeś informacje o typie dla wartości. Robiąc to:

const int ARR_SIZE = 10; 

uzyskujesz bezpieczeństwo typów. Edycja: To jest „C ++ – ism that doesn” t pracować w C. Moja wina! Ale reszta porady w następnym akapicie jest poprawna, o ile wiem.

W przypadku makr, które pobierają parametry, istnieje ryzyko, że zostaną użyte w nieoczekiwany sposób i spowodują trudne do debugowania problemy. Na szczęście nie zrobiłeś tego tutaj. Ale ogólnie, jeśli zdarzy ci się sięgnąć po makro, zadaj sobie pytanie, czy lepiej byłoby, gdybyś miał stałą lub funkcję. Prawie zawsze tak będzie (zakładając, że możesz użyć stałej w żądany sposób).

Błędy

W Twoim kodzie są błędy. W make_pair() nie sprawdzasz, czy jeśli malloc() powiodło się. Jeśli to się nie powiedzie, żadna pamięć nie zostanie przydzielona, a pair wskazuje na NULL. Przy próbie przypisania pair->val lub pair->word nastąpi awaria.

Jeśli to naprawiłeś, table_insert() używa wyniku make_pair() bez sprawdzania, czy najpierw jest to „s NULL. Ten wygrał” t awarii natychmiast, ponieważ „przypisujesz po prostu wskaźnik tablicy w tbl->pairs[tbl->sz], aby uzyskać wartość pair. Co się stanie później, gdy jeśli spróbujesz wyszukać, wydrukować lub wstawić inny element, wystąpi awaria podczas iteracji tego wpisu w tabeli i próby zrobienia z nim czegokolwiek.

Możesz uniemożliwić te błędy, jeśli nie dynamiczne przydzielanie wpisów tablicy. Po prostu utwórz tablicę jako tablicę struktur Pair zamiast wskaźnika do nich.

Nazewnictwo

Wiele z twoich nazw jest naprawdę dobrych . Pair i Table to przyzwoite, czytelne nazwy tego zadania. make_pair(), table_insert() itp. mają charakter informacyjny. Jednak niektóre można by ulepszyć. tbl nie oszczędza dużo pisania w table. Po prostu użyj całego słowa. To samo dotyczy sz i size. i jest akceptowalna jako nazwa zmiennej pętli, ale byłoby jeszcze lepiej, gdyby była bardziej opisowa. Na przykład entry_index lub pair_index. Powinien opisywać, nad czym „powtarzasz iterację”.

Komentarze

  • po zmianie definicji na const int ARR_SIZE = 10; wyświetla mi się błąd w struct Table { struct Pair* pairs[ARR_SIZE]; size_t sz; };, że ARR_SIZE nie jest stałą, więc jak w takim razie użyć go w takim przypadku?
  • Przepraszam – że ' w języku C ++. Myślałem, że to pytanie w C ++. Mój błąd. ' zaktualizuję swoją odpowiedź.
  • I ' Jestem zagorzałym zwolennikiem opisowych nazw zmiennych, ale dzięki prostocie pętli w tym programie myślę, że nazwa zmiennej i działa dobrze.
  • Jeśli malloc() nie powiedzie się, przypisywanie do pair->val lub może zawiesić się, jeśli masz szczęście . Może po prostu trwać i dawać złe wyniki lub może spowodować coś zupełnie nieoczekiwanego. To ' to radość z niezdefiniowanego zachowania!

Odpowiedź

  • Nie rzutuj wyniku malloc.

    malloc zwraca void *, aw C można przekonwertować void * na dowolny wskaźnik. Jeśli rzutujesz w celu pominięcia ostrzeżenia o konwersji liczby całkowitej na wskaźnik, oznacza to, że kompilator nie ma prototypu malloc i domyślnie zwraca int (to starożytna konwencja języka C). Teraz, jeśli int i wskaźnik mają inny rozmiar, wartość zwracana przez malloc zostanie obcięta, ze wszystkimi nieprzyjemnymi konsekwencjami.

  • Nie zaleca się sizeof(type). Preferuj sizeof(expression). W Twoim przypadku rozważ

     struct Pair * pair = malloc(sizeof(*pair)); 
  • table_insert ślepo wstawia do pełnej tabeli. Powinien sprawdzać, czy tbl->sz < ARR_SIZE i zwróć informację o błędzie, jeśli tak nie jest.

  • Rzeczywiste wstawienie

     struct Pair* pair = make_pair(word, val); tbl->pairs[tbl->sz] = pair; // add pair at the last position 

    powinno być naprawdę jedną linią:

     tbl->pairs[tbl->sz] = make_pair(word, val); 

Komentarze

  • dlaczego rzutowanie malloc nie jest dobrym pomysłem? W C ++ nawet wystąpiłby błąd kompilatora?
  • @Sa ndro4912 Pytanie jest oznaczone C. C ++ to inny język, szczególnie w tym zakresie. Jeśli kompilator C narzeka, oznacza to, że brakuje prototypu malloc, co może prowadzić do nieprzyjemnych problemów.
  • tak, wiem, że to c. Zastanawiałem się tylko, dlaczego tutaj nie jest dobrą praktyką wskazywanie zmian typu za pomocą rzutowania
  • @ Sandro4912 Zobacz edycję.
  • @ Sandro4912 pair = malloc(sizeof *pair) jest łatwiejszy do poprawnego zakodowania za pierwszym razem, łatwiejszy do przejrzenia i utrzymania.

Odpowiedź

Włącz wszystkie ostrzeżenia kompilatora

Przy dobrze włączonym kompilatorze for (int i = 0; i < tbl->sz; ++i) powinien był ostrzec o mieszaniu oznaczeń i i tbl->sz oraz range. Oszczędzaj czas i włącz wszystkie ostrzeżenia i używaj for (size_t i = 0; i < tbl->sz; ++i).

Ogólnie kod jest mało skomplikowany przy używaniu int,size_t prawie zamiennie . Przebudowuję architekturę i używam tylko size_t.

Mieszane użycie płytkiej i głębokiej kopii

make_pair(const char* word, int val) przydziela i tworzy zupełnie nowy struct Pair (głęboka kopia), ale nie kopiuje tego, na co word wskazuje.

Być może

// pair->word = word; pair->word = strdup(word); 

Użyj const

search_word() nie modyfikuje *tbl, więc użyj const to convey that. Same for table_find () , table_print () , print_search_result () `.

// int search_word(struct Table* tbl, const char* word) int search_word(const struct Table* tbl, const char* word) 

Nazewnictwo

Kod używa const char* word;, ale nie jest to „słowo”, ale ciąg używany w strcmp().

—– Dodatki

Naruszenie umowy?

Nic w wymaganiach „Zaimplementuj tabelę przeglądową z operacjami takimi jak …” nie wskazuje, że const char* jest wskaźnikiem do ciągu znaków . Wywołanie strcmp() jest wątpliwa, chyba że istnieją nieokreślone wymagania. Jako char * kod może używać prostego porównania

// if (strcmp(tbl->pairs[i]->word, word) == 0) if (tbl->pairs[i]->word == word) 

Zastosowanie assert()

Czy moje zastosowania assert są właściwe?

Jeśli char * wskaźnik dodawania / wyszukiwania nie jest określony jako ciąg , assert(word); nie jest prawidłowy, ponieważ word == NULL nie jest nieprawidłowy.

assert(return_val) w table_find(struct Table* tbl, const char* word, int* return_val) jest w porządku, ale przeprojektowałbym, aby zezwolić na return_val == NULL

if (i != -1) { // add if (return_val) { *return_val = tbl->pairs[i]->val; } return 0; } 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *