Chapter 4
Arrays, Strings, and Pointers

  • How to use arrays
  • How to define and initialize arrays of different types
  • How to use the range-based for loop with an array
  • How to define and use multidimensional arrays
  • How to use pointers
  • How to define and initialize pointers of different types
  • The relationship between arrays and pointers
  • How to define references and some initial ideas on their uses

You can find the wrox.com code downloads for this chapter on the Download Code tab at www.wrox.com/go/beginningvisualc. The code is in the Chapter 4 download and individually named according to the names throughout the chapter.

HANDLING MULTIPLE DATA VALUES OF THE SAME TYPE

You already know how to define and initialize variables of various types that each holds a single item of information; I’ll refer to single items of data as data elements. The most obvious extension to the idea of a variable is to be able to reference several data elements of a particular type with a single variable name. This would enable you to handle applications of a much broader scope.

Let’s consider an example. Suppose that you needed to write a payroll program. Using a separate variable for each individual’s pay, tax liability, and so on, would be an uphill task to say the least. A more convenient way to handle such a problem would be to reference an employee by some kind of generic name — employeeName to take an imaginative example — and to have other generic names for the kinds of data related to each employee, such as pay and tax. Of course, you would need some means of picking out a particular employee from the whole bunch, together with the data from the generic variables associated with them. This kind of requirement arises with any collection of like entities that you want to handle, whether they’re baseball players or battleships. Naturally, C++ provides you with a way to deal with this.

Arrays

One way to solve these problems is to use an array. An array is a number of memory locations called array elements or simply elements, each of which stores an item of data of the same given data type, and which are all referenced through the same variable name. The employee names in a payroll program could be stored in one array, the pay for each employee in another, and the tax due for each employee could be stored in a third array.

You select an element in an array using an index value. An index is an integer representing the sequence number of the element in the array. The first element has the index 0, the second 1, and so on. You can also envisage the index for an array element as being the offset from the first element. The first element has an offset of 0 and therefore an index of 0, and an index value of 3 will refer to the fourth element of an array.

The basic structure of an array is illustrated in Figure 4-1.

image

FIGURE 4-1

Figure 4-1 shows an array with the name height that has six elements. These might be the heights of the members of a family, for instance, recorded to the nearest inch. Because there are six elements, the index values run from 0 through 5. You refer to a particular element by writing the array name followed by the index value of the element between square brackets. The third element is height[2], for example. If you think of the index as the offset from the first element, it’s easy to see that the index for the fourth element will be 3.

The memory required to store each element is determined by its type, and all the elements of an array are stored in a contiguous block of memory.

Declaring Arrays

You define an array in essentially the same way as you defined the variables that you have seen up to now. The only difference is that you specify the number of array elements between square brackets following the array name. For example, you could define the integer array height, shown in the previous figure, with the following statement:

long height[6];

A long value occupies 4 bytes, so the whole array requires 24 bytes. Arrays can be of any size, subject to the constraints imposed by the amount of memory in the computer on which your program is running.

Arrays can be of any type. For example, to define arrays to store the capacity and power output of a series of engines, you could write:

double engine_size[10];      // Engine size in cubic inches
double horsepower[10];       // Engine power output

If auto mechanics is your thing, this would enable you to store the cubic capacity and power output of up to 10 engines, referenced by index values from 0 to 9. As you have seen with other variables, you can define several arrays of a given type in a single statement, but in practice it is almost always better to define them in separate statements.

Initializing Arrays

To initialize an array in its definition, you put the initializing values in an initializer list. Here’s an example:

int engine_size[5] { 200, 250, 300, 350, 400 };

The array has the name engine_size and has five elements that each store a value of type int. The values in the initializing list correspond to successive index values, so in this case engine_size[0] has the value 200, engine_size[1] the value 250, engine_size[2] the value 300, and so on.

You must not specify more initializing values than there are elements in the array, but you can include fewer. If there are fewer, the values are assigned to successive elements, starting with the first — which is the one corresponding to index 0. Array elements for which you don’t provide a value are initialized with zero. This isn’t the same as supplying no initializing list. Without an initializing list, the array elements contain junk values. You can initialize all array elements to zero with an empty initializer list. For example:

long data[100] {};          // Initialize all elements to zero

You can also omit the dimension of an array, provided you supply initializing values. The number of array elements will be the number of initializing values. For example:

int value[] { 2, 3, 4 };

This defines an array with three elements that have initial values 2, 3, and 4.

Using the Range-based for Loop

You have seen that you can use a for loop to iterate over all the elements in an array. The range-based for loop makes this even easier. The loop is easy to understand through an example:

double temperatures[] {65.5, 68.0, 75.0, 77.5, 76.4, 73.8,80.1};
double sum {};
int count {};
for(double t : temperatures)
{
  sum += t;
  ++count;
}
double average = sum/count;

This calculates the average of the values in the temperatures array. The parentheses following for contain two things separated by a colon; the first specifies the variable that will access each of the values from the collection specified by the second. The t variable will be assigned the value of each element in the temperatures array in turn before executing the loop body. This accumulates the sum of the array elements . The loop also accumulates the total number of elements in count so the average can be calculated after the loop.

You could also write the loop using the auto keyword:

for(auto temperature : temperatures)
{
  sum += temperature;
  ++count;
}

The auto keyword tells the compiler to determine the type for the local variable that holds the current value from the array type. The compiler knows that the array elements are of type double, so t will be of type double.

You cannot modify the values of the array elements in the range-based for loop as it is written here. You can only access the element values for use elsewhere. With the loop written as it is, element values are copied to the loop variable. You could access the array elements directly specifying the loop variable as a reference. You learn about references later in this chapter.

Multidimensional Arrays

Arrays with one index are referred to as one-dimensional arrays. You can define an array with more than one index, in which case it is a multidimensional array. Suppose you have a field in which you are growing bean plants in rows of 10, and the field contains 12 rows so there are 120 plants in all. You could define an array to record the weight of beans produced by each plant using the statement:

double beans[12][10];

This defines the two-dimensional array beans, the first index being the row number, and the second index the plant number within the row. To refer to an element requires two index values. For example, you could set the value of the element reflecting the fifth plant in the third row with the statement:

beans[2][4] = 10.7;

Remember that index values start from zero, so the row index is 2 and the index for the fifth plant within the row is 4.

