Expert C++
上QQ阅读APP看书,第一时间看更新

Pointers

C++ is a unique language in the way that it provides access to low-level details such as addresses of variables. We can take the address of any variable declared in the program using the & operator as shown:

int answer = 42;
std::cout << &answer;

This code will output something similar to this:

0x7ffee1bd2adc

Notice the hexadecimal representation of the address. Although this value is just an integer, it is used to store in a special variable called a pointer. A pointer is just a variable that is able to store address values and supports the * operator (dereferencing), allowing us to find the actual value stored at the address.

For example, to store the address of the variable answer in the preceding example, we can declare a pointer and assign the address to it:

int* ptr = &answer;

The variable answer is declared as an int, which usually takes 4 bytes of memory space. We already agreed that each byte has its own unique address. Can we conclude that the answer variable has four unique addresses? Well, yes and no. It does acquire four distinct but contiguous memory bytes, but when the address operator is used against the variable, it returns the address of its first byte. Let's take a look at a portion of code that declares a couple of variables and then illustrate how they are placed in the memory:

int ivar = 26;
char ch = 't';
double d = 3.14;

The size of a data type is implementation-defined, though the C++ standard states the minimum supported range of values for each type. Let's suppose the implementation provides 4 bytes for an int, 8 bytes for a double, and 1 byte for char. The memory layout for the preceding code should look like this:

Pay attention to  ivar in the memory layout; it resides in four contiguous bytes.

Whenever we take the address of a variable, whether it resides in a single byte or more than one byte, we get the address of the first byte of the variable. If the size doesn't affect the logic behind the address operator, then why do we have to declare the type of the pointer? In order to store the address of ivar in the preceding example, we should declare the pointer as an int*:

int* ptr = &ivar;
char* pch = &ch;
double* pd = &d;

The preceding code is depicted in the following diagram:

Turns out, the type of the pointer is crucial in accessing the variable using that very pointer. C++ provides the dereferencing operator (the * symbol before the pointer name):

std::cout << *ptr; // prints 26

It basically works like this:

  1. Reads the contents of the pointer
  2. Finds the address of the memory cell that is equal to the address in the pointer
  3. Returns the value that is stored in that memory cell

The question is, what if the pointer points to the data that resides in more than one memory cell? That's where the type of the pointer comes in. When dereferencing the pointer, its type is used to determine how many bytes it should read and return starting from the memory cell that it points to.

Now that we know that a pointer stores the address of the first byte of the variable, we can actually read any byte of the variable by moving the pointer forward. We should remember that the address is just a number, so adding or subtracting another number from it will produce another address. What if we point to an integer variable with a char pointer?

int ivar = 26;
char* p = (char*)&ivar;

When we try to dereference the p pointer, it will return only the first byte of ivar

Now, if we want to move to the next byte of ivar, we add 1 to the char pointer:

// the first byte
*p;
// the second byte
*(p + 1);
// the third byte
*(p + 2);

// dangerous stuff, the previous byte
*(p - 1);

Take a look at the following diagram; it clearly shows how we access bytes of the ivar integer:

If you want to read the first or the last two bytes, you can use a short pointer:

short* sh = (short*)&ivar;
std::cout << *sh; // print the value in the first two bytes of ivar
std::cout << *(sh + 1); // print the value in the last two bytes of ivar
You should be careful with pointer arithmetics, as adding or subtracting a number will actually move the pointer by the defined size of the data type. Adding 1 to an int pointer will add sizeof(int) * 1 to the actual address.

What about the size of a pointer? As mentioned previously, a pointer is just a variable that is special in the way that it can store a memory address and provide a dereferencing operator that returns the data located at that address. So if the pointer is just a variable, it should reside in memory as well. We might consider that the size of a char pointer is less than the size of an int pointer just because the size of a char is less than the size of an int.

Here's the catch: the data that is stored in the pointer has nothing to do with the type of data the pointer points to. Both char and int pointers store the address of the variable, so to define the size of the pointer, we should consider the size of the address. The size of the address is defined by the system we work in. For example, in a 32-bit system, the address size is 32 bits long, and in a 64-bit system, the address size is 64 bits long. This leads us to a logical conclusion: the size of the pointer is the same regardless of the type of data it points to:

std::cout << sizeof(ptr) << " = " << sizeof(pch) << " = " << sizeof(pd);

It will output 4 = 4 = 4 in a 32-bit system, and 8 = 8 = 8 in a 64-bit system.