CS 475 - Operating Systems
Lab 2: Pointers and Addressing (Not Graded)
This is the second part of a multi-part primer on C. In this tutorial-assignment, you’ll gain an appreciation for the way values and variables are stored in memory. You’ll be introduced to pointers, as well as the connection between pointers and arrays.
Related Reading
Student Outcomes
- To understand how values and variables are stored in memory.
- To be familiar with pointers and references.
- To understand the connection between pointers and arrays.
Instructions
Open your VS Code and get connected to your Remote Development environment. If you don’t know what I’m referring to, then you need to complete Hwk 1.
- Once you’re logged in, you can open a terminal from the
Terminalmenu.
Preamble: Notepads, Street Addresses, and Buildings
Pointers are powerful structures in C. To get a sense of what pointers are, let’s use a real-world analogy:
- Data values in this analogy are like buildings in a city.
- A pointer (or reference) is a building’s street address.
- A pointer variable is a notepad with a street address written on it.
Therefore, you wouldn’t ever say that a street address is itself a building, but it does tell you where to go to find it.
| Real-World Analogy | Explanation |
|---|---|
| Any building always has a corresponding street address. You just have to ask for it! | This is what the *address-of operator & provides.* |
| You can navigate to the building located at an address: examine it, destroy it, change it. | This is what it means to *de-reference a pointer.* |
| You can reuse the notepad by writing a different building address on it. | The pointer variable can be reassigned to point at a different piece of data. |
| You can write an address on a notepad, and share the notepad with others. | A pointer can be passed in as function input-parameters so it can find the piece of data too. |
| You can check out the neighboring building, and the one after that, … | This is called pointer arithmetic. Once you have an address, you can visit the nearby element effortlessly. |
Under this scheme, think about what pointers would enable us to do:
- You can efficiently pass massive data structures to functions: Don’t pass the whole building, pass its street address!
- You can write functions that can change the variables in the function caller’s scope!
- You can create linked structures (linked lists, trees). A node contains a data element, and a pointer variable (notepad) holding the address of the next node.
Part 1: Memory and Data Types
How is data stored and managed inside your machine? Think of your computer’s memory as being a giant array of bytes. Each byte corresponds to a unique address in memory. Depending on the data’s type, a piece of data may occupy a range of bytes. Consider the following code snippet:
1
2
3
char letter = 'p'; // chars are 1 byte long
int days = 365; // ints are 4 bytes long
double amt = 90000.75; // doubles are 8 bytes long
Let’s take a look at a snapshot of what my memory might look like as it runs the above code

