Skip to content

Solutions: Lecture 13 In-Class Exercises

Solutions to the Lecture 13 in-class exercises. Try each exercise yourself before reading these. Every C solution starts from:

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

Compile ordinary programs with clang -Wall -Wextra -std=c17 myprog.c -o myprog; build the shared library in B5 with clang -Wall -Wextra -std=c17 -fPIC -shared stats.c -o libstats.so and run its driver with python3 driver.py.


Part A - Reasoning on paper (pen and paper)

Exercise A1 - What each mode does to a file

log.txt starts with three lines (boot ok / disk ok / net ok).

  1. fopen("log.txt", "r") - succeeds (the file exists). Contents unchanged, all three lines still there. Cursor at the start.
  2. fopen("log.txt", "w") - succeeds, but "w" truncates the file to empty on open. Immediately after the open, before any write, log.txt is zero bytes. Cursor at the start (of an empty file).
  3. fopen("log.txt", "a") - succeeds. Contents unchanged (all three lines kept). Cursor at the end, so any write lands after net ok.
  4. fopen("missing.txt", "r") - fails, returns NULL. "r" requires the file to already exist.
  5. fopen("missing.txt", "w") - succeeds, creating a new empty missing.txt. Cursor at the start.

The classmate's disappearing lines: fopen("log.txt", "w") truncated the file to empty the instant it opened, long before fprintf ran - the three original lines were gone before anything was written. The one-character fix is to open with "a" instead of "w": append keeps the old contents and adds the new line at the end.

  • Check your understanding: "r" on a missing file returns NULL, and the next fread/fgets on that NULL handle dereferences a null pointer and crashes - so the check is mandatory. A program that only ever "w"-opens a fresh scratch file almost always succeeds, so NULL feels impossible - but "w" can still fail: a read-only or full disk, a bad path, missing directory permissions. Those are exactly the conditions that show up in production and never on your laptop, which is why "almost never NULL" is not safe enough to ship.

Exercise A2 - Index the command line by hand

For ./grade -k 3 results.txt bonus:

  1. argc = 5.
  2. The vector:
argv[0] -> "./grade"
argv[1] -> "-k"
argv[2] -> "3"
argv[3] -> "results.txt"
argv[4] -> "bonus"
argv[5] -> NULL
  1. The program name is argv[0] ("./grade"); the first real argument is argv[1] ("-k").
  2. strtol(argv[1], NULL, 10) parses "-k", which has no leading digits, so strtol returns 0 - not the 3 the classmate wanted. The 3 is in argv[2], one slot further along; the fix is strtol(argv[2], NULL, 10).

  3. Check your understanding: argv[2] is a char * - the address of the string "3", not the number 3. Writing argv[2] * 2 tries to multiply a pointer, which is not even legal arithmetic; even if it compiled it would do math on an address, not on the value three. Convert the text to a number first (long n = strtol(argv[2], NULL, 10);), then compute n * 2.


Part B - At the keyboard

Exercise B1 - Sum the command-line arguments

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

int main(int argc, char *argv[]) {
    long total = 0;
    for (int i = 1; i < argc; i++) {       /* skip argv[0], the program name */
        total += strtol(argv[i], NULL, 10);
    }
    printf("%ld\n", total);
    return 0;
}
  • The loop runs i from 1 to argc - 1, converting each argument with strtol and accumulating. With no arguments argc is 1, the loop body never runs, and total stays 0 - exactly the required behavior.
  • Check your understanding: the loop starts at 1 because argv[0] is the program name ("./add"), not a number to sum. Starting at 0 would run strtol("./add", NULL, 10), which finds no leading digits and returns 0, so ./add 3 4 5 would still print 12 - but only by luck. Give the program a name that starts with a digit and the count breaks; skipping argv[0] is the correct reason, not a coincidence.

Exercise B2 - Write a file, then read it back

#include <stdio.h>