Being a successful bean farmer, you might have several identical fields planted with beans in the same pattern. Assuming that you have eight fields, you could use a three-dimensional array to record data about these, defined thus:

double beans[8][12][10];

This records production for the 10 plants in each of the 12 rows in a field and the leftmost index references one of the 8 fields. If you ever get to bean farming on an international scale, you can use a four-dimensional array, with the extra dimension designating the country. Assuming that you’re as good a salesman as you are a farmer, growing this quantity of beans is likely to affect the ozone layer.

Arrays are stored in memory such that the rightmost index varies most rapidly. Thus, the array data[3][4] is three one-dimensional arrays of four elements each. The arrangement of this array is illustrated in Figure 4-2.

image

FIGURE 4-2

The elements of the array are stored in a contiguous block of memory, as indicated by the arrows in Figure 4-2. The first index selects a particular row within the array, and the second index selects an element within a row.

A two-dimensional array is really a one-dimensional array of one-dimensional arrays. An array with three dimensions is actually a one-dimensional array of elements where each element is a one-dimensional array of one-dimensional arrays. This is not something you need to worry about most of the time. However, it implies that for the array in Figure 4-2, the expressions data[0], data[1], and data[2] reference one-dimensional arrays.

Initializing Multidimensional Arrays

To initialize a multidimensional array, you use an extension of the method used for a one-dimensional array. For example, you can define and initialize a two-dimensional array, data, with the statement:

long data[2][4] {
                   { 1,  2,  3,  5 },
                   { 7, 11, 13, 17 }
                };

The initial values for each row are within their own pair of braces. Because there are four elements in each row, there are four initial values in each group, and because there are two rows, there are two groups between braces, each group of initial values being separated from the next by a comma.

You can omit initial values in any row, in which case the remaining elements in the row are zero. For example:

long data[2][4] {
                   { 1,  2,  3       },
                   { 7, 11           }
                };

I have spaced out the initial values to show where values have been omitted. The elements data[0][3], data[1][2], and data[1][3] have no initializing values and are therefore zero.

To initialize the entire array with zeros you can write:

long data[2][4] {};

If you are initializing arrays with even more dimensions, remember that you need as many nested braces for groups of initial values as there are dimensions in the array — unless you’re initializing the array with zeros.

You can let the compiler work out the first dimension in an array, but only the first, regardless of the number of dimensions.

WORKING WITH C-STYLE STRINGS

An array of char elements is called a character array and is generally used to store a C-style string. A character string is a sequence of characters with a special character appended to indicate the end of the string. This character is defined by the escape sequence . It’s referred to as the null or NUL character because it’s a byte with all bits zero. A string terminated by null is referred to as a C-style string because it originated in the C language.

This is not the only representation of a string. You’ll meet much safer representations in Chapter 8. You should avoid using C-style strings in new code, but they often occur in existing programs so you need to know about them.

Each character in a non-Unicode string occupies one byte, so with the terminating null, the number of bytes a string occupies is one more than the number of characters in the string. You can define a character array and initialize it with a string literal like this:

char movie_star[15] {"Marilyn Monroe"};     // 14 characters plus null

The terminating '' is supplied automatically. If you include one explicitly in the string literal, you’ll end up with two. You must allow for the terminating null when you specify the array dimension.

You can omit the dimension and let the compiler work it out:

char president[] {"Ulysses Grant"};

The compiler allocates enough elements to hold the characters in the string plus the terminating null, so this array will have 14 elements. Of course, if you use the array later to store a different string, the new string must not exceed 14 bytes including its terminating null character. In general, it is your responsibility to ensure that an array is large enough for any string you store in it.

You can create strings of Unicode characters, the characters in the string being of type wchar_t:

wchar_t president[] {L"Ulysses Grant"};

The L prefix indicates that the literal is a wide character string, so each character, including the terminating null, will occupy two bytes. Of course, indexing the string references characters, not bytes, so president[2] corresponds to the character L'y'.

The Unicode encoding for type wchar_t is UTF-16. There are other encodings such as UTF-8 and UTF-32. Whenever I refer to just Unicode in the book I mean UTF-16.

String Input

The iostream header contains definitions of functions for reading characters from the keyboard. The one that you’ll look at here is the getline() function that reads a sequence of characters from the keyboard and stores it in an array as a string terminated by ''. You typically use getline()like this:

const int MAX {80};               // Maximum string length including 
char name[MAX];                   // Array to store a string
cin.getline(name, MAX, '
'),     // Read input line as a string

These statements define the char array name with MAX elements and then read characters from cin using getline(). The source of the data, cin, is written as shown, with a period separating it from the function name. The period indicates that the getline() function is the one belonging to the cin object. You will learn more about this syntax when you learn about classes. Meanwhile, just take it for granted. The significance of each argument to the getline() function is shown in Figure 4-3.

image

FIGURE 4-3

Because the last argument is ' '(newline or end line character) and the second argument is MAX, characters are read from cin until the ‘ ' character is read, or when MAX-1 characters have been read, whichever occurs first. The maximum number of characters read is MAX-1 rather than MAX to allow for the '' character to be appended to the characters stored in the array. The ' ' character is generated when you press the Return key and is therefore usually the most convenient character to end input. You can specify something else by changing the last argument. The ' ' isn’t stored in the array name, but as I said, '' is stored at the end of the input string in the array.

String Literals

You have seen that you can write a string literal between double quotes and you can add L as a prefix to specify a Unicode string. You can split a long string over more than one line with each segment between double quotes. For example:

"This is a very long string that "

"has been spread over two lines."

C++ supports the use of regular expressions through the regex header. I don’t have the space to cover these in this book, but regular expressions typically involve strings with lots of backslash characters. Having to use the escape sequence for each backslash character makes regular expressions hard to enter correctly and even harder to read. The raw string literal gets over the problem. A raw string literal can contain any character, without necessitating the use of escape sequences. Here’s an example:

R"(The " " escape sequence is a tab character.)"

As a normal string literal, this would be:

"The "\t" escape sequence is a tab character."

The R indicates the start of a raw string literal and the string is delimited by "( and )". All characters between the delimiters are “as is” — escape sequences are not recognized as such. This immediately raises the question of how you include )" as part of a raw string literal. This is not a problem. The delimiters for a raw string literal in general can be "char_sequence( at the beginning and )char_sequence" at the end. char_sequence is a sequence of characters that must be the same at both ends and can be up to 16 characters; it must not contain parentheses, spaces, control characters, or backslashes. Here’s an example:

