Arrays
An array is a series of elements of the same type placed in contiguous memory locations that can be individually referenced by adding an index to a unique identifier.
That means that, for example, we can store 5 values of type int in an array without having to declare 5 different variables, each one with a different identifier. Instead of that, using an array we can store 5 different values of the same type, int for example, with a unique identifier.
For example, an array to contain 5 integer values of type int called billy could be represented like this:
where each blank panel represents an element of the array, that in this case are integer values of type int. These elements are numbered from 0 to 4 since in arrays the first index is always 0, independently of its length.
Like a regular variable, an array must be declared before it is used. A typical declaration for an array in C++ is:
type name [elements];
where type is a valid type (like int, float...), name is a valid identifier and the elements field (which is always enclosed in square brackets []), specifies how many of these elements the array has to contain.
Therefore, in order to declare an array called billy as the one shown in the above diagram it is as simple as:
int billy [5];
NOTE: The elements field within brackets [] which represents the number of elements the array is going to hold, must be a constant value, since arrays are blocks of non-dynamic memory whose size must be determined before execution. In order to create arrays with a variable length dynamic memory is needed, which is explained later in these tutorials.
Initializing arrays.
When declaring a regular array of local scope (within a function, for example), if we do not specify otherwise, its elements will not be initialized to any value by default, so their content will be undetermined until we store some value in them. The elements of global and static arrays, on the other hand, are automatically initialized with their default values, which for all fundamental types this means they are filled with zeros.
In both cases, local and global, when we declare an array, we have the possibility to assign initial values to each one of its elements by enclosing the values in braces { }. For example:
int billy [5] = { 16, 2, 77, 40, 12071 };
This declaration would have created an array like this:
The amount of values between braces { } must not be larger than the number of elements that we declare for the array between square brackets [ ]. For example, in the example of array billy we have declared that it has 5 elements and in the list of initial values within braces { } we have specified 5 values, one for each element.
When an initialization of values is provided for an array, C++ allows the possibility of leaving the square brackets empty [ ]. In this case, the compiler will assume a size for the array that matches the number of values included between braces { }:
int billy [] = { 16, 2, 77, 40, 12071 };
After this declaration, array billy would be 5 ints long, since we have provided 5 initialization values.
Accessing the values of an array.
In any point of a program in which an array is visible, we can access the value of any of its elements individually as if it was a normal variable, thus being able to both read and modify its value. The format is as simple as:
name[index]
Following the previous examples in which billy had 5 elements and each of those elements was of type int, the name which we can use to refer to each element is the following:
For example, to store the value 75 in the third element of billy, we could write the following statement:
billy[2] = 75;
and, for example, to pass the value of the third element of billy to a variable called a, we could write:
a = billy[2];
Therefore, the expression billy[2] is for all purposes like a variable of type int.
Notice that the third element of billy is specified billy[2], since the first one is billy[0], the second one is billy[1], and therefore, the third one is billy[2]. By this same reason, its last element is billy[4]. Therefore, if we write billy[5], we would be accessing the sixth element of billy and therefore exceeding the size of the array.
In C++ it is syntactically correct to exceed the valid range of indices for an array. This can create problems, since accessing out-of-range elements do not cause compilation errors but can cause runtime errors. The reason why this is allowed will be seen further ahead when we begin to use pointers.
At this point it is important to be able to clearly distinguish between the two uses that brackets [ ] have related to arrays. They perform two different tasks: one is to specify the size of arrays when they are declared; and the second one is to specify indices for concrete array elements. Do not confuse these two possible uses of brackets [ ] with arrays.
1
2
int billy[5]; // declaration of a new array
billy[2] = 75; // access to an element of the array.
If you read carefully, you will see that a type specifier always precedes a variable or array declaration, while it never precedes an access.
Some other valid operations with arrays:
1
2
3
4
billy[0] = a;
billy[a] = 75;
b = billy [a+2];
billy[billy[a]] = billy[2] + 5;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// arrays example
#include
using namespace std;
int billy [] = {16, 2, 77, 40, 12071};
int n, result=0;
int main ()
{
for ( n=0 ; n<5 ; n++ )
{
result += billy[n];
}
cout << result;
return 0;
}
12206
Multidimensional arrays
Multidimensional arrays can be described as "arrays of arrays". For example, a bidimensional array can be imagined as a bidimensional table made of elements, all of them of a same uniform data type.
jimmy represents a bidimensional array of 3 per 5 elements of type int. The way to declare this array in C++ would be:
int jimmy [3][5];
and, for example, the way to reference the second element vertically and fourth horizontally in an expression would be:
jimmy[1][3]
(remember that array indices always begin by zero).
Multidimensional arrays are not limited to two indices (i.e., two dimensions). They can contain as many indices as needed. But be careful! The amount of memory needed for an array rapidly increases with each dimension. For example:
char century [100][365][24][60][60];
declares an array with a char element for each second in a century, that is more than 3 billion chars. So this declaration would consume more than 3 gigabytes of memory!
Multidimensional arrays are just an abstraction for programmers, since we can obtain the same results with a simple array just by putting a factor between its indices:
1
2
int jimmy [3][5]; // is equivalent to
int jimmy [15]; // (3 * 5 = 15)
With the only difference that with multidimensional arrays the compiler remembers the depth of each imaginary dimension for us. Take as example these two pieces of code, with both exactly the same result. One uses a bidimensional array and the other one uses a simple array:
multidimensional array pseudo-multidimensional array
#define WIDTH 5
#define HEIGHT 3
int jimmy [HEIGHT][WIDTH];
int n,m;
int main ()
{
for (n=0;n for (m=0;m {
jimmy[n][m]=(n+1)*(m+1);
}
return 0;
}
#define WIDTH 5
#define HEIGHT 3
int jimmy [HEIGHT * WIDTH];
int n,m;
int main ()
{
for (n=0;n for (m=0;m {
jimmy[n*WIDTH+m]=(n+1)*(m+1);
}
return 0;
}
None of the two source codes above produce any output on the screen, but both assign values to the memory block called jimmy in the following way:
We have used "defined constants" (#define) to simplify possible future modifications of the program. For example, in case that we decided to enlarge the array to a height of 4 instead of 3 it could be done simply by changing the line:
#define HEIGHT 3
to:
#define HEIGHT 4
with no need to make any other modifications to the program.
Arrays as parameters
At some moment we may need to pass an array to a function as a parameter. In C++ it is not possible to pass a complete block of memory by value as a parameter to a function, but we are allowed to pass its address. In practice this has almost the same effect and it is a much faster and more efficient operation.
In order to accept arrays as parameters the only thing that we have to do when declaring the function is to specify in its parameters the element type of the array, an identifier and a pair of void brackets []. For example, the following function:
void procedure (int arg[])
accepts a parameter of type "array of int" called arg. In order to pass to this function an array declared as:
int myarray [40];
it would be enough to write a call like this:
procedure (myarray);
Here you have a complete example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// arrays as parameters
#include
using namespace std;
void printarray (int arg[], int length) {
for (int n=0; n cout << arg[n] << " ";
cout << "\n";
}
int main ()
{
int firstarray[] = {5, 10, 15};
int secondarray[] = {2, 4, 6, 8, 10};
printarray (firstarray,3);
printarray (secondarray,5);
return 0;
}
5 10 15
2 4 6 8 10
As you can see, the first parameter (int arg[]) accepts any array whose elements are of type int, whatever its length. For that reason we have included a second parameter that tells the function the length of each array that we pass to it as its first parameter. This allows the for loop that prints out the array to know the range to iterate in the passed array without going out of range.
In a function declaration it is also possible to include multidimensional arrays. The format for a tridimensional array parameter is:
base_type[][depth][depth]
for example, a function with a multidimensional array as argument could be:
void procedure (int myarray[][3][4])
Notice that the first brackets [] are left empty while the following ones specify sizes for their respective dimensions. This is necessary in order for the compiler to be able to determine the depth of each additional dimension.
Arrays, both simple or multidimensional, passed as function parameters are a quite common source of errors for novice programmers. I recommend the reading of the chapter about Pointers for a better understanding on how arrays operate.
Character Sequences
As you may already know, the C++ Standard Library implements a powerful string class, which is very useful to handle and manipulate strings of characters. However, because strings are in fact sequences of characters, we can represent them also as plain arrays of char elements.
For example, the following array:
char jenny [20];
is an array that can store up to 20 elements of type char. It can be represented as:
Therefore, in this array, in theory, we can store sequences of characters up to 20 characters long. But we can also store shorter sequences. For example, jenny could store at some point in a program either the sequence "Hello" or the sequence "Merry christmas", since both are shorter than 20 characters.
Therefore, since the array of characters can store shorter sequences than its total length, a special character is used to signal the end of the valid sequence: the null character, whose literal constant can be written as '\0' (backslash, zero).
Our array of 20 elements of type char, called jenny, can be represented storing the characters sequences "Hello" and "Merry Christmas" as:
Notice how after the valid content a null character ('\0') has been included in order to indicate the end of the sequence. The panels in gray color represent char elements with undetermined values.
Initialization of null-terminated character sequences
Because arrays of characters are ordinary arrays they follow all their same rules. For example, if we want to initialize an array of characters with some predetermined sequence of characters we can do it just like any other array:
char myword[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
In this case we would have declared an array of 6 elements of type char initialized with the characters that form the word "Hello" plus a null character '\0' at the end.
But arrays of char elements have an additional method to initialize their values: using string literals.
In the expressions we have used in some examples in previous chapters, constants that represent entire strings of characters have already showed up several times. These are specified enclosing the text to become a string literal between double quotes ("). For example:
"the result is: "
is a constant string literal that we have probably used already.
Double quoted strings (") are literal constants whose type is in fact a null-terminated array of characters. So string literals enclosed between double quotes always have a null character ('\0') automatically appended at the end.
Therefore we can initialize the array of char elements called myword with a null-terminated sequence of characters by either one of these two methods:
1
2
char myword [] = { 'H', 'e', 'l', 'l', 'o', '\0' };
char myword [] = "Hello";
In both cases the array of characters myword is declared with a size of 6 elements of type char: the 5 characters that compose the word "Hello" plus a final null character ('\0') which specifies the end of the sequence and that, in the second case, when using double quotes (") it is appended automatically.
Please notice that we are talking about initializing an array of characters in the moment it is being declared, and not about assigning values to them once they have already been declared. In fact because this type of null-terminated arrays of characters are regular arrays we have the same restrictions that we have with any other array, so we are not able to copy blocks of data with an assignment operation.
Assuming mystext is a char[] variable, expressions within a source code like:
1
2
mystext = "Hello";
mystext[] = "Hello";
would not be valid, like neither would be:
mystext = { 'H', 'e', 'l', 'l', 'o', '\0' };
The reason for this may become more comprehensible once you know a bit more about pointers, since then it will be clarified that an array is in fact a constant pointer pointing to a block of memory.
Using null-terminated sequences of characters
Null-terminated sequences of characters are the natural way of treating strings in C++, so they can be used as such in many procedures. In fact, regular string literals have this type (char[]) and can also be used in most cases.
For example, cin and cout support null-terminated sequences as valid containers for sequences of characters, so they can be used directly to extract strings of characters from cin or to insert them into cout. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// null-terminated sequences of characters
#include
using namespace std;
int main ()
{
char question[] = "Please, enter your first name: ";
char greeting[] = "Hello, ";
char yourname [80];
cout << question;
cin >> yourname;
cout << greeting << yourname << "!";
return 0;
}
Please, enter your first name: John
Hello, John!
As you can see, we have declared three arrays of char elements. The first two were initialized with string literal constants, while the third one was left uninitialized. In any case, we have to specify the size of the array: in the first two (question and greeting) the size was implicitly defined by the length of the literal constant they were initialized to. While for yourname we have explicitly specified that it has a size of 80 chars.
Finally, sequences of characters stored in char arrays can easily be converted into string objects just by using the assignment operator:
1
2
3
string mystring;
char myntcs[]="some text";
mystring = myntcs;
Pointers
We have already seen how variables are seen as memory cells that can be accessed using their identifiers. This way we did not have to care about the physical location of our data within memory, we simply used its identifier whenever we wanted to refer to our variable.
The memory of your computer can be imagined as a succession of memory cells, each one of the minimal size that computers manage (one byte). These single-byte memory cells are numbered in a consecutive way, so as, within any block of memory, every cell has the same number as the previous one plus one.
This way, each cell can be easily located in the memory because it has a unique address and all the memory cells follow a successive pattern. For example, if we are looking for cell 1776 we know that it is going to be right between cells 1775 and 1777, exactly one thousand cells after 776 and exactly one thousand cells before cell 2776.
Reference operator (&)
As soon as we declare a variable, the amount of memory needed is assigned for it at a specific location in memory (its memory address). We generally do not actively decide the exact location of the variable within the panel of cells that we have imagined the memory to be - Fortunately, that is a task automatically performed by the operating system during runtime. However, in some cases we may be interested in knowing the address where our variable is being stored during runtime in order to operate with relative positions to it.
The address that locates a variable within memory is what we call a reference to that variable. This reference to a variable can be obtained by preceding the identifier of a variable with an ampersand sign (&), known as reference operator, and which can be literally translated as "address of". For example:
ted = &andy;
This would assign to ted the address of variable andy, since when preceding the name of the variable andy with the reference operator (&) we are no longer talking about the content of the variable itself, but about its reference (i.e., its address in memory).
From now on we are going to assume that andy is placed during runtime in the memory address 1776. This number (1776) is just an arbitrary assumption we are inventing right now in order to help clarify some concepts in this tutorial, but in reality, we cannot know before runtime the real value the address of a variable will have in memory.
Consider the following code fragment:
1
2
3
andy = 25;
fred = andy;
ted = &andy;
The values contained in each variable after the execution of this, are shown in the following diagram:
First, we have assigned the value 25 to andy (a variable whose address in memory we have assumed to be 1776).
The second statement copied to fred the content of variable andy (which is 25). This is a standard assignment operation, as we have done so many times before.
Finally, the third statement copies to ted not the value contained in andy but a reference to it (i.e., its address, which we have assumed to be 1776). The reason is that in this third assignment operation we have preceded the identifier andy with the reference operator (&), so we were no longer referring to the value of andy but to its reference (its address in memory).
The variable that stores the reference to another variable (like ted in the previous example) is what we call a pointer. Pointers are a very powerful feature of the C++ language that has many uses in advanced programming. Farther ahead, we will see how this type of variable is used and declared.
Dereference operator (*)
We have just seen that a variable which stores a reference to another variable is called a pointer. Pointers are said to "point to" the variable whose reference they store.
Using a pointer we can directly access the value stored in the variable which it points to. To do this, we simply have to precede the pointer's identifier with an asterisk (*), which acts as dereference operator and that can be literally translated to "value pointed by".
Therefore, following with the values of the previous example, if we write:
beth = *ted;
(that we could read as: "beth equal to value pointed by ted") beth would take the value 25, since ted is 1776, and the value pointed by 1776 is 25.
You must clearly differentiate that the expression ted refers to the value 1776, while *ted (with an asterisk * preceding the identifier) refers to the value stored at address 1776, which in this case is 25. Notice the difference of including or not including the dereference operator (I have included an explanatory commentary of how each of these two expressions could be read):
1
2
beth = ted; // beth equal to ted ( 1776 )
beth = *ted; // beth equal to value pointed by ted ( 25 )
Notice the difference between the reference and dereference operators:
& is the reference operator and can be read as "address of"
* is the dereference operator and can be read as "value pointed by"
Thus, they have complementary (or opposite) meanings. A variable referenced with & can be dereferenced with *.
Earlier we performed the following two assignment operations:
1
2
andy = 25;
ted = &andy;
Right after these two statements, all of the following expressions would give true as result:
1
2
3
4
andy == 25
&andy == 1776
ted == 1776
*ted == 25
The first expression is quite clear considering that the assignment operation performed on andy was andy=25. The second one uses the reference operator (&), which returns the address of variable andy, which we assumed it to have a value of 1776. The third one is somewhat obvious since the second expression was true and the assignment operation performed on ted was ted=&andy. The fourth expression uses the dereference operator (*) that, as we have just seen, can be read as "value pointed by", and the value pointed by ted is indeed 25.
So, after all that, you may also infer that for as long as the address pointed by ted remains unchanged the following expression will also be true:
*ted == andy
Declaring variables of pointer types
Due to the ability of a pointer to directly refer to the value that it points to, it becomes necessary to specify in its declaration which data type a pointer is going to point to. It is not the same thing to point to a char as to point to an int or a float.
The declaration of pointers follows this format:
type * name;
where type is the data type of the value that the pointer is intended to point to. This type is not the type of the pointer itself! but the type of the data the pointer points to. For example:
1
2
3
int * number;
char * character;
float * greatnumber;
These are three declarations of pointers. Each one is intended to point to a different data type, but in fact all of them are pointers and all of them will occupy the same amount of space in memory (the size in memory of a pointer depends on the platform where the code is going to run). Nevertheless, the data to which they point to do not occupy the same amount of space nor are of the same type: the first one points to an int, the second one to a char and the last one to a float. Therefore, although these three example variables are all of them pointers which occupy the same size in memory, they are said to have different types: int*, char* and float* respectively, depending on the type they point to.
I want to emphasize that the asterisk sign (*) that we use when declaring a pointer only means that it is a pointer (it is part of its type compound specifier), and should not be confused with the dereference operator that we have seen a bit earlier, but which is also written with an asterisk (*). They are simply two different things represented with the same sign.
Now have a look at this code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// my first pointer
#include
using namespace std;
int main ()
{
int firstvalue, secondvalue;
int * mypointer;
mypointer = &firstvalue;
*mypointer = 10;
mypointer = &secondvalue;
*mypointer = 20;
cout << "firstvalue is " << firstvalue << endl;
cout << "secondvalue is " << secondvalue << endl;
return 0;
}
firstvalue is 10
secondvalue is 20
Notice that even though we have never directly set a value to either firstvalue or secondvalue, both end up with a value set indirectly through the use of mypointer. This is the procedure:
First, we have assigned as value of mypointer a reference to firstvalue using the reference operator (&). And then we have assigned the value 10 to the memory location pointed by mypointer, that because at this moment is pointing to the memory location of firstvalue, this in fact modifies the value of firstvalue.
In order to demonstrate that a pointer may take several different values during the same program I have repeated the process with secondvalue and that same pointer, mypointer.
Here is an example a little bit more elaborated:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// more pointers
#include
using namespace std;
int main ()
{
int firstvalue = 5, secondvalue = 15;
int * p1, * p2;
p1 = &firstvalue; // p1 = address of firstvalue
p2 = &secondvalue; // p2 = address of secondvalue
*p1 = 10; // value pointed by p1 = 10
*p2 = *p1; // value pointed by p2 = value pointed by p1
p1 = p2; // p1 = p2 (value of pointer is copied)
*p1 = 20; // value pointed by p1 = 20
cout << "firstvalue is " << firstvalue << endl;
cout << "secondvalue is " << secondvalue << endl;
return 0;
}
firstvalue is 10
secondvalue is 20
I have included as a comment on each line how the code can be read: ampersand (&) as "address of" and asterisk (*) as "value pointed by".
Notice that there are expressions with pointers p1 and p2, both with and without dereference operator (*). The meaning of an expression using the dereference operator (*) is very different from one that does not: When this operator precedes the pointer name, the expression refers to the value being pointed, while when a pointer name appears without this operator, it refers to the value of the pointer itself (i.e. the address of what the pointer is pointing to).
Another thing that may call your attention is the line:
int * p1, * p2;
This declares the two pointers used in the previous example. But notice that there is an asterisk (*) for each pointer, in order for both to have type int* (pointer to int).
Otherwise, the type for the second variable declared in that line would have been int (and not int*) because of precedence relationships. If we had written:
int * p1, p2;
p1 would indeed have int* type, but p2 would have type int (spaces do not matter at all for this purpose). This is due to operator precedence rules. But anyway, simply remembering that you have to put one asterisk per pointer is enough for most pointer users.
Pointers and arrays
The concept of array is very much bound to the one of pointer. In fact, the identifier of an array is equivalent to the address of its first element, as a pointer is equivalent to the address of the first element that it points to, so in fact they are the same concept. For example, supposing these two declarations:
1
2
int numbers [20];
int * p;
The following assignment operation would be valid:
p = numbers;
After that, p and numbers would be equivalent and would have the same properties. The only difference is that we could change the value of pointer p by another one, whereas numbers will always point to the first of the 20 elements of type int with which it was defined. Therefore, unlike p, which is an ordinary pointer, numbers is an array, and an array can be considered a constant pointer. Therefore, the following allocation would not be valid:
numbers = p;
Because numbers is an array, so it operates as a constant pointer, and we cannot assign values to constants.
Due to the characteristics of variables, all expressions that include pointers in the following example are perfectly valid:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// more pointers
#include
using namespace std;
int main ()
{
int numbers[5];
int * p;
p = numbers; *p = 10;
p++; *p = 20;
p = &numbers[2]; *p = 30;
p = numbers + 3; *p = 40;
p = numbers; *(p+4) = 50;
for (int n=0; n<5; n++)
cout << numbers[n] << ", ";
return 0;
}
10, 20, 30, 40, 50,
In the chapter about arrays we used brackets ([]) several times in order to specify the index of an element of the array to which we wanted to refer. Well, these bracket sign operators [] are also a dereference operator known as offset operator. They dereference the variable they follow just as * does, but they also add the number between brackets to the address being dereferenced. For example:
1
2
a[5] = 0; // a [offset of 5] = 0
*(a+5) = 0; // pointed by (a+5) = 0
These two expressions are equivalent and valid both if a is a pointer or if a is an array.
Pointer initialization
When declaring pointers we may want to explicitly specify which variable we want them to point to:
1
2
int number;
int *tommy = &number;
The behavior of this code is equivalent to:
1
2
3
int number;
int *tommy;
tommy = &number;
When a pointer initialization takes place we are always assigning the reference value to where the pointer points (tommy), never the value being pointed (*tommy). You must consider that at the moment of declaring a pointer, the asterisk (*) indicates only that it is a pointer, it is not the dereference operator (although both use the same sign: *). Remember, they are two different functions of one sign. Thus, we must take care not to confuse the previous code with:
1
2
3
int number;
int *tommy;
*tommy = &number;
that is incorrect, and anyway would not have much sense in this case if you think about it.
As in the case of arrays, the compiler allows the special case that we want to initialize the content at which the pointer points with constants at the same moment the pointer is declared:
char * terry = "hello";
In this case, memory space is reserved to contain "hello" and then a pointer to the first character of this memory block is assigned to terry. If we imagine that "hello" is stored at the memory locations that start at addresses 1702, we can represent the previous declaration as:
It is important to indicate that terry contains the value 1702, and not 'h' nor "hello", although 1702 indeed is the address of both of these.
The pointer terry points to a sequence of characters and can be read as if it was an array (remember that an array is just like a constant pointer). For example, we can access the fifth element of the array with any of these two expression:
1
2
*(terry+4)
terry[4]
Both expressions have a value of 'o' (the fifth element of the array).
Pointer arithmetics
To conduct arithmetical operations on pointers is a little different than to conduct them on regular integer data types. To begin with, only addition and subtraction operations are allowed to be conducted with them, the others make no sense in the world of pointers. But both addition and subtraction have a different behavior with pointers according to the size of the data type to which they point.
When we saw the different fundamental data types, we saw that some occupy more or less space than others in the memory. For example, let's assume that in a given compiler for a specific machine, char takes 1 byte, short takes 2 bytes and long takes 4.
Suppose that we define three pointers in this compiler:
1
2
3
char *mychar;
short *myshort;
long *mylong;
and that we know that they point to memory locations 1000, 2000 and 3000 respectively.
So if we write:
1
2
3
mychar++;
myshort++;
mylong++;
mychar, as you may expect, would contain the value 1001. But not so obviously, myshort would contain the value 2002, and mylong would contain 3004, even though they have each been increased only once. The reason is that when adding one to a pointer we are making it to point to the following element of the same type with which it has been defined, and therefore the size in bytes of the type pointed is added to the pointer.
This is applicable both when adding and subtracting any number to a pointer. It would happen exactly the same if we write:
1
2
3
mychar = mychar + 1;
myshort = myshort + 1;
mylong = mylong + 1;
Both the increase (++) and decrease (--) operators have greater operator precedence than the dereference operator (*), but both have a special behavior when used as suffix (the expression is evaluated with the value it had before being increased). Therefore, the following expression may lead to confusion:
*p++
Because ++ has greater precedence than *, this expression is equivalent to *(p++). Therefore, what it does is to increase the value of p (so it now points to the next element), but because ++ is used as postfix the whole expression is evaluated as the value pointed by the original reference (the address the pointer pointed to before being increased).
Notice the difference with:
(*p)++
Here, the expression would have been evaluated as the value pointed by p increased by one. The value of p (the pointer itself) would not be modified (what is being modified is what it is being pointed to by this pointer).
If we write:
*p++ = *q++;
Because ++ has a higher precedence than *, both p and q are increased, but because both increase operators (++) are used as postfix and not prefix, the value assigned to *p is *q before both p and q are increased. And then both are increased. It would be roughly equivalent to:
1
2
3
*p = *q;
++p;
++q;
Like always, I recommend you to use parentheses () in order to avoid unexpected results and to give more legibility to the code.
Pointers to pointers
C++ allows the use of pointers that point to pointers, that these, in its turn, point to data (or even to other pointers). In order to do that, we only need to add an asterisk (*) for each level of reference in their declarations:
1
2
3
4
5
6
char a;
char * b;
char ** c;
a = 'z';
b = &a;
c = &b;
This, supposing the randomly chosen memory locations for each variable of 7230, 8092 and 10502, could be represented as:
The value of each variable is written inside each cell; under the cells are their respective addresses in memory.
The new thing in this example is variable c, which can be used in three different levels of indirection, each one of them would correspond to a different value:
c has type char** and a value of 8092
*c has type char* and a value of 7230
**c has type char and a value of 'z'
void pointers
The void type of pointer is a special type of pointer. In C++, void represents the absence of type, so void pointers are pointers that point to a value that has no type (and thus also an undetermined length and undetermined dereference properties).
This allows void pointers to point to any data type, from an integer value or a float to a string of characters. But in exchange they have a great limitation: the data pointed by them cannot be directly dereferenced (which is logical, since we have no type to dereference to), and for that reason we will always have to cast the address in the void pointer to some other pointer type that points to a concrete data type before dereferencing it.
One of its uses may be to pass generic parameters to a function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// increaser
#include
using namespace std;
void increase (void* data, int psize)
{
if ( psize == sizeof(char) )
{ char* pchar; pchar=(char*)data; ++(*pchar); }
else if (psize == sizeof(int) )
{ int* pint; pint=(int*)data; ++(*pint); }
}
int main ()
{
char a = 'x';
int b = 1602;
increase (&a,sizeof(a));
increase (&b,sizeof(b));
cout << a << ", " << b << endl;
return 0;
}
y, 1603
sizeof is an operator integrated in the C++ language that returns the size in bytes of its parameter. For non-dynamic data types this value is a constant. Therefore, for example, sizeof(char) is 1, because char type is one byte long.
Null pointer
A null pointer is a regular pointer of any pointer type which has a special value that indicates that it is not pointing to any valid reference or memory address. This value is the result of type-casting the integer value zero to any pointer type.
1
2
int * p;
p = 0; // p has a null pointer value
Do not confuse null pointers with void pointers. A null pointer is a value that any pointer may take to represent that it is pointing to "nowhere", while a void pointer is a special type of pointer that can point to somewhere without a specific type. One refers to the value stored in the pointer itself and the other to the type of data it points to.
Pointers to functions
C++ allows operations with pointers to functions. The typical use of this is for passing a function as an argument to another function, since these cannot be passed dereferenced. In order to declare a pointer to a function we have to declare it like the prototype of the function except that the name of the function is enclosed between parentheses () and an asterisk (*) is inserted before the name:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// pointer to functions
#include
using namespace std;
int addition (int a, int b)
{ return (a+b); }
int subtraction (int a, int b)
{ return (a-b); }
int operation (int x, int y, int (*functocall)(int,int))
{
int g;
g = (*functocall)(x,y);
return (g);
}
int main ()
{
int m,n;
int (*minus)(int,int) = subtraction;
m = operation (7, 5, addition);
n = operation (20, m, minus);
cout < return 0;
}
8
In the example, minus is a pointer to a function that has two parameters of type int. It is immediately assigned to point to the function subtraction, all in a single line:
int (* minus)(int,int) = subtraction;
Dynamic Memory
Until now, in all our programs, we have only had as much memory available as we declared for our variables, having the size of all of them to be determined in the source code, before the execution of the program. But, what if we need a variable amount of memory that can only be determined during runtime? For example, in the case that we need some user input to determine the necessary amount of memory space.
The answer is dynamic memory, for which C++ integrates the operators new and delete.
Operators new and new[]
In order to request dynamic memory we use the operator new. new is followed by a data type specifier and -if a sequence of more than one element is required- the number of these within brackets []. It returns a pointer to the beginning of the new block of memory allocated. Its form is:
pointer = new type
pointer = new type [number_of_elements]
The first expression is used to allocate memory to contain one single element of type type. The second one is used to assign a block (an array) of elements of type type, where number_of_elements is an integer value representing the amount of these. For example:
1
2
int * bobby;
bobby = new int [5];
In this case, the system dynamically assigns space for five elements of type int and returns a pointer to the first element of the sequence, which is assigned to bobby. Therefore, now, bobby points to a valid block of memory with space for five elements of type int.
The first element pointed by bobby can be accessed either with the expression bobby[0] or the expression *bobby. Both are equivalent as has been explained in the section about pointers. The second element can be accessed either with bobby[1] or *(bobby+1) and so on...
You could be wondering the difference between declaring a normal array and assigning dynamic memory to a pointer, as we have just done. The most important difference is that the size of an array has to be a constant value, which limits its size to what we decide at the moment of designing the program, before its execution, whereas the dynamic memory allocation allows us to assign memory during the execution of the program (runtime) using any variable or constant value as its size.
The dynamic memory requested by our program is allocated by the system from the memory heap. However, computer memory is a limited resource, and it can be exhausted. Therefore, it is important to have some mechanism to check if our request to allocate memory was successful or not.
C++ provides two standard methods to check if the allocation was successful:
One is by handling exceptions. Using this method an exception of type bad_alloc is thrown when the allocation fails. Exceptions are a powerful C++ feature explained later in these tutorials. But for now you should know that if this exception is thrown and it is not handled by a specific handler, the program execution is terminated.
This exception method is the default method used by new, and is the one used in a declaration like:
bobby = new int [5]; // if it fails an exception is thrown
The other method is known as nothrow, and what happens when it is used is that when a memory allocation fails, instead of throwing a bad_alloc exception or terminating the program, the pointer returned by new is a null pointer, and the program continues its execution.
This method can be specified by using a special object called nothrow, declared in header , as argument for new:
bobby = new (nothrow) int [5];
In this case, if the allocation of this block of memory failed, the failure could be detected by checking if bobby took a null pointer value:
1
2
3
4
5
int * bobby;
bobby = new (nothrow) int [5];
if (bobby == 0) {
// error assigning memory. Take measures.
};
This nothrow method requires more work than the exception method, since the value returned has to be checked after each and every memory allocation, but I will use it in our examples due to its simplicity. Anyway this method can become tedious for larger projects, where the exception method is generally preferred. The exception method will be explained in detail later in this tutorial.
Operators delete and delete[]
Since the necessity of dynamic memory is usually limited to specific moments within a program, once it is no longer needed it should be freed so that the memory becomes available again for other requests of dynamic memory. This is the purpose of the operator delete, whose format is:
1
2
delete pointer;
delete [] pointer;
The first expression should be used to delete memory allocated for a single element, and the second one for memory allocated for arrays of elements.
The value passed as argument to delete must be either a pointer to a memory block previously allocated with new, or a null pointer (in the case of a null pointer, delete produces no effect).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// rememb-o-matic
#include
#include
using namespace std;
int main ()
{
int i,n;
int * p;
cout << "How many numbers would you like to type? ";
cin >> i;
p= new (nothrow) int[i];
if (p == 0)
cout << "Error: memory could not be allocated";
else
{
for (n=0; n {
cout << "Enter number: ";
cin >> p[n];
}
cout << "You have entered: ";
for (n=0; n cout << p[n] << ", ";
delete[] p;
}
return 0;
}
How many numbers would you like to type? 5
Enter number : 75
Enter number : 436
Enter number : 1067
Enter number : 8
Enter number : 32
You have entered: 75, 436, 1067, 8, 32,
Notice how the value within brackets in the new statement is a variable value entered by the user (i), not a constant value:
p= new (nothrow) int[i];
But the user could have entered a value for i so big that our system could not handle it. For example, when I tried to give a value of 1 billion to the "How many numbers" question, my system could not allocate that much memory for the program and I got the text message we prepared for this case (Error: memory could not be allocated). Remember that in the case that we tried to allocate the memory without specifying the nothrow parameter in the new expression, an exception would be thrown, which if it's not handled terminates the program.
It is a good practice to always check if a dynamic memory block was successfully allocated. Therefore, if you use the nothrow method, you should always check the value of the pointer returned. Otherwise, use the exception method, even if you do not handle the exception. This way, the program will terminate at that point without causing the unexpected results of continuing executing a code that assumes a block of memory to have been allocated when in fact it has not.
Dynamic memory in ANSI-C
Operators new and delete are exclusive of C++. They are not available in the C language. But using pure C language and its library, dynamic memory can also be used through the functions malloc, calloc, realloc and free, which are also available in C++ including the header file (see cstdlib for more info).
The memory blocks allocated by these functions are not necessarily compatible with those returned by new, so each one should be manipulated with its own set of functions or operators.
Data Structures
We have already learned how groups of sequential data can be used in C++. But this is somewhat restrictive, since in many occasions what we want to store are not mere sequences of elements all of the same data type, but sets of different elements with different data types.
Data structures
A data structure is a group of data elements grouped together under one name. These data elements, known as members, can have different types and different lengths. Data structures are declared in C++ using the following syntax:
struct structure_name {
member_type1 member_name1;
member_type2 member_name2;
member_type3 member_name3;
.
.
} object_names;
where structure_name is a name for the structure type, object_name can be a set of valid identifiers for objects that have the type of this structure. Within braces { } there is a list with the data members, each one is specified with a type and a valid identifier as its name.
The first thing we have to know is that a data structure creates a new type: Once a data structure is declared, a new type with the identifier specified as structure_name is created and can be used in the rest of the program as if it was any other type. For example:
1
2
3
4
5
6
7
struct product {
int weight;
float price;
} ;
product apple;
product banana, melon;
We have first declared a structure type called product with two members: weight and price, each of a different fundamental type. We have then used this name of the structure type (product) to declare three objects of that type: apple, banana and melon as we would have done with any fundamental data type.
Once declared, product has become a new valid type name like the fundamental ones int, char or short and from that point on we are able to declare objects (variables) of this compound new type, like we have done with apple, banana and melon.
Right at the end of the struct declaration, and before the ending semicolon, we can use the optional field object_name to directly declare objects of the structure type. For example, we can also declare the structure objects apple, banana and melon at the moment we define the data structure type this way:
1
2
3
4
struct product {
int weight;
float price;
} apple, banana, melon;
It is important to clearly differentiate between what is the structure type name, and what is an object (variable) that has this structure type. We can instantiate many objects (i.e. variables, like apple, banana and melon) from a single structure type (product).
Once we have declared our three objects of a determined structure type (apple, banana and melon) we can operate directly with their members. To do that we use a dot (.) inserted between the object name and the member name. For example, we could operate with any of these elements as if they were standard variables of their respective types:
1
2
3
4
5
6
apple.weight
apple.price
banana.weight
banana.price
melon.weight
melon.price
Each one of these has the data type corresponding to the member they refer to: apple.weight, banana.weight and melon.weight are of type int, while apple.price, banana.price and melon.price are of type float.
Let's see a real example where you can see how a structure type can be used in the same way as fundamental types:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// example about structures
#include
#include
#include
using namespace std;
struct movies_t {
string title;
int year;
} mine, yours;
void printmovie (movies_t movie);
int main ()
{
string mystr;
mine.title = "2001 A Space Odyssey";
mine.year = 1968;
cout << "Enter title: ";
getline (cin,yours.title);
cout << "Enter year: ";
getline (cin,mystr);
stringstream(mystr) >> yours.year;
cout << "My favorite movie is:\n ";
printmovie (mine);
cout << "And yours is:\n ";
printmovie (yours);
return 0;
}
void printmovie (movies_t movie)
{
cout << movie.title;
cout << " (" << movie.year << ")\n";
}
Enter title: Alien
Enter year: 1979
My favorite movie is:
2001 A Space Odyssey (1968)
And yours is:
Alien (1979)
The example shows how we can use the members of an object as regular variables. For example, the member yours.year is a valid variable of type int, and mine.title is a valid variable of type string.
The objects mine and yours can also be treated as valid variables of type movies_t, for example we have passed them to the function printmovie as we would have done with regular variables. Therefore, one of the most important advantages of data structures is that we can either refer to their members individually or to the entire structure as a block with only one identifier.
Data structures are a feature that can be used to represent databases, especially if we consider the possibility of building arrays of them:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// array of structures
#include
#include
#include
using namespace std;
#define N_MOVIES 3
struct movies_t {
string title;
int year;
} films [N_MOVIES];
void printmovie (movies_t movie);
int main ()
{
string mystr;
int n;
for (n=0; n {
cout << "Enter title: ";
getline (cin,films[n].title);
cout << "Enter year: ";
getline (cin,mystr);
stringstream(mystr) >> films[n].year;
}
cout << "\nYou have entered these movies:\n";
for (n=0; n printmovie (films[n]);
return 0;
}
void printmovie (movies_t movie)
{
cout << movie.title;
cout << " (" << movie.year << ")\n";
}
Enter title: Blade Runner
Enter year: 1982
Enter title: Matrix
Enter year: 1999
Enter title: Taxi Driver
Enter year: 1976
You have entered these movies:
Blade Runner (1982)
Matrix (1999)
Taxi Driver (1976)
Pointers to structures
Like any other type, structures can be pointed by its own type of pointers:
1
2
3
4
5
6
7
struct movies_t {
string title;
int year;
};
movies_t amovie;
movies_t * pmovie;
Here amovie is an object of structure type movies_t, and pmovie is a pointer to point to objects of structure type movies_t. So, the following code would also be valid:
pmovie = &amovie;
The value of the pointer pmovie would be assigned to a reference to the object amovie (its memory address).
We will now go with another example that includes pointers, which will serve to introduce a new operator: the arrow operator (->):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// pointers to structures
#include
#include
#include
using namespace std;
struct movies_t {
string title;
int year;
};
int main ()
{
string mystr;
movies_t amovie;
movies_t * pmovie;
pmovie = &amovie;
cout << "Enter title: ";
getline (cin, pmovie->title);
cout << "Enter year: ";
getline (cin, mystr);
(stringstream) mystr >> pmovie->year;
cout << "\nYou have entered:\n";
cout << pmovie->title;
cout << " (" << pmovie->year << ")\n";
return 0;
}
Enter title: Invasion of the body snatchers
Enter year: 1978
You have entered:
Invasion of the body snatchers (1978)
The previous code includes an important introduction: the arrow operator (->). This is a dereference operator that is used exclusively with pointers to objects with members. This operator serves to access a member of an object to which we have a reference. In the example we used:
pmovie->title
Which is for all purposes equivalent to:
(*pmovie).title
Both expressions pmovie->title and (*pmovie).title are valid and both mean that we are evaluating the member title of the data structure pointed by a pointer called pmovie. It must be clearly differentiated from:
*pmovie.title
which is equivalent to:
*(pmovie.title)
And that would access the value pointed by a hypothetical pointer member called title of the structure object pmovie (which in this case would not be a pointer). The following panel summarizes possible combinations of pointers and structure members:
Expression What is evaluated Equivalent
a.b Member b of object a
a->b Member b of object pointed by a (*a).b
*a.b Value pointed by member b of object a *(a.b)
Nesting structures
Structures can also be nested so that a valid element of a structure can also be in its turn another structure.
1
2
3
4
5
6
7
8
9
10
11
12
struct movies_t {
string title;
int year;
};
struct friends_t {
string name;
string email;
movies_t favorite_movie;
} charlie, maria;
friends_t * pfriends = &charlie;
After the previous declaration we could use any of the following expressions:
1
2
3
4
charlie.name
maria.favorite_movie.title
charlie.favorite_movie.year
pfriends->favorite_movie.year
(where, by the way, the last two expressions refer to the same member).
Other Data Types
Defined data types (typedef)
C++ allows the definition of our own types based on other existing data types. We can do this using the keyword typedef, whose format is:
typedef existing_type new_type_name ;
where existing_type is a C++ fundamental or compound type and new_type_name is the name for the new type we are defining. For example:
1
2
3
4
typedef char C;
typedef unsigned int WORD;
typedef char * pChar;
typedef char field [50];
In this case we have defined four data types: C, WORD, pChar and field as char, unsigned int, char* and char[50] respectively, that we could perfectly use in declarations later as any other valid type:
1
2
3
4
C mychar, anotherchar, *ptc1;
WORD myword;
pChar ptc2;
field name;
typedef does not create different types. It only creates synonyms of existing types. That means that the type of myword can be considered to be either WORD or unsigned int, since both are in fact the same type.
typedef can be useful to define an alias for a type that is frequently used within a program. It is also useful to define types when it is possible that we will need to change the type in later versions of our program, or if a type you want to use has a name that is too long or confusing.
Unions
Unions allow one same portion of memory to be accessed as different data types, since all of them are in fact the same location in memory. Its declaration and use is similar to the one of structures but its functionality is totally different:
union union_name {
member_type1 member_name1;
member_type2 member_name2;
member_type3 member_name3;
.
.
} object_names;
All the elements of the union declaration occupy the same physical space in memory. Its size is the one of the greatest element of the declaration. For example:
1
2
3
4
5
union mytypes_t {
char c;
int i;
float f;
} mytypes;
defines three elements:
1
2
3
mytypes.c
mytypes.i
mytypes.f
each one with a different data type. Since all of them are referring to the same location in memory, the modification of one of the elements will affect the value of all of them. We cannot store different values in them independent of each other.
One of the uses a union may have is to unite an elementary type with an array or structures of smaller elements. For example:
1
2
3
4
5
6
7
8
union mix_t {
long l;
struct {
short hi;
short lo;
} s;
char c[4];
} mix;
defines three names that allow us to access the same group of 4 bytes: mix.l, mix.s and mix.c and which we can use according to how we want to access these bytes, as if they were a single long-type data, as if they were two short elements or as an array of char elements, respectively. I have mixed types, arrays and structures in the union so that you can see the different ways that we can access the data. For a little-endian system (most PC platforms), this union could be represented as:
The exact alignment and order of the members of a union in memory is platform dependant. Therefore be aware of possible portability issues with this type of use.
Anonymous unions
In C++ we have the option to declare anonymous unions. If we declare a union without any name, the union will be anonymous and we will be able to access its members directly by their member names. For example, look at the difference between these two structure declarations:
structure with regular union structure with anonymous union
struct {
char title[50];
char author[50];
union {
float dollars;
int yen;
} price;
} book;
struct {
char title[50];
char author[50];
union {
float dollars;
int yen;
};
} book;
The only difference between the two pieces of code is that in the first one we have given a name to the union (price) and in the second one we have not. The difference is seen when we access the members dollars and yen of an object of this type. For an object of the first type, it would be:
1
2
book.price.dollars
book.price.yen
whereas for an object of the second type, it would be:
1
2
book.dollars
book.yen
Once again I remind you that because it is a union and not a struct, the members dollars and yen occupy the same physical space in the memory so they cannot be used to store two different values simultaneously. You can set a value for price in dollars or in yen, but not in both.
Enumerations (enum)
Enumerations create new data types to contain something different that is not limited to the values fundamental data types may take. Its form is the following:
enum enumeration_name {
value1,
value2,
value3,
.
.
} object_names;
For example, we could create a new type of variable called colors_t to store colors with the following declaration:
enum colors_t {black, blue, green, cyan, red, purple, yellow, white};
Notice that we do not include any fundamental data type in the declaration. To say it somehow, we have created a whole new data type from scratch without basing it on any other existing type. The possible values that variables of this new type color_t may take are the new constant values included within braces. For example, once the colors_t enumeration is declared the following expressions will be valid:
1
2
3
4
colors_t mycolor;
mycolor = blue;
if (mycolor == green) mycolor = red;
Enumerations are type compatible with numeric variables, so their constants are always assigned an integer numerical value internally. If it is not specified, the integer value equivalent to the first possible value is equivalent to 0 and the following ones follow a +1 progression. Thus, in our data type colors_t that we have defined above, black would be equivalent to 0, blue would be equivalent to 1, green to 2, and so on.
We can explicitly specify an integer value for any of the constant values that our enumerated type can take. If the constant value that follows it is not given an integer value, it is automatically assumed the same value as the previous one plus one. For example:
1
2
3
enum months_t { january=1, february, march, april,
may, june, july, august,
september, october, november, december} y2k;
In this case, variable y2k of enumerated type months_t can contain any of the 12 possible values that go from january to december and that are equivalent to values between 1 and 12 (not between 0 and 11, since we have made january equal to 1).