C++ Pointers and Memory

What is a pointer?

A pointer is a variable that only stores the memory address of another variable. It points to something else. Pointers are often abbreviated as ptr. If you read someone’s code and see ptr, it means that it’s a pointer variable.

Why use pointers?

Let’s say you have a huge object in memory. Instead of making an exact copy of it, which would double the amount of memory required, you might just want to use a pointer instead. You might also want the ability to manually allocate and deallocate memory, which you can do with pointers.

Another reason to use a pointer is if you’re using C and have to use a pointer instead of something else due to the limitations of the language. Another reason is something called pointer arithmetic, which will be explained later in this chapter.

That being said, I don’t advise using pointers. It’s good to at least know about them, but that’s about it.

Why don’t other languages have pointers?

Because they’re bad design. Pointers confuse students and lead software developers to make mistakes that can cause security issues, among other things. I don’t know anyone, aside from a single professor, who actually likes pointers. I think the pro-pointer professor I’m talking about might be biased because he teaches a class about abstract data types in C++. But it’s common for people to dislike them.

Memory safety vulnerabilities

A ZDnet article said that Microsoft presented their findings and said that 70% of security vulnerabilities are related to memory. That means stuff like new, delete, free, null, uninitialized stuff, and pointers (uninitialized, improperly initialized, dangling, null, etc). This is because a lot of Microsoft software is written in C or C++, which are not good for memory safety.

How to create a pointer:

int* numpointer;

The above is an uninitialized pointer. The * indicates that it’s a pointer.

Uninitialized pointers can be potentially dangerous. That’s why it’s best to initialize them when you make them. They can lead to security or stability issues.

Here is how you can assign a pointer to the memory address of another variable:

#include <iostream>

using namespace std;

int main() {

int mynum = 123;

int* numpointer = &mynum;

return 0;

}

Address of (&some_variable):

In the above example, int* means numpointer is a pointer that points to an int variable. But & in &mynum is the “address of” operator, meaning it will set the value of the numpointer pointer variable to the address of mynum, rather than the literal value of mynum.

Dereference: so what do you do if you want to use the pointer?

Would you use it like any other variable, like in the below example?

#include <iostream>

using namespace std;

int main() {

int mynum = 123;

int* numpointer = &mynum;

cout << numpointer << endl;

return 0;

}

Here is the output of the above code:

0x62ff08

It shows a memory address, which is not the 123 value of mynum. numpointer points to mynum, but if you reference it directly, it will only show the memory address of mynum. So if you want to see the actual value it points to, you need to do something called pointer dereferencing, like so:

#include <iostream>

using namespace std;

int main() {

int mynum = 123;

int* numpointer = &mynum;

cout << *numpointer << endl;

return 0;

}

The output of the above code is:

123

Where it is vs. what it is

*numpointer means “use the contents of the variable that numpointer’s memory address refers to,” while numpointer means “use the memory address of the variable that numpointer points to.”

Just like how you can remember classes vs. interface with the is-a vs. has-a relationship, here’s an easy way to remember what a pointer means in a given context:

Where it is: using a pointer directly (i.e. numpointer)

“numpointer points to something that is located at the memory address 0x62ff08”

What it is: dereferencing a pointer (i.e. *numpointer)

“The thing *numpointer is pointing to has a value of 123”

Arrays and pointer arithmetic:

An array is a contiguous block of memory. If you have an array of 10 ints, and an int is 4 bytes long, that means an array is 40 bytes, all next to each other in memory. So if you know the memory address of one int within the array, you can use arithmetic to find the other addresses too.

#include <iostream>

using namespace std;

int main() {

int arrayExample[] = {23, 76, 122, 55, 97};

int* test = &arrayExample[0];

cout << “Address of element 0: “;

cout << test << endl;

cout << “Address of element 1: “;

cout << test + 1 << endl;

cout << “Value of element 1: “;

cout << *(test + 1) << endl;

return 0;

}

In the above example, an int array is declared and initialized. It has a size of 5, meaning the indices are 0-4. Index 0 is 23.

A pointer is created, called test, which is set to the address of the zeroth element in the integer array. Then test is printed, but not dereferenced, meaning it will print out the memory address of element 0.

Adding 1 to test will actually add the value of the size of the data type. Because an int in this case is 4 bytes, test + 1 is the memory address of test + 1 * the size of an int, so 1*4. You can verify that by viewing the output when the program is run.

Lastly, test + 1 is dereferenced, meaning it will cout the value of element 1.

