Chapter 13: Using Pointers

A pointer is a variable that holds a value; that value is the location (or memory address) of another value. The pointer data type has two important roles. First, it identifies the variable identifier as a pointer. Second, it specifies the kind of value that will be accessed at the location held by the pointer.

It is essential to learn how to verbally differentiate the address of notation (a pointer value) and the target of notation (the value found at the address that the pointer points to). In this chapter, we will strive to demystify C pointers.

Learning how to properly use pointers expands both the expressiveness of C programs, as well as the range of problems that can be solved.

The following topics will be covered in this chapter:

  • Dispelling some myths and addressing some truths about C pointers
  • Understanding where values are stored and how they are accessed
  • Declaring pointers and naming them appropriately
  • Using pointer arithmetic
  • Accessing pointers and their targets
  • Understanding the NULL pointer and void*
  • Comparing pointers
  • Talking and thinking about pointers correctly
  • Using pointers in function parameters
  • Accessing structures via pointers

Technical requirements

Continue to use the tools you chose from the Technical requirements section of Chapter 1Running Hello, World!.

The source code for this chapter can be found at https://github.com/PacktPublishing/Learn-C-Programming-Second-Edition/tree/main/Chapter13.

Addressing pointers – the boogeyman of C programming

Before we begin our understanding of declaring, initializing, and using pointers, we must address some common misconceptions and unavoidable truths about C pointers.

C pointers are often considered one of the most troublesome concepts in C, so much so that many modern languages claim to have improved on C by removing pointers altogether. This is unfortunate. In many cases, this limits the language's power and expressiveness. Other languages still have pointers but severely restrict how they may be used.

Pointers in C are one of its most powerful features. With great power comes great responsibility. That responsibility is nothing more than knowing how to correctly and appropriately use the power that is given. This responsibility also involves knowing when not to use that power and understanding its limits. 

Untested and unverified programs that incorrectly or haphazardly employ pointers may appear to behave in a random fashion. Programs that once worked reliably but have been modified without understanding or proper testing become unpredictable. The program flow and, consequently, program behavior becomes erratic and difficult to grasp. Improper use of pointers can wreak real havoc. This is often exacerbated by an overly complex and obfuscated syntax, both with and without pointers. Pointers, in and of themselves, are not the cause of these symptoms.

It is my point of view that full knowledge of C pointers greatly enhances a programmer's understanding of how programs and computers work. Furthermore, most – if not all – programming errors that use pointers arise from untested assumptions, improper conceptualization, and poor programming practices. Therefore, they should be avoided. We will focus on proper conceptualization, methods of testing assumptions, and good programming practices.

In the preceding chapters, we emphasized a test and verify approach to programming behavior. This is not only a good approach in general but is also especially important when using pointers. This approach, hopefully, is now somewhat ingrained in your thinking about developing programs. With experience – based on practical experiments and proof by example (both of which we covered in earlier chapters) – your knowledge and understanding of pointers are reinforced so that they can be mastered quickly and used safely with confidence.

Important Note

The importance of a test and verify approach cannot be overstated for many reasons. First, implementations of C vary over time. As the C standard evolves, features are added, removed (rarely), and refined. What may have worked on an older standard version may no longer work, work slightly differently, or cause defined behavior. Second, any C compiler is implemented by humans. This means that not everyone has perfect knowledge or every implementation is perfectly implemented according to a C standard. Lastly, the C Standards Committee purposely left many implementation details of the language up to the compiler writers for any given environment. Not all CPUs have the same capabilities, and not all environments have the same hardware resources. This is called "undefined behavior" of the C standard, but I prefer to think of it as "unspecified behavior" in that the implementors get to specify the final behavior for their target environment. Knowing all of this, the programmer must verify program behaviors for any environment they write their programs for.

So, the programming task is not only to write the program but also to write test programs that verify behaviors of critical parts of it. The test programs aid the verification process when a program is moved from one environment to another. To be a complete programmer, thorough knowledge and practice of various testing methods must be mastered, along with programming itself.

Why use pointers at all?

If pointers are so problematic, why use them at all? First and foremost, pointers are not the problem; misuse of pointers is the problem.

Nonetheless, there are four main uses for pointers:

  • To overcome the call-by-value restriction in function parameters: Pointers expand the flexibility of function calls by allowing variable function parameters.
  • As an alternative to array subscripting: Pointers allow access to array elements without subscripting.
  • To manage C strings: Pointers allow easy (ahem, easier) allocation and manipulation of C strings.
  • For dynamic data structures: Pointers allow memory to be allocated at runtime for useful dynamic structures, such as linked lists, trees, and dynamically sized arrays.

We will deal with the first point, the mechanics of pointers and variable function parameters, in this chapter. The second point will be explored in Chapter 14Understanding Arrays and Pointers. The third point will be explored in Chapter 15Working with Strings. Finally, the fourth point will be explored in Chapter 18Using Dynamic Memory Allocation.

Pointers allow our programs to model real-world objects that are dynamic – that is, their size or the number of elements is not known when the program is written, and their size or the number of elements will change as the program runs. One such real-world example is a grocery list. Imagine your list could only ever hold, say, six items; what do you do if you needed seven items? Also, what if when you got to the store, you remembered three more items to add to your list?

Because pointers give us an alternative means to access structures, arrays, and function parameters, their use enables our programs to be more flexible. Pointers, then, become another mechanism for accessing values from which we can choose the best fit for the needs of our program.

Introducing pointers

pointer is a variable whose value is the location (or memory address) of some other variable. This concept is so basic yet so essential to understanding pointers that it bears elaboration. 

A variable identifies a value stored at a fixed location. It consists of a type and an identifier. Implicit in the definition is its location. This location is fixed and cannot be changed. The value is stored at that location. The location is primarily determined by where a variable is declared in a program. The variable identifier, then, is our assigned name for the location of that value; it is a named location to store a value of a given type. We rarely, if ever, care about the specific value of that location – in fact, we never do. So, we never care about the specific address of a variable. We only care about the name of the location or our variable identifier.

