Skip to content

Handout: Lecture 13 In-Class Exercises

Part A is pen-and-paper: reason about fopen modes and index a command line by hand, no compiler needed. Part B is at the keyboard: read command-line arguments, write and read files (text and binary), and build your own shared library and call it from Python. None of these repeats an earlier exercise; they drill today's moves - fopen/fgets/fprintf, argc/argv with strtol, fread/fwrite, and -fPIC -shared + ctypes.

Try each one yourself first; we will discuss in class, and the solutions are in a separate document afterward. Compile ordinary programs with warnings on:

clang -Wall -Wextra -std=c17 myprog.c -o myprog

Build a shared library (Exercise B5) with two extra flags, then run the Python driver:

clang -Wall -Wextra -std=c17 -fPIC -shared mylib.c -o libmylib.so
python3 driver.py

Reminders that will keep you out of trouble today:

  • fopen can return NULL (file missing, no permission). Check it every time, like malloc. Every fopen needs one fclose.
  • Mode strings: "r" read (must exist), "w" write (truncates!), "a" append (keeps old contents), add b for binary ("rb", "wb").
  • Read a file line by line with while (fgets(buf, sizeof buf, f) != NULL); fgets returns NULL at end of file, and the line still holds its '\n'.
  • main(int argc, char *argv[]): argv[0] is the program name, real arguments start at argv[1], argv[argc] is NULL. Arguments are strings - convert with strtol. Check argc and return nonzero on misuse.
  • fwrite(ptr, size, count, f) / fread(ptr, size, count, f) move raw bytes; both return the number of items transferred.
  • A shared library needs -fPIC -shared and has no main. In ctypes, set .argtypes and .restype or ctypes assumes every value is an int.

Set up

mkdir -p ~/cmsc14300/lec13
cd ~/cmsc14300/lec13

Start every C exercise from these includes:

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

Part A - Reasoning on paper (pen and paper)

Exercise A1 - What each mode does to a file

A file log.txt already exists on disk with these three lines:

boot ok
disk ok
net  ok

For each of the following, say (i) whether the fopen succeeds, (ii) what log.txt contains immediately after the open, before any read or write, and (iii) where the read/write cursor sits.

  1. fopen("log.txt", "r")
  2. fopen("log.txt", "w")
  3. fopen("log.txt", "a")
  4. fopen("missing.txt", "r")
  5. fopen("missing.txt", "w")

Then: a classmate writes FILE *f = fopen("log.txt", "w"); fprintf(f, "%s\n", name); and is surprised the three original lines vanished. Explain exactly what destroyed them, and which single mode change keeps the old lines and adds the new one at the end.

  • Check your understanding: why must you check f for NULL after opening "missing.txt" with "r", but a program that only ever uses "w" on a brand-new scratch file can almost get away without the check - and why is "almost" still not good enough to ship?

Exercise A2 - Index the command line by hand

A user runs:

./grade -k 3 results.txt bonus
  1. What is argc?
  2. Write out argv[0] through argv[argc], giving the exact string (or NULL) each holds.
  3. Which entry is the program name? Which is the first real argument?
  4. A classmate writes long k = strtol(argv[1], NULL, 10); intending to read the 3. What value do they actually get, and what is the off-by-one fix?

  5. Check your understanding: the argument "3" is a string of one character. What goes wrong if you try to use it directly in arithmetic as argv[2] * 2, and what is the right way to turn it into the number 3?


Part B - At the keyboard

Exercise B1 - Sum the command-line arguments

Write add - a program that treats each command-line argument as an integer and prints their sum. With no arguments, print 0. Convert each argument with strtol.

int main(int argc, char *argv[]);   /* loop argv[1..argc-1], strtol each, add */
$ ./add 3 4 5
12
$ ./add 100
100
$ ./add
0
  • Loop i from 1 to argc - 1 (skip argv[0], the program name).
  • long v = strtol(argv[i], NULL, 10); then total += v;.
  • Check your understanding: why does the loop start at 1 and not 0, and what would ./add 3 4 5 print if you started at 0?

Exercise B2 - Write a file, then read it back

Write a program that (1) opens tasks.txt for writing and writes three to-do lines with fprintf, closes it, then (2) reopens it for reading and prints every line back with the fgets loop, counting how many lines it read. NULL-check both opens.

FILE *f = fopen("tasks.txt", "w");   /* phase 1: write */
/* ... fprintf three lines, fclose ... */
f = fopen("tasks.txt", "r");         /* phase 2: read back */
/* ... while (fgets(...)) print + count, fclose ... */
buy milk
call ada
ship code
3 lines
  • Phase 1: fprintf(f, "buy milk\n"); and two more, then fclose(f).
  • Phase 2: char line[256]; and while (fgets(line, sizeof line, f)) { fputs(line, stdout); count++; }.
  • Check your understanding: run the program twice. Does tasks.txt grow to six lines the second time? Explain in terms of what "w" does on the second open, and what mode you would use if you did want to keep appending.

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

Write wclite - it takes a filename as a command-line argument, opens it, and prints the number of lines, words, and characters, like a stripped-down wc. If no filename is given, print a usage message to stderr and return 1. If the file will not open, perror it and return 1.

if (argc < 2) { fprintf(stderr, "usage: %s <file>\n", argv[0]); return 1; }
FILE *f = fopen(argv[1], "r");
if (f == NULL) { perror(argv[1]); return 1; }
/* fgets loop: chars += strlen(line); lines++; words via whitespace split */
$ ./wclite tasks.txt
  3   6  28 tasks.txt
$ ./wclite
usage: ./wclite <file>
$ ./wclite nope.txt
nope.txt: No such file or directory
  • Count characters as the total length of every line read (strlen(line)).
  • Count words by walking each line and counting transitions from whitespace into non-whitespace (a simple in_word flag is enough).
  • Check your understanding: the program has three different return values - 0, and two 1s. Why does a command-line tool bother to distinguish "worked" from "failed" with its exit code, when it already printed a message a human can read?

Exercise B4 - Binary dump and reload

Define a small record and an array of them, fwrite the whole array to a binary file, then fread it back into a fresh array and verify every field matches the original. Use binary mode ("wb" / "rb").

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

Book shelf[3] = { {"K&R C", 1978}, {"SICP", 1985}, {"TAPL", 2002} };
/* fwrite shelf -> books.dat, then fread into Book reload[3], compare */
wrote 3 books
read  3 books
round-trip OK
  • Write: fwrite(shelf, sizeof(Book), 3, f); - it returns the number of items written; check it is 3.
  • Read: into Book reload[3], fread(reload, sizeof(Book), 3, f); also returns the item count.
  • Compare each pair with strcmp on the title and == on the year; print round-trip OK only if all three match.
  • Check your understanding: open books.dat in your text editor. Why is most of it unreadable, and what is the one advantage this unreadable format has over writing the same data with fprintf?

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

Write stats.c containing one function, double average(const double *a, int n), that returns the mean of an array. Build it into libstats.so, then write driver.py that loads the library with ctypes and calls average on an array, printing the result.

/* stats.c - no main; this is a library */
double average(const double *a, int n) {
    /* sum the n doubles, divide by n; return 0.0 if n == 0 */
}
# 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))          # expect 6.0
clang -Wall -Wextra -std=c17 -fPIC -shared stats.c -o libstats.so
python3 driver.py
6.0
  • Build with -fPIC -shared; there is no main in stats.c.
  • In Python, the two lines that matter most are argtypes and restype. Try deleting the restype line and rerun - note the nonsense you get back when ctypes assumes the return type is int.
  • Check your understanding: why does ctypes need you to spell out argtypes and restype, when a C compiler figures the same types out from the function's declaration on its own?

Stretch - take it further

You called libstats.so from Python; now call it from C. Write main.c that declares double average(const double *a, int n);, fills a small array, and prints average(...). Compile against the library you already built, then run it:

clang -Wall -Wextra -std=c17 main.c -L. -lstats -Wl,-rpath,. -o usestats
./usestats

-L. says "look in the current directory for libraries," -lstats links libstats.so, and -Wl,-rpath,. records that directory so the program can find the .so at run time. Confirm you get the same average as the Python driver - one compiled library, two callers.

Benchmark C-via-ctypes against pure Python

Extend Exercise B5's average (or reuse the notes' array_sum) and, in Python, time it against a pure-Python loop over a large array (ten million elements). Python's time.perf_counter() around each call is enough. Confirm the C version through ctypes is many times faster, and write one sentence explaining where the pure-Python loop spends the time the C loop does not.