int main(void) {
    FILE *f = fopen("tasks.txt", "w");             /* phase 1: write */
    if (f == NULL) { perror("tasks.txt"); return 1; }
    fprintf(f, "buy milk\n");
    fprintf(f, "call ada\n");
    fprintf(f, "ship code\n");
    fclose(f);

    f = fopen("tasks.txt", "r");                   /* phase 2: read back */
    if (f == NULL) { perror("tasks.txt"); return 1; }
    char line[256];
    int count = 0;
    while (fgets(line, sizeof line, f) != NULL) {
        fputs(line, stdout);
        count++;
    }
    fclose(f);

    printf("%d lines\n", count);
    return 0;
}
  • Phase 1 opens with "w", writes three lines, and closes so the data is flushed to disk. Phase 2 reopens the same file with "r" and walks it with the fgets loop, echoing each line and counting.
  • Check your understanding: running it a second time does not grow the file to six lines. Each run's phase-1 "w" open truncates tasks.txt back to empty first, so it always ends at exactly three lines. To keep appending across runs you would open phase 1 with "a" instead - then a second run would leave six lines.

Exercise B3 - A line/word/character counter (wc-lite)

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

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "usage: %s <file>\n", argv[0]);
        return 1;                                  /* no filename given */
    }

    FILE *f = fopen(argv[1], "r");
    if (f == NULL) {
        perror(argv[1]);                           /* say why it failed */
        return 1;
    }

    long lines = 0, words = 0, chars = 0;
    char line[1024];
    while (fgets(line, sizeof line, f) != NULL) {
        lines++;
        chars += (long) strlen(line);              /* incl. the trailing '\n' */
        int in_word = 0;
        for (const char *p = line; *p != '\0'; p++) {
            if (isspace((unsigned char) *p)) {
                in_word = 0;
            } else if (!in_word) {
                in_word = 1;                        /* entering a new word */
                words++;
            }
        }
    }
    fclose(f);

    printf("%3ld %3ld %3ld %s\n", lines, words, chars, argv[1]);
    return 0;
}
  • Characters are the summed strlen of every line (counting each '\n', as real wc does). Words are counted by the classic in_word state machine: each transition from whitespace into a non-space character starts a new word. isspace is cast through unsigned char because passing a raw (possibly negative) char to a <ctype.h> function is undefined.
  • On the tasks.txt from B2 this prints 3 6 28 tasks.txt (28 = 9 + 9 + 10 characters).
  • Check your understanding: the printed message is for a human, but the exit code is for a program. A shell script or a pipeline that runs wclite needs a machine-checkable way to know whether it worked - if ./wclite f; then ... - and that is the exit code, not the text. Returning 0 on success and nonzero on failure is the contract that lets tools be composed.

Exercise B4 - Binary dump and reload

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

typedef struct {
    char title[32];
    int  year;
} Book;

int main(void) {
    Book shelf[3] = { {"K&R C", 1978}, {"SICP", 1985}, {"TAPL", 2002} };

    FILE *f = fopen("books.dat", "wb");            /* binary write */
    if (f == NULL) { perror("books.dat"); return 1; }
    size_t wrote = fwrite(shelf, sizeof(Book), 3, f);
    fclose(f);
    printf("wrote %zu books\n", wrote);

    Book reload[3];
    f = fopen("books.dat", "rb");                  /* binary read */
    if (f == NULL) { perror("books.dat"); return 1; }
    size_t got = fread(reload, sizeof(Book), 3, f);
    fclose(f);
    printf("read  %zu books\n", got);

    int ok = (got == 3);
    for (size_t i = 0; i < got; i++) {
        if (strcmp(shelf[i].title, reload[i].title) != 0 ||
            shelf[i].year != reload[i].year) {
            ok = 0;
        }
    }
    printf("%s\n", ok ? "round-trip OK" : "round-trip FAILED");
    return 0;
}
wrote 3 books
read  3 books
round-trip OK
  • fwrite copies the three Book structs to disk as raw bytes and returns the item count (3); fread pulls the same bytes back into a fresh array and also returns the count. Comparing title (with strcmp) and year (with ==) confirms every field survived.
  • Check your understanding: books.dat looks like garbage in an editor because the int years and the struct's padding bytes are not printable characters - only the title text is legible. The advantage over fprintf is that writing and reading are just a raw byte copy: no formatting on the way out, no parsing on the way back, and the file is smaller. The cost is portability - the layout is specific to this machine's integer size, padding, and byte order.

