Table of Contents
C++ Boot Camp - Intermediate Syntax
Written by: Milad Fatenejad
During the "Basic Syntax" portion of the boot camp, we learned about some introductory C++ concepts - many of which you've probably been exposed to in other languages. In this section, we will move onto some more interesting topics.
Example: Quadratic Formula
Using what you learned in the basic syntax section, create a program to solve an equation of the form

Using the following steps:
- Ask the user to enter the double precision numbers, a, b, and c
- Write two functions which return double precision numbers. The functions will return the two solutions, which are given by:

- Call the functions and output the solutions.
If you have trouble, my version of the program is stored in the file quadratic-single.cpp
Multi-file Programming
Large computer programs usually consist of multiple source files. In C++, writing a multi-file program is not entirely trivial. When a large C++ program is compiled, the compiler simply works its way down each file, one at a time. The compiler only uses information from the source file it is currently operating on. This is best illustrated with a simple example. The file file1.cpp contains the definition of the function area, as shown below:
double area(double x, double y) { return x*y; }
If we want to use this function in a different file, file2.cpp, then we have to declare it or the compiler will complain. That is because when the compiler compiles file2.cpp it does not know anything about file1.cpp. So file2.cpp would contain a declaration like:
double area(double x, double y);
A similar rule applies when we want to use global variables that are defined in a program. We've already seen that to define a variable, you write the variable's type followed by its name. For example, file1.cpp contains the following line outside of any curly braces:
double a;
This line instructs the compiler to create a double precision variable called a. Because this line is exterior to any curly braces, the variable is in the global scope - it is a global variable. This means that it can be accessed from outside of file1.cpp. In order to use a in file 2 we have to declare that such a variable exists - just like we did in the case of the function area. The extern keyword allows you to declare that a variable exists. If we want to use a in file2.cpp, we must include the line:
extern double a;
This tells the compiler that a variable called a exists and has type double. Thus, when a is used further down in file2.cpp the compiler will know what it is. Now imagine that our fictitious program contains many files that use the area function and variable a defined in file1.cpp. We must include the declarations in everyone of these files. This will be time consuming and can lead to errors that are difficult to track down. It would be much easier to put the definitions of area and a in a separate header file. Then we can insert the header file where ever we need it using a #include directive.
The directory quadratic-multi contains a simple multi-file program for computing the roots of a quadratic equation. There are two source files used in this program, main.cpp and roots.cpp
main.cpp
| Line | |
|---|---|
| 1 | // Compute the roots of a quadratic equation. |
| 2 | #include <iostream> |
| 3 | #include <cmath> |
| 4 | |
| 5 | #include "declarations.hpp" |
| 6 | |
| 7 | // The coefficients |
| 8 | double a, b, c; |
| 9 | |
| 10 | int main() |
| 11 | { |
| 12 | // Get the coefficients of the quadratic equation: |
| 13 | std::cout << "Enter a, b, and c:\n"; |
| 14 | std::cin >> a >> b >> c; |
| 15 | |
| 16 | // Check to make sure the roots are real: |
| 17 | if((b*b - 4*a*c) < 0) { |
| 18 | std::cout << "ERROR: Roots are not real!\n"; |
| 19 | return 1; |
| 20 | } |
| 21 | |
| 22 | std::cout << "The roots are: " |
| 23 | << root_minus() << " " << root_plus() << std::endl; |
| 24 | return 0; |
| 25 | } |
roots.cpp
| Line | |
|---|---|
| 1 | // Source file to contain functions for finding roots |
| 2 | #include <cmath> |
| 3 | |
| 4 | #include "declarations.hpp" |
| 5 | |
| 6 | double root_minus() |
| 7 | { |
| 8 | return (-b - sqrt(b*b - 4*a*c))/2/a; |
| 9 | } |
| 10 | |
| 11 | |
| 12 | double root_plus() |
| 13 | { |
| 14 | return (-b + sqrt(b*b - 4*a*c))/2/a; |
| 15 | } |
The program can be compiled by issuing the following command:
g++ main.cpp roots.cpp -o quadratic-multi
The functions root_plus and root_minus are used in main.cpp and are defined in roots.cpp, while the global variables a, b, and c are defined in main.cpp and are used in roots.cpp. For the program to compile successfully, the appropriate declarations must be placed in the two source files. They are added by including the header file declarations.hpp. This file is shown below, note that it is simply a listing of the relevant declarations.
declarations.hpp
| Line | |
|---|---|
| 1 | // Header file containing declarations for quadratic function program. |
| 2 | |
| 3 | double root_plus(); |
| 4 | double root_minus(); |
| 5 | |
| 6 | // The extern keyword is used to "declare" variables: |
| 7 | extern double a, b, c; |
Header files typically end in one of the following extensions: .h, .hpp, or .hxx - I prefer .hpp. Notice that when we compile the source code we only compile the two source files main.cpp and roots.cpp and - not the header file. That is because the header file is already a part of the source files through the #include preprocessor directive. Notice on line 4 of roots.cpp that the file name for the #include preprocessor directive is placed within quotation marks and not angle brackets, as is the case when we are including iostream. Typically, angle brackets are used only for the standard header files or built in libraries (libraries that are installed in some standard, system wide location like /usr/lib). If you are including header files that are part of your program, the convention is to write the file name in quotation marks.
Aside: Standard Library Header Files
Of course, we are already familiar with the header files associated with the standard library, like iostream. Notice that the header files that are part of the standard library do not end in and extension, such as .hpp. This was simply a design decision that the C++ standards committee made. As was stated earlier, these header files typically contain only declarations of functions and variables. If you look in iostream you will notice the following lines:
extern istream cin; ///< Linked to standard input extern ostream cout; ///< Linked to standard output extern ostream cerr; ///< Linked to standard error (unbuffered) extern ostream clog; ///< Linked to standard error (buffered)
We now know that these lines are simply declaring that the variables cin, cout, etc... exist and are telling the compiler what their types are.
Pass by Value vs. Pass by Reference
So far all of the functions we have written have involved passing arguments by value. Lets look at the example below:
passbyref.cpp
| Line | |
|---|---|
| 1 | // Simple program to demonstrate the difference between passing |
| 2 | // arguments by value and by reference |
| 3 | #include <iostream> |
| 4 | #include <cmath> |
| 5 | |
| 6 | double a = 3.0; |
| 7 | double b = -5.0; |
| 8 | double c = 0.3; |
| 9 | |
| 10 | void roots_value(double sp, double sm) |
| 11 | { |
| 12 | sp = (-b + sqrt(b*b - 4*a*c))/2.0/a; |
| 13 | sm = (-b - sqrt(b*b - 4*a*c))/2.0/a; |
| 14 | } |
| 15 | |
| 16 | void roots_ref(double &sp, double &sm) |
| 17 | { |
| 18 | sp = (-b + sqrt(b*b - 4*a*c))/2.0/a; |
| 19 | sm = (-b - sqrt(b*b - 4*a*c))/2.0/a; |
| 20 | } |
| 21 | |
| 22 | int main() |
| 23 | { |
| 24 | double solnp = 0.0, solnm = 0.0; |
| 25 | |
| 26 | roots_value(solnp, solnm); |
| 27 | std::cout << "roots_value solution = " << solnp << " " << solnm << std::endl; |
| 28 | |
| 29 | roots_ref(solnp, solnm); |
| 30 | std::cout << "roots_ref solution = " << solnp << " " << solnm << std::endl; |
| 31 | |
| 32 | return 0; |
| 33 | } |
This program generates the following output:
roots_value solution = 0 0 roots_ref solution = 1.60434 0.0623311
We define two functions in this program - roots_value and roots_ref. Both functions change the value of their arguments, but roots_value doesn't seem to have any effect on solnp and solnm in main. That is because the arguments to this function are passed by value. When we call root_value, we give it solnp and solnm as arguments. These are then copied and called sp and sm in the function. When we change sp and sm we are changing copies of solnp and solnm. The second function, roots_ref, does change the values of solnp and solnm. That is because the arguments are passed by reference - as is indicated by the '&'s on line 16.
You might pass values to a function by reference for two reasons. First, it helps in situations where you want to return more than one value from a function. For example, in the quadratic function examples above we had to create two functions to compute the roots - one computes the positive root and the other computes the negative root. But now we can have a single function and the solutions can be "returned" using the arguments if we pass them by reference. The other reason to pass values by reference is that passing them by value can be slow - especially if the argument needs a lot of memory. Image the argument is a very long string. If we passed it by value, the compiler would copy the string. This would be a relatively slow operation and would consume a lot of memory. If instead we pass by reference, no copy is made and the function call can be executed much more quickly.
Pointers and References
The data associated with all of the variables we create in our program are stored in memory. When you create a variable, the computer assigns reserves memory for it at some location or address. Internally, the computer is keeping track of the address associated with each variable so that it can be retrieved when needed. In many computer languages, the address of variables is hidden from the programmer, but this is not the case in C++. When you create a variable, you can find it's address using the reference operator: &. The following two lines of code
int a = 4; std::cout << "The variable, a, is stored at: " << &a << std::endl;
produce this output:
The variable, a, is stored at: 0x7fff6370d89c
The memory address is represented by an integer, which is printed in the example above. The "0x" in front of the address means that the number is written in hexadecimal. So the address in the example is the hexadecimal number 7fff6370d89c. Many C++ programs need to store various addresses so that they can be used later; this is accomplished using a special variable called a pointer. A pointer is a variable that is designed to hold a memory address. The syntax for assigning and manipulating pointers can be a little strange; the example below demonstrates some of this syntax:
pointer.cpp
| Line | |
|---|---|
| 1 | // Simple program to demonstrate the use of pointers |
| 2 | #include <iostream> |
| 3 | #include <string> |
| 4 | |
| 5 | void mod_pointer(int *p) |
| 6 | { |
| 7 | // Use the dereference operator to access the data at the end of the |
| 8 | // pointer: |
| 9 | *p = 3; |
| 10 | } |
| 11 | |
| 12 | int main() |
| 13 | { |
| 14 | int i1 = 7, i2 = 5; |
| 15 | |
| 16 | // Create and initialize some pointers: |
| 17 | int *p_i = &i1; |
| 18 | mod_pointer(&i1); |
| 19 | |
| 20 | // Print and change the pointer: |
| 21 | std::cout << *p_i << std::endl; |
| 22 | p_i = &i2; |
| 23 | std::cout << *p_i << std::endl; |
| 24 | |
| 25 | |
| 26 | // Create a string and a pointer to a string; |
| 27 | std::string str = "Hello, World\n;"; |
| 28 | std::string *p_str = &str; |
| 29 | |
| 30 | // If we want to use member functions we have to use the -> |
| 31 | // operator: |
| 32 | std::cout << str.size() << std::endl |
| 33 | << p_str->size() << std::endl |
| 34 | << (*p_str).size() << std::endl; |
| 35 | |
| 36 | return 0; |
| 37 | } |
Line 17 demonstrates the creation a pointer. On that line we create the variable p_i which is a pointer to an integer. We indicate that we are creating a pointer by putting an asterisk between the type and the variable name. This pointer is initialized to the address of the integer variable, i1. Notice that pointer are associated with a specific type of variable. For example, we cannot do the following:
double a; int *p = &a;
The above code would generate an error because a pointer to an integer cannot point to a double precision number. I will mention that there is a way to force the above operation to work, but I it will not be presented until the "Advanced Syntax" section. Line 18 includes a call to the function named mod_pointer. Notice that this function, defined on lines 5 through 10, takes one argument which is a pointer to an integer. Therefore, we must call the function using the referencing operator - we must use mod_pointer(&i1) on line 18, not mod_pointer(i1). The function mod_pointer contains one statement on line 9. This line demonstrates the use of the dereferencing operator, represented by an asterisk. Remember that a pointer is essentially just a big number that represents a memory location. Many times we are interested not in the memory location, but in the data stored at that location. In the case of line 9, we want to modify the integer stored at the memory address referred to by the pointer p. By modifying this data, we have also changed i1 in the function main. That is because p was pointer to the same underlying memory that was being use to store i1. So, passing pointers to functions has a similar effect to passing values by reference. On line 21, we wish to output the data stored at memory location p_i. Again, in order to access the underlying data, as opposed to the memory address, we must use the dereferencing operator. Line 21 results in the number 3 being sent to the terminal. That is because p_i pointed to i1, and i1 was modified in the function mod_pointer. Using what we've just learned, you should understand how lines 22 and 23 work.
Lines 27 and 28 again demonstrate the creation and assignment of a pointer. Line 32 illustrates how to print the size of the string, str, using the size member function. What if we wanted to access the string's size using a pointer to it? In this case p_str is a pointer to the string str. The member function size is a member of the string type - it is not a member of a the pointer to a string type. Therefore, to access the string's size using a pointer, we must dereference it first. This is shown on line 34. It can be annoying to constantly have to dereference pointers to access member functions, so C++ has a special operator, ->, which can be used for this purpose as is demonstrated on line 33. The -> operator simply tells the compiler to go to the member function associated which the pointer points to.
Aside: A Pointer is Just a Big Number
You might be asking yourself why it is necessary to specify the type of data a pointer points to. Why can't you simply create a pointer which represents any memory address. Specifying the type of data helps ensure that you don't accidentally make the pointer point to the wrong thing. For example, if you make a pointer to an int and try to make it point to a double, the compiler will give you an error, because this is probably not what you want to actually be doing. There are times, however, when you might want to create a pointer that can point to anything. In this case, you can make a pointer to void, as shown in the short example below:
int a; double b; void * p; p = &a; p = &b;
This code will generate no errors.
At the moment, it may not be clear exactly why pointers are necessary and it is difficult to come up with an example without presenting some more advanced concepts. Nevertheless, we can imagine a case where they are necessary. Imagine that there is a type called BigType which consumes a lot of memory. For example, creating one variable of type BigType will consume 100 megabytes of memory. Consider the program below, where we've introduced some new concepts.
| Line | |
|---|---|
| 1 | #include <iostream> |
| 2 | #include <string> |
| 3 | #include "BigType.h" |
| 4 | |
| 5 | BigType* p = NULL; |
| 6 | |
| 7 | int main() |
| 8 | { |
| 9 | std::string ans; |
| 10 | |
| 11 | std::cout << "Should I create a BigType?\n"; |
| 12 | std::cin >> ans; |
| 13 | |
| 14 | if(ans == "yes") { |
| 15 | p = new BigType; // Only create a BigType if the user asks us for it! |
| 16 | } |
| 17 | |
| 18 | ... // Code where we use p |
| 19 | |
| 20 | if(p) delete p; // Clear up after our selves |
| 21 | return 0; |
| 22 | } |
In this example, we only want to create a BigType if the user asks for it. If they do, then we create one and use the pointer p to keep track of it. Since p is a global variable, it can be accessed throughout the rest of our program. We'll go through this example line by line in a moment, but I just want to emphasize that this program demonstrates how we can use pointers to conditionally create variables. We only create a BigType if we need it because creating a BigType is really expensive. This is something we could not do using the tools we learned in the "basic syntax" section.
On line 5, we create the global pointer to a BigType, p. Notice that we initialize this variable to something called NULL. NULL is basically the number 0. The convention in C++ is to set pointers to NULL before they have been given a value. This ensures that someone doesn't accidentally create a pointer that isn't pointing to something. We can then test that the pointer is initialized before we use it by writing if(p) or if(p != NULL). Line 15 contains some syntax we haven't seen before. We have asked the user if they want to create a variable of type BigType and they have said "yes". So we now we have to create a variable of type BigType and use it to set our pointer p. Then the rest of the program can use our BigType variable using p. Lets say we performed this operation using the following code:
... if(ans == "yes") { BigType bt; p = &bt; } ...
The compiler would compile this code, but there is a problem. Variables declared within a certain scope are destroyed when execution leaves that scope. So, when the compiler exits the if statement, the memory associate with bt is automatically reclaimed and will be used to store other things later on. But p still points to that memory - so after exiting the body of the if statement p points to garbage. Furthermore, it is not automatically set to zero, it still points to the memory location that used to be occupied by bt. If we attempted to use p, our program would probably terminate with a Segmentation Fault.
So we need a way to tell the compiler to allocate memory and create a BigType and not to destroy that variable until we say so. That is exactly what the operators new and delete do. We can use new to create an object when we want it, as is shown on line 15. The compiler will not reclaim this memory until we tell it to do so. We might want to free the memory right before the program exits. You can use delete to tell the compiler that we no longer need the memory, as is done on line 20.
Aside: Pointers can be Tricky
From the examples above, we've seen that using pointers can easily lead to a lot of problems that are notoriously difficult to track down. For example, we might forget to use delete to free memory we claimed with new (This problem is sometimes referred to as a memory leak). We might also forget to initialize our pointer, or we may accidentally delete the underlying data while something still points to it. To address some (but not all) of these issues, C++ introduced references. Before references existed, extensive use of pointers was required to create working programs in the C language.
C++ also introduces the concept of a reference, which is similar to a pointer. To create a reference to a type, you place an ampersand between the type name and variable name. For example:
int b; int &r = b;
creates a reference to an integer. A reference is a pointer that:
- Must be initialized
- Cannot be changed
- Is used like the underlying variable (no reference or dereference operators required)
So the following lines of code generate lots of errors:
int b = 5, c = 4; int &r; // ERROR: Reference must be initialized. int &r = &b; // ERROR: References act like normal variables - no & allowed! int &r = b; // OK r = c; // OK
You might think that the last line violates point 2 above, namely that references cannot be changed. When we type r = c, we are changing not what r points to (in this case the address of the variable b) but we are changing the value of the underlying data (in other words, we are changing b). We already saw an example of how references are useful when see introduced the idea of passing values by reference. If you now reexamine that example, you will see that when you pass a value by reference, you are simply saying that the function arguments are references to some type, rather than the type itself.
It should be noted that there exists the concept of a pointer to a pointer and a reference to a pointer. Imagine for a moment you'd like a function to alter a pointer that you've passed into it. You can see that it would be nice to have a reference to a pointer. Similarly, you might imagine reasons there exist pointers to pointers. We won't pursue these peculiar notions in the bootcamp.
Vectors and Arrays
We've already seen how to create single variables, but how do we create vectors and arrays? First, there are two different types of arrays in C++ - C-style arrays and vectors. They each are essentially used to perform similar operations, namely storing a list of single items in consecutive order in memory, but vectors are significantly more user friendly than C-style arrays. They were introduced into C++ fairly recently, but before they existed, people relied solely on C-style arrays. Lets begin by looking at vectors. To use vectors, you must include the standard library header file vector. The syntax for creating and using vectors is fairly natural. Lets look at the following example:
vector.cpp
| Line | |
|---|---|
| 1 | // Simple program to demonstrate the use of vectors |
| 2 | #include <iostream> |
| 3 | #include <vector> |
| 4 | #include <string> |
| 5 | |
| 6 | |
| 7 | int main() |
| 8 | { |
| 9 | // Create a vector strings with 5 elements: |
| 10 | std::vector<std::string> vec_str(5); |
| 11 | |
| 12 | // Create an empty vector of integers: |
| 13 | std::vector<int> vec_int; |
| 14 | |
| 15 | // |
| 16 | std::cout << "vec_str.size() = " << vec_str.size() << std::endl |
| 17 | << "vec_int.size() = " << vec_int.size() << "\n\n"; |
| 18 | |
| 19 | // Set the elements of vec_str: |
| 20 | vec_str[0] = "Hel"; |
| 21 | vec_str[1] = "lo,"; |
| 22 | vec_str[2] = " Wo"; |
| 23 | vec_str[3] = "rld"; |
| 24 | vec_str[4] = "!\n"; |
| 25 | |
| 26 | // Print the vector of strings: |
| 27 | int i; |
| 28 | for(i = 0; i < vec_str.size(); i++) { |
| 29 | std::cout << vec_str[i]; |
| 30 | } |
| 31 | std::cout << std::endl; |
| 32 | |
| 33 | // Add elements to the vector of integers: |
| 34 | vec_int.push_back(2); |
| 35 | vec_int.push_back(3); |
| 36 | vec_int.push_back(5); |
| 37 | vec_int.push_back(7); |
| 38 | |
| 39 | // Subtract remove from the back of the vector: |
| 40 | vec_int.pop_back(); |
| 41 | |
| 42 | std::cout << "First element = " << vec_int.front() << std::endl |
| 43 | << "Last element = " << vec_int.back() << std::endl; |
| 44 | |
| 45 | return 0; |
| 46 | } |
The code on line 10 creates a vector which contains strings. We indicate that we want to create a vector by starting the line with std::vector. We then write the type of data we want the vector to contain within angle brackets. This notation with angled brackets is part of using templates. We haven't seen exactly what templates are yet, but we will later on. Line 10 creates a vector of strings called vec_str and initializes it to a length of 5, meaning that the vector will initially contain 5 empty strings. The code on line 13 creates a vector of integers. Since no length is provide, the vector initially contains no elements. We will see that it is possible to add elements to the vector. Lines 16 and 17 demonstrate that we can use the size member function to determine the length of a vector.
Lines 20 through 24 show us how to set the elements of a vector. In C++ we use square brackets to access an element and the elements are numbered from zero to one less than the size of the vector. Lines 28 through 30 use a loop to print the elements of the vector. The vector of integers, vec_int, is empty, so to do anything useful with it we have to add some elements. We can use the push_back member function to add an integer to the vector. After executing line 37 vec_int contains the values 2, 3, 5, and 7. You can also remove elements from the vector using the pop_back member function. After executing line 40, the vector contains the values 2, 3, and 5. Finally, lines 42 and 43 show you two more useful member functions. The function front returns the first element in the vector, while the function back returns the last element.
Aside: The Standard Template Library (STL)
The vector type that we saw above is part of something called the standard template library (STL) which is a relatively recent addition to the C++ standard library. The STL contains other containers besides the vector. There is a linked list type, std::list, a map type, std::map, a stack type, std::stack, etc... These containers can help reduce the complexity of C++ programs a great deal. If you need a common data structure like the ones listed here, check to see if it is not already included in the STL.
I've now shown you some of the basic features of the STL vector. It is an extremely powerful tool and as you learn more about C++, you will learn that there are many more features than the ones we shown here. It also has the benefit of being very easy to use. Furthermore, it is easy to grow and shrink the vectors as the program runs. There are times, however, when C-style arrays are still necessary, so I will present a brief introduction here. The following example demonstrates how to create an use C-style arrays:
carray.cpp
| Line | |
|---|---|
| 1 | // Simple example to demonstrate the use of C-style arrays |
| 2 | #include <iostream> |
| 3 | |
| 4 | int main() |
| 5 | { |
| 6 | // Create two arrays of integers which contain 5 elements: |
| 7 | int arr1[5] = {0,2,4,6,8}; |
| 8 | int arr2[5]; |
| 9 | |
| 10 | // Set the values of the array: |
| 11 | arr1[0] = 5; |
| 12 | arr1[1] = -1; |
| 13 | arr1[2] = 5; |
| 14 | |
| 15 | std::cout << *(arr1 + 1) << std::endl; |
| 16 | |
| 17 | // An array is really a pointer: |
| 18 | int n; |
| 19 | std::cout << "How big an array do you want? "; |
| 20 | std::cin >> n; |
| 21 | |
| 22 | // An example of dynamic memory allocation: |
| 23 | int *arr3 = new int[n]; |
| 24 | |
| 25 | arr3[0] = 5; |
| 26 | arr3[1] = 4; |
| 27 | arr3[2] = -1; |
| 28 | |
| 29 | // Make sure to release the memory before we quit: |
| 30 | delete [] arr3; |
| 31 | |
| 32 | return 0; |
| 33 | } |
Lines 7 and 8 demonstrate how to create a C-style array. In both cases, we are creating an array of integers which contain 5 elements. On line 7, we initialize the array to some set of integers. Lines 11 through 13 show us that accessing array elements is similar whether one is using a C-style array or a vector. A key point to understand is that a C-style array is simply a pointer to some memory. The address it points to is the address of the first element of the array. So naturally, arr1+1 points to the second element in the array. We can access the data within the second element using arr1[1] or using pointer arithmetic and writing *(arr1 + 1) as is done on line 15.
int arr3[n];
we would generate a compiler error. That is because the value of n is not known until the program is run. To initialize the C-style array we must use the syntax shown on line 23. But line 23 looks like it is initializing a pointer because there is an asterisk between the type name and the variable name. As was explained above, a C-style array is identical to a pointer. In this case, to initialize the array we must allocate memory for it manually using the new operator. If we were to write:
int *arr3 = new int;
We would create a pointer to an integer called arr3 and would also create an integer for it to point to. You can instruct the compiler to create more than one integer (or an array of integers) using the bracket notation on line 23. Lines 25 through 27 demonstrate that you can use pointers just like they are arrays. Finally, any time you use new to create something, there must be an associated delete, otherwise we may introduce a memory leak into our program. If you use new[n] to create something, you must delete it using the delete [] operator (note the empty square brackets).
C-style arrays are dangerous and can lead you to introduce many errors. First, once you create the C-style array, there is no way to find its size. The programmer must manually keep track of the size of C-style arrays. Also, consider the following example:
int *a = new int; a[20] = 5;
We created a pointer to an integer and created one integer for it to point to. Then we told the compiler to access the 20th element of the array which a represents - but a doesn't point to an array of integers, just a single integer! The compiler will compile this code even though it makes no sense. Therefore, you should use vectors whenever possible - they are easier to use and safer.
C-style Strings and Command Line Arguments
Thus far, when we have wanted to use a string we have included the string header file in our program using the #include preprocessor directive. Then we created our string using code similar to the following:
std::string str = "Hello, World!\n";
However, the std::string type has not always existed. Before std::string was added to C++, people used C-style strings. A C-style string is simply an array of characters. The built-in type which represents the character is called char. So to declare a C-style string, you can do the following:
char str1[20] = "Hello, World!\n"; char str2[] = "Hello, World!\n";
The first version allocates space for a 20 character string, and we only use the first 15 characters. The second version essentially allocates enough space to store the string to the right of the equal sign (the compiler will automatically compute the size of the character array). Since C-style strings are simply C-style arrays of characters, we can treat str1 and str2 as pointers. This can lead to unexpected behavior. For example, based on what I've told you above you might expect the following:
char str1[20] = "Hello, World!\n"; char *p = str1; std::cout << p;
to print some large hexadecimal number which represents a memory address, because p is a pointer and a pointer is just a big number. Instead, it prints, the string "Hello, World!\n". In other words, pointers to character are sometimes treated like they aren't pointers. However, in the following context:
| Line | |
|---|---|
| 1 | char str1[20] = "Hello, World!\n"; |
| 2 | char str2[20] = "Hello, World!\n"; |
| 3 | str1 = str2; |
| 4 | str2[2] = 'b'; |
str1 and str2 are treated as pointers. In other words, line 3 above forces str1 and str2 to point to different things. So if we changed the second character in str2 on line 4, we also affect str1! These examples demonstrate (and just take my word for it) that the std::string type is much, much easier to use than old C-style strings. So, why am I telling you this at all? Well, you may run into it. Also, if you want your program to use command line arguments, you need to know about C-style strings. To have your program accept command line arguments you have to declare your main function using:
int main(int argc, char * argv[])
rather than using
int main()
The former tells the compiler that main is a function that takes two arguments instead of no arguments. The first argument is the number of command line arguments. The second is an array of C-style strings, which stores the command line arguments themselves. Take a look at the following example:
command-line-args.cpp
| Line | |
|---|---|
| 1 | // Simple program to demonstrate the use of command line arguments |
| 2 | #include <iostream> |
| 3 | |
| 4 | int main(int argc, char *argv[]) |
| 5 | { |
| 6 | // Loop over all command line arguments and print each one. The |
| 7 | // first command line argument is the name of the executable: |
| 8 | |
| 9 | for(int i = 0 ; i < argc; i++) { |
| 10 | std::cout << argv[i] << std::endl; |
| 11 | } |
| 12 | |
| 13 | return 0; |
| 14 | } |
If you compile and run the program, you should see the following:
g++ command-line-args.cpp -o command-line-args ./command-line-args 5 Hello 6 command-line-args 5 Hello 6
This program just prints the command line arguments. Notice that the first argument is always the program name. For my day to day programming needs, I rarely use C-style arrays or C-style strings. Command line arguments are one of the few places where they pop up, however.
Aside: Converting std::string into a C-style String
Sometimes you have to rely upon code other people wrote - and that code may require you to use a C-style string. You can still use std::string in your program. When you need to "convert" a std::string into a C-style string, you can use the c_str() member function. Consider the following example:
std::string file_name = "input.txt" std::ifstream input_stream(file_name); // ERROR: You need a C-style string std::ifstream input_stream(file_name.c_str()); // OK
To create an input stream using a specific file, you must specify that file's name using a C-style string. For that reason, line 2 above, doesn't work, while line 3 does.
Example: Printing Arrays
Create a program that will ask users to enter an array, then output the array to the terminal. The program should do the following:
- Ask the user for the size of the array, then the array elements. The elements should be stored in the vector of integers
- Create a function that takes a vector of integers as an argument. Pass the argument by reference not by value.
- This function should output the vector of data to the screen.
- As a bonus, call the function from main, but define it in a separate source file.
My version of this program is stored in the directory array-print
const
One powerful feature of C++ involves creating variables that are "const". A const, or constant, variable cannot be changed. For example:
const int a = 5; a = 4; // Error - cannot change a const variable
You can also use const on non-built-in types, std::string, for example:
const std::string str = "Hello, World!\n"; str = "Goodbye, World!\n" // Error - cannot change a const variable
As part of the previous exercise, we wrote a function which accepted a reference to a vector of integers. That function is stored in the file output.cpp, shown below:
output.cpp
| Line | |
|---|---|
| 1 | #include <iostream> |
| 2 | #include <vector> |
| 3 | |
| 4 | void output_vec(std::vector<int>& vec) |
| 5 | { |
| 6 | for(int i = 0; i < vec.size(); i++) { |
| 7 | std::cout << vec[i] << std::endl; |
| 8 | } |
| 9 | } |
The problem with this function is that it might accidentally change the vector, vec. Because this vector is passed by reference, this could have a detrimental effect on the rest of the program. We can guarantee that the function, output_vec, will not change vec by using a const reference, instead of just a reference. This is done simply by changing line 4 to the following:
void output_vec(const std::vector<int>& vec)
Inserting the word const ensures that the vector will not be modified (Note: if you made this change, you would have to change the declaration of output_vec in array-print.cpp). So, the const keyword combined with passing values by reference vs. by value can give us information about whether function arguments are being used for output or input. Imagine that you are responsible for writing one part of a large application. Your friend has written a function you need to use which has the following declaration:
int friend_func(int a, std::string& name, const std::string& arg);
Based on this declaration we can tell that:
- a is an input variable. We know this because it is passed by value, so any changes made to it will modify the copy.
- name is an output variable. We know this because it is passed as a non-const reference.
- arg is an input variable. It is passed by reference, but it is const so we know that it won't be changed.
const Pointers
What happens if we apply the idea of const to pointers? Well...this is where things get a bit tricky. Take a look at the following example:
| Line | |
|---|---|
| 1 | int a = 4, b = 5; |
| 2 | const int *p = &a; // p is a pointer to a const int |
| 3 | p = &b; // This is OK |
| 4 | *p = 3; // ERROR: p is a pointer to a const int, so you can't change what p points to! |
At first glance, the fact that line 3 doesn't cause an error might be counterintuitive. You might be saying to yourself, "We made p const, but we just changed it!" This is not quite true...line 3 doesn't create a const pointer to an integer, it creates a non-const pointer to a const integer. Line 3 says that p is a pointer that points to something that is const. So we can't change what p points to, but we can change p itself. That is why line 4 will cause a compiler error. So, how do we tell the compiler that p is itself const? We insert the word constant between the * and the variable name. Take a look at the following short example:
| Line | |
|---|---|
| 1 | int a, b; |
| 2 | |
| 3 | int *p1 = &a; // Define a non-const pointer to non-const data |
| 4 | p1 = &b // OK - p1 is a non-const pointer |
| 5 | *p1 = 3; // OK - p1 points to non-const data |
| 6 | |
| 7 | const int *p2 = &a; // Define a non-const pointer to const data |
| 8 | p1 = &b; // OK - p1 is a non-const pointer |
| 9 | *p1 = 3; // ERROR - p1 points to const data |
| 10 | |
| 11 | int * const p2 = &a; // Define a const pointer to non-const data |
| 12 | p1 = &b; // ERROR - p1 is a non-const pointer |
| 13 | *p1 = 3; // OK - p1 points to non-const data |
| 14 | |
| 15 | const int * const p2 = &a; // Define a const pointer to const data |
| 16 | p1 = &b; // ERROR - p1 is a const pointer |
| 17 | *p1 = 3; // ERROR - p1 points to const data |
To summarize, when talking about const being applied to pointers there are two separate concepts.
- Is the pointer itself const
- Is the data that is being pointed to const
The above examples demonstrate the difference combinations of const when applied to pointers.
![(please configure the [header_logo] section in trac.ini)](/cgi-bin/hackerwithin.fcgi/chrome/site/thwlogo-small.png)