Here is the output of the above program:

Address of element 0: 0x62fef8

Address of element 1: 0x62fefc

Value of element 1: 76

Process finished with exit code 0

9 comes after 8, but you see that test + 1 is not 8+1. It’s 8+4. You can verify this by checking hexadecimal numbers:

Hex goes 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F to represent the values of 0-15, because hexadecimal is base 16. Just like how decimal is base 10 but only represents values 0-9 in a single digit place.

If you move 4 places over from 8, you get C, which is basically a single-digit representation of 12 in hexadecimal. But we added 1 to test, not 4. But it means move one place over, given the size of the data type.

Using a pointer by its name with no additional syntax only shows the address. But in the last part of the program, *(test + 1) means take the value of test, add 1 to it (really 4 bytes), and then dereference the entire thing. So it outputs 76, which is the value of arrayExample[1].

Uninitialized pointers

Uninitialized pointers are very bad, but they can still exist. That’s C++ in a nutshell: “you should never do this, but C++ will still let you do it anyway.”

Here is an example of an uninitialized pointer:

#include <iostream>

using namespace std;

int main() {

int* test;

cout << test << endl;

return 0;

}

And here is the output of the above program for me (but it can be different for you, as it’s not predictable):

0x401d1b

There was nothing in the code that defined a memory address for it to point to. But as you can see, the output clearly shows that it has a memory address to point to, but it’s not something you defined.

Being uninitialized means it will keep whatever random garbage was in RAM when it was created, rather than setting it to an initialized value. Because it can be anything, an uninitialized pointer can be dangerous because it can point to something that it shouldn’t point to.

So what can you do instead? Use a null pointer.

Null pointers

What is a null pointer?

Null means nothing. It’s not the same as zero, because zero is still something. Null is not the same as being uninitialized, because there will be an unpredictable value that exists with an uninitialized pointer. A pointer is a variable that contains the memory address of something else. A null pointer is a pointer that does not point to anything, not even a random uninitialized address.

How do you use a null pointer?

Here is how you can create a null pointer:

#include <iostream>

using namespace std;

int main() {

int* test = NULL;

return 0;

}

Why would you want to use a null pointer?

You’d want to use it so that it won’t be uninitialized. A null pointer is initialized to null, and you can use if/else to check if a pointer is null so that you don’t mess things up by using a null pointer.

If you want to declare a pointer before it’s initialized, then you’d use a null pointer. Uninitialized pointers are more dangerous, and it’s hard to tell if a pointer was uninitialized or just initialized to some legitimate memory address. By contrast, it’s very easy to see if a pointer is null.

Here’s how you can check if a pointer is null or not:

#include <iostream>

using namespace std;

int main() {

int* test = NULL;

if (test == NULL) {

cout << “null pointer” << endl;

} else {

cout << “pointer memory address is : ” << test << endl;

cout << “pointer value is ” << *test << endl;

}

return 0;

}

Here’s another way to do the same thing:

#include <iostream>

using namespace std;

int main() {

int* test = NULL;

if (!test) {

cout << “null pointer” << endl;

} else {

cout << “pointer memory address is : ” << test << endl;

cout << “pointer value is ” << *test << endl;

}

return 0;

}

! means not. It means if not pointer. If a pointer is null, it will evaluate to false. If a pointer has a memory address, whether specifically initialized or just containing uninitialized memory that has random values in it, then it will evaluate to true.

What are some problems associated with null pointers?

Null pointer dereferencing. You’re not supposed to dereference null pointers. Instead, you should surround pointers with conditionals to check if they’re null or not, and only after that, you will proceed with things like dereferencing.

Null pointer dereference:

Here’s an example of null pointer dereferencing (which you should avoid doing):

#include <iostream>

using namespace std;

int main() {

int* test = NULL;

cout << *test << endl;

cout << “hello” << endl;

return 0;

}

Here is the output of the above program:

Process finished with exit code -1073741819 (0xC0000005)

As you can see, the program exited with a non-zero return value, meaning it encountered an error. And it never ran the lines after the null pointer dereference line. It didn’t print “hello” because it crashed before it could reach that line. A null pointer dereference will crash your program and nothing after the null pointer dereference will happen.

Note that your IDE or compiler will not complain if you save and compile code that dereferences a null pointer. If this were Java instead of C++, the compiler would tell you that it’s a mistake and you can’t compile it. But because C++ is more hands-off, it won’t give you an error from the compiler, and instead will compile and then only experience the issue when running. It won’t even necessarily give you an error message, and the only way you’ll know there’s an error is with the return value from the main function. This makes C++ debugging a little harder than in newer programming languages.

Unfortunately, C++ does not have an easy way to perform exception handling for null pointer dereferencing. However, you can try to avoid undefined behavior from null pointer dereferencing by using if/else.

Null pointer vs. null:

In the previous examples, I assigned a null pointer the value of NULL. However, that’s not the best way to do it. There is a special keyword in C++ called nullptr. You should initialize a null pointer to nullptr, such as in the example below:

#include <iostream>

using namespace std;

int main() {

int* test = nullptr;

return 0;

}

Long story short, NULL is NULL and can mean different things, but nullptr is null specifically for pointers. As such, it is better to initialize a null pointer to nullptr instead of NULL.

Dangling pointer:

A dangling pointer is a pointer that used to point to something, but now the thing it points to no longer exists because it was deleted. So it used to be valid, but now it’s problematic. It’s kind of like declaring and using a pointer without explicitly initializing it. Because the thing it points to is now gone, the pointer can point to any random thing now, which is bad. If you delete something, make sure you get rid of the pointers associated with it too, or at least set them to null and check if they’re null before using them.

When a dangling pointer is used after being freed but hasn’t been reallocated, it is called use-after-free. free() is more known for C than C++, but it’s a similar concept. C++ has the delete keyword instead.

Use-after-free vulnerabilities can be used for arbitrary code execution or even remote code execution.

Pointer vs. reference:

A pointer points to something. A reference also points to something. But a reference can’t be reassigned. A pointer can be assigned to a different memory address than the one it originally pointed to.

References are even seen in languages that don’t have pointers. References don’t need to be dereferenced. You can’t use a reference with a *. Pointers are potentially more versatile, but also more confusing and potentially problematic than references. In order to keep things simple, newer programming languages have kept references and gotten rid of pointers.

Pointers have pointer arithmetic, such as *(test + 1) example mentioned earlier in the book. You can’t do reference arithmetic in the same way. If you perform arithmetic on an int reference, it will merely change the value, not the memory address.

Pass-by-value, pass-by-reference, and pass-by-pointer:

Arguments can be passed to functions in many different ways, such as by value, by reference, or by pointer. How an argument is passed can change the behavior of a program.

Here is an example of pass-by-value:

#include <iostream>

using namespace std;

void valueTest(int byValue) {

byValue += 10;

}

int main() {

int passByValue = 5;

valueTest(passByValue);

cout << passByValue << endl;

return 0;

}

At a first glance, it might look like you’re taking an integer with a value of 5, then adding 10 to it, and then printing out the value of 5+10. But that’s not true. To pass an argument by value is to only take a copy of the value of the function argument, which means it doesn’t change the original at all.

So here is the actual output of the above pass-by-value demonstration:

5

Process finished with exit code 0

In arguments that are passed by value to a function, the original does not change at all. It’s still 5, regardless of what the function does to it.

Here’s an example of pass-by-reference, which actually will change what was passed to the function:

#include <iostream>

using namespace std;

void referenceTest(int &byRef) {

byRef += 10;

}

int main() {

int passByRef = 5;

referenceTest(passByRef);

cout << passByRef << endl;

return 0;

}

And here is the output of the above code:

15

Process finished with exit code 0

The way to tell the difference between pass-by-value and pass-by-reference is that passing by reference requires an ampersand in front of an argument name, such as int &byRef.

The following is an example of pass-by-pointer:

#include <iostream>

using namespace std;

void pointerTest(int* byPointer) {

byPointer += 10;

}

int main() {

int passByPointer = 5;

pointerTest(&passByPointer);

cout << passByPointer << endl;

return 0;

}

In the above example, the function has been changed to pass-by-pointer. That requires an int to use the address-of operator, such as &passByPointer.

The output of the above pass-by-pointer example is as follows:

5

Process finished with exit code 0

This is because the function did not dereference the pointer argument. When you use a pointer directly, such as byPointer, you are referring to the memory address, not the thing that actually resides at that memory address. Remember where it is vs. what it is.

If you change the pass-by-pointer example to use dereferencing within the body of the function, then the output will be different:

#include <iostream>

using namespace std;

void pointerTest(int* byPointer) {

*byPointer += 10;

}

int main() {

int passByPointer = 5;

pointerTest(&passByPointer);

cout << passByPointer << endl;

return 0;

}

