C++ Tutorial
Pointers I - 2020
Two of the major problems that plague C++ programmers are memory leaks and memory corruption. A memory leak occurs when memory is allocated but never freed.
This causes wasting memory, and eventually leads to a potentially fatal out-of-memory. A memory corruption occurs when the program writes data to the wrong memory location, overwriting the data that was there, and failing to update the intended location of memory. Both of these problems falls squarely on the pointer.
Though powerful tool, a pointer, can be a devil's advocate. If a pointer points to memory that has been freed, or if it is accidentally assigned a nonzero integer or floating point value, it becomes a dangerous way of corrupting memory, because data written through it can end up anywhere. Likewise, when pointers are used to keep track of allocated memory, it is very easy to forget to free the memory when it is no longer needed, and this leads to memory leak.
In the book "Accelerated C++", the authors summarized problems caused by pointers including memory leak as follows:
- Copying a pointer does not copy the corresponding object, leading to surprises if two pointers inadvertently points to the same object.
- Destroying a pointer does not destroy its object, leading to memory leaks.
- Destroying an object without destroying a pointer to it leads to a dangling pointer, which causes undefined behavior if the program uses the pointer.
- Creating a pointer without initializing it leaves the pointer unbound, which also causes undefined behavior if the program uses it. This uninitialized pointer is sometimes called wild pointer.
So, good coding practices are critical to avoid pointer-related memory problems. Nonetheless, we may get some help from the tools like IBM Purify or we can use a replacement of memory related library, http://dmalloc.com/.
- Un-initialized pointer operation - invalid access resulting with an attempt to read or write using a NULL pointer.
- Invalid array indexing - out of bound array indexing.
- Illegal stack operation - a program passes a pointer of the wrong type to a function.
- Accessing an illegal address.
- Infinite loop - invalid array indexing when the loop index exceeds the array bounds and corrupts memory.
- Invalid object pointer - invoking a method for an illegal object pointer.
- Corruption of the v-table pointer.
- Not checking for memory allocation failure.
- Check for new returning a NULL pointer.
- Use assert.
- Use stack trace.
- Use memory and array bound checking tools.
In this section, we'll dive into the example demonstrating the characteristics of pointers rather than describe what the pointer is and how we manipulate it. We'll look at the details of pointer in later sections.
Let's define a structure:
struct account { char* name; char id[8]; int balance; }
So, when we make a struct account object:
account Customer[4];
we are allocating memory for 4 Customer objects:
Let's assign some values to the members.
Customer[0].balance = 1500; Customer[2].name = strdup("Sam");
strdup("Sam") does dynamic memory allocation for the character array including the end character '\0' and returns the address of the heap memory.
What's the memory diagram looks like if we do the following?
Customer[3].name = Customer[0].id + 7;
When the compiler sees :
Customer[0].id + 7;
the number 7 really represents the hop count. The unit of the hop comes from the Customer[0].id which is the pointer to an array of character. So, hop here, becomes one byte. 7-byte offset from the base address is marked as blue in the picture and the Customer[3].name gets the address at Customer[0].id + 7.
Let's assign id for Customer[1] using strcpy():
strcpy(Customer[1].id, "1234567");
The strcpy() is copying characters one by one onto the allocated stack memory space.
How about the following line:
strcpy(Customer[3].name, "abcd");
The Customer[3].name is pointing to the Customer[0].id + 7 and we are assigning a constant character to that address. The memory diagram looks like this:
As we see in the picture, we put a character by character into the memory starting from Customer[0].id + 7 to 5-byte after that even overwriting an area which was previously allocated for 4-byte integer.
Things can get messy if we are doing what we're not supposed to do. Here, we are assigning a character to a location not allocated for us. No problem assigning it but if we have additional local variables, it will overwrite them and anything can happen later if we do this:
Customer[7].id[11] = 'A';
The compiler doesn't care, it just follows the rule of how we walk through the memory. So, it goes to the Customer[7].id[0] and move to 11-chararcter offset and assigns 'A' to that location. That's it.
For more detail info on pointers, please download the linked pdf: Pointers and Memory, Stanford_2008(pdf)
A pointer is a special variable that can contain/store an address of a value rather than store the value directly. Pointers give us the ability to work directly and efficiently with memory.
Here is the description of pointer from wiki:
In computer science, a pointer is a programming language data type whose value refers directly to (or "points to") another value stored elsewhere in the computer memory using its address. For high-level programming languages, pointers effectively take the place of general purpose registers in low-level languages such as assembly language or machine code, but may be in available memory.
A pointer references a location in memory, and obtaining the value at the location a pointer refers to is known as dereferencing the pointer. A pointer is a simple, less abstracted implementation of the more abstracted reference data type (although it is not as directly usable as a C++ reference). Several languages support some type of pointer, although some are more restricted than others.
Pointers to data significantly improve performance for repetitive operations such as traversing strings, lookup tables, control tables and tree structures. In particular, it is often much cheaper in time and space to copy and dereferences pointers than it is to copy and access the data to which the pointers point.
Pointers are also used to hold the addresses of entry points for called subroutines in procedural programming and for run-time linking to dynamic link libraries (DLLs). In Object-oriented programming, pointers to functions are used for binding methods, often using what are called virtual method tables.
Interesting pointers:
- Null pointers
- Pointers II - void pointer
- Pointers III - void pointer
- Pointers III - Bad pointer
- this pointer
- a pointer to a pointer **
How are pointers implemented?
Every area of memory in the machine has a numeric address like 0x0045f7(hexa)/17911(decimal).
A pointer to an area of memory is actually just an integer which is storing the address of that area of memory. The dereference operation looks at the address, and goes to that area of memory to retrieve the object stored there. Pointer assignment just copies the numeric address from one pointer to another. The NULL value is generally just the numeric address 0, and the computer just never allocates an object at 0 so that address can be used to represent NULL.
A bad pointer is really just a pointer which contains a random address. In other words, it is just like an uninitialized int variable which starts out with a random int value. The pointer has not yet been assigned the specific address of a valid object. This is why dereference operations with bad pointers are so unpredictable. They operate on whatever random area of memory they happen to have the address of.
Two main reasons:
- Pointers allow different sections of code to share information easily. We can get the same results by copying information back and forth, but pointers handle the issue smarter way.
- Pointers allow us to build complex data structures like linked lists and binary trees more easily.
To declare a pointer, we use '*' as:
int *ptr;
A pointer is declared to point to a specific type of a value. The ptr is a pointer to int. This means that it can only point to an int value. It can't point to a float or a char. In other words, the pointer ptr can only store the address of an int.
When we declare a pointer, we can put whitespace on either side of the *. So, following three lines are the same.
int *ptr; int* ptr; int * ptr;
We indicate for each declaration whether a variable is a pointer or not. All pointer variables are declared with an explicit * and we can have pointers to any type:
int x; int *p, *q;
This declares x as an integer and p and q as pointers to integers. As with any variable, we must first initialize a pointer by assigning it a value. We can assign the pointer an address in one of four ways:
- using the result of a new call
p = new int;
- from another pointer variable
q = p;
- to NULL
p = NULL:
- to the location of an int
p = &x;
Again, we want to ensure that an object has been given a value before we use it. In other words, we want our pointers to be initialized, and the object they point to have been initialized as well.
int *pi0; // uninitialized - asking for trouble int *pi1 = new int; // allocate an uninitialized int int *pi2 = new int(4); // allocate and initialize it to 4 int *pi3 = new int[8]; // allocate 8 uninitialized int
Memory allocated by new is not initialized for built-in types. Note that in the above example, we used () for initialization and used [] to indicate array.
For the user defined types, we have better initialization control. If we have a type T, and T has a default contructor, we get:
T *pT = new T; // one T initialized by default T *pT2 = new T[20]; // 20 Ts initialized by defalut
If a type U has a constructor, but not a default constructor, we should use explicit initialization:
U *pU = new U; // error: no default constructor U *pU2 = new U[30]; // error: no default constructor U *pU3 = new U(40); // OK: initialized to U(40)
If we have no other pointer to use for initializing a pointer, we use 0;
int *ptr = 0;
Here, assigning 0 to a pointer has special meaning. It makes the pointer point to nothing. It's like a remote controller with no programming in it. So, with that remote controller we can't do anything. When we are talking about a pointer, the value zero, 0, is called a null pointer.
int *ptr = 0;
The main job of pointer is to store address of an object. So, we need a way to put address into the pointer. One way of doing it is to retrieve the memory address of an existing object and assign it to a pointer.
#include <iostream> using namespace std; int main () { int myScore = 92; int *ptr; ptr = &myScore; cout << "&myScore; = " << &myScore; << endl; cout << "ptr = " << ptr << endl; return 0; }with an output:
&myScore; = 0017FF28 ptr = 0017FF28
Using the &, the address of operator, we assign the address of a variable to a pointer.
ptr = &myScore;
The dereference operation follows a pointer's reference to get the value of it is pointing to. When the dereference operation is used correctly, it's really simple. It just accesses the value of it points to. The only restriction is that the pointer must have an object for the dereference to access. Almost all bugs in pointer code involve violating that one restriction. A pointer must be assigned an object that it refers before dereference operations will work.
We dereference a pointer by using *, the deference operator.
cout << "myScore = " << *ptr << endl;
The assignment operation (=) between two pointers makes them point to the same object. After assignment, the == test comparing the two pointers will return true. The assignment operation also works with the NULL value. An assignment operation with a NULL pointer copies the NULL value from one pointer to another.
Two pointers which both refer to a single object are said to be sharing. That two or more entities can cooperatively share a single memory structure is a key advantage of pointers in all computer languages. Pointer manipulation is just technique but the sharing itself is often one of the premier goals of pointer.
Contrary to a reference which we can't reassign it to a different object, a pointer can point to a different object during of its life. Let's do a fact check with a code.
#include <iostream> using namespace std; int main () { int myScore = 92; int *ptr; ptr = &myScore; cout << "&myScore; = " << &myScore; << endl; cout << "ptr = " << ptr << endl; cout << "myScore = " << *ptr << endl; cout << endl; int myNewScore = 97; ptr = &myNewScore; cout << "&myNewScore; = " << &myNewScore; << endl; cout << "ptr = " << ptr << endl; cout << "myNewScore = " << *ptr << endl; return 0; }with an output:
&myScore; = 0017FF28 ptr = 0017FF28 myScore = 92 &myNewScore; = 0017FF10 ptr = 0017FF10 myNewScore = 97
Until now, we've been using a pointer to store the address of a build-in type int. We can use pointers with objects in the same way. Here is a simple example:
#include <iostream> #include <string> using namespace std; int main () { string str = "Bad artists copy. Good artists steal."; string *pStr = &str; cout << "*pStr: " << *pStr << endl; cout << "(*pStr).size() is " << (*pStr).size() << endl; cout << "pStr->size() is " << pStr->size() << endl; return 0; }with an output:
*pStr: Bad artists copy. Good artists steal. (*pStr).size() is 37 pStr->size() is 37
I created a string object, str, and a pointer which points to that string object, pStr. pStr is a pointer to
string str = "Bad artists copy. Good artists steal."; string *pStr = &str;
We can access an object through a pointer using dereference operator, *.
cout << "*pStr: " << *pStr << endl;
By using the dereference operator, I send the object, str, to which pStr points, to cout.
We can call the member functions of an object through a pointer.
cout << "(*pStr).size() is " << (*pStr).size() << endl;
(*pStr).size() says "Take the dereferencing result of pStr and call the object's member function, size() ."
Or we can use -> operator with pointer to access the members of objects:
cout << "pStr->size() is " << pStr->size() << endl;
We can use the keyword const to put some constraints on the way pointers are working. The const key word can act as safeguards and can make coder's intention more clear.
A regular pointer can point to different objects during its life cycle. But we can restrict the pointer so it can point only to the object at the time of its initialization by using a const pointer.
int myScore = 83; int* const cpScore = &myScore; // a constant pointer
This creates a constant pointer, pcScore. Like all constants, we must initialize a constant pointer at the time when we first declare it. So, the following line is an error.
int* const cpScore; // illegal: initialization is missing
Since cpScore is a constant pointer, it can't point to any object other than the original object. So, the following line is also an error.
pcScore = &newScore; // illegal: pcScore can't point to another object
However, we can change the value of the object to which our cpScore is pointing to. So, this is legal.
*pcScore = 91; // OK. change the value from 83 to 91
In a sense, a constant pointer is similar to reference since like a reference, a pointer can refer only to the object it was initialized to refer to.
We were able to change the values to which pointers point. But once again by using the const key word, we can restrict a pointer so it can't be used to change the value to which it points to. A pointer like this is called a pointer to a constant.
int finalScore = 89; const int* pcFinalScore; // a pointer to a constant pcFinalScore = &finalScore;It's declaring a pointer to a constant pcFinalScore. If somebody is not satisfied with the score and try to change like this:
*pcFinalScore = 99;He will get an error message from the compiler, something like this:
'pcFinalScore' : you cannot assign to a variable that is constHowever, the pointer itself can point to other score of somebody else's.
int scoreOfSomebody = 95; pcFinalScore = &scoreOfSomebody;
Here is the complete code:
#include <iostream> #include <string> using namespace std; int main () { int finalScore = 89; const int* pcFinalScore; // a pointer to a constant pcFinalScore = &finalScore; cout << "*pcFinalScore = " << *pcFinalScore << endl; //*pcFinalScore = 99; // illegal int scoreOfSomebody = 95; pcFinalScore = &scoreOfSomebody; cout << "*pcFinalScore = " << *pcFinalScore << endl; return 0; }
Output is:
*pcFinalScore = 89 *pcFinalScore = 95
A constant pointer to a constant can only point to the object that it was initialized to point to. This pointer can't be used to change the value of the object to which is points.
int finalScore = 89; const int* const cpcReallyFinal = &finalScore; ; *cpcReallyFinal = 99; // illegal - can't change value through pointer cpcReallyFinal = &anotherScore; // illegal - cpcReallyFinal can't point to another object
For pointers, we can specify whether the pointer itself is const, the data it points to is const, both, or neither:
char str[] = "constantness"; char *p = str; //non-const pointer to non-const data const char *pc = str; //non-const pointer to const data char *cp = str; //const pointer to non-const data const char *cpc = str; //const pointer to const data
When const appears to the left of the *, what's pointed to is constant, and if const appears to the right of the *, the pointer itself is constant. If const appears on both sizes, both are constant.
Using const with pointers has subtle aspects. Let's declare a pointer to a constant:
int year = 2012; const int *ptr = &year; *ptr = 2020; // not ok because ptr points to a const int
How about the following code:
const int year = 2012; int *p = &year; // not ok
C++ doesn't allow the last line for simple reason: if we can assign the address of year to p, then we can cheat and use p to modify the value of year. That doesn't make sense because year is declared as const. C++ prohibits us from assigning the address of a const to a non-const pointer.
Sharing an object can enable communication between two functions. One function passes a pointer to the value of an object to another function. Both functions can access the value of that object, but the value itself is not copied. This level of communication is called shallow since instead of making and sending a (may be huge) copy of the value of an object, a (relatively small) pointer is sent and the value of that object is shared. The recipient needs to understand that they have a shallow copy, so they know not to change or delete it since it is shared. The alternative where a complete copy is made and sent is known as a deep copy. Deep copies are simpler in a way, since each function can change their copy without interfering with the other copy, but deep copies run slower because of all the copying.
// shallow copy int *p = new int(99); int *q = p; // copy the pointer p *p = 100; // change the value of the int pointerd to by p // deep copy int *p = new int(99); int *q = new int(*p); // allocate a new int before copying the value pointed to by p *p = 100; // change the value of the int pointed to by p
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization