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:
Build a shared library (Exercise B5) with two extra flags, then run the Python driver:
Reminders that will keep you out of trouble today:
fopencan returnNULL(file missing, no permission). Check it every time, likemalloc. Everyfopenneeds onefclose.- Mode strings:
"r"read (must exist),"w"write (truncates!),"a"append (keeps old contents), addbfor binary ("rb","wb"). - Read a file line by line with
while (fgets(buf, sizeof buf, f) != NULL);fgetsreturnsNULLat end of file, and the line still holds its'\n'. main(int argc, char *argv[]):argv[0]is the program name, real arguments start atargv[1],argv[argc]isNULL. Arguments are strings - convert withstrtol. Checkargcand 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 -sharedand has nomain. Inctypes, set.argtypesand.restypeor ctypes assumes every value is anint.
Set up¶
Start every C exercise from these includes:
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:
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.
fopen("log.txt", "r")fopen("log.txt", "w")fopen("log.txt", "a")fopen("missing.txt", "r")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
fforNULLafter 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:
- What is
argc? - Write out
argv[0]throughargv[argc], giving the exact string (orNULL) each holds. - Which entry is the program name? Which is the first real argument?
-
A classmate writes
long k = strtol(argv[1], NULL, 10);intending to read the3. What value do they actually get, and what is the off-by-one fix? -
Check your understanding: the argument
"3"is a string of one character. What goes wrong if you try to use it directly in arithmetic asargv[2] * 2, and what is the right way to turn it into the number3?
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.
- Loop
ifrom1toargc - 1(skipargv[0], the program name). long v = strtol(argv[i], NULL, 10);thentotal += v;.- Check your understanding: why does the loop start at
1and not0, and what would./add 3 4 5print if you started at0?
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 ... */
- Phase 1:
fprintf(f, "buy milk\n");and two more, thenfclose(f). - Phase 2:
char line[256];andwhile (fgets(line, sizeof line, f)) { fputs(line, stdout); count++; }. - Check your understanding: run the program twice. Does
tasks.txtgrow 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_wordflag is enough). - Check your understanding: the program has three different
returnvalues -0, and two1s. 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 */
- Write:
fwrite(shelf, sizeof(Book), 3, f);- it returns the number of items written; check it is3. - Read: into
Book reload[3],fread(reload, sizeof(Book), 3, f);also returns the item count. - Compare each pair with
strcmpon the title and==on the year; printround-trip OKonly if all three match. - Check your understanding: open
books.datin your text editor. Why is most of it unreadable, and what is the one advantage this unreadable format has over writing the same data withfprintf?
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
- Build with
-fPIC -shared; there is nomaininstats.c. - In Python, the two lines that matter most are
argtypesandrestype. Try deleting therestypeline and rerun - note the nonsense you get back when ctypes assumes the return type isint. - Check your understanding: why does ctypes need you to spell out
argtypesandrestype, when a C compiler figures the same types out from the function's declaration on its own?
Stretch - take it further¶
Link your shared library into a C program¶
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:
-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.