Today’s lab will give you a chance to practice using pointers in C. As we did last week, you’ll work in a git repository shared with your lab partner. Follow these steps to set up that repository:
You may still be signed in, but if you see a Sign In button in the upper right, click it and log in with the GrinCo-AAD option below the form.
On the page for the pointers repository, click the Fork button in the upper right.
On the next page, leave all the options set to their defaults and click the Fork Repository button.
You’ll end up back on a page that looks like the original repository, but the title in the upper left should be “YOUR_USERNAME/pointers” with a subtitle that says “forked from csc161-s25/pointers”.
Click the Settings button in the upper right and then choose the Collaborators section.
For each member of your group, type their username and click Add Collaborator. That should immediately add them to the list of collaborators. This should be the default, but make sure each collaborator has Write permission (displayed next to the button to remove a collaborator). If you can’t find a lab group member, they might not have set up their Gitea account. Fix this now before moving on.
Once you’ve finished adding your lab group to the repository, click the Code button to go back to the repository’s main page. Find the SSH button and click it if it isn’t already selected. Copy the text that appears in the field next to the SSH button.
Open a terminal and run the following commands to clone your repository.
Make sure you replace COPIED_TEXT with the text you just copied:
$ cd ~/csc161/labs
$ git clone COPIED_TEXT
$ cd pointers
$ code .
You should now see VSCode running with the pointers lab directory open.
Don’t forget to commit and push your changes after each part of the lab.
Before we get into complex examples, we’ll start by reading and predicting the behavior of code that uses pointers. The exercises below reference this code fragment:
// Code for exercise 1:
int a = 1;
int b = 2;
int* c = &a;
int* d = &b;
b++;
printf("a: %d, b: %d, *c: %d, *d: %d\n", a, b, *c, *d);
// Code for exercise 2:
*c = *c + 1;
*d = *c + b;
d = c;
printf("a: %d, b: %d, *c: %d, *d: %d\n", a, b, *c, *d);
// Code for exercise 3:
int* e = &a;
int* f = e;
int g = 99;
(*e)++;
e = &g;
(*f)++;
printf("a: %d, b: %d, *c: %d, *d: %d, *e: %d, *f: %d, g: %d\n",
a, b, *c, *d, *e, *f, g);
The exercises below ask you to draw box and arrow diagrams to make predictions about what each code fragment will do.
If you are asked to submit this work, you will only need to turn in your predictions, not the pictures you draw.
Write your responses to non-coding questions in the file responses.txt included in the starter code.
Read through the first block of code above.
Draw a box and arrow diagram (like the one we completed in class to show how the values change up to the first printf.
Then, write down predictions for what the printf will print.
Check your predictions by running the first block of code in a new source file (you’ll have to add #include and a main function).
If you predicted any incorrectly, update your diagram and explain what you missed.
Follow the same process for the second block of code. Make sure you can explain why the program prints what it does before moving on, especially if your prediction was incorrect.
Repeat the process again, this time for the third block of code.
Optional Challenge: See if you can predict what the block of code below will do. This code uses a double pointer, which is a pointer to a pointer value. Box and arrow diagrams will still work for this example, but double pointers can be tricky.
int a = 0;
int b = 1;
int* p = &a;
int* q = &b;
int** r = &p;
**r = 10;
*r = q;
*p = 11;
printf("a: %d, b: %d, *p: %d, *q: %d, **r: %d\n", a, b, *p, *q, **r);
Compile and run this code to check your predictions.
Most modern languages have a notion of values and references, especially for function parameters. Passing a parameter to a function by value makes the value available during the function’s execution, but the function cannot change the value stored in the caller’s scope. Passing a parameter by reference allows the function to use the value of that parameter, but also to modify its state in a way that will remain after the function returns.
In C, all parameters are passed by value. Pointer parameters are the mechanism we use to pass references to a function; the pointer itself is a value, but the function can use the pointer to reference the location the pointer points to.
The following exercises explore some of the uses for pointer parameters in C, but first let’s look at some of the applications of pointer parameters.
The simplest case where we would pass a pointer parameter is when we want a function to be able to change a value for us. Here’s an example function:
void increment(int* n) {
*n = *n + 1;
}
We would call the function like this:
int i = 5;
increment(&i);
// i is now 6
increment(&i);
// i is now 7
...
As with any programming language, things can fail in a C program and we have to deal with failures appropriately.
A common way to do this in C is to make a function return an int to indicate whether it succeeded (0) or failed (-1 or some other non-zero value).
This may seem backwards, but it allows for some easy error checking we’ll see in a moment.
First, here’s an example function that uses this pattern:
int divide(int dividend, int divisor, int* quotient) {
// Check for division by zero
if (divisor == 0) {
return -1;
}
*quotient = dividend / divisor;
return 0;
}
We would call the function like this (assume variables x and y are ints):
int answer;
divide(x, y, &answer);
printf("x/y is %d\n", answer);
But what if y is zero?
Our code doesn’t check for errors, but it should.
We can do that with this updated example:
int answer;
if (divide(x, y, &answer)) {
printf("Error: attempted to divide by zero.\n");
} else {
printf("x/y is %d\n", answer);
}
It may seem odd that the function’s main output goes to a parameter instead of its return value, but this is common practice in C (including many standard functions you will use).
It may also seem odd that we are allowed to pass a pointer to answer without giving answer a value, but you’ll notice the program doesn’t access the value of answer until after the divide function returns (and only if it succeeds).
The answer parameter, often called an output parameter, allows us to get around C’s limitation that functions can only return one value.
You’ll explore this further in the exercises.
max.c where you will implement the following function:
/**
* Find the larger of two numbers and write the answer to result
* \param m A number
* \param n A number
* \param result location for the larger of m and n
*/
void set_max(int m, int n, int* result);
Write a main function to test your implementation on a few different numbers.
Integer division doesn’t just produce a quotient;
it gives us a remainder as well.
Write a new version of the divide function above that computes both a quotient and a remainder.
Do not remove the error check from divide though: your implementation should still return 0 on success and -1 on error.
Write your implementation in the source file named divide.c.
Your test should include at least three calls to divide: once with parameters that will return an error, once where the answer will have zero remainder, and a third time with a non-zero remainder.
time.c where you will implement the following function:
/**
* Take in a total number of seconds and split it up into parts
*
* \param total_seconds The input number of seconds
* \param days Output: the number of whole days in total_seconds
* \param hours Output: the number of whole hours in total_seconds
* \param minutes Output: the number of whole minutes in total_seconds
* \param seconds Output: the remaining seconds
*/
void split_time(int total_seconds, int* days, int* hours, int* minutes, int* seconds);
This function takes one input parameter and has four output parameters. Seconds and minutes should never be larger than 59 and hours should never be greater than 23.
Once you’ve completed your implementation, write a main function to test your function on a few values.
exchange.c and implement the exchange function below.
The p and q parameters should point to locations that have values assigned.
After calling the function, the two variables should have their values swapped.
In this case, p and q are both input and output parameters.
For example, if we have two variables a set to 543.21 and b set to 123.45 and call exchange(&a, &b) the result should be that a is now 123.45 and b is now 543.21.
/**
* Exchange the values in two locations
* \param p A pointer to one value
* \param q A pointer to another value
* \post The values at p and q have been exchanged
*/
void exchange(double* p, double* q);