There are several things worth noting:
- There’s no guarantee that the variables are grouped together like this in memory.
- A word (4 contiguous bytes in this figure) is what we call a unit of data transfer between the memory and CPU. Although the figure only shows the starting addresses for each word, it should be noted that each byte is also addressable.
- Word Alignment: There are 3 wasted bytes that follow the
'p'character. Sure, we could use those bytes to store 3 out 4 bytes ofdays, but nowdayswould span across two words. This is not ideal, because it would require 2 transfers just to read/write thedaysvariable. To avoid this, your system prefers to align data on the order of words. - Yes, it would require this system to make 2 transfers to read/write
amtsince it spans 2 words – a reason why earlier systems preferred avoidingdoubles if you didn’t need its range and precision. This is whyfloattypes exist – they are only 1 word wide!
Important C Operator: sizeof()
Notice from the figure above that that an int takes up four contiguous bytes, a char requires just one byte, and a double requires eight. The specific space requirements for each data type actually vary across architectures, so how did I know these storage requirements apply to my machine? C provides an important operator sizeof() for this purpose. It inputs the name of a variable, a data type, or an expression, and returns the size in bytes that it occupies in memory.
Part 2: Understanding Addressing and Pointers
Every piece of data in your program, whether it’s a variable or a literal (like 3.14), is stored in two pieces: its content and its address. It is possible, for programmers to ask the OS for the addresses of your data, but we can’t tell it where to place them. This section focuses on the support for working with a variable’s location in C. In particular, we will focus on three syntax items: the address-of operator, the pointer-declaration operator, and the de-referencing operators.
-
Let’s now consider the code below. Read through it before moving on.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
char letter = 'p'; int days = 365; double amt = 90000.75; int *ptr; //declare pointer to an int ptr = &days; //point ptr at days printf("There are %d days\n", days); printf("There are %d days\n", *ptr); (*ptr)--; //decrement days by 1 printf("There are now %d days\n", days); printf("There are now %d days\n", *ptr); //print addresses printf("Location of days: %p\n", &days); printf("Location of ptr: %p\n", &ptr); printf("Value of ptr: %p\n", ptr);
-
In this simplified example, we’ll assume that the operating system places
daysin bytes 1112 to 1115,letterin byte 1116, andamtin bytes 1120 to 1127. -
Here is an example output when this program is executed.
1 2 3 4 5 6 7
There are 365 days There are 365 days There are now 364 days There are now 364 days Location of days: 0x458 Location of ptr: 0x8A2C Value of ptr: 0x458
-
Let’s now go back and explain the source code.
-
On Line 5, we see a new kind of variable-declaration syntax:
1
int *ptr; //declare pointer to an int
This declares a new variable named
ptr, and unlike anything we’ve seen before, it holds a memory address, which references anintvalue. In other words,ptris a pointer to an integer. Of course,ptris itself a variable that requires storage, and our figure shows thatptritself is located in byte addresses35372to35375. -
On Line 6:
1
ptr = &days; //point ptr at the address of days
The operator
&varreturns the address ofvar. Even thoughdayoccupies four bytes because it is anint, only the address of its first byte (1112) is returned. Thus,ptr = &dayswill assign 1112 toptr. That’s how pointers (called “references” in Java) work! They’re just variables that store addresses to data. -
Line 8 introduces an important operation, called dereferencing.
1
printf("There are %d days\n", *ptr); // *ptr is used to chase the pointer to the content!
Dereferencing is used when we’re interested in uncovering the content that’s referenced by
ptr. If we simply output the value ofptr, we’d still get 1112, which is not what we want in this case. Therefore, when the objective is to “follow” the pointer to its destination, we use the dereferencing operator*var, wherevaris a pointer variable. (This irks me a bit, because the * operator now has 3 interpretations in C: multiply, declaration of a pointer variable, and pointer dereference. Expect this to lead to headaches down the line.) -
On Line 10:
1
(*ptr)--; //decrement the content of 'days' by 1
Okay this is a strange one.
ptris first de-referenced to return the content365. The de-referenced content is then decremented to364. -
On Lines 15-17: shows that we can use the output specifier,
%pto print an address (in hexadecimal).1 2 3
printf("Location of days: %p\n", &days); // 0x458 printf("Location of ptr: %p\n", &ptr); // 0x8A2C printf("Value of ptr: %p\n", ptr); // 0x458
The addresses of
days(0x458 == 1112) andptr(0x8A2C == 35372) are first printed. This is followed by printing the contents ofptr, which unsurprisingly, stores the address ofdays.
-
- Important: In the examples above, we demonstrated that the
&address-of operator returns only the address of the first byte of the associated variable (just like in our earlier figure). For instance,&daysreturns simply0x458, even thoughdaysoccupies the next three bytes as well. When we de-reference*ptron Line 8 and Line 12, the system was intelligent enough to know that the next three bytes are part ofdays’s value. How does the system know exactly three more bytes (and not zero, or one, or seven, or a hundred) trailed first byte ofdays.- Answer: Because you told the system how big it was! It’s why we declare data types in the first place. When you declared
daysas anint, the C compiler knows it needs to reserve 4 bytes.
- Answer: Because you told the system how big it was! It’s why we declare data types in the first place. When you declared
-
Exercises (ungraded)
-
Suppose we know that a pointer to an int (
int*) occupies 4 bytes on my machine by callingsizeof(int*). What would the size be for a pointer to achar, or a pointer to adouble, or a pointer to somestructon my machine? (Ans: 4 bytes. Pointers are nothing more than addresses, no matter what kind of data you’re pointing to. Addresses are fixed length.) -
You can also create a pointer to a
voiddata type, which seems odd at first. Do some searching on the web, and figure out what avoid*pointer means, and why it’s useful. (Hint: Think generics in Java).
-
Part 3: Pointer Operators: &, *
Let’s put everything together.
-
Address-Of Operator: Given a variable var,
&varreturns the address of var’s location in memory. -
A pointer variable stores the address of some data. This data can be a variable, an array, or even another pointer. To declare a pointer, you use the following syntax:
1
data-type *ptr; // Pointer to some data that's described by the given data type.
When assigning a pointer
qto another pointerp, it causes them both to point to the same data.1 2 3 4
double *a = NULL, *b = NULL; double c = 10; b = &c; // point b at c a = b; // point a at c (why don't I need to use &b here?)
-
Memory contents after the declaration:

-
Memory contents after the assignment statements on Lines 3, 4:

-
You must first
#include <stdlib.h>to get access to theNULLconstant.
-
-
The De-reference Operator: Given an already-declared pointer
ptr, we use*ptrto access the value at the location referenced byptr. As I lamented earlier, I wish we chose a different syntax for dereferencing, because*ptralready has a different meaning!1 2
*b = 15; // de-reference b to get to c's content! c is now 15 *a += 5; // de-reference a to get to c's content! c is now 20
-
Memory contents after
*b = 15.

-
Memory contents after
*a += 5.

-
Practice Questions
- What happens to your program when you try to de-reference a pointer to
NULL? (Ans: In Java, you’d get the NullPointerException, but there are no such things as Exceptions in C… This really is something you should try out.)
- What happens to your program when you try to de-reference a pointer to
Part 4: Pointers as Input Parameters
Remember how I mentioned that, for efficiency, you can pass an address/pointer into a function instead of passing the entire building? Consider the following function that modifies a large struct without using pointers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct employee_t {
char ssn[11];
char name[100];
int salary;
// more members omitted
// ...
} employee_t;
employee_t increaseSalary(employee_t emp) {
emp.salary *= 1.03;
return emp;
}
void main() {
employee_t david;
strcpy(david.ssn, "123");
strcpy(david.ssn, "David C");
david.salary = 20000;
david = increaseSalary(david); // yikes
}
This code runs, but it’s really cumbersome. To call increaseSalary(david), C needs to package up all those values in the struct and pass the whole thing as a value. Because any changes done in increaseSalary(david) are local to it, they’d be lost if you didn’t return the entire struct back to the caller! Look, we’re moving the entire building … twice!
Intead, let’s just pass the address of the employee to the function. The function can follow the address to make the salary updates. This saves the C runtime system from making a clone of the employee and passing it around!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct employee_t {
char ssn[11];
char name[100];
int salary;
// more members omitted
// ...
} employee_t;
/** Now inputs a pointer to an employee! */
void increaseSalary(employee_t *emp) {
emp->salary *= 1.03; // What's this struct->member operator????
}
void main() {
employee_t david;
strcpy(david.ssn, "123");
strcpy(david.ssn, "David C");
david.salary = 20000;
increaseSalary(&david); // done in place!
}
In the code above, notice:
- Main simply sends along an employee’s address (using the
&operator), not their entire content! - If
increaseSalary()knows exactly where to go to reach the affected employee, then any changes to it are done directly! No need to return the employee anymore. (That’s what we remember doing in Java!) - Important: When
pis a pointer to astruct, you can de-refernce one ofp’s members using either:(*p).member = expression– this de-referencespfirst, then applies the usual dot notation to access its member.- Or a nice shortcut is usually done in practice:
p->member = expression
Part 5: “Output” Parameters
Have you ever wished that a function/method could return more than one thing? To do this Java, you always had to create a new class that stored multiple values and return an object of that class, or you returned an array of values. Sure, you can also do any of the above in C, but pointers give us another way to emulate “returning” multiple values (without actually calling return to do it).
“Output Parameters”: An output parameter refers to a pointer that is input into a function, and the function modifies its contents before exiting. After the function call, one just needs to dereference the pointer to obtain the updated value(s).
-
You’ve also seen this in action already when you used
scanf()to accept user input. For example, whenscanf("%d", &var)is used, we input the address ofvar(i.e., a pointer), and we expect the contents ofvarto have changed afterwards. -
I strongly recommend that you clearly name and comment when a parameter is an output parameter. For instance:
1 2 3
void sum(int inX, int inY, int* outSum) { *outSum = inX + inY; }
-
In practice you often see the above function written out like this for clarity. Yeah it’s ugly, but it makes clear to the reader that this function will put something in the location given to
*sum.1 2 3 4 5 6
void sum( int x, /* IN */ int y, /* IN */ int *sum /* OUT */) { *sum = x + y; }
-
Do this output parameter exercise: Write a function
void compareAndAssign(int n, int m, int *larger, int *smaller)that puts the larger ofnandminlargerand the smaller value insmaller. How would you call this function?
Part 6: Connection to Arrays (Pointer Arithmetic)
In this section, we’ll explore the relationship between pointers and arrays.
-
Study the following source file, then compile and run it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
#include <stdio.h> #define BUFFERSIZE 4 int main(int argc, char* argv[]) { int arr[BUFFERSIZE] = {9,8,7,6}; int i; printf("*** where is arr[0] stored? ***\n"); printf("arr[0] location: %p\n", &arr[0]); printf("\n*** where is arr stored? ***\n"); printf("arr location: %p\n", arr); printf("\n*** print out contents using pointer arithmetic ***\n"); for (i = 0; i < BUFFERSIZE; i++) printf("%d ", *(arr+i)); printf("\n\n*** print out contents using familiar subscript syntax ***\n"); for (i = 0; i < BUFFERSIZE; i++) printf("%d ", arr[i]); return 0; }
-
Arrays represent a contiguous sequence of elements in memory. It is therefore not surprising to find
arrbeing represented as in the figure below, with eachintelement occupying 4 bytes. When compiled and executed, this program outputs something akin to the following:
1 2 3 4 5 6 7 8 9 10 11
*** where is arr[0] stored? *** arr[0] location: 0x4318 *** where is arr stored? *** arr location: 0x4318 *** print out contents using pointer arithmetic *** 9 8 7 6 *** print out contents using familiar subscript syntax *** 9 8 7 6
-
Looking at the source code,
-
Lines 11 and 14: Suppose we want to find the address of the 0th element in
arr. We can simply apply the&operator on elementarr[0]to get its address:1
printf("arr[0] location: %p\n", &arr[0]);
The code on Line 14, however, may be slightly unexpected, and it’s equivalent! There’s no address-of operator (that’s not a typo!)
1
printf("arr location: %p\n", arr);
It would appear that an array’s variable name is already a pointer to the location of its 0th element! By the way,
0x4318is hexadecimal for17176(for the figure below). -
Line 16-18: Knowing this, let’s try something else. Because
arris just a pointer, can we also dereference it to access the array elements? Yes!!*(arr+0), or simply,*arrreturns 9
Pointer Arithmetic Exciting! How would we access the array element at index 1? The runtime is smart enough to know that the next element is 4 bytes away because the array was declared to store
ints. So adding 1 to the pointer will automatically skip the next 3 bytes and move the pointer to the next item in the array!*(arr+1)returns 8*(arr+2)returns 7*(arr+3)returns 6
-
Line 20-22 (Important!) Finally, the array indexing syntax
[i]we’re all familiar with, is merely a convenience for programmers: Indeed,arr[i]is actually just a shorthand for*(arr+i)- (Full circle now – Zero-based Addressing): This may have only come up briefly in a previous course, but now we can appreciate why array indices are 0-based in just about every language, and it’s due to pointer arithmetic! If we store the first item in location
[1], then the C compiler would need to subtract 1 when performing each array index lookup. That’s just an unnecessary overhead!
- (Full circle now – Zero-based Addressing): This may have only come up briefly in a previous course, but now we can appreciate why array indices are 0-based in just about every language, and it’s due to pointer arithmetic! If we store the first item in location
-
-
Arrays are “passed by reference”: Now that we know an array variable is just the address of its 0th element, take a look at the following functions that manipulate the array. Each of the following functions have the same effect (initializes all elements to -1)! Make sure you read through each and understand why.
1 2 3 4 5 6
void initArray(int *A, const int SIZE) { int i; for (i = 0; i < SIZE; i++) { A[i] = -1; } }
By the way, the following code also works:
1 2 3 4 5 6
void initArray(int A[], const int SIZE) { int i; for (i = 0; i < SIZE; i++) { A[i] = -1; } }
Important side note: Because arrays are passed as pointers, you can now appreciate why modifications to arrays persist after the function terminates (this is also true in Java!).
Important Summary: Why Do We Need Pointers?
Let’s pause here and ask why pointers are needed at all? There are several reasons to use pointers:
-
Pass by Reference. Suppose you want a function to make changes to a
struct, array, or any other variable that’s input into it. Without pointers, the input would be “passed to the function by value,” so the function gets a local copy of the input which can be huge! Any changes you make to the local copy are lost when the function exits, so you’d have to return it to the caller. But that’s a lot of data transfer and space usage! (Imagine wanting to sort a large array and your functions have to copy and return all elements in the array each time it’s called!) Instead, it’s far more efficient to pass the argument “by reference (by its address)” and have the changes be made directly, without making a separate, local copy. -
For returning structs from functions. For the same reasons outlined above, consider a function that creates and returns a complex structure or array. It is far more efficient to return a pointer to the structure rather than the entire structure itself!
-
For “returning” multiple values Sometimes, you might want a function to return mutiple values. For instance, say you wrote a function that can find the min and max of an array. In C (and most languages), you are limited to one return value. However, if you use output parameters (which are just pointers to variables), you can just store the results in there directly. You’re not actually returning anything, per se, but the net effect is that those variables will be populated with the correct results after your function call.
-
For memory management at runtime. Last, but not least, pointers are necessary when you need to ask the OS for an arbitrary chunk of memory during runtime. Say you want to create a new node in a linked list. Each node that’s created at runtime requires that you request space to store the data for that node. The OS, for reasons just mentioned, returns a pointer to the memory storing the node, rather than returning the entire chunk of data pertaining to the node itself. We tackle this point (dynamic memory allocation) in the next Lab.
Credits
Written by David Chiu. 2022.