R"*("a = b*(c-d)")*" is equivalent to ""a = b*(c-d)""

The raw string contains the characters between "*( and )*". You can define a raw string of wide characters by prefixing R with L.

Using the Range-based for Loop with Strings

You can use a range-based for loop to access the characters in a string:

char text[] {"Exit signs are on the way out."};
int count {};
cout << "The string contains the following characters:" << endl;
for (auto ch : text)
{
  ++count;
  cout << ch <<  "  ";
}  
cout << endl << "The string contains " 
<< (count-1) << " characters." << endl;

The loop outputs each string character, including the null at the end, and accumulates a count of the total number of characters. The count includes the null that terminates the string so its value is reduced by 1 before output.

INDIRECT DATA ACCESS

Variables you have dealt with so far provided you with the ability to name a memory location in which you can store data of a particular type. The contents of a variable are either entered from an external source, such as the keyboard, or calculated from other values. There is another kind of variable that does not store data that you normally enter or calculate, but greatly extends the power and flexibility of your programs. This kind of variable is called a pointer.

What Is a Pointer?

Each memory location in your PC has an address. The address provides the means for the hardware to reference that location. A pointer is a variable that stores the address of another variable of a given type. A pointer has a variable name just like any other variable and also has a type that designates what kind of variables its contents refer to. Note that the type of a pointer variable includes the fact that it’s a pointer. A variable that is a pointer, that can hold an address of a location containing a value of type int, is of type ‘pointer to int'.

Declaring Pointers

A definition for a pointer is similar to that of an ordinary variable, except that the pointer name has an asterisk in front of it to indicate that it’s a pointer. For example, to define a pointer pnumber of type pointer to long, you could use the following statement:

long* pnumber;

This definition has been written with the asterisk close to the type name. If you want, you can also write it as:

long *pnumber;

The compiler won’t mind; however, the type of pnumber is ‘pointer to long', which is often indicated by placing the asterisk close to the type name. Whichever way you choose to write a pointer type, be consistent.

You can mix definitions of ordinary variables and pointers in the same statement. For example:

long* pnumber, number {99};

This defines pnumber of type ‘pointer to long' as before, and also defines the variable number, of type long. On balance, it’s probably better to define pointers separately from other variables; otherwise, the statement can appear misleading as to the type of variables defined, particularly if you prefer to place the * adjacent to the type name. The following statements certainly look clearer, and putting definitions on separate lines enables you to add comments for them individually, making for a program that is easier to read:

long number {99};    // Declaration and initialization of long variable
long* pnumber;       // Declaration of variable of type pointer to long

It’s a common convention to use variable names beginning with p to denote pointers. This makes it easier to see which variables are pointers, which in turn can make a program easier to follow.

Let’s take an example to see how this works, without worrying about what it’s for. I will get to how you use pointers very shortly. Suppose you have the long variable number containing the value 99 because you defined it in the preceding code. You could use the pointer pnumber of type pointer to long to store the address of number. But how do you obtain the address of a variable?

The Address-of Operator

What you need is the address-of operator, &. This is a unary operator that obtains the address of a variable. It’s also called the reference operator, for reasons I discuss later in this chapter. To set up the pointer, you could write this assignment statement:

pnumber = &number;            // Store address of number in pnumber

The result of this operation is illustrated in Figure 4-4.

image

FIGURE 4-4

The & operator obtains the address of any variable, but you need a pointer of the appropriate type to store it. To store the address of a double variable for example, the pointer must be of type double*, which is ‘pointer to double'.

Using Pointers

Taking the address of a variable and storing it in a pointer is all very well, but the really interesting aspect is how you can use it. Fundamental to using a pointer is accessing the data in the variable to which it points. You do this using the indirection operator, *.

The Indirection Operator

You use the indirection operator, *, to access the contents of the variable to which a pointer points. The name “indirection operator” stems from the fact that the data is accessed indirectly. It is also called the dereference operator, and the process of accessing the data in the variable pointed to by a pointer is termed de-referencing the pointer.

One aspect of this operator that can seem confusing is the fact that you now have several different uses for the same symbol, *. It is the multiply operator, it is the indirection operator, and it is used in the definition of a pointer. Each time you use *, the compiler can distinguish its meaning by the context. When you multiply two variables, A*B for instance, there’s no meaningful interpretation of this expression for anything other than a multiply operation.

Why Use Pointers?

A question that usually springs to mind at this point is, “Why use pointers at all?” After all, taking the address of a variable you already know and sticking it in a pointer so that you can dereference it seems like overhead you can do without. There are several reasons why pointers are important.

As you will see, you can use pointer notation to operate on data stored in an array. Also, when you get to define your own functions, you will see that pointers are used extensively for enabling access within a function to large blocks of data, such as arrays, that are defined outside the function. Most importantly, you will see that you can allocate space for variables dynamically — that is, during program execution. This capability allows your program to adjust its use of memory depending on the input. Because you don’t know in advance how many variables you are going to create dynamically, the way for doing this is using pointers — so make sure you get the hang of this bit.

Initializing Pointers

Using pointers that aren’t initialized is extremely hazardous. You can easily overwrite random areas of memory through an uninitialized pointer. The resulting damage depends on how unlucky you are, so it’s more than just a good idea to initialize your pointers. It’s very easy to initialize a pointer to the address of a variable that has already been defined. Here you can see that I have initialized the pointer pnumber with the address of the variable number just by using the operator & with the variable name:

int number {};                       // Initialized integer variable
int* pnumber {&number};              // Initialized pointer

When initializing a pointer with the address of another variable, remember that the variable must already have been defined prior to the pointer definition.

Of course, you may not want to initialize a pointer with the address of a specific variable when you define it. In this case, you can initialize it with the pointer equivalent of zero, nullptr, which is a pointer that doesn’t point to anything. You can define and initialize a pointer using the following statement:

int* pnumber {nullptr};              // Pointer not pointing to anything

Because nullptr is the equivalent of zero for pointers, an empty initializer list would work just as well. Setting a pointer to nullptr ensures that it doesn’t contain an address that will be accepted as valid, and provides the pointer with a value that you can check in an if statement, such as:

if(pnumber == nullptr)
   cout << endl << "pnumber does not point to anything.";

