Pointer
Pointers are one of the most important concepts in C, and one of the most difficult to understand.
Introduction
What is a pointer? Firstly, it is a value which represents a memory address, so a pointer is equivalent to a signpost to a memory address.
The character *
denotes a pointer, usually followed by the type keyword, indicating what type of value the pointer is pointing to. For example, char*
denotes a pointer to a character, and float*
denotes a pointer to a value of type float
.
int* intPtr;
The above example declares a variable intPtr
, which is a pointer to a memory address that holds an integer.
The asterisk *
can be placed anywhere between the variable name and the type keyword, and the following write-ups are valid.
int * intPtr;
int * intPtr;
int * intPtr;
This book uses the asterisk immediately following the type keyword (i.e. int* intPtr;
) because it reflects that a pointer variable is just a normal variable, except that its value is a memory address.
One thing to note about this way of writing is that if two pointer variables are declared on the same line, then they need to be written like this.
// Correct
int * foo, * bar;
// incorrect
int* foo, bar;
In the example above, the second line of execution results in foo
being an integer pointer variable and bar
being an integer variable, i.e. *
only works for the first variable.
A pointer to a pointer may still be a pointer, which would be indicated by two asterisks **
.
int** foo;
The above example indicates that the variable foo
is a pointer to what is still a pointer, and that the second pointer points to an integer.
* operator
The symbol *
, in addition to representing a pointer, can also be used as an operator to retrieve the value inside the memory address pointed to by the pointer variable.
void increment(int* p) {
*p = *p + 1;
}
In the above example, the argument to the function increment()
is an integer pointer p
. Inside the function body, *p
represents the value pointed to by the pointer p
. Assigning a value to *p
means changing the value inside the address pointed to by the pointer.
The function above does this by adding 1
to the value of the argument. The function does not have a return value because the address is passed in and operations inside the function body on the value contained in that address will affect the function outside, so there is no need to return a value. In fact, it is common practice in C to pass the value to the outside, via a pointer, inside the function.
There is another advantage to passing variable addresses, rather than variable values, into functions. For large variables that require a lot of storage space, copying the variable value into the function is a huge waste of time and space and is not as efficient as passing in a pointer.
The ## & operator
The &
operator is used to take out the memory address where a variable is located.
int x = 1;
printf("x's address is %p\n", &x);
In the above example, x
is an integer variable and &x
is the memory address where the value of x
is located. The %p
of printf()
is a placeholder for the memory address, which prints out the memory address.
The function in the previous subsection, with the argument variable plus 1
, can be used like the following.
void increment(int* p) {
*p = *p + 1;
}
int x = 1;
increment(&x);
printf("%d\n", x); // 2
In the above example, after calling the increment()
function, the value of the variable x
is incremented by 1. The reason for this is that what is passed into the function is the address &x
of the variable x
.
The &
operator and the *
operator are inverse operations, and the following expression always holds.
int i = 5;
if (i == *(&i)) // correct
Initialisation of pointer variables
After declaring a pointer variable, the compiler allocates a memory space for the pointer variable itself, but the value inside this memory space is random, that is, the value pointed to by the pointer variable is random. It is important not to read or write to the address pointed to by the pointer variable at this point, as that address is random and may well lead to serious consequences.
int* p;
*p = 1; // error
The above code is wrong because the address pointed to by p
is random, and writing 1
to this random address will lead to unexpected results.
The correct way to do this is to have a pointer variable declared and then have it point to an allocated address before reading or writing, this is called initialising a pointer variable.
int* p;
int i;
p = &i;
*p = 13;
In the above example, p
is a pointer variable, and when this variable is declared, p
will point to a random memory address. This is the time to point it to an already allocated memory address. The above example is another declaration of an integer variable i
, the compiler will allocate the memory address for i
and then let p
point to i
's memory address (p = &i;
). Once initialization is complete, the memory address pointed to by p
can be assigned (*p = 13;
).
To prevent reading and writing uninitialised pointer variables, you can make it a habit to set uninitialised pointer variables to NULL
.
int* p = NULL;
NULL
is a constant in C that indicates a memory space with address 0
, an address that is unusable and reading or writing to it will report an error.
Pointer arithmetic
A pointer is essentially an unsigned integer that represents a memory address. It can perform arithmetic, but the rules are not those of integer arithmetic.
(1) Adding and subtracting pointers and integer values
The operation of a pointer with an integer value represents the movement of a pointer.
short* j;
j = (short*)0x1234;
j = j + 1; // 0x1236
In the above example, j
is a pointer to the memory address 0x1234
. You may think that j + 1
is equal to 0x1235
, but the correct answer is 0x1236
. The reason for this is that j + 1
means that the pointer moves one unit to the high bit of the memory address, and a unit of type short
occupies two bytes of width, so it is equivalent to moving two bytes to the high bit. Similarly, j - 1
gives a result of 0x1232
.
The units that a pointer moves are related to the type of data it points to. The data type takes up as many bytes as it moves per unit.
(2) Pointer and pointer addition operations
Pointers can only add or subtract with integer values; it is illegal to add two pointers.
unsigned short* j;
unsigned short* k;
x = j + k; // illegal
The above example is two pointers added together, which is illegal.
(3) Subtraction of pointers and pointers
Pointers of the same type are allowed to be subtracted, returning the distance between them, i.e. how many data units apart.
Subtracting a high address from a low address returns a positive value; subtracting a low address from a high address returns a negative value.
In this case, the value returned by subtraction is of type ptrdiff_t
, which is a signed integer type alias, the exact type of which varies depending on the system. The prototype of this type is defined in the header file stddef.h
.
short* j1;
short* j2;
j1 = (short*)0x1234;
j2 = (short*)0x1236;
ptrdiff_t dist = j2 - j1;
printf("%d\n", dist); // 1
In the above example, j1
and j2
are two pointers to the short type, the variable dist
is the distance between them, type ptrdiff_t
, the value of 1
, because the difference of 2 bytes exactly to store a short type of value.
(4) Pointer to pointer comparison operations
The comparison operation between pointers compares which of their respective memory addresses is larger, and the return value is the integer 1
(true) or 0
(false).