Recall our earlier discussion about the computer’s main memory. In your computer, the main memory is made up of individual bits or switches that can be either on or off. An average computer these days has about 35 billion bits worth of main memory. Every piece of information the computer stores, it stores in terms of 0’s and 1’s by flipping bits on and off in main memory. In order to keep this massive array organized, the memory is divided up into eight-bit pieces called bytes and each byte is given a number called an address. This address tells the computer where in main memory the given byte is located.
In C, a variable that stores a memory address is called a
pointer. Pointers are stored in memory just like any
other variable. The reason why it is called a pointer is because,
instead of storing a useful value, it simply “points” to another
location in memory. (Think of a citation in a paper; it doesn’t give you
any immediate information, it simply tells you where to find it.) The
&
operator can be used to retrieve the address of a
variable. If a variable is a pointer, the *
operator can be
used to refer the value stored at that address. The &
and *
operators are called the reference
and dereference operators respectively. Consider the
following example:
int x;
int y;
int *p;
x = 5;
p = &x; /* p now holds the address of x */
y = *p; /* y now holds the value 5 */
*p = 7; /* x now holds the value 7 */
p = &y; /* p now points to y */
This example demonstrates that you can use pointers both to read from
and to write to the given location in memory. In the above example,
y = *p
assigns the value of x
to
y
. This is because, at that point in the program,
p
points to x
and the expression
*p
evaluates to the value stored at the address stored in
p
. On the other hand, the line *p = 7
assigns
the value of 7 to x
. This is because
*p = <expression>
stores the value of the expression
in whatever memory location is pointed to by p
; in this
case p
is pointing to x
.
This example also gives us an example of a pointer declaration:
int *p;
. This notation is suggestive of the fact that
*p
is an integer. The type of a pointer depends on the type
of the data to which it is pointing. This allows the compiler to know
the type of *p
. When it comes to the type of a pointer, you
can think of it in two ways. One is to consider p
as being
a variable of type int *
or “pointer to integer”. Another
perspective is to think of p
as being a pointer to a
variable of type “integer”. Depending on the context, both of these
perspectives are useful.
When it comes to function arguments, there are two major models: pass-by-reference and pass-by-value. In C, the default is to pass arguments by value. When a function is called, the values of each of the arguments are copied into the newly created argument variables. Because the function is working with a copy of the data, any changes it may make get thrown away when the function returns. Another way to pass arguments is by passing a reference or a pointer to the original variable. In this way, changes that happen to that variable in the function are reflected in the value of the variable when the function returns.
In C, you can implement the pass-by-reference model using pointers. Consider the following function:
void swap(int *x, int *x)
{
int z;
z = *x;
*x = *y;
*y = z;
}
Suppose the swap
function were called as follows:
int a;
int b;
a = 5;
b = 23;
swap(&a, &b);
When the swap
function returns, the value of
a
will be 23, while the value of b
will be 4.
But how does this work when C copies the values of the arguments into
the argument variables? Since the variables are pointers, the values of
&a
and &b
are copied. However, these
values are addresses in memory, so x
points to the same
address as &a
which is the address of a
.
In this way, when we execute *x = *y
, the value of
*y
(which is the value of b
) gets stored at
the address pointed to by x
(which is the address of
a
). In this way, we can get around the restrictions of the
pass-by-value model.
You need to be careful with the pass-by-reference model. First, it is
very easy to abuse and you should always think about what you’re doing
before you assume that pass-by-reference is the right way to do it.
However, there are some places where pass-by-reference is exactly what
we need. For example, the scanf
function uses
pass-by-reference to return the values it reads from the terminal. There
are two reasons why scanf
uses pass-by-reference. First is
that it is already doing something else useful with its return value: it
returns the number of fields properly read from the input. Second is
that scanf
is capable of reading multiple values with one
function call and you can only return one value at a time. Returning
multiple values is probably the most common reason for
pass-by-reference.
You should also be wary when using pass-by-reference that you do not
use the same argument for both input and output. There are a few times
where this is useful but it is not generally a good idea. In the case of
our swap
function above, both variables are inputs and both
are outputs; however this makes perfect sense for a function that swaps
two things. What you should never do is use the same variable
for both input and output just because you want to save on the number of
arguments. It is also a good idea to document somewhere in the comments
what variables are inputs and what variables are outputs.
It is frequently useful to use the pass-by-reference model with
structures to write functions that modify the structure in-place. More
complicated data structures frequently cannot be directly copied, so you
have to refer to them using a pointer. If you have a pointer to a
structure, you can access the structure’s members using the
->
(arrow) operator. The expressions
(*p).member
and p->member
are exactly
equivalent. We will see some of these more complicated structures when
we start talking about dynamic memory.
Previously, we had a brief discussion of arrays in which we declared
them and talked about accessing elements using the []
operator. But how are arrays connected to pointers? Recall that arrays
are stored sequentially by simply placing the elements of the array next
to each other in memory. The resulting variable is basically just a
pointer to the first element in the array. The only effective difference
between an array and a pointer is that if p
is a pointer,
sizeof(p)
will give you the size a memory address consumes
in memory while sizeof(arr)
will give you the amount of
memory consumed by the array arr
. Beyond the effects of
sizeof
, pointers and arrays are identical; specifically,
*p
is equivalent to p[0]
.
You can also perform integer arithmetic on pointers. If you add an
integer to a pointer, it simply advances it that many places in memory.
In this way, arr + 6
points to the sixth element of
arr
. The exact address of
<pointer> + <integer>
depends on the type of
the data pointed to by <pointer>
. This is because the
distance between the first and second elements in an array of type
char
is one byte, while the distance between the first and
second elements in an array of type int
may be 4 bytes.
Because of this, the expressions *(arr + n)
and
arr[n]
are equivalent. You can also use the increment and
decrement operators on pointers.