This part of the tutorial takes a look at variables, automatic conversions, memory and pointers / references.
The concepts presented may seem a bit abstract if they’re new to you but it will get more concrete once you start writing some real code.
As for types of variables this section uses only the built-in basic types such as int, double, char and pointers.
In C++ the type of an object determines the possible data that an object may contain and what operations that object may perform.
At the same time, C++ gives the programmer a lot of control in terms of what to assign to variables of different types and will let you assign types that are not directly matching through the use of automatic conversions.
It is therefore vital to understand what happens if you assign a value to a variable that it isn’t defined to use.
Conversions of basic types
You can think of conversion with basic types as being either “widening” or “narrowing”.
Widening conversions happen when you assign data to a variable that is narrower in scope than what the type is capable of handling.
An example of a widening conversion could be when you assign a integer value to a floating type variable.
int i = 10; double d = i;
In this example variable d would have the value 10.0 after the conversion has taken place, no information is lost in a widening conversion.
Widening conversions also happen automatically when you mix integer and floating types in an expression:
double sum = i + 5.5;
Here variable i will be auto converted into the double value 10.0 before being added to 5.5, resulting in 15.5.
double pi = 3.14159; int i = pi;
What value does i get? It gets 3, because when we put a fractional number into an integer variable, the fractional part is truncated, as in ignored, wasted.
This is an example of narrowing conversion.
The range of values that fits in an int is more narrow than what fits in a double.
Narrowing conversions can easily be a source of errors.
In expressions containing literal values, int is the default type for numbers without decimals and double is the default for numbers containing a decimal part. This can be overridden using type specifiers following the value and will be covered later.
bool b = false; char c = 'A'; int i = c; b = i;
Now, what is the value of i and b?
i gets the ASCII to integer value 65, and b gets the value true.
A Boolean may only contain 0 for false or 1 for true. Any value that is not 0 will be converted to true.
Also, if we use characters in an arithmetic expression, the result is converted to an integer.
int i = 5; double d = i;
d gets the value 5.0, as one might expect.
But what happens in this example:
int i = 123456; short s = i; cout << s << endl;
This leads to Undefined behavior. You can put an int into a short, but It will overflow, literally if the value of the int is larger than what a short can handle.
But the compiler accepts it, so be aware!
Memory, Pointers and References
So what are pointers and references?
They are both a way to indirectly access stored memory locations.
When we assign a value to a variable, that variable is a named memory location and internally the variable has a memory address that contain the actual data.
By using pointers and references we can access and alter these dynamic memory locations indirectly from the variable itself.
Many attempts to explain pointers using various analogies have been tried out but in my view they mostly fail to convey the intentions and usefulness of pointers in C++ and its C heritage.
All though a shallow understanding of pointers may get you by your task at hand, a deep understanding of heap and stack memory and pointers are required for understanding C and C++ on a deeper level.
Whether you want or need this “deep” understanding is off course up to you, but I recommend wrapping your head around pointers if you wish to be able to manipulate data with persistence and confidence in C and C++.
Faster and more efficient code can be written because pointers are closer to the hardware.
That is, the compiler can more easily translate the operation into machine code. There
is not as much overhead associated with pointers as might be present with other
Going deep into various uses of pointers fall outside the scope of this basic tutorial, but I encourage you to read the following
book about C and Pointers which also for the most part relates directly to C++ as well.
Understanding and Using C Pointers by Richard Reese
As your programs get more complex and include things like multi-threading and concurrency, the need to understand pointers also increases significantly as you then have to work with multiple stacks and other goodies.
Both the heap and the stack is stored in RAM.
The stack is a limited source of memory, the heap is a dynamic source of memory.
The stack is set to a fixed size, and can not grow past it’s fixed size. If there is not enough room on the stack to handle the memory being assigned to it, a stack overflow occurs.
This can happen when a lot of nested functions are being called, or if there is an infinite recursive call.
If the current size of the heap is too small to accommodate new memory, then more memory can be added to the heap by the operating system. This is one of the big differences between the heap and the stack.
A program generally works with three types of memory:
Static / Global Memory
Includes static and global scope variables. The lifetime of these variables is the whole lifetime of the application.
All functions have access to the globally scoped variables but static variables are limited to the defining functions where they are created.
Static variables are basically global variables with limited scope and there are always only one copy.
Statics / globals are useful for memory that you know you will always need and you know that you don’t ever want to de-allocate.
Stored on the stack.
Automatic / Local
Variables declared within a function. Created when the function is called and the scope and lifetime is limited to the time the function is executing.
Stored on the stack.
Memory is allocated from the heap and can be managed manually as nessecary (released). A pointer references the allocated memory.
The scope is limited to the pointer or pointers that references the actual memory and they exist until they are released.
Stored on the heap.
How to define and use pointers and references
The symbols * and & are the two basic symbols used to obtain pointers and create references.
A reference can be thought of as an alias for a variable. Once you bind a reference to an object you can’t re-assign it to another object.
A reference is not a type. This is a distinctive difference between references and pointers.
int i = 42; int &r1 = i;
You can create multiple references pointing to the same variable.
int &r2 = i; r2 = 43;
This changes the value stored by variable i.
A pointer is an actual type, and is not bound to its initial assignment in the way references are.
int i2 = 43; int *p1 = &i // & here means "address of" (see below)
Now p1 “points to” the location of i’s data and can change the data indirectly.
p1 = &i2;
Now p1 “points to” the location of i2’s data (43). variable i is not affected.
The & and * symbols are “context sensitive”. They mean different things in different contexts.
It’s very important to be able to interpret the symbols in the correct context!
Syntax for using * and &, an overview:
int i = 42; int &r = i; // & following a type and is part of a declaration: & means reference int *p; // * following a type and is part of a declaration: * means pointer p = &i; // & used in an expression: & means "address of" *p = i; // * used in an expression: * means "de-reference" (get value of) int &r2 = *p; // & is part of a declaration (reference), * means de-reference
You can (should) only access or de-reference a pointer that is valid, e.g. that points to an object!
A null pointer does not point to any object.
You can check if a pointer is null before using it and you should always do so.
There are a couple of ways to obtain a null pointer:
int *p3 = 0; int *p4 = nullptr;
If you include the C Standard Library (cstdlib) you can also do:
int *p3 = NULL;
The preferred way in C++11 is to use the second option:
int *p = nullptr;
Nullptr is a literal and has a type that can be converted to any other pointer type.
In the next post I’ll go into some more details of constants and how they relate to pointers and references.