# CS 211 - Lecture 06 - File I/O and Structs Bernhard Firner 2025-09-18 --- ## Review! * Header files * Makefiles --- ## Modular Code * `modular` code is `reusable` by multiple programs * Libraries of functions like `
` * To make our own libraries * Declare functions (and variables) into `.h` file * Put implementations into `.c` file * Compile `.c` files separately, then users link with their programs --- ## Makefiles * Managing all of those steps on your own is a pain * Makefiles manage it for you! ``` # Compile anything with debug flags (-g) debug_%.o: %.c gcc -g -c $^ -o $@ # Compile anything with debug flags debug_%: debug_%.o gcc -g $^ -o $@ # Any filename that ends in .o is made with the command gcc
-o
# $@ is the list of targets (the thing before the colon) # $^ is the list of dependencies (the things after the colon) %.o: %.c gcc -c $^ -o $@ # Compiles directly to a target. %: %.c gcc $^ -o $@ # The sieve_example program depends upon both using_sieve.o and sieve.o # $^ expands to 'using_sieve.o sieve.o' # $@ expands to 'sieve_example' sieve_example: using_sieve.o sieve.o gcc $^ -o $@ debug_sieve_example: debug_using_sieve.o debug_sieve.o gcc $^ -o $@ ``` -v- ## Note on Makefile copy & paste * If you copy and paste that makefile code it may not work * Something is converting tabs into spaces, but makefiles require tabs in the rules * Converting the indentations into tabs fixes the problem --- ## Managing Data ```C unsigned long* sieve(size_t max, size_t *size); ``` * Passing around our pointers with their sizes is cumbersome and error inducing * Having two return pathways is also confusing --- ## Structs * Allow us to group related data under a new name * A lightweight precursor to classes you've seen in other languages * Typical of C, `struct`s also support some low level optimizations --- ## Declaring a struct * `struct
{ declaration-list };` ```C struct twoNumbers { int a; int b; }; ``` --- ## new struct * The name of the struct is "struct twoNumbers" * It can be used anywhere you would use another type * function arguments and returns * array declarations * inside other structs --- ## typedef * The names of our structs are awkward * `struct name` * C has a way to make new names, and we can use it to simplify our structs * `typedef
` * You can do this before making the struct, which is a `forward declaration` ```C typedef struct nicePtr nicePtr; struct nicePtr { unsigned long* memory; size_t elements; }; ``` --- ## More C Cruft * A quick note * You will see some odd struct usage in the wild * You can instantiate a variable of the new type immediately ```C struct twoNumbers { int a; int b; } instance; ``` --- ## Avoid confusion ```C typedef struct twoNumbers twoNumbers; struct twoNumbers { int a; int b; }; twoNumbers instance; ``` --- ## Member initialization ```C typedef struct twoNumbers twoNumbers; struct twoNumbers { int a; int b; }; twoNumbers first = {10, 20}; twoNumbers second = {.a = 10, .b = 20}; ``` --- ## Accessing struct members * The '.' (dot) operator * Used in optional initialization syntax * also used to access individual values ```C twoNumbers first = {10, 20}; first.a = 50; ``` --- ## note on operators * Our regular operators (+, -, etc) aren't supported * We will have to write our own functions ```C twoNumbers addTwoNumbers(twoNumbers left, twoNumbers right) { twoNumbers sum = {.a = left.a + right.a, .b = left.b + right.b}; return sum; } ``` --- ## Passing structs * Function arguments are copied when passed * Avoid passing by value if structs are large * Use a pointer to the struct in those cases * Use `const` to protect current values --- ## passing pointers to struct ```C twoNumbers addTwoNumbers(twoNumbers* left, teoNumbers* right) { twoNumbers sum{.a = (*left).a + (*right).a, .b = (*left).b + (*right).b}; return sum; } ``` --- ## -> operator Annoying to keep using * to get the pointer values ```C twoNumbers addTwoNumbers(twoNumbers* left, teoNumbers* right) { twoNumbers sum = {.a = left->a + right->a, .b = left->b + right->b}; return sum; } ``` --- ## Using const qualifier * Some functions we've seen have `const` in their arguments * This tells the compiler that the variable, or what it points to, should not be modified --- ## Const example ```C /* * Demonstrate const with struct pointers */ #include
typedef struct bigData bigData; struct bigData { long long numbers[1000]; }; // This function modifies data's members, so it cannot be const. void modifyData(bigData* data) { for (int i = 0; i < sizeof(data->numbers)/sizeof(long long); ++i) { data->numbers[i] = i; } } // This function does not modify data's members, so it can be const. long long sumData(const bigData* data) { long long sum = 0; for (int i = 0; i < sizeof(data->numbers)/sizeof(long long); ++i) { sum += data->numbers[i]; } return sum; } int main(void) { bigData data = {.numbers = {}}; // Modify the data modifyData(&data); // Sum the data printf("Sum is %lli\n", sumData(&data)); return 0; } ``` --- ## Passing pointers in structs * Passing a pointer does not copy that memory * Likewise, passing a struct with a pointer only copies the pointer * So passing a struct of pointers is okay * Only copies the pointers, which are numbers * This means that we can return structs with pointer members safely * Just be sure to have a way to indicate errors --- ## Pointers with Sizes ```C typedef struct nicePtr nicePtr; struct nicePtr { unsigned long* memory; size_t elements; }; ``` --- ## sieve example ```C #include
#include
#include
#include
typedef struct nicePtr nicePtr; struct nicePtr { unsigned long* memory; size_t elements; }; nicePtr sieve(size_t max) { size_t num_primes = 0; bool maybe_prime[max]; memset(maybe_prime, true, max-3); maybe_prime[0] = false; maybe_prime[1] = false; for (int i = 2; i < max; ++i) { // Mark multiples as not prime // Go through every multiple of i, starting with 2*i // Then 2*i+i, 3*i+i, etc if (maybe_prime[i]) { num_primes += 1; for (int j = 2*i; j < max; j += i) { maybe_prime[j] = false; } } } // Allocate memory for all of the primes unsigned long* primes = calloc(num_primes, sizeof(unsigned long)); unsigned long* cur_prime = primes; int index = 0; for (int i = 0; i < max; ++i) { if (maybe_prime[i]) { *cur_prime = i; ++cur_prime; } } nicePtr result = {.memory = primes, .elements = num_primes}; return result; } int main(int argc, char** argv) { if (argc < 2) { printf("Give me an argument!\n"); return 1; } size_t max = atoi(argv[1]); if (max < 3) { printf("Give me a larger number!\n"); return 1; } nicePtr primes = sieve(max); for (int i = 0; i < primes.elements; ++i) { printf("%lu is prime.\n", primes.memory[i]); } free(primes.memory); return 0; } ``` --- ## Pause * So far, so good? * Next, we'll makea simple data structure --- ## Managing complex data * You are going to encounter a lot of matrices in you courses * We haven't been doing it, but 2D arrays are easy to make: * `float I[3][3] = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}};` * But then we would need to make functions to support every possible size --- ## Matrix struct * With structs and dynamic memory allocation we can do better * struct will hold both the memory and the size * Makes it feasible to manage variable sized matrices --- ## The struct ```C /* * The header file for 2D matrix operations. */ typedef struct Matrix Matrix; struct Matrix { float** values; size_t height; size_t width; }; ``` --- ## Important Operations ```C // Create a new matrix. Matrix newMatrix(size_t height, size_t width); // Free the memory of a matrix. void freeMatrix(Matrix); // Create a string representing the matrix. // Memory must be freed by the caller. void printMatrix(Matrix); // Add two matrices. Matrix addMatrix(Matrix, Matrix); ``` --- ## Implementation ```C /* * Implementation of the matrix operations. */ #include
#include
// We include the header file for the definition of the Matrix struct. #include "matrix_lib.h" // Create a new matrix. Matrix newMatrix(size_t height, size_t width) { Matrix nm; // First we allocate pointers to each row. nm.values = calloc(height, sizeof(float*)); for (size_t i = 0; i < height; ++i) { nm.values[i] = calloc(width, sizeof(float)); } nm.height = height; nm.width = width; return nm; } // Free the memory of a matrix. void freeMatrix(Matrix m) { // Free each row, then free the pointer to the rows. for (size_t i = 0; i < m.height; ++i) { free(m.values[i]); } free(m.values); } ``` --- ## Printing a Matrix ```C // Print a matrix void printMatrix(Matrix m) { for (size_t row = 0; row < m.height; ++row) { for (size_t column = 0; column < m.width; ++column) { printf("%.3f", m.values[row][column]); // Print a comma, except print a new line on the last value. if (column < m.width-1) { printf(", "); } else { printf("\n"); } } } } ``` --- ## Adding Two Matrices ```C // Add two matrices. Matrix addMatrix(Matrix left, Matrix right) { // If the sizes don't match, return a matrix with NULL data and 0 size. Matrix sum = {.values = NULL, .width = 0, .height = 0}; if (left.height != right.height || left.width != right.width) { return sum; } sum = newMatrix(left.height, left.width); for (size_t row = 0; row < sum.height; ++row) { for (size_t column = 0; column < sum.width; ++column) { sum.values[row][column] = left.values[row][column] + right.values[row][column]; } } return sum; } ``` --- ## Testing * We can test with a simple program * Make sure to also test with valgrind afterwards! --- ```C #include
#include "matrix_lib.h" int main(int argc, char** argv) { Matrix a = newMatrix(3, 3); Matrix b = newMatrix(3, 3); for (size_t row = 0; row < a.height; ++row) { for (size_t column = 0; column < a.width; ++column) { a.values[row][column] = 1; } } for (size_t row = 0; row < b.height; ++row) { for (size_t column = 0; column < b.width; ++column) { b.values[row][column] = row*column; } } Matrix sum = addMatrix(a, b); printf("Sum of matrix\n"); printMatrix(a); printf("And matrix\n"); printMatrix(b); printf("Is matrix\n"); printMatrix(sum); freeMatrix(a); freeMatrix(b); freeMatrix(sum); return 0; } ``` --- ## Utility * Now that we can manage more complex data, entering it by hand isn't good enough * Reading the matrix values from a file would be much better --- ## File I/O * New functions to work with files: * `FILE *fopen(const char *restrict pathname, const char *restrict mode);` * Open a file. Mode is 'r' for read, 'w' for write. * `int fclose(FILE *stream);` * Close the file. * `ssize_t getline(char **restrict lineptr, size_t *restrict n, FILE *restrict stream);` * Read a line from the file * Put the line into the string pointed to by *lineptr*. --- ## Line Parsing * The `scanf` functions are the opposite of `printf` * `sscanf` works on a string. * `int sscanf(const char *restrict str, const char *restrict format, ...);` * Give it a string to parse, and a format string. * Other arguments are where the results go * So they are pointers, unlike with `printf` --- ## Data file ``` 1, 2, 3 4, 5, 6 7, 8, 9 0.5, 0.5, 1.5 0.5, 1.5, 0.5 1.5, 0.5, 0.5 ``` --- ## Reading the File ```C #include
#include
#include "matrix_lib.h" //Read a three by three matrix from the FILE* Matrix readMatrix(FILE* datafile) { Matrix m = newMatrix(3, 3); // Now read values from a file // A buffer to store lines as we read them. size_t buf_size = 1000; char* char_buffer = calloc(buf_size, sizeof(char)); // Read values into matrix a for (size_t row = 0; row < 3; ++row) { // Read a line from the file. getline(&char_buffer, &buf_size, datafile); // Now read three comma separated values from that line float row_data[3]; sscanf(char_buffer, "%f, %f, %f", row_data, row_data+1, row_data+2); for (size_t column = 0; column < 3; ++column) { m.values[row][column] = row_data[column]; } } // Finished with the buffer free(char_buffer); return m; } ``` --- ## Full Example ```C #include
#include
#include "matrix_lib.h" //Read a three by three matrix from the FILE* Matrix readMatrix(FILE* datafile) { Matrix m = newMatrix(3, 3); // Now read values from a file // A buffer to store lines as we read them. size_t buf_size = 1000; char* char_buffer = calloc(buf_size, sizeof(char)); // Read values into matrix a for (size_t row = 0; row < 3; ++row) { // Read a line from the file. getline(&char_buffer, &buf_size, datafile); // Now read three comma separated values from that line float row_data[3]; sscanf(char_buffer, "%f, %f, %f", row_data, row_data+1, row_data+2); for (size_t column = 0; column < 3; ++column) { m.values[row][column] = row_data[column]; } } // Finished with the buffer free(char_buffer); return m; } int main(int argc, char** argv) { if (argc < 2) { printf("This program requires a data file.\n"); return 1; } // Open datafile for reading. FILE* datafile = fopen(argv[1], "r"); if (datafile == NULL) { printf("Error opening file.\n"); return 2; } // Make our two new matrices Matrix a = readMatrix(datafile); Matrix b = readMatrix(datafile); fclose(datafile); Matrix sum = addMatrix(a, b); printf("Sum of matrix\n"); printMatrix(a); printf("And matrix\n"); printMatrix(b); printf("Is matrix\n"); printMatrix(sum); freeMatrix(a); freeMatrix(b); freeMatrix(sum); return 0; } ``` --- ## Next Class * More structs * More I/O