A pointer variable, like any other variable, also identifies a value stored at a fixed location. It also consists of a type and an identifier. The value it holds, however, is the location of another variable or named location. We don't specifically care about the value of the pointer variable itself, except that it contains the location of another variable whose value we do care about.

So, while a variable's location cannot change, its contents can. For pointer variables, this means that a pointer's value can be initialized to one named location (address) and then later reassigned to another named location (address). Two or more pointers can even have the same-named location; they can point to the same address.

To highlight the difference between a variable that is named and contains a value and a pointer variable that is also named but whose value is an address, we first need to understand the concept of the direct addressing and indirect addressing of values.

Understanding direct addressing and indirect addressing

When we use a non-pointer variable, we are, in fact, accessing the value directly through the variable's identifier (its named location). This is called direct addressing.

When we use a pointer variable to access a value at its assigned, named location (a variable in a different location), we are accessing that value through the pointer variable. Here, we access the variable indirectly through the pointer. In essence, from the pointer value, we get the address of the location that holds the value we want, and then we go to that address or location to get the actual value.

Before going further, we need to understand some background concepts – memory and memory addressing.

Understanding memory and memory addressing

First, it is essential to understand that everything that runs on a computer is in memory. When we run a program, it is read from the disk, loaded into memory, and becomes the execution stream. When we read from a file on a disk, CD, or flash drive, it is first read into memory and accessed from there, not from its original location (this is an oversimplification since there is a bit more that goes on). All of the functions we call and execute are in memory. All of the variables, structures, and arrays we declare are given their own locations in memory. Finally, all of the parts of the computer that we can read from or write to are accessible through some predefined memory location. How the OS handles all of the system devices, system resources (memory), and the filesystem is beyond the scope of this book.

Second, once we understand that everything is in memory, we must know that each byte of memory is addressable. The memory address is the starting location of a value, a function, or even a device. To a running program, memory is seen as a continuous block of bytes, each having its own address from 1 to the largest unsigned int value available on that computer. The 0 address has a special meaning, which we will see later on in this chapter. An address, then, is the nth byte in the range of all possibly addressable bytes on the computer.

If a computer uses 4 bytes for an unsigned int value, then the address space of that computer's memory is from 1 .. 4,294,967,295, just over 4 billion bytes or 4 gigabytes (GB). This is known as a 32-bit address space. It may seem like a large number, but most computers today come with at least this much memory, some with 16 GB, 64 GB, 256 GB, or even more GB of memory (RAM). Because of this, a 4 GB address space is insufficient and leaves most of the other memory unaddressable, and so inaccessible.

If a computer uses 8 bytes for an unsigned int value, then the address space of that computer's memory is from 1 .. 18,446,744,073,709,551,615 bytes, or over 18 quintillion bytes. This is known as a 64-bit address space and holds many orders of magnitude more memory than any computer can hold today or in the foreseeable future. 

The actual physical memory (which is physically present on the machine) may be far smaller than the amount of memory that is virtually addressable. A 64-bit computer can address memory of over 18 quintillion bytes, but computers of any size rarely have anything even close to that much physical memory. The OS provides mechanisms to map the virtual address space into a much smaller physical address space and manage that mapping as needed.

A 64-bit address space provides a very large working address space available to a program. Programs that model subatomic reactions, perform finite-element analysis on extremely large structures (such as very long bridges), simulate jet engine performance, or simulate astronomical models of our galaxy require enormous address spaces.

Lucky for us, dealing with the need for such large address spaces is not a problem for us today, nor is it a problem for us tomorrow. We can have a much simpler working concept of an address space while basking in the knowledge that we will not have to worry about the limits of our programs in a 64-bit address space anytime soon. 

Managing and accessing memory

C provides ways for a program to allocate, release, and access virtual memory in our physical address space; it is then up to the OS to manage the physical memory. The OS swaps virtual memory in and out of the physical memory as needed. In this way, our program is only concerned with the virtual memory address space.

C also provides some limits on what memory can be accessed and how it can be manipulated. In Chapter 17, Understanding Memory Allocation and Lifetime, and Chapter 18, Using Dynamic Memory Allocation, we will explore some of the ways that C gives us limited control of our program's memory. In Chapter 20, Getting Input from the Command Line, and Chapter 23, Using File Input and File Output, we will explore how C allows us to get data dynamically from a user via the command line, as well as read and write data files. In each of these chapters, we will expand our conceptualization of memory and how it is used by our programs.

C was written before Graphical User Interfaces (GUIs) were developed; therefore, there is no concept of pixels and colorspaces, audio ports, or network interfaces. To C, these are all just Input/Output (I/O) streams that are tied to a device accessed through memory that we can read from or write to via an intermediary program or library.

Lastly, every time we run our program, the memory addresses within it will likely change. This is why we are concerned with named locations and never specific memory addresses. 

So, to summarize, we now know the following:

  • Memory is seen as one large contiguous block.
  • Everything in a computer is stored somewhere in memory or is accessible via a memory location.
  • Every byte in a computer's memory has an address.
  • Named locations (variables) are fixed addresses in memory.

Exploring some analogies in the real world

Real-life analogies for pointers are abundant. We'll explore two analogies to help provide some clarity about them.

In our first analogy, John, Mary, Tom, and Sally each own a different thing that they will give to us when we ask them for it. John owns a book, Mary owns a cat, Tom owns a song, and Sally owns a bicycle. If you want, say, a song, you ask Tom. If you want a bicycle, you ask Sally. If we want something, we go directly to the owner of it. That is direct addressing.

Now, say that we don't know who each of them is or what they own. Instead, there is someone else we know – say, Sophia – who knows each person and what they own. To get something that we want, we have to go to Sophia, who then goes to the proper person, gets what they own, and gives it to us. Now, to get a book, we go to Sophia, who then goes to John to get the book and gives it to us. To get a cat, we again go to Sophia, who then goes to Mary to get the cat and gives it to us. We still don't know anyone but Sophia, but we don't care because we can still get everything indirectly through Sophia. This is indirect addressing.

John, Mary, Tom, and Sally are similar to variables. When we want the things they hold, we go directly to them. Sophia is similar to a pointer variable. We don't know where each thing we want is held, so we go to the pointer, which then goes to where the thing we want is held. We go to the things we want indirectly through the pointer.

