Programmering C: Lösningar till tentamen 2013-01-19

Observera att detta är förslag på lösningar. Det kan finnas andra lösningar som också är korrekta, och det kan hända att en del av lösningarna är mer omfattande än vad som krävs för full poäng på uppgiften. En del av lösningarna är kanske inte fullständiga, utan hänvisar bara till var man kan läsa svaret.

Uppgift 1 (1 p)

a) 5

b) 8

c) -4

d) 1

Diskussion:

En kommentar från en student:

Uppgift 1 d borde vara 3/2 och inte ett: (2+2/2)/2 = (2+1)/2 = 3/2

Svar:

Nej, det blir heltalsdivision, och då blir resultatet 1. Det är därför som till exempel uttrycket

antal_gula_bilar / totalt_antal_bilar * 100
inte ger procentandelen gula bilar utan alltid blir noll eller, om alla bilar är gula, 100.

Uppgift 2 (1 p)

a = 2, b = 2, c = 5

Uppgift 3 (4 p)

#include <stdio.h>

int main(void) {
    printf("Hur många flyttal? ");
    int antal_tal;
    scanf("%d", &antal_tal);

    printf("Skriv %d reella tal:\n", antal_tal);
    int antal_negativa_tal = 0;
    for (int i = 0; i < antal_tal; ++i) {
        double detta_flyttal;
        scanf("%lf", &detta_flyttal);
        if (detta_flyttal < 0)
            ++antal_negativa_tal;
    }

    printf("Antal negativa tal: %d\n", antal_negativa_tal);
    
    return 0;
}

Uppgift 4 (4 p)

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    printf("Skriv första raden: ");
    char rad1[100 + 1];
    gets(rad1);
    printf("Skriva andra raden: ");
    char rad2[100 + 1];
    gets(rad2);

    srand(time(NULL)); // Ger pseudoslumptalsgeneratorn ett startvärde
    int radval = rand() % 2; // Ger ett slumptal som är 0 eller 1

    printf("En slumpmässigt vald rad:\n");
    if (radval == 0)
        puts(rad1);
    else
        puts(rad2);
    return 0;
}
Diskussion:
En kommentar från en student:

Personligen skulle jag aldrig använda rand i några seriösa sammanhang då den inte är en CSPRNG (Cryptographically secure pseudorandom number generator), något man kanske bör nämna i tentasvaret.

Svar:

Ja, i en del tillämpningar behöver man ha kryptografisk kvalitet på slumpen, men om man inte behöver det kan det vara bra att använda funktioner som finns i standard-C. Men det kan vara bra att varna för det.

Man bör också varna lite för standardsättet "srand(time(NULL));", som nog är ett ännu värre säkerhetshål. Säg att man vill generera en 1024-bitars kryptonyckel och använder aktuell tid som slumptalsfrö. Om en angripare kan gissa vilken arbetsdag man gjorde det, är det inte 1024 bitar man har, utan knappt 15 bitar. Vet han dessutom klockslaget, till exempel för att det står i mailet som man krypterade med den där 1024-bitarsnyckeln, så är det inga bitar alls, utan han har redan nyckeln.

Uppgift 5 (2 p)

#define MAX_NAMNLANGD 40

struct Prisuppgift {
    char vara[MAX_NAMNLANGD + 1];
    char butik[MAX_NAMNLANGD + 1];
    int pris;
};
Diskussion:
En kommentar från en student:

Nu kan man ha en intressant diskussion med vad som menas med 40 tecken. Personligen utgår jag från ASCII-tabellen och där är '\0'-tecknet med. Uppenbarligen har du inte samma syn då du lägger till ett extra element till din array så jag utgår från att du inte räknar '\0' som ett tecken.

Här borde man göra frågan tydligare och ange att man skall få plats med 40 tecken som inte innehåller några kontrolltecken, alternativ med tecknen [a-zåäöA-ZÅÄÖ0-9].

Sedan tycker jag personligen att MAX alltid bör ange storleken på arrayen och inte storleken (minus '\0') så då borde man istället ha en #define med värdet 41 (om man nu inte anser att '\0' är ett tecken).

    #define MAX_NAMNLANGD 41
då behöver man inte magiskt plusa ett över allt i koden. Ens kod blir enklare även för funktioner som inte lägger till '\0'.
   strncpy (str1, str2, MAX_NAMNLANGD)
   str2[MAX_NAMNLANGD] = '\0';
Istället för
   strncpy (str1, str2, MAX_NAMNLANGD)
   str2[MAX_NAMNLANGD + 1] = '\0';
om define inte anger längden på arrayen.

Svar:

Jo, det kan vara en intressant diskussion. Men jag tycker att det enda motargument som behövs är följande kodrad, med din lösning:

     printf("Ange varans namn (högst %d tecken): ", MAX_NAMNLANGD - 1);
Och sen lagrar vi prisuppgifterna i en databas:
     create table Prisuppgifter
     (Vara varchar(40) not null,
      Butik varchar(40) not null,
      Pris integer not null,
      primary key (Vara, Butik));
(På riktigt skulle min tabell inte se ut så.)

Om namnlängden verkligen var 41 tecken, varför skriver vi 40 i SQL? Nej, för mig framstår det som mycket tydligt att det överlägset bästa sättet att tänka är att där extra NUL-tecknet som vi behöver i C är något förutom namnlängden. Ja, det är ett tecken, men det ingår inte i maxlängden på namn. Ja, man kan tänka på det andra sättet, men det är inte lika bra, utan så mycket sämre att det är fel.

Svar på svaret:

Om makrot skall innehålla nulltecknet heller inte beror nog på namnet. Hade det slutat med SIZE tycker jag att det skall innehålla nullteckent eftersom man då syftar på arrayen storlek och slutar den på LENGTH så skall den inte innehålla det. Stämmer då även överens med hur strlen fungerar, där är inte nulltecknet medräknat.

Uppgift 6 (1 p)

struct Prisuppgift p = { "Apple MacBook Air", "Dustin Home", 9590 };

Uppgift 7 (1 p)

int samma_vara(struct Prisuppgift u1, struct Prisuppgift u2) {
    return strcmp(u1.vara, u2.vara) == 0;
}

Uppgift 8 (2 p)

int billigare(struct Prisuppgift u1, struct Prisuppgift u2) {
    return samma_vara(u1, u2) && u1.pris < u2.pris;
}

Uppgift 9 (2 p)

void visa_prisuppgift(struct Prisuppgift u) {
    printf("Vara: %s\n", u.vara);
    printf("Butik: %s\n", u.butik);
    printf("Pris: %d\n", u.pris);
}

Uppgift 10 (3 p)

void las_prisuppgift(struct Prisuppgift *up) {
    printf("Ange varans namn: ");
    gets(up->vara); // Nej, vi borde egentligen inte använda gets
    printf("Ange butikens namn: ");
    gets(up->butik); // Nej, vi borde egentligen inte använda gets
    printf("Ange priset: ");
    scanf("%d", &up->pris);
    // Nu finns ett radslutstecken kvar i inmatningsbufferten.
    // Vi måste läsa förbi det, annars tolkar nästa gets det som en tom rad.
    while (getchar() != '\n')
        ;
}

Uppgift 11 (3 p)

int main(void) {
    struct Prisuppgift prisuppgift1, prisuppgift2;

    printf("Mata in två prisuppgiftar:\n");
    las_prisuppgift(&prisuppgift1);
    las_prisuppgift(&prisuppgift2);

    if (samma_vara(prisuppgift1, prisuppgift2) == 0) {
        printf("De handlar om olika varor.\n");
    }
    else {
        printf("De handlar om samma vara. Den billigaste:\n");
        if (billigare(prisuppgift1, prisuppgift2))
            visa_prisuppgift(prisuppgift1);
        else
            visa_prisuppgift(prisuppgift2);
    }

    return 0;
}

Uppgift 12 (8 p)

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

// Definitioner enligt ovan

void avradsluta(char *s) {
    int sista = strlen(s) - 1;
    if (s[sista] == '\n')
        s[sista] = '\0';
}

int las_prisuppgift_fran_fil(FILE *f, struct Prisuppgift *up) {
    char namn[MAX_NAMNLANGD + 1 + 1]; // +1 för \n och +1 för \0
    if (fgets(namn, sizeof namn, f) == NULL)
        return 0;
    avradsluta(namn);
    strcpy(up->vara, namn);
    fgets(namn, sizeof namn, f);
    avradsluta(namn);
    strcpy(up->butik, namn);
    fscanf(f, "%d", &up->pris);
    // Läs förbi radslutstecknet i inmatningsbufferten
    while (getc(f) != '\n')
        ;
    return 1;
}

