DEV Community

Jess
Jess

Posted on

C++ Pointers

Pointers are variables that hold memory addresses of data as opposed to the data itself. In other words, the pointer variable points to the location of the data.

The syntax to declare a pointer variable is dataType *identifier.

The * can be anywhere between the dataType and identifier.

int* p;
int * p;
int *p;
Enter fullscreen mode Exit fullscreen mode

In order to assign a value to a pointer, you need to use the address of operator: &.
The address of operator is a unary operator that returns the address of its operand.

For example, if you assign 10 to the integer variable num and output num prefixed with the address of operator, you will see the memory address storing num.

int num = 10;

std::cout << &num;

// output:
0x7ffeefbff538
Enter fullscreen mode Exit fullscreen mode

0x7ffeefbff538 is the memory address holding the value of num. The hexadecimal value represending the memory location will be different for you because it won't be storing the data in your machine at the same location as mine.

You can use a pointer variable to store the memory address:

    int num = 10;
    int *numPtr = &num;
Enter fullscreen mode Exit fullscreen mode

The pointer data type has to match the data type of the variable it's pointing to.

    double num = 10.75;
    double *numPtr = &num;

    char ch = 'A';
    char *chPtr = &ch;
Enter fullscreen mode Exit fullscreen mode

If you output the value of numPtr you would get the same memory address as outputting &num. In order to retrieve the value 10 you need to dereference the pointer variable. The dereferencing operator is the * (asterisk) like the one used to create a pointer.

    int num = 10;
    int *numPtr = &num;

    std::cout << *numPtr << std::endl; // 10
Enter fullscreen mode Exit fullscreen mode

Here is a comparison of outputs:

    int num = 10;
    int *numPtr = &num;

    std::cout << "&num: " << &num << std::endl;
    std::cout << "numPtr: " << numPtr << std::endl;
    std::cout << "&numPtr: " << &numPtr << std::endl;
    std::cout << "*numPtr: " << *numPtr << std::endl;

// output:
&num: 0x7ffeefbff538
numPtr: 0x7ffeefbff538
&numPtr: 0x7ffeefbff530
*numPtr: 10
Enter fullscreen mode Exit fullscreen mode

You can see that the value of numPtr is the memory address of num, printing the memory address of numPtr shows the memory address of the pointer variable, and dereferencing numPtr gives the value stored in num.

You can use pointers with classes and structs, too.

struct DogType
{
    string name;
    char breed;
    int age;
};

DogType dog;
DogType *dogPtr = &dog;


dog.name = "Penny";

std::cout << dog.name << std::endl; // Penny

(*dogPtr).name = "Pixel";

std::cout << dog.name << std::endl; // Pixel;
Enter fullscreen mode Exit fullscreen mode

You can see that in order to use the pointer to set a new value in the memory address the pointer is pointing to, you have to put parentheses around the derefercing of dogPtr. This is because, in C++, the dot operator . has a higher precedence than the dereferencing operator *. By using the parentheses, the dereferencing operator will evaluate first.

Thankfully there is an easier way to write this, using the *member access operator arrow ->

// (*dogPtr).name = "Pixel";
// becomes

dogPtr->name = "Pixel";
Enter fullscreen mode Exit fullscreen mode

So, why bother with any of this when you can just directly access/modify the variable that's being pointed to?

By using pointers, you can create dynamic variables, which are variables created during program execution. Dynamic variables are unnamed and must be accessed indirectly by pointers.

The keyword new is used to create dynamic variables by allocationg memory in the heap.

You can use new to allocate memory for a single variable or for an array of variables.

int *p = new int;

int size = 10;
int *pArray = new int[size];
pArray[0] = 1;
Enter fullscreen mode Exit fullscreen mode