In the above example, byPointer was changed to *byPointer. And here is the output of pass-by-pointer, using dereferencing within the function that was passed a pointer:

15

Process finished with exit code 0

Instantiation, automatic variables, pointers, and destructors:

There are many different ways to instantiate things in C++.

Here’s an example of using automatic variables, with no need to use new or delete keywords in order to allocate and deallocate memory for them, as they are automatically deallocated when they are no longer in scope:

#include <iostream>

using namespace std;

class Person{

protected:

string firstName;

string lastName;

public:

Person() {

firstName = “Default”;

lastName = “Default”;

}

Person(string first, string last) {

setFirst(first);

setLast(last);

}

~Person() {

cout << “deleted” << endl;

}

string getFirst() {

return firstName;

}

string getLast() {

return lastName;

}

void setFirst(string first) {

firstName = first;

}

void setLast(string last) {

lastName = last;

}

};

int main() {

Person example1;

Person example2(“Over”, “Loaded”);

cout << example1.getFirst() << endl;

cout << example2.getFirst() << endl;

return 0;

}

But what if you use pointers instead of automatic variables?

Keep everything else the same, but change the main function to the following:

int main() {

Person example1;

Person example2(“Over”, “Loaded”);

cout << example1.getFirst() << endl;

cout << example2.getFirst() << endl;

Person* example3 = new Person(“Pointer”, “McPointy”);

cout << example3->getFirst() << endl;

return 0;

}

There are three objects now, two of which are automatic, and one of which uses pointers. If you view the output of the program, you can see that the destructor only got called twice. It didn’t get called for the third object, which is a pointer. When you use pointers and the new keyword, you will need to manually use a delete line in order to explicitly call the destructor:

int main() {

Person example1;

Person example2(“Over”, “Loaded”);

cout << example1.getFirst() << endl;

cout << example2.getFirst() << endl;

Person* example3 = new Person(“Pointer”, “McPointy”);

cout << example3->getFirst() << endl;

delete example3;

return 0;

}

With automatic variables, you can use . to access member functions and variables. But to do the same thing with a pointer, you need to use -> instead.

Automatic variables:

A local variable that gets its memory allocated and deallocated automatically, with no need to do something like call a destructor using the delete keyword. This isn’t the same as garbage collection, because it’s based on scope rather than references. In a garbage-collected language, something will be garbage-collected if it no longer has a reference to it, but in C++, automatic variables are just based on scope, and not everything is an automatic variable. So memory leaks are still possible.

Do not confuse automatic memory allocation/deallocation with the auto keyword. auto is a keyword in C++ that means automatic type inference, kind of like the static type inference var keyword in newer versions of Java.

Dynamic memory:

Manual/dynamic memory allocation is achieved in C++ through the use of new and delete. The heap is associated with dynamic memory, and the stack is associated with local variables which come and go. If you use automatic variables in C++, they are put on the stack. If you use dynamic/manual memory allocation, they are put on the heap.

new:

To manually allocate memory for something on the heap, use the new keyword. new must be used with a pointer. new combines memory allocation with constructing an object.

delete:

delete corresponds with new. delete performs memory deallocation and object destruction.

Imagine the size of a store. It has enough space for a limited number of customers. When a customer comes in, they take up space in the store. The store only has a certain capacity. If customers stayed and never left the store, it would fill up pretty quickly and that wouldn’t be good. When a customer is done, they are expected to leave the store.

When you make something in memory, you only keep it in memory for as long as it needs to be there. If you use news with no deletes, it’s like a store where customers come in but never leave.

delete will call the destructor of a class. But if the class definition has no destructor, it will use the default destructor. delete can only be used with pointer variables, not with automatic variables.

Pointers, new, and delete all go together. When you use one, you should use the other ones too. You don’t always have to use new and delete with a pointer, but you always have to use all three if you use either new or delete.

Virtual destructor:

A destructor for a basic class with no hierarchy is fairly straightforward. But what if you add inheritance into the mix? You might want to use the virtual keyword with a base class destructor.

malloc(), free() and strcpy():

new and delete were made specifically for C++. They’re the best way to do dynamic memory allocation in C++. That being said, C++ still contains relics from C, such as malloc() and strcpy(). You can still use them within C++, but it’s not advisable.

