Genomgång kodprov oktober 2017
Innehåll
1 Introduktion
Det skulle visa sig att kodprovet var svårare än jag trodde – av tre skäl:
- För många rörliga delar gjorde att rättningen tog lång tid
- Många har antingen missat eller missförstått shelves-trädet, vilket har lett till många krångliga lösningar
- Minneshanteringslogiken är svårare än avsett
2 Uppgiften
Programmet som delades ut skapade en mycket enkel databas som var väldigt lik de databaser som förekommit i lagerhanterarna. Tanken var att upprepa ett problem som alla som följt kursen borde ha stångats med på något plan. Programmet hade en event-loop som frågade efter tre saker:
- Namn på en vara
- Namn på en hylla
- Ett antal
Denna event-loop upprepades så länge användaren svarade "ja" på frågan "Fler?". Om svaret "nej" gavs avslutades programmet med en utskrift av databasen.
Varje unik vara som matades in stoppades in i databasen med varans namn som nyckel och som värdet en länkad lista vars element var hyllor. Ponera följande inmatning:
- Varan "A", hyllan "B" antal 2
- Varan "C", hyllan "B" antal 3
- Varan "A", hyllan "B" antal 4
Denna inmatning ledde i det utdelade programmet till en databas med två nycklar (varor), "A" och "C". Den länkade listan för "A" hade två element "B" med 2 varor och "B" med 4 varor, totalt 6. Den länkade listan för "C" hade ett element "B" med 3 varor. Så här såg det ut i terminalen:
A
B: 2 B: 4 Total: 6
C
B: 3 Total: 3
Uppgiften gick ut på att förändra programmet så att inmatningar av samma vara på samma hylla slogs ihop, och att inmatningen av en vara på en hylla där en annan vara redan låg ignorerades. Förväntad utdata för körningen ovan skulle därför vara:
A
B: 6 Total: 6
C
Total: 0
I syfte att göra uppgiften enklare fanns det, utöver databasen
db
som var implementerad som ett träd, ytterligare ett träd,
shelves
som var en avbildning av hyllnamn på namn på de varor
som ockuperade hyllorna. I exemplet ovan skulle vi endast lägga
till någonting i shelves
en gång: nyckeln "B" och värdet "A",
dvs. "hyllan B innehåller varor av typen A".
Så här stod det i varje kodprov:
Ledning: För att hålla koll på vilka hyllor som är i bruk används trädet
shelves
som sparar vilka hyllor som hör till vilken vara.
2.1 Skillnad mellan proven på förmiddagen och eftermiddagen
Skillnaden mellan proven på förmiddagen och eftermiddagen var att i det ena provet tillhandahöll listan en extern iterator för att söka efter hyllor, och i det andra provet en intern iterator. I ena provet gick det att iterera över en lista t.ex. så här:
iter_t *iter = list_iterator(list);
while (iter_has_next(iter))
{
... = iter_next(iter);
...
}
iter_delete(iter);
Och i det andra t.ex. så här:
int shelf_cmp(void *shelf, void *shelf_name)
{
shelf_t *s = shelf;
return strcmp(s->name, (char *)shelf_name) == 0;
}
shelf_t found = list_find(list, shelf_cmp, some_name);
Det kan tyckas lite meckigare att skapa en funktion och använda funktionspekare, men denna lösning kräver ingen minneshantering och har färre rader kod, så jag bedömde dem därför som likvärdiga.
3 Lösningsförslag
Låt VN1, HN och A vara varunamn, hyllnamn och antal – inmatat enligt ovan.
- Använd
tree_get_key(shelves, HN)
för att få ut ett varunamn VN2. - Om VN2 är
NULL
så är hyllanVN2
inte i bruk någonstans, och vi behöver inte göra någonting. - Om VN1 och VN2 inte är likaNotera att jämförelsen skall
göra med hjälp av
strcmp(VN1, VN2)
, inteVN1 == VN2
., gör ingenting eftersom vi skall ignorera detta indata. - Om VN1 och VN2 är likaSe föregående fotnot!, hitta
använd
tree_get_key(db, VN1)
för att få ut listan L av hyllor som VN1 förekommer på. Iterera genom listan med den interna eller externa iteratorn för att hitta H – hyllan vars namn är HN. - Om H inte kan hittas skapar vi en ny hylla med HN och A och lägger till den i LDetta görs redan i programmet och det går att falla tillbaka på denna logik..
- Om H hittas, modifiera dess antal med A.
3.1 Minneshantering
Ovanstående kompliceras som sagt något av manuell minneshantering. I alla fall där vi behåller VN eller HN i någon datatstruktur måste VN och HN hållas vid liv och frigöras först när programmet tar slut. I de fall där VN och HN inte sparas måste strängarna frigöras för att undvika minnesläckageHar du löst uppgiften korrekt modulo minneshanteringen har du fått rest.
Det utdelade programmet gör följande uppstädning vid avslut:
/// Tear down
tree_apply(db, delete_lists, NULL);
tree_delete(db);
tree_delete(shelves);
Den första raden tar bort allt minne i alla listor i trädet db
.
Den andra raden tar bort trädet db
. Den sista raden tar bort
trädet shelves
men dess innehåll tas aldrig bort. Beroende på
vad som stoppas in i shelves
i en lösning kan olika förändringar
av denna logik komma på fråga. En enkel lösningen som jag använder
i lösningsförslaget nedan är att skapa kopior av strängarna och
utöka logiken för att ta städa bort shelves
.
3.2 Implementation av steg 4 ovan
shelf_t *find_shelf_for_good(tree_t *db, char *good_name, char *shelf_name)
{
list_t *shelves = tree_get(db, good_name);
iter_t *iter = list_iterator(shelves);
while (iter_has_next(iter))
{
shelf_t *shelf = iter_next(iter);
if (strcmp(shelf->name, shelf_name) == 0)
{
iter_delete(iter); /// Många har glömt denna iter_delete()!
return shelf;
}
}
iter_delete(iter);
// Not found!
return NULL;
}
3.3 Konkret lösningsförslag 1/2
Först visar jag en lösning som läcker minne.
do
{
if (answer) free(answer);
char *goods_name = ask_question_string("Mata in namnet på en vara:");
char *shelf_name = ask_question_string("Mata in namnet på en hylla:");
int amount = ask_question_int("Mata in ett antal:");
list_t *list = tree_get(db, goods_name);
/// Steg 1: Kolla om shelf_name redan har använts för någon tidigare vara
char *owner = tree_get(shelves, shelf_name);
/// Om det fanns en tidigare vara, kolla om det är samma som goods_name
bool same_owner = owner && strcmp(owner, goods_name) == 0;
// If we are seeing this goods for the first time
if (list == NULL)
{
list = list_new();
tree_insert(db, goods_name, list);
/// Nedanstående rad skall inte köras här -- bara om shelf_name inte har använts förut!
/// tree_insert(shelves, shelf_name, goods_name);
}
/// Om hyllan redan är i bruk
if (owner)
{
/// Om vi använder hyllan till samma sorts vara
if (same_owner)
{
/// Steg 4: Hitta rätt hylla
shelf_t *shelf = find_shelf_for_good(db, owner, shelf_name);
/// Steg 6: Uppdatera antalet
shelf->amount += amount;
}
/// Steg 3: ingenting
}
else
{
/// Steg 5
list_append(list, shelf_new(shelf_name, amount));
/// Hyllan var inte i bruk, så lägg till den i shelves
tree_insert(shelves, shelf_name, goods_name);
}
answer = ask_question_string("Fler? (ja/nej)");
}
while (starts_with(answer, "#-----") == 0 || strcmp(uppercase(answer), "JA") == 0);
3.4 Konkret lösningsförslag 2/2
Samma kod som ovan med några få rader ändrade/tillagda för korrekt
minneshantering. För att slippa komplicerade flaggor som fångar
under vilka omständigheter vi behåller goods_name
använder vi
strdup()
vid insättning i db
, och avslutar varje loop med
free(goods_name)
. Detta medför dock att de varunamn som lagras i
shelves
och de som lagras i db
inte är samma, vilket i sin tur
innebär att vi måste lägga till ett ytterligare steg vid
borttagning av shelves
. Detta är enkelt att kopiera från
existerande kod (delete_shelves
).
do
{
if (answer) free(answer);
char *goods_name = ask_question_string("Mata in namnet på en vara:");
char *shelf_name = ask_question_string("Mata in namnet på en hylla:");
int amount = ask_question_int("Mata in ett antal:");
list_t *list = tree_get(db, goods_name);
char *owner = tree_get(shelves, shelf_name);
bool same_owner = owner && strcmp(owner, goods_name) == 0;
if (list == NULL)
{
list = list_new();
tree_insert(db, strdup(goods_name), list); /// strdup!
}
if (owner)
{
if (same_owner)
{
shelf_t *shelf = find_shelf_for_good(db, owner, shelf_name);
shelf->amount += amount;
}
free(shelf_name); /// We did not store the shelf_name!
}
else
{
list_append(list, shelf_new(shelf_name, amount));
tree_insert(shelves, shelf_name, strdup(goods_name)); /// strdup!
}
/// Always remove goods name because it is strdup'd into the trees
free(goods_name);
answer = ask_question_string("Fler? (ja/nej)");
}
while (starts_with(answer, "#-----") == 0 || strcmp(uppercase(answer), "JA") == 0);
if (answer) free(answer);
tree_apply(db, print_goods, NULL);
/// Tear down
tree_apply(db, delete_lists, NULL);
tree_delete(db);
tree_apply(shelves, delete_strings, NULL); /// Remove all values in shelves tree
tree_delete(shelves);
Där delete_strings()
är definierad på detta vis:
void delete_strings(char *key, void *string, void *p)
{
free(string);
}
Observera att det går alldeles utmärkt att lösa uppgiften utan
strdup()
och delete_string()
! Det kräver istället att anropen
till free(shelf_name)
och free(goods_name)
distributeras över
koden. Vi tar bort ett inläst hyllnamn om det redan fanns sparat
och ett varunamn i det fall vi ser att varan redan finns. Båda
lösningarna förekommer i inlämningarna.
4 Hur jag har betygsatt
Med några undantag beroende på diverse omständigheter:
- Den som har löst båda del-uppgifternaAlltså sammanslagning av samma vara på samma hylla och ignorerandet av olika varor på samma hylla. utan minnesläckage eller invalid reads/writes har blivit GODKÄND.
- Den som har löst en av del-uppgifterna utan minnesläckage eller invalid reads/writes har fått REST.
- Den som har löst minst en av del-uppgifterna men har minnesläckage eller invalid reads/writes har fått REST.
- Alla andra har blivit UNDERKÄNDA. (I något fall där jag har hittat program som varit snubblande nära att falla i någon av kategorierna ovan har jag givit REST.)
Program som inte har funkat med testerna har jag manuellt jagat för att se om det t.ex. berodde på att vederbörande ändrat logiken för inläsning. I vissa fall har jag givit rest för för stora modifikationer av inläsning visavi testskriptet.
4.1 Hur fungerar rest?
Det förklaras på mail som kommer till dig som fått rest.
5 Vanliga fel
Nedan går jag igenom de vanligaste felen. De första två,
missuppfattning av shelves
och missat existensen av
shelves
, är de två vanligaste.
5.1 Missuppfattning av shelves
Förvånansvärt många har missuppfattat shelves
och har trott att
det är en avbildning av namn på hyllor på shelf_t
-pekare. Dessa
har förmodligen brottats med kraschande program eftersom den
sträng med varunamn som returnerats från tree_get()
har tolkats
som en pekare och en int.
/// Exempel från ett faktiskt kodprov (modulo namn)
shelf_t *shelf = tree_get(shelves, shelf_name);
5.2 Missat existensen av shelves
Många verkar helt ha missat existensen av shelves
, eller så har
man missuppfattat hur shelves
fungera och till slut givit upp
och försökt lösa uppgiften utan hjälp av shelves
. Detta har i
regel lett till väldigt krångliga lösningar med jobba anrop till
tree_apply()
. Dessa har tyvärr ofta varit felaktiga.
5.3 Minnesläckage
Några har missat att anropa iter_delete()
för att ta bort
iteratorn. Vissa har kommit ihåg att ta bort iteratorn efter
iterator-loopen, men gjort en return
innifrån iterator-loopen,
precis som i min kod ovan, och där glömt att städa upp iteratorn.
Några har missat att ta bort goods_name
och/eller shelf_name
i de fall när de inte sparas.
5.4 Jämföra strängar med ==
Några använt ==
istället för strcmp()
för strängjämförelse.
Observera att i C betyder detta jämför adresserna och inte
jämför strängarna. En sådan jämförelse kommer aldrig vara true
i detta program med mindre än att vi frigör strängar för tidigt.
/// Exempel från ett faktiskt kodprov (modulo namn)
if (shelf->name == shelf_name) ...
Några som har använt strcmp()
verkar ha glömt att funktionen
returnerar 0
när två strängar är lika.
6 Statistik och reflektioner
Statistiken talar sitt tydliga språk: provet lite för svårt! Samtidigt verkar provet fylla sitt syfte – i stor utsträckning har man antingen klarat allt eller inget än fått rest.
Betyg | Förmiddag | Eftermiddag | andel |
---|---|---|---|
Godkänd | 20 | 14 | 37% |
Rest | 10 | 14 | 26% |
Underkänd | 18 | 15 | 36% |
Totalt | 48 | 43 | 100% |
Vi kommer att ordna ett extra kodprovstillfälle – enbart för C-delen – som är öppet för alla de som fick underkänt eller inte skrev föregående kodprov.