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)
iremove(struct table*, const char*)
. Tabela może być tablicą składającą się zstruct
pary lub par tablic (const char*[]
iint*
); 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
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 wstruct 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 dopair->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
zwracavoid *
, 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 prototypumalloc
i domyślnie zwracaint
(to starożytna konwencja języka C). Teraz, jeśliint
i wskaźnik mają inny rozmiar, wartość zwracana przez malloc zostanie obcięta, ze wszystkimi nieprzyjemnymi konsekwencjami. -
Nie zaleca się
sizeof(type)
. Preferujsizeof(expression)
. W Twoim przypadku rozważstruct Pair * pair = malloc(sizeof(*pair));
-
table_insert
ślepo wstawia do pełnej tabeli. Powinien sprawdzać, czytbl->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; }
*
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 doconst char
element. Zwróć również uwagę, że dzięki temu jest bardziej jasne, jaki typb
znajduje się wint *a, b;
.