Loops, variables, and conditionals are terrific building blocks, but there is much more to learn. The topics covered next include many features that are designed to help C++ programmers with their code as well as a few features that are often more confusing than helpful. If you are a C programmer with little C++ experience, you should read this section carefully.
Pointers and Dynamic Memory
Dynamic memory allows you to build programs with data that is not of fixed size at compile time. Most nontrivial programs make use of dynamic memory in some form.
The Stack and the Heap
Memory in your C++ application is divided into two parts — the stack and the heap. One way to visualize the stack is as a deck of cards. The current top card represents the current scope of the program, usually the function that is currently being executed. All variables declared inside the current function will take up memory in the top stack frame, the top card of the deck. If the current function, which we’ll call foo() calls another function bar(), a new card is put on the deck so that bar() has its own stack frame to work with. Any parameters passed from foo() to bar() are copied from the foo() stack frame into the bar() stack frame. The mechanics of parameter passing and stack frames are covered in Chapter 13. Figure 1-2 shows what the stack might look like during the execution of a hypothetical function foo() that has declared two integer values.
Stack frames are nice because they provide an isolated memory workspace for each function. If a variable is declared inside the foo() stack frame, calling the bar() function won’t change it unless you specifically tell it to. Also, when the foo() function is done running, the stack frame goes away, and all of the variables declared within the function no longer take up memory. The heap is an area of memory that is completely independent of the current function or stack frame. You can put variables on the heap if you want them to exist even when the function in which they were declared has completed. The heap is less structured than the stack. You can think of it as just a pile of bits. Your program can add new bits to the pile at any time or modify bits that are already in the pile.
Dynamically Allocated Arrays
Due to the way that the stack works, the compiler must be able to determine at compile time how big each stack frame will be. Since the stack frame size is predetermined, you cannot declare an array with a variable size. The following code will not compile because the arraySize is a variable, not a constant.
int arraySize = 8; int myVariableSizedArray[arraySize]; // This won’t compile!
Because the entire array must go on the stack, the compiler needs to know exactly what size it will be so variables aren’t allowed. However, it is possible to specify the size of an array at run time by using dynamic memory and placing the array in the heap instead of the stack. To allocate an array dynamically, you first need to declare a pointer:
int* myVariableSizedArray; The * after the int type indicates that the variable you are declaring refers to some integer memory in the heap. Think of the pointer as an arrow that points at the dynamically allocated heap memory. It does not yet point to anything specific because you haven’t assigned it to anything; it is an uninitialized variable.
To initialize the pointer to new heap memory, you use the new command: myVariableSizedArray = new int[arraySize];
This allocates memory for enough integers to satisfy the arraySize variable. Figure 1-3 shows what the stack and the heap both look like after this code is executed. As you can see, the pointer variable still resides on the stack, but the array that was dynamically created lives on the heap.
Stack myVariableSizedArray Heap myVariableSizedArray[0] myVariableSizedArray[1] myVariableSizedArray[2] myVariableSizedArray[3] myVariableSizedArray[4] myVariableSizedArray[5] myVariableSizedArray[6] myVariableSizedArray[7]
Some C++ compilers actually do support the preceding declaration, but is not currently a part of the C++ specification. Most compilers offer a “strict” mode that will turn off these nonstandard extensions to the language.
Now that the memory has been allocated, you can work with myVariableSizedArray as though it were a regular stack-based array: myVariableSizedArray[3] = 2; When your code is done with the array, it should remove it from the heap so that other variables can use the memory. In C++, you use the delete command to do this. delete[] myVariableSizedArray; The brackets after delete indicate that you are deleting an array.
Working with Pointers
There are other reasons to use heap memory besides dynamically allocating arrays. You can put any variable in the heap by using a similar syntax: int* myIntegerPointer = new int; In this case, the pointer points to just a single integer value. To access this value, you need to dereference the pointer. Think of dereferencing as following the pointer’s arrow to the actual value in the heap. To set the value of the newly allocated heap integer, you would use code like the following:
Notice that this is not the same as setting myIntegerPointer to the value 8. You are not changing the pointer, you are changing the memory that it points to. If you were to reassign the pointer value, it would point to the memory address 8, which is probably random garbage that will eventually make your program crash. Pointers don’t always point to heap memory. You can declare a pointer that points to a variable on the stack, even another pointer. To get a pointer to a variable, you use the & “address of” operator: int i = 8; int* myIntegerPointer = &i; // Points to the variable with the value 8 C++ has a special syntax for dealing with pointers to structures. Technically, if you have a pointer to a structure, you can access its fields by first dereferencing it with *, then using the normal . syntax, as in the code that follows, which assumes the existence of a function called getEmployee(). EmployeeT* anEmployee = getEmployee(); cout << (*anEmployee).salary << endl; The C++ commands new and delete are similar to malloc() and free() from C. The syntax of new and delete is simpler because you don’t need to know how many bytes of memory are required.
This syntax is a little messy. The -> (arrow) operator lets you perform both the dereference and the field access in one step. The following code is equivalent to the preceding code, but is easier to read. EmployeeT* anEmployee = getEmployee(); cout << anEmployee->salary << endl;
Normally, when you pass a variable into a function, you are passing by value. If a function takes an integer parameter, it is really a copy of the integer that you pass in. Pointers to stack variables are often used in C to allow functions to modify variables in other stack frames, essentially passing by reference. By dereferencing the pointer, the function can change the memory that represents the variable even though that variable isn’t in the current stack frame. This is less common in C++ because C++ has a better mechanism, called references, which is covered below.
Strings in C++
There are three ways to work with strings of text in C++. There is the C-style, which represents strings as arrays of characters; the C++ style, which wraps that representation in an easier-to-use string type; and the general class of nonstandard approaches.
C-Style Strings
A string of text like “Hello, World” is internally represented as an array of characters with the character ‘\0’ representing the end of the string. As you’ve seen, arrays and pointers are sometimes related. You could use either one to represent a string, as shown here: char arrayString[20] = “Hello, World”; char* pointerString = “Hello, World”;
For the arrayString, the compiler allocates space for 20 characters on the stack. The first 13 characters in the array are filled in with ‘H’, ‘e’, etc., ending with the character ‘\0’. The characters in positions 13 to 19 contain whatever random values happen to be in memory. The ‘\0’ character tells code that uses the string where the content of the string ends. Even though the array has a length of 20, functions that process or output the string should ignore everything after the ‘\0’ character. For the pointerString, the compiler allocates enough memory on the stack just to hold the pointer. The pointer points to an area of memory that the compiler has set aside to hold the constant string “Hello, World.” In this string, there is also a ‘\0’ character after the ‘d’ character. The C language provides a number of standard functions for working with strings, which are described in the
header file. The details of the standard library are not covered here because C++ provides a much cleaner and simpler way of working with strings.
C++ Strings
C-style strings are important to understand because they are still frequently used by C++ programmers. However, C++ includes a much more flexible string type. The string type, described by the header file, acts just like a basic type. Just like I/O streams, the string type lives in the “std” package. The example that follows shows how strings can be used just like character arrays.
A Crash Course in C++ // stringtest.cpp #include #include using namespace std; int main(int argc, char** argv) { string myString = “Hello, World”; cout << “The value of myString is “ << myString << endl; return 0; }
The magic of C++ strings is that you can use standard operators to work with them. Instead of using a function, like strcat() in C to concatenate two strings, you can simply use +. If you’ve ever tried to use the == operator to compare two C-style strings, you’ve discovered that it doesn’t work. == when used on C-style strings is actually comparing the address of the character arrays, not their contents. With C++ strings, == actually compares two strings. The example that follows shows some of the standard operators in use with C++ strings. // stringtest2.cpp #include #include using namespace std; int main(int argc, char** argv) { string str1 = “Hello”; string str2 = “World”; string str3 = str1 + “ “ + str2; cout << “str1 is “ << str1 << endl; cout << “str2 is “ << str2 << endl; cout << “str3 is “ << str3 << endl; if (str3 == “Hello World”) { cout << “str3 is what it should be.” << endl; } else { cout << “Hmmm . . . str3 isn’t what it should be.” << endl; } return (0); } The preceding examples show just a few of the many features of C++ strings. Chapter 13 goes into further detail.
Nonstandard Strings
There are several reasons why many C++ programmers don’t use C++-style strings. Some programmers simply aren’t aware of the string type because it was not always part of the C++ specification. Others have discovered over the years that the C++ string doesn’t provide the behavior they need and have developed their own string type. Perhaps the most common reason is that development frameworks and operating systems tend to have their own way of representing strings, such as the CString class in Microsoft’s MFC. Often, this is for backward compatibility or legacy issues. When starting a project in C++, it is very important to decide ahead of time how your group will represent strings.
References
The pattern for most functions is that they take in zero or more parameters, do some calculations, and return a single result. Sometimes, however, that pattern is broken. You may be tempted to return two values or you may want the function to be able to change the value of one of the variables that were passed in.
In C, the primary way to accomplish such behavior is to pass in a pointer to the variable instead of the variable itself. The only problem with this approach is that it brings the messiness of pointer syntax into what is really a simple task. In C++, there is an explicit mechanism for “pass-by-reference.” Attaching & to a type indicates that the variable is a reference. It is still used as though it was a normal variable, but behind the scenes, it is really a pointer to the original variable. Below are two implementations of an addOne() function. The first will have no effect on the variable that is passed in because it is passed by value. The second uses a reference and thus changes the original variable. void addOne(int i) { i++; // Has no real effect because this is a copy of the original } void addOne(int& i) { i++; // Actually changes the original variable } The syntax for the call to the addOne() function with an integer reference is no different than if the function just took an integer. int myInt = 7; addOne(myInt); Exceptions C++ is a very flexible language, but not a particularly safe one. The compiler will let you write code that scribbles on random memory addresses or tries to divide by zero (computers don’t deal well with infinity). One of the language features that attempts to add a degree of safety back to the language is exceptions.
An exception is an unexpected situation. For example, if you are writing a function that retrieves a Web page, several things could go wrong. The Internet host that contains the page might be down, the page might come back blank, or the connection could be lost. In many programming languages, you would handle this situation by returning a special value from the function, such as the NULL pointer. Exceptions provide a much better mechanism for dealing with problems. Exceptions come with some new terminology. When a piece of code detects an exceptional situation, it throws an exception. Another piece of code catches the exception and takes appropriate action. The following example shows a function, divideNumbers(), that throws an exception if the caller passes in a denominator of zero. #include double divideNumbers(double inNumerator, double inDenominator) { if (inDenominator == 0) { throw std::exception(); } return (inNumerator / inDenominator); } When the throw line is executed, the function will immediately end without returning a value. If the caller surrounds the function call with a try-catch block, as shown in the following code, it will receive the exception and be able to handle it. #include #include int main(int argc, char** argv) { try { std::cout << divideNumbers(2.5, 0.5) << std::endl; std::cout << divideNumbers(2.3, 0) << std::endl; } catch (std::exception exception) { std::cout << “An exception was caught!” << std::endl; } } The first call to divideNumbers() executes successfully, and the result is output to the user. The second call throws an exception. No value is returned, and the only output is the error message that is printed when the exception is caught. The output for the preceding block of code is:
An exception was caught!
Exceptions can get tricky in C++. To use exceptions properly, you need to understand what happens to the stack variables when an exception is thrown, and you have to be careful to properly catch and handle the necessary exceptions. The preceding example used the built-in std::exception exception type, but it is preferable to write your own exception types that are more specific to the error being thrown. Unlike the Java language, the C++ compiler doesn’t force you to catch every exception that might occur.
If your code never catches any exceptions but an exception is thrown, it will be caught by the program itself, which will be terminated. These trickier aspects of exceptions are covered in much more detail in
The Many Uses of const
The keyword const can be used in several different ways in C++. All of its uses are related, but there are subtle differences. One of the authors has discovered that the subtleties of const make for excellent interview questions! In Chapter 12, you will learn all of the ways that const can be used. The following sections outline the most frequent uses.
Const Constants
If you assumed that the keyword const has something to do with constants, you have correctly uncovered one of its uses. In the C language, programmers often use the preprocessor #define mechanism to declare symbolic names for values that won’t change during the execution of the program, such as the version number. In C++, programmers are encouraged to avoid #define in favor of using const to define constants. Defining a constant with const is just like defining a variable, except that the compiler guarantees that code cannot change the value. const float kVersionNumber = “2.0”; const string kProductName = “Super Hyper Net Modulator”; Const to Protect Variables
In C++, you can cast a non-const variable to a const variable. Why would you want to do this? It offers some degree of protection from other code changing the variable. If you are calling a function that a coworker of yours is writing, and you want to ensure that the function doesn’t change the value of a parameter you pass in, you can tell your coworker to have the function take a const parameter. If the function attempts to change the value of the parameter, it will not compile. In the following code, a char* is automatically cast to a const char* in the call to mysteryFunction(). If the author of mysteryFunction() attempts to change the values within the character array, the code will not compile. There are actually ways around this restriction, but using them requires conscious effort. C++ only protects against accidentally changing const variables. // consttest.cpp void mysteryFunction(const char* myString); int main(int argc, char** argv) { char* myString = new char[2]; myString[0] = ‘a’; myString[1] = ‘\0’; mysteryFunction(myString); return (0); }
Const References
You will often find code that uses const reference parameters. At first, that seems like a contradiction. Reference parameters allow you to change the value of a variable from within another context. const seems to prevent such changes. The main value in const reference parameters is efficiency. When you pass a variable into a function, an entire copy is made. When you pass a reference, you are really just passing a pointer to the original so the computer doesn’t need to make the copy. By passing a const reference, you get the best of both worlds — no copy is made but the original variable cannot be changed. const references become more important when you are dealing with objects because they can be large and making copies of them can have unwanted side effects. Subtle issues like this are covered in Chapter 12.
Click Here To Purchase This Book