Our second analogy involves a mailman and the mailboxes where we receive our mail. In a neighborhood or city, each building has a street name and number, as well as a city, state, or province and a postal code. All of these together uniquely identify a building's address. The building could be a home, a farm, an office, a factory, and so on. In front of or attached to the buildings are mailboxes, each one associated with a specific building. The mailboxes could stand alone or be grouped together. In the mailboxes, envelopes, parcels, magazines, and so on are delivered. Anyone can place something in any mailbox and the addressee (the resident of the building) can remove the content.

We can think of the mailboxes as variable names (fixed locations) and the content placed in those mailboxes as the values we assign to variables. Furthermore, each mailbox has a unique address, comprising several parts, just as variables have a unique address, comprising just a single number – its byte address. Just like a mailbox, a value can be assigned to a variable, and the value can be accessed from that variable.

If we want to send a parcel to an address, we, in essence, give it to the mailman (a named identifier) to deliver it for us. The mailman travels to the address indicated and places the parcel in the mailbox for that address.

We can think of the mailman as the pointer that takes an address, travels to it, and takes or leaves a value at that address.

Analogies, however, are rarely perfect. The preceding two analogies, in the end, are not as accurate as we might like. So, let's look at a diagram of memory with some variables and a pointer variable that points to one of the variable's locations:

Figure 13.1 – A memory diagram of three integer variables and a pointer variable

 

Figure 13.1 – A memory diagram of three integer variables and a pointer variable

Here, memory is seen as a linear stream of bytes. In this diagram, we are not concerned with the actual byte addresses and so they are not shown. We see three named integer locations (variables) – lengthwidth, and height. Because these are int values, they each take up 4 bytes. We also see a pointer variable, pDimension, which takes up 8 bytes and points to the location named height. The value of pDimension is the address of the height named location. We never need to be concerned about the actual value of a pointer. Instead, we should be concerned about the named location it points to.

Note that there are some bytes that are neither named nor used. These are the padding bytes that the compiler uses to align variables of different sizes. We saw this before with structures in Chapter 9, Creating and Using Structures. While we need to be aware that padding bytes exist, we should also be aware that we cannot predict their presence or absence, nor can we control them. Therefore, we should not be overly concerned about them.

We will come back to this diagram once we have more details about declaring and assigning pointers, which we'll be looking at in the next section.

Declaring the pointer type, naming pointers, and assigning addresses

The following are the most basic aspects of pointers:

  • We can declare a variable of the pointer type.
  • We can assign an already-declared named location to it.
  • We can perform a limited number of operations on pointers.

So, while a pointer is a variable and can change, we do not assign values to it willy-nilly. A pointer should only be assigned a value that is an already-declared and named location. This means that a pointer must point to something that already exists in memory.

Because pointers give us values somewhat differently than simple variables, we also need to consider some naming conventions that set them apart from regular variables. These are conventions only and are intended to make the purpose of the variable as a pointer clear.

Declaring the pointer type

A pointer is a variable. Therefore, it has a type and an identifying name. It is distinguished as a pointer at declaration with the * notation.

The syntax for a pointer is type * identifier;, where type is either an intrinsic type or a custom type, * indicates a pointer to the given type, and identifier is the name of the pointer variable. The actual type of a pointer variable is not just type but also type*. This is what distinguishes a direct variable from an indirect variable.

A pointer must have a type for the thing it points to. A pointer type can be any intrinsic type (such as intlongdoublechar, and byte) or any already-defined custom type (such as an array, struct, and typedef). The pointer's value (an address) can be any named location (the variable identifier) that has that type. We will see why this is essential when we access the value at the pointer's address.

An example of an integer pointer is the following:

int height;
int width;
int length;
int* pDimension;

Here, we can see three integer variables – heightwidth, and length – and a single pointer variable, pDimension, which can hold the address of any integer variable. pDimension cannot hold the address of a variable of the floatdouble, or char types (to name just three) – only int. The type of pDimension is int*.

In this code fragment, none of the variables, nor the pointer, have been assigned a value.

Naming pointers

Because pointers hold addresses of values and not the desired values themselves, it is a good idea to differentiate between them by naming the pointers slightly differently than the direct variables. There are several naming conventions that are more or less in widespread use. This includes prefixing or suffixing ptr or p to the name of the variable identifier. So, our identifiers may appear as follows:

int anInteger;
int* ptrAnInteger;  // prefix ptr-
int* pAnInteger;    // prefix p- (shorthand)
int* anIntegerPtr;  // suffix -Ptr
int* anIntegerP;    // suffix -P (shorthand)

The general advice is to pick one of these conventions and use it consistently throughout your code. Of the four shown, the p- shorthand prefix is probably the most common and easiest to both type (with your keyboard) and read. This convention will be used for the remainder of this book. So, when we see, say, pDimension, we know immediately that it is a variable that is a pointer. This will help us to correctly assign and access it.

Assigning pointer values (addresses)

As with all other variables, a pointer has no meaningful value until one is assigned to it. Any variable declaration merely states what value the variable is capable of holding. We must assign a meaningful value to the pointer.

A pointer variable holds the address of another named location. This is the target of the pointer. A pointer points to another variable's location. That variable's value is its target. The way to assign an address value to a pointer is to use the & operator and the variable identifier, as follows:

int  height;
int* pDimension;
pDimension = &height;

This assigns the address of the height named location to the pDimension pointer variable. As previously mentioned, we don't care about the specific value of &height. But now, we know that pDimension points to the same memory location as height. Another way to express this is that height is the current target of pDimension.

We'll explore how to use this a bit more in the next section.

Operations with pointers

At this point, the only operations that work reasonably with pointers are the following:

  • Assignment
  • Accessing pointer targets
  • Limited pointer arithmetic
  • The comparison of pointers

We will explore each of these in turn. As we do, we must also consider the NULL special pointer value (the zeroth address), or a null pointer, and the void* special, unspecified pointer type, or a void pointer type.

Assigning pointer values

We have just seen how to assign an address to a pointer variable by using another variable's named location, as follows:

int height;
int width;
int length
int* pDimension;
pDimension = &height;

