C Primer (Part 2)

This primer contains more information about certain types in C including more information about pointers and arrays.



Adding “*” to a variable declaration makes the variable type a pointer.

// an int variable: int anInt; // an int pointer: int* anIntPointer;

Applying the “&” operator to a variable returns its address.

// an int variable: int anInt; // an int pointer: int* anIntPointer = &anInt; // a pointer to a pointer int** pointerSquared = &anIntPointer

Applying the “*” operator to a pointer derefrences the pointer and recovers the value it points to.

// an int variable: int anInt = 343; // an int pointer: int* anIntPointer = &anInt; printf("The int hidden in my little pointy pointer is: %d\n", *anIntPointer); // The above prints: // The int hidden in my little pointy pointer is: 343


Arrays can actually act as pointers!

Arrays are blocks of contiguous memory used to store multipe elements of the same type. Each “slot” in the array is then an object stored in one of these memory locations. Because the slots are back-to-back, the object stored at location 0 in the array is then also the memory location of the array itself.

Consider the following code snippet (modified from the Pointers, Arrays, Structures website):

#include <stdio.h> #include <stdlib.h> int main(){ int myArray[5]; int* myPointer = myArray; // p points to array // assign to the array for (int i = 0; i < 5; ++I){ myArray[i] = 2*i; //index times 2 } *myPointer = 2023; // Now, our array looks like: // 2023, 2, 4, 6, 8 }

Pointer Arithmetic

Remember pointers are just addresses which you can dereference. The name of an array behaves like the address of the first element of the array in memory. And addresses are just large numbers. As with any number, we can use arithmatic operations on pointers to reach nearby places in memory.

Let’s look at how we would access the 5th element of an array of 7 characters:

int main() { char myArray[7] = {'h', 'e', 'l', 'l', 'o', '!', '\0'}; char fifthChar = myArray[4]; }

Since myArray is the address of the first element in the array, we can instead get the address of the fifth char in the array by just adding 4 bytes to it (each char is a byte long):

char* fifthCharAddress = myArray + 4;

Then all we need to do to recover the fifth character is dereference this memory location:

char fifthChar = *fifthCharAddress;

Or, to shorten the code involved, we can substitute the fifthCharAddress variable like so:

fifthChar = *(myArray + 4);

In fact, underneath the hood, indexing an array with the [] operator really desugars into an addition and then dereferencing operation like we just did.

String Literals

A string literal is a sequence of chars enclosed in double quotation marks, for example:

// A pointer of the string literal char* aWord = "cs0300"; // An array initialized by a string literal char aPhrase[] = "cs0300 TAs are great...right?";

The compiler automatically adds a terminating NUL character (‘\0’) to string literals. String literals are not modifiable.

char* aWord = "cs0300"; aWord[0] = 'a'; // Undefined behavior char anotherWord[] = "Hi!"; anotherWord[0] = 'A'; // valid as another word is not a string literal // Note that the length of anotherWord is 4, as it includes '\0'


Structures are a great way of storing multiple pieces of information in one instance. The below example gives way of defining a struct and how to use it.

#include <stdio.h> // A declaration of a point struct which holds two position values struct point2D { float x; float y; }; float distance(struct point2D p1, struct point2D p2) { return sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)); } int main() { struct point2D p1; // Create an instance of the point struct p1.x = 0.5f; p1.y = 12.5f; // Initialize fields // You can also initialize a struct with an initialzer list constant: struct point2D p2 = { 20.4f, -9.0f }; printf("The distance from point 1 to point 2 is %f", distance(p1, p2)); }

Declarations vs. Definitions

Since C programs are compiled from top to bottom, the order we define functions in matters. Consider the following example program:

/* wrong_order.c */ int multiply(int x, int y) { int ans = 0; for (int i = 0; i < x; i++) { ans = add(ans, y); } return y; } int add(int x, int y) { return x + y; }

If you tried to compile this, GCC would spit out an error saying it doesn’t know what the add function is on line 6. This is because we define it on line 11. Function declarations are useful because they enable us to tell the compiler about a function before we define it. Consider this alternate program:

/* example.c */ // Declarations int multiply(int x, int y); int add(int x, int y); // Definitions int multiply(int x, int y) { int ans = 0; for (int i = 0; i < x; i++) { ans = add(ans, y); } return y; } int add(int x, int y) { return x + y; }

This program correctly compiles because our declarations on lines 4 and 5 warn the compiler about the functions before we implement them. Now when it sees the add function being used on line 11, it won’t freak out even though it hasn’t seen its definition yet.

Header Files

Header files are files with the extension .h. These files are technically the exact same as source (.c) files, except by convention, we put all of our declarations in header files and all definitions in source files.

We call pairs of header and source files modules in C. For example, we could refactor the code in the last section to be a module:

/* example.h */ int multiply(int x, int y); int add(int x, int y);
/* example.c */ #include "example.h" int multiply(int x, int y) { int ans = 0; for (int i = 0; i < x; i++) { ans = add(ans, y); } return y; } int add(int x, int y) { return x + y; }

You might notice the #include preprocessor directive at the top of example.c. This line tells the compiler to literally paste the contents of example.h right on top of the #include line in example.c. (You can do this with any file, not just a ‘.h’ file.)

If we have a completley different file which needs to use the functions declared in example.h, we can use this include directive like so:

/* main.c */ #include "example.h" int main(){ int twelve = add(4, multiply(4, 2)); }

To compile this file, we need to let the compiler know where the contents of example.h are implemented, and the linker will take care of everything for us:

~$ gcc example.c main.c

Acknowledgments and Extra Materials: Pointers, Arrays, Structures, Header Files