int main(void) {
    char sokt_vara[MAX_NAMNLANGD + 1];
    printf("Ange vara: ");
    gets(sokt_vara); // Jaja. Vi vet.
    FILE* f = fopen("allapriser.txt", "r");
    if (f == NULL) {
        fprintf(stderr, "Filen 'allapriser.txt' gick inte att öppna.\n");
        exit(EXIT_FAILURE);
    }
    int varan_hittad = 0;
    struct Prisuppgift billigast_hittills;
    struct Prisuppgift u;
    while (las_prisuppgift_fran_fil(f, &u) != 0) {
        if (strcmp(sokt_vara, u.vara) == 0) {
            if (varan_hittad == 0 || billigare(u, billigast_hittills)) {
                billigast_hittills = u;
                varan_hittad = 1;
            }
        }
    }
    fclose(f);
    if (varan_hittad == 0) {
        printf("Det finns inga prisuppgifter för den varan.\n");
    }
    else {
        printf("Billigast på %s (%d kronor).\n",
               billigast_hittills.butik, billigast_hittills.pris);
    }
    return EXIT_SUCCESS;
}
Diskussion:
En kommentar från en student:

char namn[MAX_NAMNLANGD + 1 + 1]; // +1 för \n och +1 för \0
Varför behöver du plussa en extra för \n? Eftersom fgets som används för att läsa in endast kommer att lägga till '\n' om den får plats innanför arrayen. I de fall som den får det tar man bort den och i de fall där den hämnar utanför så kommer den inte att läggas till arrayen ändå.

Svar:

Här tycker jag att det är en mindre skillnad i brahet mellan ditt och mitt synsätt. Men om man inte tar med den extra platsen för radslutstecknet, och användaren matar in maximalt tillåtet antal tecken, så ligger radslutstecknet kvar och skräpar i inmatningsbufferten. Då måste man hantera det, och dessutom kan man inte kolla om man ska skriva ut en varning för att något konstigt hänt (för lång rad, filslut) genom att titta om det finns ett radslutstecken i namn-variabeln.

Studenten:

Okej så syftet med att ha med radslutstecknet är att kunna kontrollera ifall användaren har skrivit för många tecken, men i koden som följer görs inte en sådan koll.

Svar:

Dels det, men kanske mer en allmän regel att om alla indata är korrekta och inom de specificerade gränserna så bör man följa en och samma "normala" exekveringsväg genom koden, och helst undvika att specialhantera kantfall. I det här fallet skulle det bli en exekveringsväg för alla inmatningar på namn på 0-39 tecken, en annan exekveringsväg för 40 tecken, och (om vi antar att vi har felkontroll) en tredje för namn på 41 eller fler tecken.

En annan kommentar från en student:

Nu när jag tittar lite noggrannare på koden i svaret till uppgift 12 undrar jag om det inte är en bug i den. Om vi säger att användaren skriver in 42 tecken innan personen trycker på enter, då kommer en buffer overflow att inträffa då strcpy kommer att kopiera förbi up->vara alternativt up->butik längd eftersom '\0' kommer att vara på position 41 i namn arrayen.

Svar:

Ja, det stämmer. Den är lite slarvigt gjord, utan felkontroll, i stil med att jag använde gets för inmatningen från standardinmatningen, och här har jag bara ersatt gets med fgets. Matar man in för långa varu- eller butiksnamn blir det fel, också eftersom resterande del av det för långa namnet ligger kvar till nästa inläsning. Jag ska nog lägga in det också i lösningsförslagen så det framgår.

Uppgift 13 (5 p)

#include <stdio.h>

int main(void) {
    FILE *bsin = fopen("tal.bin", "rb");
    FILE *tsin = fopen("tal.txt", "r");
    float bintal;
    float texttal;
    int hittat_skillnad = 0;
    while (fread(&bintal, sizeof bintal, 1, bsin) == 1 && !hittat_skillnad) {
        if (fscanf(tsin, "%f", &texttal) != 1)
            hittat_skillnad = 1;
        else if (bintal != texttal)
            hittat_skillnad = 1;
    }
    // Om det finns tal kvar på textfilen är filerna olika!
    if (fscanf(tsin, "%f", &texttal) != EOF)
        hittat_skillnad = 1;
    fclose(bsin);
    fclose(tsin);
    if (hittat_skillnad == 0)
        printf("Filerna innehåller samma tal.\n");
    else
        printf("Filerna innehåller inte samma tal.\n");
    return 0;
}


Thomas Padron-McCarthy (thomas.padron-mccarthy@oru.se), 28 januari 2013