Skip to content

C Lang Module 10 Notes

Updated: at 09:12 AM

There are four built-in data types for C:

The width of each of these data types is machine dependent, but here is their scope:

C also has datatype modifiers that can change the width of data types by preceding them in declaration:

Casting:

Any datatype can be made into an array by following the variable name with array syntax: char arr[5]; //creates a char array of length 5

The name of an array in C is essentially a label for the address where it begins. Elements are stores sequentially, and the index operator([]) is just an offset from the label in data memory.

You can also create structs or user defined data types in C.

struct fraction{
	int numerator;
	int denominator;
};

struct fraction myFraction;

myFraction.numerator = 5;
myFraction.denominator = 12;

You can also use typedef for long variable type declarations:

typedef unsigned char BYTE;
BYTE b1, b2;
b1 = 0xFF;
b2 = 0xAA; //Note: these will be stored as their hex values, not as chars

You can also combine these two to make syntax simpler

typedef struct {
	int x;
	int y;
} coordinate;

coordinate home_coords; //now we can drop the struct keyword

Pointers in C

A pointer is a variable that contains the memory address of another variable, or “points” to that variable.

int my_int = 320;
int* my_ptr = 0x7FFB; //will point to 0x7FFB in data memory

Note that the syntax states that my_ptr is an integer pointer, and can only point to an int variable. The * operator is the dereferencing operator, which will look up the value of the variable pointed to by a pointer when preceding a pointer.

In C, to get a variables reference, we use the reference operator &.

int my_int = 234;
int* my_ptr = &my_int; //this stores the address to my_int in my_ptr

You can have pointers to any datatype in C, including user defined types.

Pointers and Functions

One big use of pointers is being able to pass addresses to functions. Take the following code:

int square(int var) {
	var = var * var;
	return (var);
}

int main() {
	int a = 10;
	a = square(a);
}

Note how in this code, when we run square(a), the value that gets updated in that function is a copy, who’s value is returned. Therefore, we must also assign that value to a.

We can pass a as a pointer to the function instead, and the value will be updated at that reference itself.

void square(int* var) {
	*var = ( *var ) * ( *var );
}

int main(){
	int a = 10;
	square(&a);
}

Now we can perform this operation directly on a, and there is nothing we have to return.

Pointers vs Arrays

You can have a pointer point to an array in C. Remember that the name of an array just acts as a label to the first value. In other words, if we had an array char arr[], then arr is the same as &arr[0]. These will both return the reference to that array. Note that the indexing operator will add the index to the offset and then dereference that address.

Remember that at the end of the day, these references are just numbers, and can be operated on as regular data types.

Function Calls

You cannot pass a copy of an array in C. This is a very important concept, and to account for this, C essentially adds invisible operators to convert the passing of any arrays as a pointer. If you pass in an array to a function that takes an array as its input, you are essentially passing in a reference that points to the original array. Any edits to the array in the function will modify the original array. This is because a whole array is not a datatype or an object. It is simply a reference label to the beginning of the array.

Note that when you pass an array, you must also make sure the pass the length of that array to the function as well, since within a function there is no way to tell the length of the array that was passed to it.

Pointers to Pointers

Remember that pointers act very much like their own datatypes, and are variables themselves. This means that we can have a pointer to a pointer.

int a = 5;
int* b = &a;
int** c = &b;

In this case, c points to the reference of a variable b, who’s value is the data memory address of a.

Why is this useful? Take the following code below:

void swap_ptrs(int* a_ptr, int* b_ptr) {
	int* tmp_ptr;
	
	tmp_ptr = a_ptr;
	a_ptr = b_ptr;
	b_ptr = tmp_ptr;
}

int main() {
	int a = 1;
	int b = 2;
	int* a_ptr = &a;
	int* b_ptr = &b;
	
	swap_ptrs (a_ptr, b_ptr);
}

This code does not work.

Why? When we call swap_ptrs(a_ptr, b_ptr), remember that we are sending a copy of the value, which in this case, would be the reference to a. However, we don’t want to change a, we want to change a_ptr. This means that we want to pass in a pointer to a_ptr, which would be a double pointer. A proper implementation is shown below:

void swap_ptrs(int** a_ptr, int** b_ptr) {
	int tmp_ptr;
	
	tmp_ptr = *a_ptr;
	*a_ptr = *b_ptr;
	*b_ptr = tmp_ptr;
}

int main() {
	int a = 1;
	int b = 2;
	int* a_ptr = &a;
	int* b_ptr = &b;
	
	swap_ptrs (&a_ptr, &b_ptr);
}

Null Pointers

Remember that exceptions are good things: they allow us to catch bugs in our code before they happen. What’s worse is having unexpected behavior that isn’t caught, because that can be really annoying to debug, especially if it looks like nothing is wrong at first.

One example of this is an uninitialized pointer. Take the following code:

int main() {
	int a = 5;
	int* b_ptr;

	print(a);
	print(b_ptr);
	print(*b_ptr);
}

This code will not produce an error. However, we have no clue what the last line will print out, since we have no idea what b_ptr will point to.

This is why you should always initialize pointers to null.

int main() {
	int a = 5;
	int* b_ptr = NULL;

	print(a);
	print(b_ptr);
	print(*b_ptr);
}

In this case, the last line is guaranteed to produce a segmentation fault. This way, we can catch errors and avoid unexpected functionality.

Pointers to Old Data Locations

Take the following code. Why won’t it work?

int* make_array() {
	int a[2] = {5, 6};
	int* b = a;
	return (b);
}

int main() {
	int* array = NULL;
	array = make_array();
}

This doesn’t work because we return a reference to a local stack variable in main_array(). This stack doesn’t have memory safety after we have exited the function and popped it from the stack, since the variable there itself is out of scope.

Void* Pointers

There is another primitive data type in C: void. It’s more of a keyword to indicate the absence of data than anything(e.g. when it precedes a function, there is nothing to return).

Since void is a datatype, we can also have a pointer of that type. It basically represents a typeless pointer, so it can point to any type of data. This is possible since all pointers allocate the same amount of space(since they are data memory addresses). Note that you also cannot dereference a void pointer. They are only really useful for passing memory addresses around, and you need to cast them if you want to access the value.

Constants and Constant Pointers

There are two ways to make a constant in C.