Lab 6 — UML class diagrams
Slides. Creating UML class diagrams to show the structure of an object-oriented program.
Assignment 4
Here I've tried to compile some advice for writing assignment 4. If you need help compiling your project, refer back to lab 3 where I've included a Makefile example now that the lab is over. You can turn in either a single file, or multiple files with a Makefile, depending on how you want to organize things.
Handling data in C
C doesn't come with any built-in data structures, such as lists or dictionaries, like Python does. Instead if we want the
functionality of one of these from Python we'll have to build it ourselves. At the start, all C gives us to work with are
basic types such as: int, char and float; pointers; structs; and static arrays. If
we want a complex data type with multiple values, like a class with member variables, we can create a struct. The biggest
difference is here we can't associate member functions with structs like we can with classes in Python.
struct Data {
char *name;
int length;
float direction;
};
/* create one of our structs and set values inside it with the `.` operator */
struct Data my_data;
my_data.name = "Hello";
my_data.length = 5;
my_data.direction = 3.1415f;
We can also create a static, fixed-size array of structs like this:
struct Data my_array[16];
my_array[0] = my_data; /* copy the values of `my_data` into the first element */
my_array[1].name = "World"; /* modify the second element in-place */
my_array[1].length = 5;
Unlike in Python, we cannot grow or shrink this array, but we can mimic that behavior by starting with a large array and keeping an index to the last element we've modified. Because the array and its length are associated we bundle them together in a struct:
struct Array {
struct Data array[64];
int length;
};
Then it would be a good idea to define how the array is used in a function, so that we don't have to manually adjust the length.
void insert_array(struct Array *array, struct Data data) {
if (array->length == 64) {
/* handle the case where we've overfilled the array */
} else {
int index = array->length;
array->array[index] = data;
array->length += 1;
}
}
Notice how here I pass the array as a pointer, because I want to modify that specific array. Otherwise this would create a new array in the function
and not modify it outside of the function like I want. I also use the arrow operator -> rather than the dot operator to access member
variables of array through the pointer.
Reading strings
To read input from a file in C, we can use the fread function. By the way, for all of the functions mentioned here, if you are on Linux or macOS
you can type the command man <function> into your terminal to get a help page about that specific function. We can grab the first line of
our input with the following code:
/* place to store the characters we read */
char buffer[512];
/* "r" here means: open the file in read mode */
FILE *fp = fopen("input.txt", "r");
/* fgets will read up to the first newline or *
* the end of the file, whichever comes first */
char *line = fgets(buffer, sizeof buffer, fp);
if (line == NULL) {
/* there was nothing read */
}
/* we have to manually close the file after we're done */
fclose(fp);
Here we allocate an array of characters for the text to be read into, named creatively buffer. We make it 512 characters long, which should be
sufficient for the input files you'll be working with. Next we open input.txt by calling fopen, which returns a pointer to the
file handle. So far, this is exactly equivalent to with open("input.txt", "r") as file: in Python. The last line in this block reads a line
from the input file and stores it in the character pointer line. The fgets function takes as arguments: our buffer, its length that we get
with the keyword sizeof, and the file handle pointer fp at the end.
This just grabs the first line for us, and we want to be able to read the entire input file. We can do this by calling fgets in a loop until it
returns NULL, which means it has ran into the end of the file.
/* place to store the characters we read */
char buffer[512];
/* we also need to store each line we read so that we can refer back to it later */
char *lines[64];
/* keep track of how many lines we've actually read, because C won't stop us from
* trying to read beyond it */
int line_count = 0;
/* open the file and get the first line */
FILE *fp = fopen("input.txt", "r");
char *line = fgets(buffer, sizeof buffer, fp);
for (int i = 0; line != NULL && i < sizeof lines; i++) {
/* copy the line we just read into `lines` */
lines[i] = strndup(line, sizeof buffer);
/* get the next line */
line = fgets(buffer, sizeof buffer, fp);
line_count++;
/* your code for processing `line` can go here */
/* ... */
}
/* we can close the file now since all of its lines have been read into `lines` */
fclose(fp);
Here we use a for-loop. We check if line is NULL and if we've read more lines than we can store
in each iteration, and if not, we can process it and then grab the next line. This should be enough to read the input, just keep
in mind that each line will have a newline character at the end.
Parsing strings
In Python, if you are given a string of comma-delimited values, you can simply call string.split(",") to get
a list of those values. In C, an equivalent is the function strtok. Its usage is a bit odd to get used to at first.
If we have the string "Peer,Can you help me on this?,12-01-2024" from the input file, we want to split it up by comma.
To do this with strtok, we do:
/* let's use the first line we read previously */
char *input = lines[0];
/* `strtok` splits the string by a delimiter.
* let's say input = "EMAIL Peer,Can you help me on this?,12-01-2024 */
char *command = strtok(input, " ");
/* variables to store parts of the string we're interested in */
char *sender;
char *subject;
char *date;
/* `strcmp` tells us how many characters differ in two strings. if 0 differ,
* the strings are equal */
if (strcmp(command, "EMAIL") == 0) {
/* now we want to split by ",". our string is already in `strtok`'s memory
* so we call `strtok` with NULL */
sender = strtok(NULL, ",");
subject = strtok(NULL, ",");
date = strtok(NULL, ",");
}
The first call takes the input string and the delimiter, the ",", and returns the part of the string up to the first instance of the delimiter
character. What is really going on is: In C, a string is just an array of characters of any length. To know when a string ends, there is always a '\0'
character at the end of the array, and every string processing function in C knows to stop when it hits this character. So strtok looks for the
first instance of the delimiter it sees, replaces it with a '\0', and returns a pointer to the start of the string.
The second call to strtok takes NULL rather than the input string again, because the function saves its location in the string. Otherwise
it would start at the beginning again, but this time find the '\0' character that it replaced before and stop, which returns nothing. So the second line
starts after the character it replaced, and does the same thing, replacing the second comma with a '\0' and returning a pointer to the string after the
first comma.
When strtok is finished, it will return NULL, or it may also return NULL if there are any errors in its input, so it is important to check that
it is returning strings when you expect strings, and NULL when you expect it to be done. So after the lines above, you could put:
if (sender == NULL || subject == NULL || date == NULL) {
printf("[Error] One of the parsed strings is NULL\n");
}
/* we need to make sure `strtok` returns NULL once so that we can use it again
* on a new string */
if (strtok(NULL, ",") != NULL) {
printf("[Error] Expected strtok to return NULL when finished processing input\n");
}
/* just print sender, subject, date so we can see if we read and parsed correctly */
printf("%s:%s:%s\n", sender, subject, date);