While new allocates memory and creates an object, malloc() only allocates memory and does nothing else. free() deallocates memory. malloc() is more suitable for C, not C++. strcpy() copies a source char array to a destination char array. It’s a very awkward way of dealing with what are basically strings. C++ actually supports strings, which are easier to use than char arrays.

You can learn about malloc() and strcpy() if you want, but don’t bother using them in C++. I guess the only value they really provide is that they make you appreciate newer programming features like strings and the new and delete keywords more.

How memory is handled

When we refer to memory in programming, it usually means RAM, but in RAM, there are two main distinctions: the stack and the heap. When you make a variable, where does it go in RAM? When a function is run, where does that happen in RAM? That’s where the stack and heap come into play. Keep in mind that these definitions are specific to memory, not just the general data structures of stack and heap, which can be used in a wide variety of things that aren’t just about placing things in memory for a programming language.

Data structures are covered more in-depth in chapter 13’s sections 13.0 and 13.1.

Memory stack:

Variables for functions are stored on a stack. Because a stack data structure is LIFO (Last In, First Out), it lends itself well to how functions and nested functions work. Things in the stack don’t last. A more limited scope. Not all variables need to stick around forever, and they are only allocated RAM when they need it, and then they get popped off the stack and go away when they’re no longer needed.

Memory heap:

The heap is where more permanent things are stored in RAM. It’s not as permanent as saving to a file, but it’s more long-term than the stack.

Stack overflow:

Stacks have a certain size. When you put more stuff on the stack than it can fit, it results in a stack overflow, which is not good.

Heap overflow:

Writing over data in the heap when you’re not supposed to.

Stack overflows and heap overflows can lead to issues with stability or security.

What happens if a program tries to access another program’s RAM?

Long story short, it’s quite hard to accidentally do something like that. Modern operating systems use something called virtual addressing, which gives a virtual memory address space to a particular program that only it can use. The virtual addressing is the block of memory addresses that a program can use. Think of it like namespaces, but for RAM. The 0x401d1b address for one program might be different from the 0x401d1b of another program.

How can two programs talk to each other in RAM if they’re separated with virtual addressing?

Two programs talking to each other on the same computer is called interprocess communication, or IPC. This can be achieved with the use of message queues. However, that is beyond the scope of this book.

Address Space Layout Randomization (ASLR):

In order to make it harder for hackers to perform memory-related attacks, such as stack or heap overflows, an operating system might do something called address space layout randomization, or ASLR for short. It means randomizing memory addresses, so that things aren’t in a predictable place each and every time. Memory addresses are where things are located in RAM. If a hacker knew where important things were in RAM, and they were always at specific memory addresses, it would be easier to perform exploits or access private information. But because it’s different every time, it makes hacking more difficult.

Data vs. code:

Data is stuff that exists and shouldn’t be run as instructions. Code is stuff that gets executed, telling the computer to do things. Because all data and code are all represented the same way on a computer (1s and 0s), it can be hard to differentiate between the two. But for security purposes, software tries to distinguish between the two.

Data Execution Prevention (DEP):

DEP is one way an OS might try to stop someone from executing data. A JPEG should not be executable, as a photo is not code. A .jpg shouldn’t be executable, but a .exe can be (in some circumstances). There are some security exploits that relate to executing code in things that aren’t supposed to be executed though.

Will having DEP make your computer hack-proof? No. But it means there is one less way for hackers to exploit it. There might be a zillion different ways that a computer can be potentially hacked, and that means the software you rely on might need a zillion different mitigations to make it more secure. Security isn’t easy!

No Execute (NX) bit:

Marking whether something in RAM can be executed or not. Just like how you can have an executable file mode bit set for a file on a hard drive or SSD, an NX bit can tell whether data in memory can be executed or not.

Final thoughts on pointers and memory

You don’t have to manually do anything with the stack, heap, ASLR, DEP, or NX in your C++ code. But it’s good to be at least vaguely aware of these types of concepts anyway. C++ is a language where you really ought to know a lot about memory if you use it, considering that it doesn’t do a lot of automatic memory management and garbage collection the way that newer programming languages do. If that sounds like too much for you, then stick with languages like Java, Python, JavaScript, and PHP – they’re all useful languages that don’t require as much manual memory stuff.

I’ve heard people at my university say that pointers “separate the wheat from the chaff,” implying that it’s somehow a good thing that people are confused by it. I don’t agree with that at all. I think we should do away with pointers entirely.

← Previous | Next →

C++ Topic List

Main Topic List

Leave a Reply

Your email address will not be published. Required fields are marked *