Before nullptr was added to C++, 0 or NULL (which is a macro for which the compiler will substitute 0) was used to initialize a pointer, and of course, these still work. However, it is much better to use nullptr.

Because the literal nullptr can be implicitly converted to type bool, you can check the status of the pointer pnumber like this:

if(!pnumber)
   cout << endl << "pnumber does not point to anything.";

nullptr converts the bool value to false, and any other pointer value converts to true. Thus, if pnumber contains nullptr, the if expression will be true and will cause the message to be written to the output stream.

Pointers to char

A pointer of type const char* has the interesting property that it can be initialized with a string literal. For example, you can define and initialize such a pointer with the statement:

const char* proverb {"A miss is as good as a mile."};

This looks similar to initializing a char array, but it’s quite different. This creates a string literal (actually an array of type const char[]) with the character string appearing between the quotes and terminating with '', and stores the address of the literal in the pointer proverb. The address of the literal will be the address of its first character. This is shown in Figure 4-5.

image

FIGURE 4-5

The sizeof Operator

The sizeof operator produces an integer value of type size_t that gives the number of bytes occupied by its operand, where size_t is a type defined by the standard library. Many standard library functions return a value of type size_t, and size_t is defined using a typedef statement to be equivalent to one of the fundamental types, usually unsigned int. The reason for using size_t rather than a fundamental type directly is that it allows flexibility in what the actual type is in different C++ implementations. The C++ standard permits the range of values accommodated by a fundamental type to vary, to make the best of a given hardware architecture, and size_t can be defined to be the equivalent of the most suitable fundamental type in the current machine environment.

Look at this statement that refers to dice in the previous example:

cout << sizeof dice;

The value of the expression sizeof dice is 4 because dice is type int and therefore occupies 4 bytes. Thus, this statement outputs the value 4.

The sizeof operator can be applied to an element in an array or to the whole array. When you apply the operator to an array name by itself, it produces the number of bytes occupied by the whole array, whereas when you apply it to a single element, it results in the number of bytes occupied by that element. In the last example, you could output the number of elements in pstr with the expression:

cout << (sizeof pstr)/(sizeof pstr[0]);

The expression (sizeof pstr)/(sizeof pstr[0]) divides the number of bytes occupied by the whole array, by the number of bytes occupied by the first element. Because each array element occupies the same amount of memory, the result is the number of elements in the array. The code fragment you saw earlier that computed the average for an array of temperatures could be written like this:

double temperatures[] {65.5, 68.0, 75.0, 77.5, 76.4, 73.8, 80.1};
double sum {};
for(auto t : temperatures)
  sum += t;
double average = sum/((sizeof temperatures)/(sizeof temperatures[0]));

Of course, as I noted earlier, you can use _countof() to obtain the number of array elements and this is much clearer and will result in a compile-time error message if you pass a pointer to it instead of an array name.

You can apply the sizeof operator to a type name, in which case the result is the number of bytes occupied by a variable of that type. In this case, the type name should be enclosed in parentheses. For example:

size_t long_size {sizeof(long)};

The variable long_size will be initialized with the value 4. The variable long_size is of type size_t to match the type of value produced by the sizeof operator. Using a different integer type for long_size may result in a warning message from the compiler.

Constant Pointers and Pointers to Constants

You defined pstr in Ex4_08.cpp like this:

   const char* pstr[]  { "Robert Redford",  // Initializing a pointer array
                         "Hopalong Cassidy",
                         "Lassie",
                         "Slim Pickens",
                         "Boris Karloff",
                         "Oliver Hardy"
                       };

Each pointer in the array is initialized with the address of a string literal, "Robert Redford", "Hopalong Cassidy", and so on. The type of a string literal is ‘array of const char,’ so you are storing the address of a const array in a const pointer. This prevents modification of the literal used as the initializer, which is quite a good idea. There is no ambiguity about the const-ness of the strings pointed to by the elements of the pstr pointer array. If you now attempt to change these strings, the compiler flags this as an error at compile time.

However, you could still legally write this:

pstr[0] = pstr[1];

Those lucky individuals due to be awarded Mr. Redford would get Mr. Cassidy instead since both pointers now point to the same name. Note that this isn’t changing the strings pointed to — it is changing the address stored in pstr[0]. You probably want to inhibit this kind of change as well; some people may reckon that good old Hoppy may not have the same sex appeal as Robert. You can do this with the following statement:

   // Array of constant pointers to constants
   const char* const pstr[] = { "Robert Redford",
                                "Hopalong Cassidy",
                                "Lassie",
                                "Slim Pickens",
                                "Boris Karloff",
                                "Oliver Hardy"
                              };

Now the characters in the strings cannot be modified and neither can any of the addresses in the array.

You can distinguish three situations relating to const, pointers, and the objects to which they point:

  • A pointer to a constant object
  • A constant pointer to an object
  • A constant pointer to a constant object

In the first situation, the object pointed to cannot be modified, but you can set the pointer to point to something else:

int value {5};
const int* pvalue {&value};
*pvalue = 6;                                // Will not compile!
pvalue = nullptr;                           // OK

In the second situation, the address stored in the pointer can’t be changed, but the object pointed to can be:

int value {5};
int* const pvalue {&value};
*pvalue = 6;                                // OK
pvalue = nullptr;                           // Will not compile!

Finally, in the third situation, both the pointer and the object pointed to have been defined as constant and, therefore, neither can be changed:

int value {5};
const int* const pvalue {&value};
*pvalue = 6;                                // Will not compile!
pvalue = nullptr;                           // Will not compile!

Pointers and Arrays

Array names can behave like pointers under some circumstances. In most situations, if you use the name of a one-dimensional array by itself, it is automatically interpreted as a pointer to the first array element . Note that this is not the case when the array name is used as the operand of the sizeof operator.

If you have these definitions,

double* pdata {};
double data[5];

you can write this assignment:

pdata = data;       // Initialize pointer with the array address

This assigns the address of the first element of the data to the pointer pdata. Using the array name by itself refers to the address of the array. If you use the array name data with an index value, it refers to the contents of the element corresponding to that index value. So, to store the address of an element in the pointer you use the address-of operator like this:

pdata = &data[1];

Here, pdata contains the address of the second array element.

Pointer Arithmetic