A diagram of the memory layout for these declarations is given in the Accessing pointer targets section.

We can later reassign pDimension, as follows:

pDimension = &width;

This assigns the address of width to the pDimension variable. width and *pDimension are now at the same memory address. The target of pDimension is now width.

Each time we assign an address to pDimension, it is the address of an already-defined variable identifier, as follows:

pDimension = &height;
  // Do something.
pDimension = &width;
  // Do something else.
pDimension = &length;
  // Do something more.
pDimension = &height;

First, we make height the target of pDimension, then width, and then length. Finally, we set height to again be the target of pDimension

Differentiating between the NULL pointer and void*

A pointer variable should always have an assigned value. Its value should never be unknown. However, there are times when a proper address cannot be assigned or the desired address is currently unknown. For these instances, there is a constant NULL pointer. This value is defined in stddef.h and represents a value of 0. It is defined as follows:

#define NULL ((void*)0)

Here, (void*) specifies a pointer to the void type. The void type represents no type – a type that is unknown or a non-existent value. You cannot assign a variable to have the void type, but a function can return void (nothing). As we have already seen, functions with the void return type don't return anything.

So, when we declare a pointer variable but do not yet have a target for it, it is best to assign the value of NULL to the pointer variable. In this way, we can test for NULL and know that the pointer variable is not in use. If we don't initialize a pointer variable to null, it may have a random value that we cannot differentiate from a valid memory address. The NULL pointer is the way to be certain.

Accessing pointer targets

A pointer must know the type of the value that it points to so that it can correctly get the correct number of bytes for that value. Without an associated type, pointer access will not know how many bytes to use when returning a value. For example, an int value is 4 bytes. An integer pointer variable (8 bytes) will then use its value as an address to go there and get 4 bytes to return an int value in the correct range. It does not matter that a pointer variable is 8 bytes. It does matter that the correct number of bytes is interpreted at the target.

To access a value indirectly via a pointer variable, we must dereference the pointer – that is to say, we must use the address stored in the pointer variable to go and get the value it points to, or we go to its target. To assign a value to the pointer's target, we use the * operator, as follows:

int  height;
...
int* pDimension = &height;
...
height = 10;
...
*pDimension = 15;

height is assigned a value of 10 directly through that variable identifier. In the next statement, 15 is assigned to the target of pDimension. Because pDimension points to height, that now has a value of 15 via *pDimension. Referenced in two different ways, height and *pDimension are the same memory location.

Note that the * operator is used in both the pointer declaration and in pointer dereferencing.

Pointer dereferencing can also be used to access values, as follows:

pDimension = &height;
int aMeasure;
...
aMeasure = height;
...
aMeasure = *pDimension;

Here, the value at the height direct variable and the pDimension indirect variable is accessed (retrieved) in two identical ways. In the first method, aMeasure is assigned the value directly from height, or, more precisely, the following occurs:

  1. height evaluates to the value at its fixed location.
  2. The value is assigned to aMeasure.

In the second method, because pDimension points to height – in other words, height is the target of pDimension – the following occurs:

  1. *pDimension evaluates its target – in this case, height.
  2. The target (height) evaluates to the value of that target.
  3. The value is assigned to aMeasure.

Let's illustrate direct and indirect access using pointers with a simple program.

In order to print out the addresses and target values that pDimension will hold, we use the %p format specifier for pointers:

#include <stdio.h>
int main( void )  {
  int height = 10;
  int width  = 20;
  int length = 40;
  int* pDimension = NULL;  
  printf( "
Values:

");
  printf( "  sizeof(int)  = %2lu
" , sizeof(int) );
  printf( "  sizeof(int*) = %2lu
" , sizeof(int*) );
  printf( "  [height, width, length] = [%2d,%2d,%2d]

" ,
             height , width , length );
  printf( "  address of pDimension = %p
" , &pDimension  );
  printf( "
Using address of each named variables...

");
  pDimension = &height;
  printf( "  address of height = %p, value at address = %2d
" ,
          pDimension , *pDimension );
  pDimension = &width;
  printf( "  address of width  = %p, value at address = %2d
" ,
          pDimension , *pDimension );
  pDimension = &length;
  printf( "  address of length = %p, value at address = %2d
" ,
          pDimension , *pDimension );
  return 0;
}

Using your program editor, create a new file called pointers1.c and type in the preceding program. Pay particular attention to the printf() format specifiers.

In this program, the %2lu format specifier prints out two digits of an unsigned long value since that is the type of value that sizeof() returns. To print an address in hexadecimal format, we use the %p format specifier, which prints a long hexadecimal value prepended with 0x to indicate that it's a hex value.

Compile and run this program. You should see something like the following output:

Figure 13.2 – A screenshot of the pointers1.c output

Figure 13.2 – A screenshot of the pointers1.c output

Each of the addresses printed is an 8-byte hexadecimal value. When you run this on your system, your addresses will certainly be different. If you look very closely at the addresses of the variables themselves, you may see something a bit unusual. The variables in our program have been defined in one order in our program but have been allocated in a different order in memory by the compiler. A diagram of memory looks something like this:

Figure 13.3 – A detailed memory diagram of three integer variables and a pointer variable with values

Figure 13.3 – A detailed memory diagram of three integer variables and a pointer variable with values

In the preceding diagram, the memory addresses do not match the output given previously because the picture was created at a different time with a different run of the program. Also, the addresses shown are 32-bit hexadecimal values. My computer is a 64-bit computer, so the addresses should include twice as many hex numbers for each address. For brevity, I've omitted the high 32-bit values.

As you study this, pay attention to the relative positions of the variables in memory. Your compiler may not order the variables in memory as mine has. Here, even though pDimension was declared last, it appears as the lowest memory address. Likewise, height, which was declared first, appears at a higher memory address. This is an important consideration – we cannot foresee exactly how the compiler will order variables in memory. It is for this reason that we must always use named memory locations (variables). Even though we declare variables in one way, we cannot guarantee the compiler will honor that order.

As an experiment, see what happens when you use %d everywhere in place of %2lu and %#lx when you compile your experiment (copy the program to another file first, and then experiment on the copy).

Understanding the void* type

There are times when the type of a pointer is not known. This occurs primarily in C library functions.

For this reason, the void* pointer type represents a generic, as yet unspecified pointer – in other words, a pointer whose type is not known at declaration. Any pointer type can be assigned to a pointer variable of the void* type. However, before that pointer variable can be accessed, the type of the data being accessed must be specified through the use of a casting operation:

void* aPtr = NULL;  // we don't yet know what it points to.
...
aPtr = &height;     // it has the address of height, but no 
                    // type yet.
...
int h = *(int*)aPtr; // with casting, we can now go to that
                     // address and fetch an integer value.

In the first statement of the preceding code block, we can see how aPtr is declared as a pointer, but we don't yet know its type or what it points to. In the next statement, aPtr is given the address of height, but we still don't know the type of the object that aPtr points to. You might think that the type could be inferred from the named variable, but C is not that smart – in other words, the compiler does not keep that kind of information about variables around for use at runtime. In the last statement, there is quite a bit going on. First, we must cast void* aPtr to int* aPtr. Having done that, we can then get the target of the integer pointer to get the value of height, which is the correct number of bytes for int. Casting tells the compiler exactly how many bytes to fetch and exactly how to interpret those bytes.

A more explicit way to express the same operation is through the use of an added ( ), as follows:

int h = *( (int*)aPtr );

Now, we can see that h is being assigned *(...) (dereferencing the pointer within (...)) and that an integer pointer is determined by (int*)aPtr, which casts the aPtr void pointer to a pointer and then to an integer. Once aPtr is cast to a type, it can be dereferenced.

Let's explore this with a simple program. The following code section uses a void pointer to point to an integer type:

int main( void)  {
  int height = 10;
  void* aPtr = NULL;
  aPtr       = &height;]
  int h = *(int*)aPtr;
  printf( "             height = [%d]
" , height );
  printf( "        *(int*)aPtr = [%d]
" , *(int*)aPtr );
  printf( "                  h = [%d]
" , h );
  printf( "   sizeof( height ) = %lu
" , sizeof( height ) );
  printf( "sizeof(*(int*)aPtr) = %lu
" , sizeof( *(int*)aPtr ) );
  *(int*)aPtr = 3;
  printf( "        *(int*)aPtr = [%d]
" , *(int*)aPtr );
  printf( "             height = [%d]

" , height );
  return 0;
}

First, a height integer variable is declared and initialized. Then, a void pointer, aPtr, is declared and given the address of height, which is now the target of aPtr. At this point, aPtr has a value but does not fully have a type. In the next line, aPtr is cast as a pointer to an integer and dereferenced so that the value at the target of aPtr is then assigned to the h integer. We use this same notation to print out the values of height, *aPtr, and h, as well as the size of each of these targets. The targets are one and the same, but we access them both directly and indirectly. The next line shows how to use the same syntax to set the value of the target to 3, and we show that target's value both directly and indirectly.

Create a voidPointerCasting.c filename and enter the preceding code segment. Remember to include stdio.h so that printf() is available to be called. Compile and run the program. An expanded version of this program can be found in the repository, which also uses aPtr for double and char. It gives the following output:

Figure 13.4 – A screenshot of the voidPointerCasting.c output

Figure 13.4 – A screenshot of the voidPointerCasting.c output

You might want to try copying the preceding code segment two more times in your program and then modify each copied segment, one for double and the other for char.

Coming back to NULL, we can see that 0 – by default, interpreted as an integer – is cast to the generic pointer type. So, the 0 integer is cast as a pointer to the 0 byte address and is named NULL. There is no value at the 0 address, ever. So, NULL becomes a useful way to set a pointer to a known value but an inconsequential and unusable target.

We can then assign NULL to any pointer, as follows:

int* pDimension = NULL;
...
pDimension = &height;
...
pDimenion = NULL;

First, pDimension is both declared as a pointer to an integer and initialized to NULL. Then, pDimension is given the address of height. Finally, pDimension is reset to NULL.

The reason for doing this will become obvious when we explore the comparison of pointer values.

Pointer arithmetic

Even though pointers are integers, only certain arithmetic operations are possible. Be aware that adding values to pointers is not quite the same as adding values to integers. So, adding 1 to an integer value gives us the next integer value – for example, 9 + 1 = 10. However, adding 1 to a pointer increases the pointer value by the value multiplied by the size of bytes of the pointer's target type. Using the preceding diagram, adding 1 to pDimension actually adds 4 to the address of pDimension because 4 equals sizeof(int). So, if pDimension = 0x328d2720, then pDimension + 1 = 0x328d2724.

Pointer arithmetic actually only makes sense in the context of arrays. We will discuss pointer arithmetic in greater detail in Chapter 14Understanding Arrays and Pointers.

Comparing pointers

As we stated earlier, we never concern ourselves about the specific value of a pointer. However, we can carry out comparison operations on pointers for the following:

  • Is a pointer equal or not equal to NULL?
  • Is a pointer equal to or not equal to a named location?
  • Is one pointer equal or not equal to another pointer?

In each case, we can either check for equality (==) or inequality (!=). Because we can never be certain of the variable ordering in memory, it makes no sense whatsoever to test whether one pointer is greater than (>) or less than (<) another pointer.

If we consistently apply the guideline to always assign a value to a pointer, even if that value is NULL, we can then make the following comparisons:

if( pDimension == NULL ) printf("pDimension points to nothing!
");
if( pDimension != NULL ) printf("pDimension points to something!
");

The first comparison checks whether pDimension points to NULL, which implies it has not yet been given a valid address or that it has been reset to NULL. The second comparison checks whether pDimension has any other value than NULL. Note that this does not necessarily that mean pDimension has a valid address; it only means that it is not NULL. If we've been consistent in always initializing or resetting our pointer to NULL, then we can be a bit more certain that pDimension does have a valid address.

Both of the preceding comparisons can be shortened to the following:

if( !pDimension ) printf( "pDimension points to nothing!
" );
if( pDimension ) printf( "pDimension points to something!
" );

If pDimension has a non NULL value alone in a comparison expression, it will evaluate to TRUE. If pDimension has a NULL value in a comparison expression, it will evaluate to FALSE. This is not what we want in the first comparison, since NULL will evaluate to 0 or FALSE, so we have to apply the not operator (!) to switch the comparison evaluation to match the condition we want.

Both comparison methods are commonly used. I prefer the more explicit form given in the first example because the intention is very clear. In the second example, ! may be overlooked or even misconstrued.

The comparisons of a pointer to a named location will look as follows:

if( pDimension == &height ) 
  printf( "pDimension points to height.
" );
if( pDimension != &height ) 
  printf( "pDimension does not point to height!
" );

The first comparison will evaluate to TRUE if pDimension points exactly to the address of height. If pDimension has any other value, even NULL, it will evaluate to FALSE. The second comparison will evaluate to FALSE if pDimension points exactly to the address of height; otherwise, if pDimension has any other value, even NULL, it will evaluate to TRUE.

The comparisons of one pointer to another will look as follows:

int* pDim1 = NULL;
int* pDim2 = NULL;
...
pDim1 = &height;
pDim1 = pDim2;
...
pDim2 = & weight;
...
if( pDim1 == pDim2 ) 
  printf( "pDim1 points to the same location as pDim2.
" );
...
if( pDim != pDim2 ) 
  printf( "pDim1 and pDim2 are different locations.
" );

We have now declared two pointers – pDim1 and pDim2 – initializing them to NULL. Later on, we make height the target of pDim1. Then, we give pDim2 the same target as pDim1. At this point, both pointers have the same target. 

Later, we assign weight as the target of pDim2. The first comparison will succeed if both pDim1 and pDim2 have exactly the same target, or if both are NULL. The second comparison will fail if both pDim1 and pDim2 have exactly the same target, or if both are NULL.

As an aside, when two pointers have the same target – in this case, height – we can assign or access the value of height directly with height by dereferencing pDim1 or by dereferencing pDim2. Each method changes the value at the height named location.

It is one thing to see C pointer syntax in code, but it is quite another to have a clear mental vision of pointer operations in your mind's eye. With explicit verbalization, we can verbally differentiate one pointer operation from another. So, let's explore how we talk about pointer syntax.

Verbalizing pointer operations

In this chapter, we have seen a variety of ways to describe pointers, what they point to, and how they are used. We will now turn our attention to how to verbalize various aspects of pointers. If we can consistently and clearly talk about pointers to ourselves, then we'll have a firmer grasp on how they operate and what we're actually doing with them.

Important Note

Talking correctly leads to thinking clearly.

The following table shows some actions that we might carry out with pointers, the C syntax for each action (what we see in code), and, lastly, how to mentally verbalize that action:

Verbalizing each of these in this manner may take some practice or simply consistent repetition.

So, we have now covered the most basic mechanics of pointers – declaring them, assigning them, accessing them, comparing them, and even talking about them. The examples given have focused on these basic mechanics. You may have already come to the conclusion that you would never need to use pointers in the manner that they have been shown. You would be correct in drawing that conclusion. However, having covered these basic mechanics, we are now ready to put pointers to good use.

For the remainder of this chapter, we'll look at how to use pointers as function parameters and, subsequently, in the function body. After that, we'll expand on pointers to variables to include pointers to structures and then use them in function parameters.

Variable function arguments

As we saw in Chapter 2Understanding Program Structure, function parameters in C are call by value. In other words, when a function is defined to take parameters, the values that the function body receives through them are copies of the values given at the function call. The following code copies the values of two values into function parameters so that the function can use those values in its function body:

double RectPerimeter( double h , double w )  {
  h += 10.0;
  w += 10.0;
  return 2*(w + h) ;
}
int main( void )  {
  double height = 15.0;
  double width  = 22.5;
  double perimeter = RectPerimeter( height , width );
}

In this simple example, the RectPerimeter() function takes two parameters – h and w – and returns a value that is based on both of them – the perimeter of the rectangle. When RectPerimeter() is called, the h and w function variables are created, and the values of height and width are assigned to them so that h has a copy of the value of height and w has a copy of the value of width. In the function body, the values of h and w are modified and then used to calculate the return value. When the function returns, h and w are deallocated (or thrown away), but the values of height and width remain unchanged.

This is how call by value works. One advantage of call by value is that we can modify the copies of values passed into the function and the original values remain unchanged. One disadvantage of call by value is that for parameters that are very large arrays or structures, this copying is significantly inefficient and may even cause the program to crash.

But what if we wanted to change the values in the original variables?

We could contrive a structure to hold all three values, as well as copying in and then copying back that structure. This will involve the following code:

typedef struct _RectDimensions  {
 double height;
   double width;
   double perimeter;
} RectDimensions;
RectDimensions RectPerimeter( RectDimensions rd )  {
  rd.height += 10.0;
   rd.width += 10.0;
   rd.perimeter = 2*(rd.height*rd.width);
   return rd ;
}
int main( void )  {
   RectDimensions rd;
   rd.height = 15.0;
   rd.width = 22.5;
  rd = RectPerimeter( rd );
}

However, that is quite cumbersome. It is also unnecessary. There is a better way of doing this with pointers, which we will explore in the last section of this chapter, Using pointers to structures.

Passing values by reference

If we wanted to change the values of parameters so that they are also changed after the function returns, we can use pointers to do so. We will assign the address of the values that we want to modify to pointer variables and then pass the pointer variables into the function. The addresses (which can't change anyway) will be copied into the function body, and we can dereference them to get the values we want. This is called passing by reference. We will modify our program as follows:

double RectPerimeter( double* pH , double *pW )  {
   *pH += 10.0;
   *pW += 10.0;
   return 2*( *pW + *pH ) ;
}
int main( void )  {
   double  height = 15.0;
   double  width  = 22.5;
   double* pHeight = &height;
   double* pWidth  = &width;
   double  perimeter = RectPerimeter( pHeight , pWidth );
}

The RectPerimeter() function now takes two pointers – pH and pW. When the function is called, pH and pW are created, and the values of pHeight and pWidth are assigned to each. pH has the same target as pHeight, while pW has the same target as pWidth. To use the desired values, pointers are dereferenced and 10.0 is added to each. Here is an example where the *pH += 10.0; shorthand comes in handy; recall that it is equivalent to *pH = *pH + 10.0;.

When the function call is complete, height now has the 25.0 value and width has the 32.5 value. You might want to verify this yourself with a similar program, but one that prints out the values of height and width both before and after the function call in main().

Any function that modifies values that exist outside of its function body is said to have side effects. In this case, these side effects were intended. However, in many cases, side effects may cause unanticipated consequences and so should be employed with careful intention and caution.

Let's return to the earlier program that we looked at in this chapter that dealt with heightwidth, and length. As you examine it, you might think that the printing part of the program is quite messy, and there might be a way to create a cleaner solution. We can create a function that takes two pointer parameters but does not have any side effects (the pointer targets are not modified in the function body).  

Copy the pointers1.c file into pointers2.c and modify it as follows:

  1. Add the following two functions (after #include <stdio.h> and before int main()):

    void showInfo( int height, int width , int length )  {

      printf( "  sizeof(int)  = %2lu " , sizeof(int) );

      printf( "  sizeof(int*) = %2lu " , sizeof(int*) );

      printf( "  [height, width, length] = [%2d,%2d,%2d] " , height , width , length );

    }

    void showVariable( char* pId , int* pDim )  {

      printf( "      address of %s = %#lx, value at address = %2d " , pId, (unsigned long)pDim , *pDim );

    }

  2. The body of our main() function should now look as follows:

    int height = 10;

      int width  = 20;

      int length = 40;

      int*  pDimension  = NULL;

      char* pIdentifier = NULL;

      printf( " Values: ");

      showInfo( height , width , length );

      printf( "  address of pDimension = %#lx " ,

               (unsigned long)&pDimension  );

      printf( " Using address of each named variables... ");

      pIdentifier = "height";

      pDimension = &height;

      showVariable( pIdentifier , pDimension );

      pIdentifier = "width ";

      pDimension = &width;

      showVariable( pIdentifier , pDimension );

      pIdentifier = "length";

      pDimension = &length;

      showVariable( pIdentifier , pDimension );

So, we move the messy bits of printf() into showInfo() and showVariable(). The showInfo() function simply uses call by value for each of the variables we want to show. This is what we did before.

The interesting part is making the two parameters to the showVariable() pointers – one a pointer to char (the target of this will be a string name of the variable's identifier) and the other a pointer to int (the target of this will be the value of the variable itself). At each call to showVariable(), we provide a pointer to the variable's identifier and a pointer to the variable's location. We will explore the relationship between pointers and strings in Chapter 15Working with Strings

Save, compile, and run this program. Your output should be exactly like that given before:

Figure 13.5 – A screenshot of the pointers2.c output

Figure 13.5 – A screenshot of the pointers2.c output

We can see both the sizes of int and a pointer to int, as well as the values stored at the heightwidth, and length named locations. Next, we can see both the addresses of each variable and, to confirm the correctness of our pointers, the values stored at those addresses. They correctly correlate to the values in our named variables. Note how each address is offset by 4 bytes; this, by no coincidence, is the size of int

Passing addresses to functions without pointer variables 

We can actually go one step further and remove the pointer variables in main() and then pass the desired addresses directly in each function call. We'll still need the pointer variables as function parameters in the function definition, just not in the main() function body.

Copy pointers2.c to pointers3.c and modify only the body of main(), as follows:

  int height = 10;
  int width  = 20;
  int length = 40;
  
  printf( "
Values:

");
  showInfo( height , width , length );
  
  printf( "
Using address of each named variables...

");
  showVariable( "height" , &height );
  showVariable( "width " , &width );
  showVariable( "length" , &length );

The showInfo() and showVariables() functions do not change. You'll also have to remove the printf() statement that prints info about pDimension. Save, compile, and run the program. As before, the output should be similar to earlier versions of this program, except we no longer see information about pDimension since it now doesn't exist:

Figure 13.6 – A screenshot of the pointers3.c output

Figure 13.6 – A screenshot of the pointers3.c output

You may notice that the variable addresses have changed since we removed pDimension. In this case, as you can see, they have not changed.

Pointers to pointers

If we can have a pointer that points to a variable, it should come as no surprise that we can have a pointer that points to another pointer, which then points to our desired variable. This is called double indirection. When using a pointer to a pointer to a variable, we must doubly dereference our starting pointer to get to the desired value. Why might we need to do that? 

Consider the following snippet in pointers2.c

  printf( "  address of pDimension = %#lx
" ,
           (unsigned long)&pDimension  );

Now, you might have observed that we didn't move this code snippet into the showInfo() function. This is because if we passed pDimension into the function as a parameter, a new temporary variable will be created and the value of pDimension will be copied into it. We will thus see the address of the function variable, which will be different from the location of pDimension. We can move it into showInfo(), but we will need to use a bit more trickery with pointers.

To show the value of the pointer itself when we pass it into a function, we have to create a pointer to the pointer. This is a pointer to a pointer to a named location, or, more precisely, it is a pointer variable that points to another pointer variable that points to a variable.

Since this is a somewhat advanced topic, rather than going into more detail, we'll see how this is done with an example. Copy pointers2.c into pointers4.c and make the following modifications:

  1. Change the showInfo() function definition. Your modifications to showInfo() should look as follows:

    {

    printf( " sizeof(int) = %2lu " , sizeof(int) );

    printf( " sizeof(int) = %2lu " , sizeof(int) );

    printf( " [height, width, length] = [%2d,%2d,%2d] " ,

    height , width , length );

    printf( " address of pDimension = %#lx " ,

               (unsigned long)ppDim );

    }

  2. Change how showInfo() is called from the main() function.
  3. Remove the printf() statement for pDimension in the main() function after the call to showInfo(). The modifications to the main() function should look as follows:

    int* pDimension = NULL;

    int** ppDimension = &pDimension;

    char* pIdentifier = NULL;

    printf( " Values: ");

    showInfo( height , width , length , ppDimension );

Save, compile, and run pointers4.c. Your output should look identical to that of pointers2.c:

Figure 13.7 – A screenshot of the pointers4.c output

Figure 13.7 – A screenshot of the pointers4.c output

If this makes your head spin, don't worry. It has done the same to many C programmers, just as it did to me. Double indirection is an advanced topic and is only included here for future reference. We will only use double indirection sparingly in upcoming programs, if at all.

Using pointers to structures

Before we finish with pointers, we need to expand the concept of a pointer pointing to a variable of an intrinsic type to that of a pointer pointing to a structure. We can then also expand the typedef specifiers to structures to include the typedef-defined pointers to structures.

Recall that a pointer points to the first byte of a target data type. We explored pointers to intrinsic types in Chapter 3Working with Basic Data Types. Also, recall that a structure is a named location that holds a collection of named values. The structure as a whole is named, as are each of the member elements of that structure. 

Once the structure type is defined, variables may be declared that are of that type. When a variable of any type is declared, the appropriate number of bytes is allocated in memory to store the values of that type. We can then access the member's structure elements directly via the structure variable's name and the . notation.

Declaring a pointer to a structure variable is no different than declaring a pointer to any other variable. The variable must already have been declared (that is, allocated). The pointer address is the first byte allocated to the structure, just as for any other variable.

For this exploration, we'll use a Date structure type, representing the numerical day, month, and year. It is defined as follows:

typedef struct {
   int day;
   int month;
   int year;
} Date;

We can then declare variables of that type, as follows:

Date anniversary;

We can then assign values to anniversary, as follows:

anniversary.month = 8;
anniversary.day   = 18;
anniversary.year  = 1990;

Now, we can declare a pointer to this structure variable, as follows:

Date* pAnniversary = &anniversary;

At this point, pAnniversary points to the structure variable as a whole, very much like other variables. Unlike intrinsic variables, we are not just interested in the structure as a whole; we are more interested in each of the structure variable's elements.

Accessing structures and their elements via pointers

We access the structure as a whole as we did with intrinsic variables so that *pAnniversary and anniversary refer to the same memory location.

To access one of the anniversary elements via the pointer, we might consider using *pAnniversary.month. However, because the. operator has higher precedence than the * operator, the element reference will fail evaluation and will be inaccessible. We can change the evaluation order with parentheses, as follows:

(*pAnniversary).day     <--  anniversary.day;
(*pAnniversary).month   <--  anniversary.month;
(*pAnniversary).year    <--  anniversary.year;

Because accessing structure elements via pointers is quite common, an alternative syntax to access structure elements via pointers is available. This is done using the --> operator and appears as follows:

pAnniversary->day     <--  (*pAnniversary).day;
pAnniversary->month   <--  (*pAnniversary).month;
pAnniversary->year    <--  (*pAnniversary).year;

The alternative syntax uses two fewer characters, which is a slight improvement. Whether you find one method easier to read than the other, the general advice is to pick one and use it consistently.

Using pointers to structures in functions

Now that we can use an indirect reference (a pointer) to a structure variable as easily as we can with a direct reference (a variable identifier) to a structure variable, we can use the indirect reference in function parameters to avoid the unnecessary copying of structures to temporary function variables. We can use the structure pointer in function parameters, as follows:

void printDate( Date* pDate );

We declare the pointer to the structure type in the function declaration. We then define the function, accessing each element, as follows:

void printDate( Date* pDate )  {
   int m, d , y;
   m = pDate->month;
   d = pDate->day;
   y = pDate->year;
   printf( "%4d-%2d-%2d
" , y , m , d );
// or
   printf( %4d-%2d-%2d
" , pDate->year , pDate->month , 
     pDate->day );
}

In the definition of printDate(), we can create local variables and assign the dereferenced pointer values to them, or we can just use the dereferenced pointer values without creating and assigning temporary variables.

We will then call printDate(), as follows:

Date anniversary = { 18 , 8 , 1990 };
Date* pAnniversary = &anniversary;
printDate( pAnniversary );
// or
printDate( &anniversary );

As we saw earlier, we can call printDate() using a pointer variable, pAnniversary, or by using the &anniversary variable reference, without using a pointer variable.

Returning to the RectDimension structure shown in an earlier section of this chapter, we can eliminate the need to copy the structure into the function and copy it back as a return value by using a pointer, as follows:

typedef struct _RectDimensions  {
   double height;
   double width;
   double perimeter;
} RectDimensions;
void CalculateRectPerimeter( RectDimensions* pRD )  {
   pRD->height += 10.0;
   pRD->width += 10.0;
   pRD->perimeter = 2*(pRD->height * pRD->width);
}
int main( void )  {
   RectDimensions rd;
   rd.height = 15.0;
   rd.width = 22.5;
   CalculateRectPerimeter( &rd );
}

In the main() function, rd is declared (allocated) and given initial values. A pointer to this structure is then passed into the CalculateRectPerimeter() function, thereby making a copy of the pointer value, not the structure, to be used in the function body. The pointer value is then used to access and manipulate the structure referenced by the pointer.

Summary

In this chapter, we learned how a pointer is a variable that points to or references a value at a named location. To use a pointer, we have learned that we must know, either through definition or through casting, the type of the pointer's target, as well as the address of the target. Pointers should always be initialized to a named location or set to NULL. We have explored the relatively few operations on pointers – assignment, access (dereference), and comparison. We will later explore pointer arithmetic. We have also extended the idea of pointers to variables to include pointers to structures and their elements. We have also seen how we can use pointers to provide greater flexibility in passing and manipulating function parameters.

This chapter is essential to understanding the next chapter, Chapter 14Understanding Arrays and Pointers, where we will extend our concepts of using pointers to arrays. We will see how to access and traverse arrays with pointers. Remember that an array is also a named location that holds a collection of unnamed values, all of the same type. The array as a whole has an identifier, but each of its elements is unnamed; they are relative to the array's name plus an offset. As we will see, these array concepts dovetail nicely with and extend our existing concepts of pointers.

Questions

  1. Does a pointer variable have a value? If so, what is that value?
  2. Does a pointer have a type?
  3. What is the target of a pointer?
  4. When a variable is passed via a function parameter, what is copied? When a pointer is passed via a function parameter, what is copied?
  5. What is dereferencing a pointer?
  6. What is addressable via a pointer? What is not?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.100.48