Lesson 1 - Introduction to pointers in the C language
Welcome to the first lesson of an advanced course about programming in the C language. In this course, we'll learn how to work with dynamically allocated memory in the C language and get to work with files. Perhaps you won't be surprised that the prerequisite for the success in this course is the knowledge of the basic constructs of the C language.
Memory addresses
When we first mentioned variables, we said that variable is a "place in
memory" where we can store some value. We also know that variables have
different data types (such as int
) and occupy as much memory as
this type requires (for example, int
takes 32 bits, i.e. 32 ones
and zeros).
We can imagine the computer's memory as a long (almost endless ) sequence of ones and zeros. Some parts of the memory are occupied by other applications, and some are considered as free space by the operating system. In order to be able to work with the memory, it's addressed like houses in a street. The addresses are mostly specified in hexadecimal system but they are still ordinary numbers. The addresses goes chronologically following one another. Each address represents 1 byte of memory (i.e. 8 bits, since addressing single bits would be impractical).
Once we declare a variable in a C source code and run the application, C asks the operating system for the memory needed for this variable. The system then reserves this memory and returns its address to which the value of the variable can be stored (simply put). We call this process memory allocation.
Retrieving variable address
The C language has completely relieved us from working with addresses till
now. It allocated memory for us, and we were working with our variables simply
using their names. Now, let's create a simple program that declares a variable
of the int
type and stores a value of 56
in it. We're
going to retrieve the address of this variable using the reference operator
&
(ampersand) and print it to the console. In the format
string, we'll use %p
which prints an address in the hexadecimal
system as it's proper when it comes to memory addresses.
int main(int argc, char** argv) { int a; a = 56; printf("The a variable with the value of %d is stored in the memory at address %p", a, &a); return (EXIT_SUCCESS); }
The result:
c_pointers
The a variable with the value of 56 is stored in the memory at address 0x23aadc
You can see that the system has chosen the address 0x23aadc
on
my computer. You'll have a different number there. The situation in the
computer's memory will look like this:
(The int
data type has 32 bits, so it occupies 4 eights of bits
on 4 addresses. We always specify the address where the value starts from.)
Pointers
Getting the address number is nice, but if we worked with memory that way, it would be rather impractical. That's why the C language supports pointers. Pointer is a variable whose value is an address to some place in the memory. However, C doesn't treat pointers as ordinary numbers, it knows it should use them as addresses. When we store something into a pointer or retrieve its value, it's not the address (the pointer's value) which is returned, but value that the pointer is pointing to is used.
Let's get back to our program again. This time, except for the variable,
we'll define a pointer to the a
variable as well. It'll also be of
the int
type, but we'll write the dereference operator * (asterisk)
before it. It's a good habit to name pointers so they start with
p_
. Get used to it. It avoids major problems in the future since
pointers are relatively dangerous, as we'll find out later. Why we should make
it clear whether a variable is a pointer or not.
int main(int argc, char** argv) { int a, *p_a; a = 56; p_a = &a; // Stores the address of the a variable into p_a *p_a = 15; // Stores the value of 15 at the address of p_a printf("The pointer p_a has the value of %d and it points to the value of %d", p_a, *p_a); return (EXIT_SUCCESS); }
The application creates an int
variable and a pointer to
int
. Pointers also always have their data type depending on the
type of the value they are pointing to. A value of 56
is stored in
the variable a
.
The address of the variable a
is stored to the pointer
p_a
(with no asterisk for now). We retrieve the address using the
reference operator &
. Now we want to store the number
15
into where the pointer p_a
points. Using the
dereference operator (*
), we don't store the value into the pointer
but rather into where the pointer points to.
Then we print the value of the pointer (which is an address in the memory,
usually a high number, here it's printed using the decimal system). And then we
print the value that the pointer points to. Whenever we work with the value of a
pointer (not it's address), we use the *
operator.
The result:
c_pointers
The pointer p_a has the value of 23374500 and it points to the value of 15
Again, let's look at the situation in the memory.
Passing by reference
Now, we can create a pointer to a variable. But what is it good for? We already could assign to a variable before. One of the advantages of pointers is passing by reference. Let's create a function that accepts 2 numbers as parameters. We'll want it to swap the numbers, assign the value of the first number to the second and vice versa. We could naively write the following code:
// This code doesn't work void swap(int a, int b) { int auxiliary = a; a = b; b = auxiliary; } int main(int argc, char** argv) { int number1 = 15; int number2 = 8; swap(number1, number2); printf("In number1 there's number %d and in number2 there's number %d.", number1, number2); return (EXIT_SUCCESS); }
The result:
c_pointers
In number1 there's number 15 and in number2 there's number 8.
Why isn't the application working? When calling the swap()
function in the main()
function, the values of the
number1
and number2
variables are taken and copied
into the variables a
and b
in the function definition.
The function further changes these a
and b
variables,
but the original number1
and number2
variables remain
unchanged. This approach, when the value of a variable is copied to a function
parameter, is called passing by value.
Notice that we need an auxiliary variable in order to swap 2
numbers. If we only wrote a = b; b = a;
in the swap()
function, there would be the value of b
in both variables, since
the a
value has been overwritten by the first command.
We can pass any variable by reference if we modify a function to accept
pointers as parameters. To call such a function, we'll use the
&
reference operator:
void swap(int *p_a, int *p_b) { int auxiliary = *p_a; *p_a = *p_b; *p_b = auxiliary; } int main(int argc, char** argv) { int number1 = 15; int number2 = 8; swap(&number1, &number2); printf("In number1 there's number %d and in number2 there's number %d.", number1, number2); return (EXIT_SUCCESS); }
The result:
c_pointers
In number1 there's number 8 and in number2 there's number 15.
Since we now pass the address to the function, it's able to change the original variables.
Some C programmers often use function parameters to return
values. However, this isn't very readable and if the performance is not a
problem, and it's at least a little possible, a function should always return
only one value using the return
statement. It can eventually return
a structure or a pointer to a structure/array.
Maybe it crossed your mind that you finally understand the
scanf()
function which stores values into variables passed through
its parameters. We use the &
operator here to pass the address
at which the function should store the data at:
int a; scanf("%d", &a);
Passing arrays
Arrays and pointers have a lot in common in the C language. Therefore, when we pass an array to a function parameter and change the array in the function, the changes will be reflected in the original array. Unlike other types, arrays are always passed by reference without us having to make any effort.
void fill_array(int array[], int length) { int i; for (i = 0; i < length; i++) { array[i] = i + 1; } } int main(int argc, char** argv) { int numbers[10]; fill_array(numbers, 10); printf("%d", numbers[5]); // Prints number 6 return (EXIT_SUCCESS); }
As we have said before, an array is actually a continuous space in memory. But we have to address such space somehow. We address it using pointers. Variables of the array type are nothing more than pointers. This means that the following assignment operation will be executed smoothly:
int array[10]; int* p_array = numbers;
In the Pointer arithmetics lesson, we're going to show that it doesn't matter whether we have an array or a pointer.
NULL
We can assign a NULL
constant to all pointers of any type. It
indicates that a pointer is empty and that it doesn't point to anything now. On
most platforms, NULL
equals to 0
, so you may encounter
assigning 0
instead of NULL
in some codes. This is
generally not recommended due to compatibility issues between different
platforms. We'll use this value quite often in the future.
Remember: Pointer is a variable in which a memory address is
stored. We can work either with this address or with the value at this address
using the *
operator. We get the address of any variable using the
&
operator.
Although we introduced pointers quite well, their real purpose is dynamic memory allocation. We'll look at it in the next lesson, Dynamic memory allocation in the C language.