You can perform arithmetic operations with pointers. You are limited to addition and subtraction, but you can also compare pointer values to produce a logical result. Arithmetic with a pointer implicitly assumes that the pointer points to an array, and that the arithmetic operation is on the address contained in the pointer. For the pointer pdata, for example, you could assign the address of the third element of the data array to it with this statement:

pdata = &data[2];

In this case, the expression pdata+1 would refer to the address of data[3], the fourth element of the data array, so you could make pdata point to this element by writing this statement:

pdata += 1;          // Increment pdata to the next element

This increments the address in pdata by the number of bytes occupied by one element of the data array. In general, pdata+n, where n can be any expression resulting in an integer, adds n*sizeof(double) to the address in pdata, because it is of type pointer to double. This is illustrated in Figure 4-7.

image

FIGURE 4-7

In other words, incrementing or decrementing a pointer works in terms of the type of the object pointed to. Increasing a pointer to long by one changes its contents to the next long address, and so increments the address by four. Similarly, incrementing a pointer to short by one increments the address by two. The more common notation for incrementing a pointer by one is using the increment operator. For example:

pdata++;            // Increment pdata to the next element

This is equivalent to (and more common than) the += operator. However, I used += earlier to make it clear that although the increment value is specified as one, the effect is always an address increment greater than one except for the case of a pointer to type char.

You can, of course, dereference a pointer on which you have performed arithmetic (there wouldn’t be much point to it otherwise). For example, if pdata is still pointing to data[2], this statement,

*(pdata + 1) = *(pdata + 2);

is equivalent to this:

data[3] = data[4];

The parentheses are necessary when you want to dereference a pointer after incrementing the address it contains because the precedence of the indirection operator is higher than that of the arithmetic operators, + and -. If you write *pdata+1, instead of *(pdata+1), this adds one to the value stored at the address in pdata, which is equivalent to executing data[2]+1. Because this isn’t an lvalue, its use in the previous assignment statement would cause the compiler to generate an error message.

You can use an array name as though it were a pointer for addressing elements of an array. Suppose you have the array defined as:

long data[5];

Using pointer notation, you can refer to the element data[3], for example, as *(data+3). This kind of notation can be applied generally so that, corresponding to the elements data[0], data[1], data[2], you can write *data, *(data+1), *(data+2), and so on.

Using Pointers with Multidimensional Arrays

Using a pointer to store the address of a one-dimensional array is relatively straightforward, but with multidimensional arrays, things can get a little complicated. If you don’t intend to use pointers with multidimensional arrays, you can skip this section, as it’s a little obscure; however, if you have previous experience with C++, this section is worth a glance.

If you have to use a pointer with multidimensional arrays, you need to keep clear in your mind what is happening. By way of illustration, you can use an array beans, defined as follows:

double beans[3][4];

You can define and assign a value to the pointer pbeans, as follows:

double* pbeans;
pbeans = &beans[0][0];

Here, you are setting the pointer to the address of the first element of the array, which is of type double. You could also set the pointer to the address of the first row in the array with the statement:

pbeans = beans[0];

This is equivalent to using the name of a one-dimensional array, which is replaced by its address. You used this in the earlier discussion; however, because beans is a two-dimensional array, you cannot set an address in the pointer with the following statement:

pbeans = beans;           // Will cause an error!!

The problem is one of type. The type of the pointer is double*, but the array is of type double[3][4]. A pointer to store the address of this array must be of type double*[4]. C++ associates the dimensions of the array with its type, and the preceding statement is only legal if the pointer has been defined with the dimension required. This can be done with a slightly more complicated notation than you have seen so far:

double (*pbeans)[4];

The parentheses here are essential; otherwise, you would be declaring an array of pointers. Now the previous statement is legal, but this pointer can only be used to store addresses of an array with the dimensions shown. The auto keyword can help out here. You can write the statement as:

auto pbeans = beans;

Now the compiler will deduce the correct type for you.

Pointer Notation with Multidimensional Arrays

You can use pointer notation with an array name to reference elements of the array. You can reference each element of the array beans that you defined earlier, which had three rows of four elements, in two ways:

  • Using the array name with two index values
  • Using the array name in pointer notation

Therefore, the following two expressions are equivalent:

beans[i][j]
*(*(beans + i) + j)

Let’s look at how these work. The first expression uses normal array indexing to refer to the element with offset j in row i of the array.

You can determine the meaning of the second expression by working from the inside outwards. beans refers to the address of the first row of the array, so beans+i refers to row i. The expression *(beans+i) is the address of the first element of row i, so *(beans+i)+j is the address of the element in row i with offset j. The whole expression therefore refers to the value of that element.

If you really want to be obscure — and it isn’t recommended that you should be — the following two statements, where you have mixed array and pointer notation, are also legal references to the same element of the array:

*(beans[i] + j)
(*(beans + i))[j]

There is yet another aspect to using pointers that is the most important of all: the ability to allocate memory dynamically. You’ll look into that next.

DYNAMIC MEMORY ALLOCATION

Working with a fixed set of variables in a program can be very restrictive. You’ll often want to allocate space for variables at execution time, depending on the input data. Any program that processes a number of data items that is not known in advance can take advantage of the ability to allocate memory at run time. For example, in a program that stores information about the students in a class, the number of students is not fixed and their names will vary in length, so to deal with the data most efficiently, you’ll want to allocate space at execution time.

Obviously, because dynamically allocated variables can’t have been defined at compile time, they can’t be named in your code. When they are created, they are identified by their address, which you store in a pointer. With the power of pointers, and the dynamic memory management tools in Visual C++, writing your programs to have this kind of flexibility is quick and easy.

The Free Store, Alias the Heap

In most instances, when your program is executed, there is unused memory in your computer. This unused memory is called the heap, or the free store. You can allocate space within the free store for a new variable of a given type using a special operator that returns the address of the space allocated. This operator is new, and it’s complemented by the operator delete, which releases memory previously allocated by new.

You can allocate space in the free store for variables in one part of a program, and then release the space and return it to the free store after you have finished with it. This makes the memory available for reuse by other dynamically allocated variables. This is a powerful technique; it enables you to use memory very efficiently and in many cases results in programs that can handle much larger problems, involving considerably more data than otherwise might be possible.

The new and delete Operators

Suppose that you need space for a double variable. You can define a pointer to type double and then request that the memory be allocated at execution time. You can do this using the new operator:

double* pvalue {};
pvalue = new double;      // Request memory for a double variable