Exercise B5 - Build a shared library, call it from Python

The library (no main):

/* stats.c */
double average(const double *a, int n) {
    if (n == 0) {
        return 0.0;                    /* avoid divide-by-zero on empty input */
    }
    double total = 0.0;
    for (int i = 0; i < n; i++) {
        total += a[i];
    }
    return total / n;
}

Build it:

clang -Wall -Wextra -std=c17 -fPIC -shared stats.c -o libstats.so

The Python driver:

# driver.py
import ctypes

lib = ctypes.CDLL("./libstats.so")
lib.average.argtypes = [ctypes.POINTER(ctypes.c_double), ctypes.c_int]
lib.average.restype  = ctypes.c_double

data = (ctypes.c_double * 5)(2.0, 4.0, 6.0, 8.0, 10.0)
print(lib.average(data, 5))            # 6.0
$ python3 driver.py
6.0
  • -fPIC -shared produces libstats.so; there is no main because a library is a bag of functions for someone else to call. (ctypes.c_double * 5)(...) builds a C array of five doubles that ctypes passes straight through as a pointer.
  • Check your understanding: a C compiler reads the function's declaration in the source and knows its argument and return types, so it emits the right machine code automatically. ctypes only has the compiled .so - a bag of bytes with no type information - so it cannot know that average takes a double * and an int and returns a double. You must tell it with argtypes and restype; otherwise it defaults to int everywhere and hands the wrong bytes in and out. Delete the restype line and rerun to see the garbage that results.

Stretch - take it further

/* main.c */
#include <stdio.h>

double average(const double *a, int n);    /* declared here, defined in libstats */

int main(void) {
    double data[5] = {2.0, 4.0, 6.0, 8.0, 10.0};
    printf("%g\n", average(data, 5));
    return 0;
}
clang -Wall -Wextra -std=c17 main.c -L. -lstats -Wl,-rpath,. -o usestats
./usestats
6.0

main.c only declares average; the definition lives in libstats.so. -L. tells the linker to search the current directory, -lstats picks up libstats.so, and -Wl,-rpath,. records that directory in the executable so it finds the .so at run time (without it you would need to set LD_LIBRARY_PATH=.). The same library now has two callers - Python via ctypes and this C program - which is exactly the point of a shared library.

Benchmark C-via-ctypes against pure Python

# bench.py
import ctypes, time

lib = ctypes.CDLL("./libstats.so")
lib.average.argtypes = [ctypes.POINTER(ctypes.c_double), ctypes.c_int]
lib.average.restype  = ctypes.c_double

n = 10_000_000
data = (ctypes.c_double * n)(*([1.0] * n))

t0 = time.perf_counter()
c_result = lib.average(data, n)
t1 = time.perf_counter()

py_total = 0.0
for x in data:
    py_total += x
py_result = py_total / n
t2 = time.perf_counter()

print(f"C via ctypes: {c_result} in {t1 - t0:.4f} s")
print(f"pure Python : {py_result} in {t2 - t1:.4f} s")

The C version runs many times faster. The pure-Python loop is slow because every iteration is interpreted: each += looks up types, boxes and unboxes a Python float object, and does bounds and reference-count bookkeeping. The C loop does none of that - it is compiled machine code adding raw doubles in registers, which is the whole reason to push a hot loop down into a shared library.