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:
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).
fopen("log.txt", "r")- succeeds (the file exists). Contents unchanged, all three lines still there. Cursor at the start.fopen("log.txt", "w")- succeeds, but"w"truncates the file to empty on open. Immediately after the open, before any write,log.txtis zero bytes. Cursor at the start (of an empty file).fopen("log.txt", "a")- succeeds. Contents unchanged (all three lines kept). Cursor at the end, so any write lands afternet ok.fopen("missing.txt", "r")- fails, returnsNULL."r"requires the file to already exist.fopen("missing.txt", "w")- succeeds, creating a new emptymissing.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 returnsNULL, and the nextfread/fgetson thatNULLhandle 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, soNULLfeels 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 neverNULL" is not safe enough to ship.
Exercise A2 - Index the command line by hand¶
For ./grade -k 3 results.txt bonus:
argc = 5.- The vector:
argv[0] -> "./grade"
argv[1] -> "-k"
argv[2] -> "3"
argv[3] -> "results.txt"
argv[4] -> "bonus"
argv[5] -> NULL
- The program name is
argv[0]("./grade"); the first real argument isargv[1]("-k"). -
strtol(argv[1], NULL, 10)parses"-k", which has no leading digits, sostrtolreturns0- not the3the classmate wanted. The3is inargv[2], one slot further along; the fix isstrtol(argv[2], NULL, 10). -
Check your understanding:
argv[2]is achar *- the address of the string"3", not the number3. Writingargv[2] * 2tries 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 computen * 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
ifrom1toargc - 1, converting each argument withstrtoland accumulating. With no argumentsargcis1, the loop body never runs, andtotalstays0- exactly the required behavior. - Check your understanding: the loop starts at
1becauseargv[0]is the program name ("./add"), not a number to sum. Starting at0would runstrtol("./add", NULL, 10), which finds no leading digits and returns0, so./add 3 4 5would still print12- but only by luck. Give the program a name that starts with a digit and the count breaks; skippingargv[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 thefgetsloop, 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 truncatestasks.txtback 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
strlenof every line (counting each'\n', as realwcdoes). Words are counted by the classicin_wordstate machine: each transition from whitespace into a non-space character starts a new word.isspaceis cast throughunsigned charbecause passing a raw (possibly negative)charto a<ctype.h>function is undefined. - On the
tasks.txtfrom B2 this prints3 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
wcliteneeds a machine-checkable way to know whether it worked -if ./wclite f; then ...- and that is the exit code, not the text. Returning0on 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;
}
fwritecopies the threeBookstructs to disk as raw bytes and returns the item count (3);freadpulls the same bytes back into a fresh array and also returns the count. Comparing title (withstrcmp) and year (with==) confirms every field survived.- Check your understanding:
books.datlooks like garbage in an editor because theintyears and the struct's padding bytes are not printable characters - only the title text is legible. The advantage overfprintfis 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:
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
-fPIC -sharedproduceslibstats.so; there is nomainbecause 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 thataveragetakes adouble *and anintand returns adouble. You must tell it withargtypesandrestype; otherwise it defaults tointeverywhere and hands the wrong bytes in and out. Delete therestypeline and rerun to see the garbage that results.
Stretch - take it further¶
Link your shared library into a C program¶
/* 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;
}
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.