The code below asks the user to press 1 to create a new dog. Each time a dog is created with the new operator, the memory address is stored in dogPtr.

    int input = -1;

    DogType *dogPtr;

    while(input != 2)
    {
        std::cout << "Press 1 to create a new dog: ";
        std::cin >> input;

        if (input == 1)
        {
            dogPtr = new DogType;
            dogPtr->name = getDogName();
            std::cout << "I'm a new dog and my name is " << dogPtr->name << endl;
            std::cout << "dogPtr is located at " << dogPtr << endl;
        }
    }

    return 0;
}

// output:
Press 1 to create a new dog: 1
I'm a new dog and my name is Elaine
dogPtr is located at 0x1006040e0

Press 1 to create a new dog: 1
I'm a new dog and my name is Murray
dogPtr is located at 0x1004045a0

Press 1 to create a new dog: 1
I'm a new dog and my name is Guybrush
dogPtr is located at 0x1004045c0

Enter fullscreen mode Exit fullscreen mode

As you can see, each time a new dog is created it's stored in a new memory address. An important thing to note is when you use new to allocate memory you need to use delete to deallocate that memory when you're done with it. Otherwise, when you point the pointer variable elsewhere, that initial memory space will still be taken and you'll have no way to access it. This is known as a memory leak.

If you delete an object in memory, but a pointer still points to that memory address, you end up with a dangling pointer, which could cause corrupted data or program termination.

nullptr

C++ doesn't initialize variables; therefore, pointer variables must be initialized to prevent them from accidentally pointing to some place in memory you didn't intend.

int *p = 0;

// is equivalent to

int *p = NULL;

Enter fullscreen mode Exit fullscreen mode

The C++11 standard introduced nullptr:

int *p = nullptr;
Enter fullscreen mode Exit fullscreen mode

Pointer Arithmetic

You can do some arithmetic on pointers for operations, but it doesn't work the same way as it does on data types such as int and double.

If you increase an int value by 1, it will increase by 1.

int num = 1;
num++;
std::cout << num << std::endl;


// output
2
Enter fullscreen mode Exit fullscreen mode

When you increase a pointer by 1, it increases by the size of the data type it's pointing to.

If you have an array the pointer will point to the memory address of the first index. Since this example is an array of integers and an int is 4 bytes, when you increase the pointer by 1 it will increase the memory address by the size of an int to point to the next index in the array.

int *p = new int[10];

cout << p << endl;

p++;

cout << p << endl;

// output
0x1030bd1c0
0x1030bd1c4
Enter fullscreen mode Exit fullscreen mode

You can see by the last digit of the output that the memory address increased by 4.

Pointer arithmetic can be dangerous because you could end up accessing the memory where other variables are stored and altering their values.

Further Reading / References

Top comments (2)

Collapse
 
pgradot profile image
Pierre Gradot • Edited

Hey!

Good job on this article :)

There is a point I would like to discuss:

So, why bother with any of this when you can just directly access/modify the variable that's being pointed to?

By using pointers, you can create dynamic variables

The main reason is because variables are passed by copy to functions in C++.

#include <iostream>
using std::cout;

void increase(int value) {
    value += 2;
}

int main() {
    int value = 2;
    cout << value << '\n';
    increase(value);
    cout << value << '\n';
}
Enter fullscreen mode Exit fullscreen mode

This code prints "2 2", not "2 4". This is why we need pointers. In fact, we need addresses. References work perfectly in such a situation:

void increase(int& value) {
    value += 2;
}
Enter fullscreen mode Exit fullscreen mode

By the way, with modern C++ (== C++11 and above), you should avoid calling new and delete by yourself and prefer using smart pointers:

#include <iostream>
#include <memory>
using std::cout;

struct Foo {
    Foo() { cout << "create" << '\n'; };  
    ~Foo() { cout << "delete" << '\n'; };
};

int main() {
    auto pointer = std::make_unique<Foo>();
}
Enter fullscreen mode Exit fullscreen mode

Output:

create
delete
Enter fullscreen mode Exit fullscreen mode

There are the best way to avoid memory leaks :)

Collapse
 
robotspacefish profile image
Jess

Thanks for the info, Pierre! :)