This is a good moment to recall that all pointers should be initialized. Using memory dynamically typically involves a number of pointers floating around, so it’s important that they should not contain spurious values. You always set a pointer that doesn’t contain a legal address value to nullptr.

The new operator in the second line of code should return the address of the memory in the free store allocated to a double variable, and this address is stored in the pointer pvalue. You can then use this pointer to reference the variable using the indirection operator, as you have seen. For example:

*pvalue = 9999.0;

Of course, the memory may not have been allocated because the free store had been used up, or because the free store is fragmented by previous usage — meaning that there aren’t sufficient contiguous bytes to accommodate the variable for which you want to obtain space. You don’t have to worry too much about this. The new operator will throw an exception if the memory cannot be allocated for any reason, which terminates your program. Exceptions are a mechanism for signaling errors in C++; you learn about these in Chapter 6.

You can initialize a variable created by new. Taking the example of the double variable that was allocated by new and the address stored in pvalue, you could have set the value to 999.0, as it was created with this statement:

pvalue = new double {999.0};   // Allocate a double and initialize it

Of course, you could create the pointer and initialize it in a single statement, like this:

double* pvalue { new double{999.0} };

When you no longer need a variable that has been dynamically allocated, you can free the memory that it occupies with the delete operator:

delete pvalue;                 // Release memory pointed to by pvalue

This ensures that the memory can be used for something else. If you don’t use delete, and you store a different address in pvalue, it will be impossible to free the memory or to use the data that it contains, because access to the address is lost. In this situation, you have what is referred to as a memory leak, especially when it recurs in your program.

You should set a pointer to nullptr when you release the memory to which it points. If you don’t, you have what is called a dangling pointer, through which you might attempt to access memory that has been freed.

Allocating Memory Dynamically for Arrays

Allocating memory for an array dynamically is very straightforward. To allocate an array of type char, you could write this statement:

char* pstr {new char[20]};     // Allocate a string of twenty characters

This allocates space for a char array of 20 characters and stores its address in pstr. To remove the array that you have just created, you use the delete operator. The statement would look like this:

delete [] pstr;                // Delete array pointed to by pstr

Note the use of square brackets to indicate that you are deleting an array. When removing arrays from the free store, you should always include the square brackets, or the results will be unpredictable. Note that you do not specify any dimensions here, simply use [].

Of course, pstr now contains the address of memory that may already have been allocated for some other purpose, so it certainly should not be used. When you use delete to discard memory you previously allocated, you should always reset the pointer, like this:

pstr = nullptr;

This ensures that you cannot access the memory that has been released.

You can initialize an array allocated in the free store:

int *data {new int[10] {2,3,4}};

This statement creates an array of 10 integer elements and initializes the first three with 2, 3, and 4. The remaining elements will be initialized to 0.

Dynamic Allocation of Multidimensional Arrays

Allocating memory in the free store for a multidimensional array involves using the new operator in a slightly more complicated form than is used for a one-dimensional array. Suppose that you define the pointer pbeans like this:

double (*pbeans)[4] {};

To obtain the space for the array beans[3][4] that you used earlier in this chapter, you could write this:

pbeans = new double [3][4];              // Allocate memory for a 3x4 array

You just specify both array dimensions between square brackets after the type name for the elements. Of course, you could do it all in one go:

double (*pbeans)[4] {new double [3][4]};

Allocating space for a three-dimensional array simply requires that you specify the extra dimension, as in this example:

auto pBigArray (new double [5][10][10]); // Allocate memory for a 5x10x10 array

This uses auto to have the pointer type determined automatically. Don’t forget — you can’t use an initializer list with auto. You could write it as:

auto pBigArray = new double [5][10][10]; // Allocate memory for a 5x10x10 array

However many dimensions there are in the array that has been created, to destroy it and release the memory back to the free store, you write the following:

delete [] pBigArray;                     // Release memory for array
pBigArray = nullptr;

You always use just one pair of square brackets following the delete operator, regardless of the dimensionality of the array.

You have already seen that you can use a variable as the specification of the dimension of a one-dimensional array to be allocated by new. This extends to two or more dimensions, but with the restriction that only the leftmost dimension may be specified by a variable. All the other dimensions must be constants or constant expressions. So, you could write this,

pBigArray = new double[max][10][10];

where max is a variable; however, specifying a variable for any dimension other than the leftmost causes an error message to be generated by the compiler.

USING REFERENCES

A reference appears to be similar to a pointer in many respects, which is why I’m introducing it here, but it really isn’t. The importance of references becomes apparent only when you get to explore their use with functions, particularly in the context of object-oriented programming. Don’t be misled by their simplicity and what might seem to be a trivial concept here. As you’ll see later, references provide some extraordinarily powerful facilities, and in some contexts enable you to achieve results that would be impossible without them.

What Is a Reference?

Essentially, a reference is a name that can be used as an alias for something else. There are two kinds of references: lvalue references and rvalue references.

An lvalue reference is an alias for another variable; it is called an lvalue reference because it refers to a persistent storage location that can appear on the left of an assignment operation. Because an lvalue reference is an alias, the variable for which it is an alias has to exist when the reference is defined. Unlike a pointer, a reference cannot be altered to represent something else.

An rvalue reference can be used as an alias for a variable, just like an lvalue reference, but it differs from an lvalue reference in that it can also reference an rvalue, which is a temporary value that is essentially transient.

Declaring and Initializing Lvalue References

Suppose that you have defined a variable as:

long number {};

You can define an lvalue reference for this variable using this statement:

long& rnumber {number};        // Declare a reference to variable number

The ampersand following the type name long and preceding the variable name rnumber, indicates that an lvalue reference is being defined, and that the variable name it represents, number, is specified as the initializing value between the parentheses; therefore rnumber is of type ‘reference to long'. You can use the reference in place of the original variable name. For example:

rnumber += 10L;

This will increment number by 10.

Note that you cannot write:

int& rfive {5};                // Will not compile!

The literal 5 is constant and cannot be changed. To protect the integrity of constant values, you must use a const reference:

const int& rfive {5};          // OK

Now you can access the literal 5 through the rfive reference. Because you define rfive as const, it cannot be used to change the value it references.

Let’s contrast the lvalue reference rnumber in the previous code with the pointer pnumber, defined in this statement:

long* pnumber {&number};       // Initialize a pointer with an address

This defines the pointer pnumber, and initializes it with the address of number. This allows number to be incremented with a statement such as:

*pnumber += 10L;               // Increment number through a pointer

There is a significant distinction between using a pointer and using a reference. You must dereference the pointer to access the variable to which it points in the expression. With a reference, there is no need for dereferencing. In some ways, a reference is like a pointer that has already been dereferenced, although it can’t be changed to reference something else. An lvalue reference is the complete equivalent of the variable for which it is a reference.

Using References in a Range-based for Loop

Earlier in this chapter you saw a code snippet using a range-based for loop to iterate over an array of temperatures:

for(auto t : temperatures)
{
  sum += t;
  ++count;
}

The t variable does not reference an array element, only its value, so you cannot use it to modify the element. However, you can by using a reference:

const double FtoC {5.0/9.0};           // Convert Fahrenheit to Centigrade
for(auto& t : temperatures)
  t = (t - 32)*FtoC;
for(auto& t : temperatures)
  cout << "  " << t;
cout << endl;

The variable t will now be of type double& and will reference each array element directly. This loop changes the values in the array from Fahrenheit to Centigrade.

Using a reference in a range-based for loop is particularly valuable when you are working with collections of objects. Copying objects can be expensive on time, so avoiding copying by using a reference type makes your code more efficient. You will learn about collections of objects in Chapter 10 when the range-based for loop comes into its own.

If you want to use references with the range-based for loop for performance reasons, but you don’t want to be able to modify the values, you can use const auto&, as in:

for (const auto& t : temperatures)
  cout << "  " << t;

Creating Rvalue References

I am explaining rvalue references here because the concept is related to that of lvalue references, but I cannot go into the significance of rvalue references at this point. Rvalue references are particularly important in the context of functions, which you’ll learn about in Chapter 5. You’ll also learn more about rvalue references in subsequent chapters.

As you know, every expression is either an rvalue or an lvalue. A variable is an lvalue because it represents a location in memory. An rvalue is different. It represents the result of evaluating an expression. Thus, an lvalue reference is a reference to a variable that has a name, and allows the contents of the memory that the variable represents to be accessed through the lvalue reference. An rvalue reference is a reference to memory containing the result of evaluating an expression.

You specify an rvalue reference type using two ampersands following the type name. Here’s an example:

int x {5};
int&& rExpr {2*x + 3};                   // rvalue reference
cout << rExpr << endl;
int& rx {x};                              // lvalue reference
cout << rx << endl;

Here, the rvalue reference is initialized to reference the result of evaluating the expression 2*x+3, which is a temporary value — an rvalue. The output will be 13. You cannot do this with an lvalue reference. Is this useful? In this case, no, indeed it is not recommended at all; but in a different context, it is very useful.

LIBRARY FUNCTIONS FOR STRINGS

The cstring standard header defines functions that operate on null-terminated strings. These are functions that are specified in the C++ standard and are defined in the std namespace. There are alternatives to some of these that are not standard and therefore not in the std namespace, but which provide a more secure implementation of the function than the original versions. In general, the secure functions have names ending with _s and I’ll use the more secure versions in examples. Let’s explore some of the most useful functions provided by the cstring header.

Finding the Length of a Null-terminated String

The strlen() function returns the length of the argument string of type char* as a value of type size_t. The wcslen() function does the same thing for strings of type wchar_t*.

Here’s how you use the strlen() function:

const char* str {"A miss is as good as a mile."};
std::cout << "The string contains " <<  std::strlen(str) << " characters.";

The output produced when this fragment executes is:

The string contains 28 characters.

As you can see from the output, the length that is returned does not include the terminating null. It is important to keep this in mind, especially when you are using the length of one string to create another.

Both strlen() and wcslen() find the length by looking for the null at the end. If there isn’t one, the functions will happily continue beyond the end of the string, checking throughout memory in the hope of finding a null. For this reason, these functions represent a security risk when you are working with data from an untrusted external source. It is generally better to use the strnlen() and wcsnlen() functions, both of which require a second argument that specifies the length of the buffer in which the string specified by the first argument is stored. For example:

char str[30] {"A miss is as good as a mile."};
std::cout << "The string contains " <<  strnlen(str, _countof(str)) 
          << " characters.";

The second argument to strnlen() is provided by the _countof() macro.

Joining Null-terminated Strings

The strcat() function that concatenates two null-terminated strings is deprecated because it is unsafe. The strcat_s() function is the safe alternative. The string specified by the second argument to strcat_s() is appended to the string specified by the first argument. Here’s an example of how you might use it:

const size_t count {30};
char str1[count] {"Many hands"};
const char* str2 {" make light work."};
        
errno_t error {strcat_s(str1, str2)};
        
if(error == 0)
    std::cout << "Strings joined successfully.
"
              << str1 << std::endl;
        
else if(error == EINVAL)
  std::cout <<"Error! Source or destination string address is a null pointer." 
            << std::endl;
        
else if(error == ERANGE)
  std::cout << "Error! Destination string too small." << std::endl;

For convenience, I defined the array size as the constant count. The first argument to strcat_s() is the destination string to which the source string specified by the second argument is to be appended. The function returns an integer value of type errno_t to indicate how things went. The return value will be zero if the operation is successful, EINVAL if the source or destination is nullptr, or ERANGE if the destination length is too small. In the event of an error, the destination will be left unchanged. The error code values EINVAL and ERANGE are defined in the cerrno header, which is included indirectly in the iostream header. Of course, you are not obliged to test for the error codes that the function might return but it is good practice.

As Figure 4-8 shows, the first character of the string specified by the second argument overwrites the terminating null of the first argument, and all the remaining characters of the second string are copied across, including the terminating null. Thus, the output from the fragment will be:

image

FIGURE 4-8

Strings joined successfully.
Many hands make light work.

The wcscat_s() function is the safe alternative to wcscat() that concatenates wide-character strings, and works in the same way as the strcat_s() function.

With the strncat_s() function you can append part of one null-terminated string to another. The first two arguments are the destination and source strings respectively, and the third argument is a count of the number of characters from the source string that are to be appended. With the strings as defined in Figure 4-8, here’s an example of using strncat_s():

  errno_t error{ strncat_s(str1, str2, 11) };

After executing this statement, str1 contains the string "Many hands make light". The operation appends 11 characters from str2 to str1, overwriting the terminating '' in str1, and then appends a final '' character. The wcsncat_s() provides the same capability as strncat_s() but for wide-character strings.

Copying Null-terminated Strings

The standard library function strcpy() copies a string from a source location to a destination. The strcpy_s() function is a more secure version of strcpy(). The first argument is a pointer to the destination, and the second is a pointer to the source string; the first argument is of type char* and the second is type const char*. strcpy_s()verifies that the source and destination are not nullptr and that the destination has sufficient space to accommodate the source string. If either argument is nullptr or the destination is too small, the program will crash and offer you the option to close the program or start debugging it, thus preventing an uncontrolled copy operation. wcscpy_s() provides analogous wide-character versions of this copy function.

Comparing Null-terminated Strings

The strcmp() function compares two null-terminated strings that you specify by arguments of type const char*. The function returns a value of type int that is less than zero, zero, or greater than zero, depending on whether the string pointed to by the first argument is less than, equal to, or greater than the string pointed to by the second argument. Here’s an example:

const char* str1 {"Jill"};
const char* str2 {"Jacko"};
int result {std::strcmp(str1, str2)};
if(result < 0)
  std::cout << str1 << " is less than " << str2 << '.' << std::endl;
else if(0 == result)
  std::cout << str1 << " is equal to " << str2 << '.' << std::endl;
else
  std::cout << str1 << " is greater than " << str2 << '.' << std::endl;

This fragment compares the strings str1 and str2, and uses the value returned by strcmp() to execute one of three possible output statements.

Comparing strings works by comparing the character codes of successive pairs of corresponding characters. The first pair of characters that are different determines whether the first string is less than or greater than the second string. Two strings are equal if they contain the same number of characters, and the corresponding characters are identical. Of course, the output is:

Jill is greater than Jacko.

The wcscmp() function is the wide-character string equivalent of strcmp().

Searching Null-terminated Strings

The strspn() function searches a string for the first character that is not in a given set and returns the index of the character found. The first argument is a pointer to the string to be searched, and the second is a pointer to a string containing the set of characters. You could search for the first character that is not a vowel like this:

char str[] {"I agree with everything."};
const char* vowels {"aeiouAEIOU "};
size_t index {std::strspn(str, vowels)};
std::cout << "The first character that is not a vowel is '" << str[index]
          << "' at position " << index << std::endl;

This searches str for the first character that is not contained in vowels. Note that I included a space in the vowels set, so a space will be ignored so far as the search is concerned. The output from this fragment is:

The first character that is not a vowel is 'g' at position 3

Another way of looking at the return value from strspn()is that it represents the length of the substring, starting from the first character in the first argument string that consists entirely of characters in the second argument string. In the example it is the first three characters "I a". The wcsspn() function is the wide-character string equivalent of strspn().

The strstr() function returns a pointer to the position in the first argument of a substring specified by the second argument. Here’s a fragment that shows this in action:

char str[] {"I agree with everything."};
const char* substring {"ever"}; 
char* psubstr {std::strstr(str, substring)};
        
if(!psubstr)
  std::cout << """ << substring << "" not found in "" << str << """ << 
std::endl;
else
  std::cout << "The first occurrence of "" << substring
            << "" in "" << str << "" is at position "
            << psubstr-str << std::endl;

The third statement calls strstr()to search str for the first occurrence of substring. The function returns a pointer to the position of the substring if it is found, or nullptr when it is not found. The if statement outputs a message, depending on whether or not substring was found in str. The expression psubstr-str gives the index position of the first character in the substring. The output produced by this fragment is:

The first occurrence of "ever" in "I agree with everything." is at position 13

SUMMARY

You are now familiar with all of the basic types of values in C++, how to create and use arrays of those types, and how to create and use pointers. You have also been introduced to the idea of a reference. However, we have not exhausted all of these topics. I’ll come back to arrays, pointers, and references later in the book.

The pointer mechanism is sometimes a bit confusing because it can operate at different levels within the same program. Sometimes it is operating as an address, and at other times it can be operating with the value stored at an address. It’s very important that you feel at ease with the way pointers are used, so if you find that they are in any way unclear, try them out with a few examples of your own until you feel confident about applying them.

EXERCISES

  1. Write a program that allows an unlimited number of values to be entered and stored in an array allocated in the free store. The program should then output the values, five to a line, followed by the average of the values entered. The initial array size should be five elements. The program should create a new array with five additional elements, when necessary, and copy values from the old array to the new.
  2. Repeat the previous exercise but use pointer notation throughout instead of arrays.
  3. Declare a character array, and initialize it to a suitable string. Use a loop to change every other character to uppercase.
    1. Hint: In the ASCII character set, values for uppercase characters are 32 less than their lowercase counterparts.
  4. Define an array of elements of type double that contains twelve arbitrary values that represent monthly average temperatures in Fahrenheit. Use a range-based for loop to convert the values to Centigrade and find and output the maximum, minimum, and average Centigrade temperatures.

WHAT YOU LEARNED IN THIS CHAPTER

TOPIC CONCEPT
Arrays An array allows you to manage a number of variables of the same type using a single name. Each dimension of an array is defined between square brackets, following the array name in the definition of the array.
Array dimensions Each dimension of an array is indexed starting from zero. Thus, the fifth element of a one-dimensional array has the index value 4.
Initializing arrays Arrays can be initialized by placing the initializing values between curly braces in the definition — in other words, in an initializer list.
Range for loop You can use the range-based for loop to iterate over each of the elements in an array.
Pointers A pointer is a variable that contains the address of another variable. A pointer is defined as a ‘pointer to type’ and may only be assigned addresses of variables of the given type.
Pointers to const and const pointers A pointer can point to a constant object. Such a pointer can be reassigned to another object. A pointer may also be defined as const, in which case it can’t be reassigned.
References A reference is an alias for something else. An lvalue reference can be used in place of the variable it references. An rvalue reference can refer to a value stored in a temporary location. A reference must be initialized when it is defined. A reference can’t be reassigned to another variable.
The sizeof operator The sizeof operator returns the number of bytes occupied by the object specified as its argument. Its argument may be a variable, or a type name between parentheses.
The new operator The new operator allocates memory in the free store. When memory has been assigned, it returns a pointer to the beginning of the memory area. If memory cannot be allocated for any reason, an exception is thrown that by default causes the program to terminate.
The delete operator You use the delete operator to release memory that you previously allocated using the new operator.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.164.164