MYF

[RA] Ch8 Pointers

Reading Assignment: All of Programming Ch8 Pointers

One of the most important and powerful aspects of the C programming language is its use of pointers. Pointers give a programmer a significant amount of control and flexibility when programming, enabling solutions that are clean and efficient. However, they can also be a common source of confusion and bugs.

Pointer Basics

Pointers are way of referring to the location of a variable.

Declaring a pointer. In C, “pointer” is not a type. It is a type constructor-a language construct which, when applied to another type gives us a new type. For example, the code char *my_char_pointer; declares a varibale with the name my_char_pointer with the type pointer to a char.

Assigning To a Pointer. To get an arrow pointing at a box(technically speaking, the address of that box in memory), we need a way to name that box, and then we need to use the & operator, which is called an ampersand, and the operator is named the address-of operator. For example, xPtr = &x;. After it is initialized, xPtr points to the variable x.

Dereferencing a pointer. Following the arrow is accomplished using the star symbol, *, a unary operator that dereferences a pointer. For example, *xPtr = 6; changes the value that xPtr points to.

A Picture is Worth a Thousand Words

When woking with pointers, always draw pictures.

Swap Revisited

1
2
3
4
5
void swap(int *x, int *y){
int temp = *x;
*x = *y;
*y = temp;
}

Pointers Under the Hood

The mechanics of pointers make a little more sense when we look under the hood at the hardware representation. When we draw boxes for our variables, we do not necessarily think about how big the box is, but that information is implicit in the type of the variable.

Addressing. A computer keeps track of all of the data by storing it in memory. The hardware names each location in memory by a numeric address. As each different address refers to one byte of data, this type of storage is called byte-addressable memory.

With more concrete understanding of memory and addresses, the hardware representation of pointers becomes clear: pointers store the addresses of the variable they point to.

A Program’s View of Memory

On a 32-bit machine, where addresses are 32 bits in size, the entire memory space begins at 0x00000000 and ends at 0xFFFFFFFF. Every program has this entire address at its disposal and there is a convention for how a program uses this range of addresses.

Code. Compiler produces the series of numbers from source code, which is readable by the computer.

Static Data. The static data area contains variables that are accessible for the entire run of the program.

Heap stores dynamically allocated data, which will be discussed in Chapter 12.

Stack stores the local variables declared by each function. The stack is divided into stack frames that are available from starting when the function is called, and last until it returns.

NULL: A Pointer to Nothing

The reason why the code does not start at address 0 is that programs use a pointer with the numeric value of 0, which is NULL, to mean does not point at anything. A pointer without actual things is useful for many reasons:

  • We can use it to answer “there is no answer”
  • We may also have functions whose job it is to create something that return NULL if they fail to do something.

When we use NULL, we will represent it as an arrow with a flat head. Whenever we have NULL, we can use the pointer itself, but we cannot follow the arrow, otherwise the program will crash with a segmentation fault–an error indicating that we attempted to access memory in an invalid way.

The NULL pointer has a special type–void *.

Pointers to Sophisticated Types

Structs

1
2
3
4
5
6
7
8
9
10
11
struct aStruct {
int *p;
int x;
};
int x = 9;
struct aStruct a;
struct aStruct *q = &a;
a.p = &y;
a.x = 3;
int b = *a.p;
int c = (*q).x;

When we have pointers to structs, we can just use * and . operators that we have seen so far, however, the order of operations means that . happen first.

Pointers to Pointers

We can have pointers to pointers(or pointers to pointers to pointers…). For example, an int ** is a pointer to a pointer to an int. We can have as many levels of “pointer” as we want.

Why do we have to have pointers to pointers? One answer is that “for all the same reasons we want pointers”.

const

As we discuss pointers to various types, it is good time to introduce the notion of const data-data which we tell the compiler we are not allowed to change. For example:

1
const int x = 3; //assigning to x is illegal

If we try to change the value of x, the compiler will produce an error.

When we have pointers, there are two different things that can be const: the data that the pointer points at, or the pointer itself. If we write:

1
const int *p = &x;

We have declared p as a pointer to a const int–that is, p points at a int, and we are not allowed to change that int. We can change where p points(e.g., p = &y; is legal if y is an int). However, changing the value in the box that p points at is illegal.

We can achieve exactly the same effect by writing:

1
int const *p = &x; // the value of x cannot be changed, but the pointer p can be changed.

If we want to specify that we can change *p but not p itself, we would write

1
int *const p = &x; // the pointed value of p can be changed, the pointer is not allowed to change to point other variable.

We can also combine both of above cases to:

1
const int* const *p = &x;

The same principle applies to pointers to pointers. For example, with an int **, we have the following combinations:

Can we change? => **p *p p
int **p Yes Yes Yes
const int **p No Yes Yes
int *const *p Yes No Yes
int **const p Yes Yes No
const int *const *p No No Yes
const int **const p No Yes No
int *const *const p Yes No No
const int *const *const p No No No

Aliasing: Multiple Names for a Box

Caution: Unless/until you understand exactly what is happening here and have a good reason to do it, you should not cast between pointer types.

Pointer Arithmatic

When adding 1 to a pointer, it has the semantics of one integer later in memory. Incrementing the pointer should have it point to the “next” integer in memory. It is a matter of the fact that the rules of C give the compiler a lot of freedom in how it lays out the variables in memory.

Use Memory Checker Tools

It is crucial to use memory checker tools, such as valgrind and/or -fsanitize=address. These will help you find erroneous behavior, and make fixing your program easier. Use them all throughout the testing process.