Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Beginning C++ Programming

You're reading from   Beginning C++ Programming Modern C++ at your fingertips!

Arrow left icon
Product type Paperback
Published in Apr 2017
Publisher Packt
ISBN-13 9781787124943
Length 526 pages
Edition 1st Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Richard Grimes Richard Grimes
Author Profile Icon Richard Grimes
Richard Grimes
Arrow right icon
View More author details
Toc

Table of Contents (11) Chapters Close

Preface 1. Starting with C++ FREE CHAPTER 2. Understanding Language Features 3. Exploring C++ Types 4. Working with Memory, Arrays, and Pointers 5. Using Functions 6. Classes 7. Introduction to Object-Orientated Programming 8. Using the Standard Library Containers 9. Using Strings 10. Diagnostics and Debugging

Examining C++ projects

C++ projects can contain thousands of files, and managing these files can be a task. When you build the project, should a file be compiled, and if so, by which tool? In what order should the files be compiled? What output will these compilers produce? How should the compiled files be combined to produce the executable?

Compiler tools will also have a large collection of options, as diverse as debug information, types of optimization, support for different language features, and processor features. Different combinations of compiler options will be used in different circumstances (for example, release builds and debug builds). If you compile from a command line, you have to make sure you choose the right options and apply them consistently across all the source code you compile.

Managing files and compiler options can get very complicated. This is why, for production code, you should use a make tool. Two are installed with Visual Studio: MSBuild and nmake. When you build a Visual C++ project in the Visual Studio environment, MSBuild will be used and the compilation rules will be stored in an XML file. You can also call MSBuild on the command line, passing it the XML project file. The nmake tool is Microsoft's version of the program maintenance utility common across many compilers. In this chapter, you will learn how to write a simple makefile to use with the nmake utility.

Before going through the basics of project management, first we have to examine the files that you will commonly find in a C++ project, and what a compiler will do to those files.

Compilers

C++ is a high-level language, designed to give you a wealth of language facilities and to be readable for you and other developers. The computer's processor executes low-level code, and it is the purpose of the compiler to translate C++ to the processor's machine code. A single compiler may be able to target several types of processor, and if the code is standard C++, it can be compiled with other compilers that support other processors.

However, the compiler does much more than this. As explained in Chapter 4, Working With Memory, Arrays, and Pointers, C++ allows you to split your code into functions, which take parameters and return a value, so the compiler sets up the memory used to pass this data. In addition, functions can declare variables that will only be used within that function (Chapter 5, Using Functions, will give more details), and will only exist while the function is executed. The compiler sets up this memory, called a stack frame. You have compiler options about how stack frames are created; for example, the Microsoft compiler options /Gd, /Gr, and /Gz determine the order in which function arguments are pushed onto the stack and whether the caller function or called function removes the arguments from the stack at the end of the call. These options are important when you write code that will be shared (but for the purpose of this book, the default stack construction should be used). This is just one area, but it should impress upon you that compiler settings give you access to a lot of power and flexibility.

The compiler compiles C++ code, and it will issue a compiler error if it comes across an error in your code. This is syntax checking of your code. It is important to point out that the code you write can be perfect C++ code from a syntax point of view, but it can still be nonsense. The syntax checking of the compiler is an important check of your code, but you should always use other checking. For example, the following code declares an integer variable and assigns it a value:

    int i = 1 / 0;

The compiler will issue an error C2124 : divide or mod by zero. However, the following code will perform the same action using an additional variable, which is logically the same, but the compiler will issue no error:

    int j = 0; 
int i = 1 / j;

When the compiler issues an error it will stop compiling. This means two things. Firstly, you get no compiled output, so the error will not find its way into an executable. Secondly, it means that, if there are other errors in the source code, you will only find out about it once you have fixed the current error and recompiled. If you want to perform a syntax check and leave compilation to a later time, use the /Zs switch.

The compiler will also generate warning messages. A warning means that the code will compile, but there is, potentially, a problem in the code that will affect how the executable will run. The Microsoft compiler defines four levels of warnings: level 1 is the most severe (and should be addressed) and level 4 is informational.

Warnings are often used to indicate that the language feature being compiled is available, but it needs a specific compiler option that the developer has not used. During development of code, you will often ignore warnings, since you may be testing language features. However, when you get closer to producing production code you should pay more attention to warnings. By default, the Microsoft compiler will display level 1 warnings, and you can use the /W option with a number to indicate the levels that you wish to see (for example, /W2 means you wish to see level 2 warnings as well as level 1 warnings). In production code, you may use the /Wx option, which tells the compiler to treat warnings as errors so that you must fix the issues to be able to compile the code. You can also use the pragmas compiler (pragmas will be explained later) and compiler options to suppress specific warnings.

Linking the code

A compiler will produce an output. For C++ code, this will be object code, but you may have other compiler outputs, such as compiled resource files. On their own, these files cannot be executed; not least because the operating system will require certain structures to be set up. A C++ project will always be two-stage: compile the code into one or more object files and then link the object files into an executable. This means that your C++ compiler will provide another tool, called a linker.

The linker also has options to determine how it will work and specify its outputs and inputs, and it will also issue errors and warnings. Like the compiler, the Microsoft linker has an option, /WX, to treat warnings as errors in release builds.

Source files

At the very basic level, a C++ project will contain just one file: the C++ source file, typically with the extension cpp or cxx.

A simple example

The simplest C++ program is shown here:

    #include <iostream> 

// The entry point of the program
int main()
{
std::cout << "Hello, world!n";
}

The first point to make is that the line starting with // is a comment. All the text until the end of the line is ignored by the compiler. If you want to have multiline comments, every line must start with //. You can also use C comments. A C comment starts with /* and ends with */ and everything between these two symbols is a comment, including line breaks.

C comments are a quick way to comment out a portion of your code.

The braces, {}, indicates a code block; in this case, the C++ code is for the function main. We know that this is a function because of the basic format: first, there is the type of the return value, then the name of the function with a pair of parentheses, which is used to declare the parameters passed to the function (and their types). In this example, the function is called main and the parentheses are empty, indicating that the function has no parameters. The identifier before the function name (int) says that the function will return an integer.

The convention with C++ is that a function called main is the entry point of the executable, that is, when you call the executable from the command line, this will be the first function in your code that will be called.

This simple example function immediately immerses you into an aspect of C++ that irritates programmers of other languages: the language may have rules, but the rules don't always appear to be followed. In this case, the main function is declared to return an integer, but the code returns no value. The rule in C++ is that, if the function declares that it returns a value, then it must return a value. However, there is a single exception to this rule: if the main function does not return a value, then a value of 0 will be assumed. C++ contains many quirks such as this, but you will soon learn what they are and get used to them.

The main function has just one line of code; this is a single statement starting with std and ending with the semicolon (;). C++ is flexible about the use of whitespace (spaces, tabs, and newlines) as will be explained in the next chapter. However, it is important to note that you have to be careful with literal strings (as used here), and every statement is delimited with a semicolon. Forgetting a required semicolon is a common source of compiler errors. An extra semicolon is simply an empty statement, so for a novice, having too many semicolons can be less fatal to your code than having too few.

The single statement prints the string Hello, world! (and a newline) to the console. You know that this is a string because it is enclosed in double quote marks (″″). The string is put to the stream object std::cout using the operator <<. The std part of the name is a namespace, in effect, a collection of code with a similar purpose, or from a single vendor. In this case, std means that the cout stream object is part of the standard C++ library. The double colon :: is the scope resolution operator, and indicates that you want to access the cout object declared in the std namespace. You can define namespaces of your own, and in a large project you should define your own namespaces, since it will allow you to use names that may have been declared in other namespaces, and this syntax allows you to disambiguate the symbol.

The cout object is an instance of the ostream class and this has already been created for you before the main function is called. The << means that a function called operator << is called and is passed the string (which is an array of char characters). This function prints each character in the string to the console until it reaches a NUL characte.
This is an example of the flexibility of C++, a feature called operator overloading. The << operator is usually used with integers, and is used too shift the bits in the integer a specified number of places to the left; x << y will return a value which has every bit in x shifted left by y places, in effect returning a value that has been multiplied by 2y. However, in the preceding code, in place of the integer x there is the stream object std::cout, and in place of the left shift index there is a string. Clearly, this does not make any sense in the C++ definition of the << operator. The C++ standard has effectively redefined what the << operator means when used with an ostream object on the left-hand side. Furthermore, the << operator in this code will print a string to the console, and so it takes a string on the right-hand side. The C++ Standard Library defines other << operators that allow other data types to be printed to the console. They are all called the same way; the compiler determines which function is compiled dependent upon the type of the parameter used.
Earlier we said that the std::cout object had already been created as an instance of the ostream class, but gave no indication of how this has occurred. This leads us to the last part of the simple source file not already explained: the first line starting with #include. The # here effectively indicates that a message of some kind will be given to the compiler. There are various types of messages you can send (a few are #define, #ifdef, #pragma, which we will return to elsewhere in this book). In this case, #include tells the compiler to copy the contents of the specified file into the source file at this point, which essentially means the contents of that file will be compiled too. The specified file is called a header file, and is important in file management and the reuse of code through libraries.

The file <iostream> (note, no extension) is part of the Standard Library and can be found in the include directory provided with the C++ compiler. The angle brackets (<>) indicate that the compiler should look in the standard directories used to store header files, but you can provide the absolute location of a header file (or the location relative to the current file) using double quotes (″″). The C++ Standard Library uses the convention of not using file extensions. You should use the extension h (or hpp and, rarely, hxx) when naming your own header files. The C Runtime Library (which is also available to your C++ code) also uses the extension h for its header files.

Creating source files

Start by finding the Visual Studio 2017 folder on the Start Menu and click on the entry for Developer Command Prompt for VS2017. This will start a Windows command prompt and set up the environmental variables to use Visual C++ 2017. However, rather unhelpfully, it will also leave the command line in the Visual Studio folder under the Program Files folder. If you intend to do any development, you will want to move out of this folder to one where creating and deleting files will do no harm. Before you do that, move to the Visual C++ folder and list the files:

C:\Program Files\Microsoft Visual Studio\2017\Community>cd %VCToolsInstallDir%
C:\Program Files\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.0.10.2517>dir

Since the installer will place the C++ files in a folder that includes the current build of the compiler, it is safer to use the environment variable VCToolsInstallDir rather than specifying a specific version so that the latest version is used (in this case 14.0.10.2517).
There are a few things to notice. First, the folders bin, include, and lib:

Folder Description
bin This contains, indirectly, the executables for Visual C++. The bin folder will contain separate folders for the CPU type you are using, so you will have to navigate below this to get to the actual folder containing the executables. The two main executables are cl.exe, which is the C++ compiler and link.exe, which is the linker.
include This folder contains the header files for the C Runtime Library and the C++ Standard Library.
lib This folder contains the static link library files for the C Runtime Library and the C++ Standard Library. Again, there will be separate folders for the CPU type

We will refer back to these folders later in this chapter.

The other thing to point out is the file vcvarsall.bat which is under the VC\Auxillary\Build folder. When you click on Developer Command Prompt for VS2017 on the Start menu, this batch file will be run. If you wish to use an existing command prompt to compile C++ code, you can set that up by running this batch file. The three most important actions of this batch file are to set up the PATH environment variable to contain a path to the bin folder, and to set up the INCLUDE and LIB environment variables to point to the include and lib folders, respectively.

Now navigate to the root directory and create a new folder, Beginning_C++, and move to that directory. Next, create a folder for this chapter called Chapter_01. Now you can switch to Visual Studio; if this is not already running, start it from the Start menu.

In Visual Studio, click the File menu, then New, and then the File... menu item to get the New File dialog, and in the left-hand tree-view, click on the Visual C++ option. In the middle panel you'll see two options: C++ File (.cpp) and Header File (.h), and C++ properties for Open folder, as shown in the following screenshot:

The first two file types are used for C++ projects, the third type creates a JSON file to aid Visual Studio IntelliSence (help as you type) and will not be used in this book.
Click on the first of these and then click the Open button. This will create a new empty file called Source1.cpp, so save this to the chapter project folder as simple.cpp by clicking on the File menu, then Save Source1.cpp As, and, navigating to the project folder, change the name in the File name box to simple.cpp before clicking on the Save button.

Now you can type in the code for the simple program, shown as following:

    #include <iostream> 

int main()
{
std::cout << "Hello, world!n";
}

When you have finished typing this code, save the file by clicking on the File menu and then the Save simple.cpp option in the menu. You are now ready to compile the code.

Compiling the code

Go to the command prompt and type cl /? command. Since the PATH environment is set up to include the path to the bin folder, you will see the help pages for the compiler. You can scroll through these pages by pressing the Return key until you get back to the command prompt. Most of these options are beyond the scope of this book, but the following table shows some that we will talk about:

Compiler Switch Description
/c Compile only, do not link.
/D<symbol> Defines the constant or macro <symbol>.
/EHsc Enable C++ exception handling but indicate that exceptions from extern ″C″ functions (typically operating system functions) are not handled.
/Fe:<file> Provide the name of the executable file to link to.
/Fo:<file> Provide the name of the object file to compile to.
/I <folder> Provide the name of a folder to use to search for include files.
/link<linker options> Pass <linker options> to the linker. This must come after the source file name and any switches intended for the compiler.
/Tp <file> Compile <file> as a C++ file, even if it does not have .cpp or .cxx for its file extension.
/U<symbol> Remove the previously defined <symbol> macro or constant.
/Zi Enable debugging information.
/Zs Syntax only, do not compile or link.

Note that some options need spaces between the switch and option, some must not have a space, and for others, the space is optional. In general, if you have the name of a file or folder that contains a space, you should enclose the name in double quotes. Before you use a switch, it is best to consult the help files to find out how it uses spaces.

At the command line, type the cl simple.cpp command. You will find that the compiler will issue warnings C4530 and C4577. The reason is that the C++ Standard Library uses exceptions and you have not specified that the compiler should provide the necessary support code for exceptions. It is simple to overcome these warnings by using the /EHsc switch. At the command line, type the cl /EHsc simple.cpp command. If you typed in the code correctly it should compile:

C:\Beginning_C++\Chapter_01>cl /EHsc simple.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.00.25017 for x86
Copyright (C) Microsoft Corporation. All rights reserved

simple.cpp

Microsoft (R) Incremental Linker Version 14.10.25017.0
Copyright (C) Microsoft Corporation. All rights reserved.
/out:simple.exe

simple.obj

By default, the compiler will compile the file to an object file and then pass this file to the linker to link as a command-line executable with the same name as the C++ file, but with the .exe extension. The line that says /out:simple.exe is generated by the linker, and /out is a linker option.

List the contents of the folder. You will find three files: simple.cpp, the source file; simple.obj, the object file which is the output of the compiler; and simple.exe, the output of the linker after it has linked the object file with the appropriate runtime libraries. You may now run the executable by typing simple on the command line:

C:\Beginning_C++\Chapter_01>simple
Hello, World!

Passing parameters between the command-line and an executable

Earlier, you found that the main function returned a value and by default this value is zero. When your application finishes, you can return an error code back to the command line; this is so that you can use the executable in batch files and scripts, and use the value to control the flow within the script. Similarly, when you run an executable, you may pass parameters from the command line, which will affect how the executable will behave.

Run the simple application by typing the simple command on the command line. In Windows, the error code is obtained through the pseudo environment variable ERRORLEVEL, so obtain this value through the ECHO command:

C:\Beginning_C++\Chapter_01>simple
Hello, World!

C:\Beginning_C++\Chapter_01>ECHO %ERRORLEVEL%
0

To show that this value is returned by the application, change the main function to return a value other than 0 (in this case, 99, shown highlighted):

    int main() 
{
std::cout << "Hello, world!n";
return 99;
}

Compile this code and run it, and then print out the error code as shown previously. You will find that the error code is now given as 99.

This is a very basic mechanism of communication: it only allows you to pass integer values, and the scripts that call your code must know what each value means. You are much more likely to pass parameters to an application, and these will be passed through your code via parameters to the main function. Replace the main function with the following:

        int main(int argc, char *argv[]) 
{
std::cout << "there are " << argc << " parameters" <<
std::endl;
for (int i = 0; i < argc; ++i)
{
std::cout << argv[i] << std::endl;
}
}

When you write the main function to take parameters from the command line, the convention is that it has these two parameters.

The first parameter is conventionally called argc. It is an integer, and indicates how many parameters were passed to the application. This parameter is very important. The reason is that you are about to access memory through an array, and this parameter gives the limit of your access. If you access memory beyond this limit you will have problems: at best you will be accessing uninitialized memory, but at worst you could cause an access violation.

It is important that, whenever you access memory, you understand the amount of memory you are accessing and keep within its limits.

The second parameter is usually called argv and is an array of pointers to C strings in memory. You will learn more about arrays and pointers in Chapter 4, Working With Memory, Arrays, and Pointers, and about strings in Chapter 9, Using Strings, so we will not give a detailed discussion here. The square brackets ([]) indicate that the parameter is an array, and the type of each member of the array is given by the char *. The * means that each item is a pointer to memory. Normally, this would be interpreted as a pointer to a single item of the type given, but strings are different: the char * means that in the memory the pointer points to there will be zero or more characters followed by the NUL character (). The length of the string is the count of characters until the NUL character.
The third line shown here prints to the console the number of strings passed to the application. In this example, rather than using the newline escape character (n) to add a newline, we use the stream std::endl. There are several manipulators you can use, which will be discussed in Chapter 6, Classes. The std::endl manipulator will put the newline character into the output stream, and then it will flush the stream. This line shows that C++ allows you to chain the use of the << put operator into a stream. The line also shows you that the << put operator is overloaded, that is, there are different versions of the operator for different parameter types (in this case, three: one that takes an integer, used for argv, one that takes a string parameter, and another that takes manipulator as a parameter), but the syntax for calling these operators is exactly the same.

Finally, there is a code block to print out every string in the argv array, reproduced here:

    for (int i = 0; i < argc; ++i) 
{
std::cout << argv[i] << std::endl;
}

The for statement means that the code block will be called until the variable i is less than the value of argc, and after each successful iteration of the loop, the variable i is incremented (using the prefix increment operator ++). The items in the array are accessed through the square bracket syntax ([]). The value passed is an index into the array.

Notice that the variable i has a starting value of 0, so the first item accessed is argv[0], and since the for loop finishes when the variable i has a value of argc, it means that the last item in the array accessed is argv[argc-1]. This is a typical usage of arrays: the first index is zero and, if there are n items in the array, the last item has an index of n-1.

Compile and run this code as you have done before, with no parameters:

C:\Beginning_C++\Chapter_01>simple
there are 1 parameters
simple

Notice that, although you did not give a parameter, the program thinks there is one: the name of the program executable. In fact, this is not just the name, it is the command used to invoke the executable. In this case, you typed the simple command (without the extension) and got the value of the file simple as a parameter printed on the console. Try this again, but this time invoke the program with its full name, simple.exe. Now you will find the first parameter is simple.exe.
Try calling the code with some actual parameters. Type the simple test parameters command in the command line:

C:\Beginning_C++\Chapter_01>simple test parameters
there are 3 parameters
simple
test parameters

This time the program says that there are three parameters, and it has delimited them using the space character. If you want to use a space within a single parameter, you should put the entire string in double quotes:

C:\Beginning_C++\Chapter_01>simple ″test parameters″
there are 2 parameters
simple
test parameters

Bear in mind that argv is an array of string pointers, so if you want to pass in a numeric type from the command line and you want to use it as a number in your program, you will have to convert from its string representation accessed through argv.

The preprocessor and symbols

The C++ compiler takes several steps to compile a source file. As the name suggests, the compiler preprocessor is at the beginning of this process. The preprocessor locates the header files and inserts them into the source file. It also substitutes macros and defined constants.

Defining constants

There are two main ways to define a constant via the preprocessor: through a compiler switch and in code. To see how this works, let's change the main function to print out the value of a constant; the two important lines are highlighted:

    #include <iostream>  
#define NUMBER 4

int main()
{
std::cout << NUMBER << std::endl;
}

The line that starts with #define is an instruction to the preprocessor, and it says that, wherever in the text there is the exact symbol NUMBER, it should be replaced with 4. It is a text search and replace, but it will replace whole symbols only (so if there is a symbol in the file called NUMBER99 the NUMBER part will not be replaced). After the preprocessor has finished its work the compiler will see the following:

    int main() 
{
std::cout << 4 << std::endl;
}

Compile the original code and run it, and confirm that the program simply prints 4 to the console.

The text search and replace aspect of the preprocessor can cause some odd results, for example, change your main function to declare a variable called NUMBER, as follows:

    int main() 
{
int NUMBER = 99;
std::cout << NUMBER << std::endl;
}

Now compile the code. You will get an error from the compiler:

C:\Beginning_C++\Chapter_01>cl /EHhc simple.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.00.25017 for x86
Copyright (C) Microsoft Corporation. All rights reserved.

simple.cpp
simple.cpp(7): error C2143: syntax error: missing ';' before 'constant'
simple.cpp(7): error C2106: '=': left operand must be l-value

This indicates that there is an error on line 7, which is the new line declaring the variable. However, because of the search and replace conducted by the preprocessor, what the compiler sees is a line that looks as follows:

    int 4 = 99;

This is not correct C++!
In the code that you have typed, it is obvious what is causing the problem because you have a #define directive for the symbol within the same file. In practice, you will include several header files, and these may include files themselves, so the errant #define directive could be in one of many files. Equally, your constant symbols may have the same names as variables in header files included after your #define directive and may be replaced by the preprocessor.

Using #define as a way to define global constants is often not a good idea and there are much better ways in C++, as you'll see in Chapter 3, Exploring C++ Types.

If you have problems you think are coming from the preprocessor replacing symbols, you can investigate this by looking at the source file passed to the compiler after the preprocessor has done its work. To do this, compile with the /EP switch. This will suppress actual compilation and send the output of the preprocessor to stdout (the command line). Be aware that this could produce a lot of text, so it is usually better to direct this output to a file and examine that file with the Visual Studio editor.

Another way to provide the values used by the preprocessor is to pass them via a compiler switch. Edit the code and remove the line starting with #define. Compile this code as normal (cl /EHsc simple.cpp), run it, and confirm that the number printed on the console is 99, the value assigned to the variable. Now compile the code again with the following line:

cl /EHsc simple.cpp /DNUMBER=4

Note that there is no space between the /D switch and the name of the symbol. This tells the preprocessor to replace every NUMBER symbol with the text 4 and this results in the same errors as above, indicating that the preprocessor is attempting to replace the symbol with the value provided.

Tools such as Visual C++ and nmake projects will have a mechanism to define symbols through the C++ compiler. The /D switch is used to define just one symbol, and if you want to define others they will have their own /D switch.

You are now wondering why C++ has such an odd facility that appears only to cause confusing errors. Defining symbols can be very powerful once you understand what the preprocessor is doing.

Using macros

One useful feature of preprocessor symbols is macros. A macro has parameters and the preprocessor will ensure that the search and replace will replace a symbol in the macro with the symbol used as a parameter to the macro.

Edit the main function to look as follows:

    #include <iostream> 

#define MESSAGE(c, v)
for(int i = 1; i < c; ++i) std::cout << v[i] << std::endl;

int main(int argc, char *argv[])
{
MESSAGE(argc, argv);
std::cout << "invoked with " << argv[0] << std::endl;
}

The main function calls a macro called MESSAGE and passes the command line parameters to it. The function then prints the first command line parameters (the invocation command) to the console. MESSAGE is not a function, it is a macro, which means that the preprocessor will replace every occurrence of MESSAGE with two parameters with the text defined previously, replacing the c parameter with whatever is passed as the first parameter of the macro, and replacing v with whatever is used as the second parameter. After the preprocessor has finished processing the file, main will look as follows:

    int main(int argc, char *argv[]) 
{
for(int i = 1; i < argc; ++i)
std::cout << argv[i] << std::endl;
std::cout << "invoked with " << argv[0] << std::endl;
}

Note that, in the macro definition, the backslash () is used as a line-continuation character, so you can have multiline macros. Compile and run this code with one or more parameters, and confirm that MESSAGE prints out the command-line parameters.

Using symbols

You can define a symbol without a value and the preprocessor can be told to test for whether a symbol is defined or not. The most obvious situation for this is compiling different code for debug builds than for release builds.

Edit the code to add the lines highlighted here:

    #ifdef DEBUG 
#define MESSAGE(c, v)
for(int i = 1; i < c; ++i) std::cout << v[i] << std::endl;
#else
#define MESSAGE
#endif

The first line tells the preprocessor to look for the DEBUG symbol. If this symbol is defined (regardless of its value), then the first definition of the MESSAGE macro will be used. If the symbol is not defined (a release build) then the MESSAGE symbol is defined, but it does nothing: essentially, occurrences of MESSAGE with two parameters will be removed from the code.

Compile this code and run the program with one or more parameters. For example:

C:\Beginning_C++\Chapter_01>simple test parameters
invoked with simple

This shows that the code has been compiled without DEBUG defined so MESSAGE is defined to do nothing. Now compile this code again, but this time with the /DDEBUG switch to define the DEBUG symbol. Run the program again and you will see that the command-line parameters are printed on the console:

C:\Beginning_C++\Chapter_01>simple test parameters
test parameters
invoked with simple

This code has used a macro, but you can use conditional compilation with symbols anywhere in your C++ code. Symbols used in this way allow you to write flexible code and choose the code to be compiled through a symbol defined on the compiler command line. Furthermore, the compiler will define some symbols itself, for example, __DATE__ will have the current date, __TIME__ will have the current time, and __FILE__ will have the current file name.

Microsoft, and other compiler producers, defines a long list of symbols that you can access, and you are advised to look these up in the manual. A few that you may find useful are as follows: __cplusplus will be defined for C++ source files (but not for C files) so you can identify code that needs a C++ compiler; _DEBUG is set for debug builds (note the preceding underscore), and _MSC_VER has the current version of the Visual C++ compiler, so you can use the same source for various versions of the compiler.

Using pragmas

Associated with symbols and conditional compilation is the compiler directive, #pragma once. Pragmas are directives specific to the compiler, and different compilers will support different pragmas. Visual C++ defines the #pragma once to solve the problem that occurs when you have multiple header files each including similar header files. The problem is that it may result in the same items being defined more than once and the compiler will flag this as an error. There are two ways to do this, and the <iostream> header file that you include next uses both of these techniques. You can find this file in the Visual C++ include folder. At the top of the file you will find the following:

    // ostream standard header 
#pragma once
#ifndef _IOSTREAM_
#define _IOSTREAM_

At the bottom, you will find the following line:

    #endif /* _IOSTREAM_ */

First the conditional compilation: the first time this header is included, the symbol _IOSTREAM_ will not be defined, so the symbol is defined and then the rest of the file will be included until the #endif line.

This illustrates good practice when using conditional compilation. For every #ifndef, there must be a #endif, and often there may be hundreds of lines between them. It is a good idea, when you use #ifdef or #ifundef, to provide a comment with the corresponding #else and #endif, indicating the symbol it refers to.

If the file is included again then the symbol _IOSTREAM_ will be defined, so the code between the #ifndef and #endif will be ignored. However, it is important to point out that, even if the symbol is defined, the header file will still be loaded and processed because the instructions about what to do are contained within the file.

The #pragma once performs the same action as the conditional compilation, but it gets around the problem of using a symbol that could be duplicated. If you add this single line to the top of your header file, you are instructing the preprocessor to load and process this file once. The preprocessor maintains a list of the files that it has processed, and if a subsequent header tries to load a file that has already been processed, that file will not be loaded and will not be processed. This reduces the time it takes for the project to be preprocessed.
Before you close the <iostream> file, look at the number of lines in the file. For <iostream> version v6.50:0009 there are 55 lines. This is a small file, but it includes <istream> (1,157 lines), which includes <ostream> (1,036 lines), which includes <ios> (374 lines), which includes <xlocnum> (1,630 lines), and so on. The result of preprocessing could mean many tens of thousands of lines will be included into your source file even for a program that has one line of code!

Dependencies

A C++ project will produce an executable or library, and this will be built by the linker from object files. The executable or library is dependent upon these object files. An object file will be compiled from a C++ source file (and potentially one or more header files). The object file is dependent upon these C++ source and header files. Understanding dependencies is important because it helps you understand the order to compile the files in your project, and it allows you to make your project builds quicker by only compiling those files that have changed.

Libraries

When you include a file within your source file, the code within that header file will be accessible to your code. Your include file may contain whole function or class definitions (these will be covered in later chapters), but this will result in the problem mentioned previously: multiple definitions of a function or class. Instead, you can declare a class or function prototype, which indicates how calling code will call the function without actually defining it. Clearly, the code will have to be defined elsewhere, and this could be a source file or a library, but the compiler will be happy because it only sees one definition.

A library is code that has already been defined; it has been fully debugged and tested, and therefore, users should not need to have access to the source code. The C++ Standard Library is mostly shared via header files, which helps you when you debug your code, but you must resist any temptation to edit these files. Other libraries will be provided as compiled libraries.

There are essentially two types of compiled libraries: static libraries and dynamic link libraries. If you use a static library, then the compiler will copy the compiled code that you use from the static library and place it in your executable. If you use a dynamic link (or shared) library, then the linker will add information used during runtime (it may be when the executable is loaded, or it may even be delayed until the function is called) to load the shared library into memory and access the function.

Windows uses the extension lib for static libraries and dll for dynamic link libraries. GNU gcc uses the extension a for static libraries and so for shared libraries.

If you use library code in a static or dynamic link library, the compiler will need to know that you are calling a function correctly-to make sure your code calls a function with the correct number of parameters and correct types. This is the purpose of a function prototype: it gives the compiler the information it needs to know about calling the function without providing the actual body of the function, the function definition.

This book will not go into the details of how to write libraries, since it is compiler-specific; nor will it go into the details of calling library code, since different operating systems have different ways of sharing code. In general, the C++ Standard Library will be included in your code through the standard header files. The C Runtime Library (which provides some code for the C++ Standard Library) will be static-linked, but if the compiler provides a dynamic-linked version you will have a compiler option to use this.

Pre-compiled headers

When you include a file into your source file, the preprocessor will include the contents of that file (after taking into account any conditional compilation directives) and, recursively, any files included by that file. As illustrated previously, this could result in thousands of lines of code. As you develop your code, you will often compile the project so that you can test the code. Every time you compile your code, the code defined in the header files will also be compiled even though the code in library header files will not have changed. With a large project, this can make the compilation take a long time.

To get around this problem, compilers often offer an option to precompile headers that will not change. Creating and using precompiled headers is compiler-specific. For example, with the GNU C++ compiler, gcc, you compile a header as if it is a C++ source file (with the /x switch), and the compiler creates a file with an extension of gch. When gcc compiles source files that use the header it will search for the gch file, and if it finds the precompiled header, it will use that; otherwise, it will use the header file.
In Visual C++ the process is a bit more complicated because you have to specifically tell the compiler to look for a precompiled header when it compiles a source file. The convention in Visual C++ projects is to have a source file called stdafx.cpp, which has a single line that includes the file stdafx.h. You put all your stable header-file includes in stdafx.h. Next, you create a precompiled header by compiling stdafx.cpp using the /Yc compiler option to specify that stdafx.h contains the stable headers to compile. This will create a pch file (typically, Visual C++ will name it after your project) containing the code compiled up to the point of the inclusion of the stdafx.h header file. Your other source files must include the stdafx.h header file as the first header file, but they may also include other files. When you compile your source files, you use the /Yu switch to specify the stable header file (stdafx.h), and the compiler will use the precompiled header pch file instead of the header.

When you examine large projects, you will often find precompiled headers are used; as you can see, it alters the file structure of the project. The example later in this chapter will show how to create and use precompiled headers.

Project structure

It is important to organize your code into modules to enable you to maintain it effectively. Chapter 7, Introduction to Object-Orientated Programming, explains object orientation, which is one way to organize and reuse code. However, even if you are writing C-like procedural code (that is, your code involves calls to functions in a linear way) you will also benefit from organizing it into modules. For example, you may have functions that manipulate strings and other functions that access files, so you may decide to put the definition of the string functions in one source file, string.cpp, and the definition of the file functions in another file, file.cpp. So that other modules in the project can use these files, you must declare the prototypes of the functions in a header file and include that header in the module that uses the functions.

There is no absolute rule in the language about the relationship between the header files and the source files that contain the definition of the functions. You may have a header file called string.h for the functions in string.cpp; and a header file called file.h for the functions in file.cpp. Or you may have just one file called utilities.h that contains the declarations for all the functions in both files. The only rule that you have to abide by is that, at compile time, the compiler must have access to a declaration of the function in the current source file, either through a header file, or the function definition itself.
The compiler will not look forward in a source file, so if function A calls another function, B, in the same source file then function B must have already been defined before function A calls it, or there must be a prototype declaration. This leads to a typical convention of having a header file associated with each source file that contains the prototypes of the functions in the source file, and the source file includes this header. This convention becomes more important when you write classes.

Managing dependencies

When a project is built with a building tool, checks are performed to see if the output of the build exists and if not, the appropriate actions to build it are performed. The common terminology is that the output of a build step is called a target and the inputs of the build step (for example, source files) are the dependencies of that target. Each target's dependencies are the files used to make them. The dependencies may themselves be a target of a build action and have their own dependencies.

For example, the following diagram shows the dependencies in a project:

In this project, there are three source files (main.cpp, file1.cpp, and file2.cpp). Each of these includes the same header, utils.h, which is precompiled (hence there is a fourth source file, utils.cpp, that only contains utils.h). All of the source files depend on utils.pch, which in turn depends upon utils.h. The source file main.cpp has the main function and calls functions in the other two source files (file1.cpp and file2.cpp), and accesses the functions through the associated header files, file1.h and file2.h.

On the first compilation, the build tool will see that the executable depends on the four object files, and so it will look for the rule to build each one. In the case of the three C++ source files, this means compiling the cpp files, but since utils.obj is used to support the precompiled header, the build rule will be different to the other files. When the build tool has made these object files, it will then link them together along with any library code (not shown here).

Subsequently, if you change file2.cpp and build the project, the build tool will see that only file2.cpp has changed, and since only file2.obj depends on file2.cpp, all the make tool needs to do is compile file2.cpp and then link the new file2.obj with the existing object files to create the executable. If you change the header file, file2.h, the build tool will see that two files depend on this header file, file2.cpp and main.cpp, and so the build tool will compile these two source files and link the new two object files file2.obj and main.obj with the existing object files to form the executable. If, however, the precompiled header source file, util.h, changes, it means that all of the source files will have to be compiled.

For a small project, dependencies are easy to manage, and as you have seen, for a single source file project you do not even have to worry about calling the linker, because the compiler will do that automatically. As a C++ project gets bigger, managing dependencies gets more complex, and this is where development environments such as Visual C++ become vital.

Makefiles

If you are supporting a C++ project, you are likely to come across a makefile. This is a text file containing the targets, dependencies, and rules for building the targets in the project. The makefile is invoked through the make tool, nmake on Windows and make on Unix-like platforms.

A makefile is a series of rules that look as follows:

    targets : dependents 
commands

The targets are one or more files that depend on the dependents (which may be several files), so that if one or more of the dependents is newer than one or more of the targets (and hence has changed since the targets were last built), then the targets need to be built again, which is done by running the commands. There may be more than one command, and each one is on a separate line prefixed with a tab character. A target may have no dependents, in which case the commands will always be called.

For example, using the preceding example, the rule for the executable, test.exe will be as follows:

    test.exe : main.obj file1.obj file2.obj utils.obj 
link /out:test.exe main.obj file1.obj file2.obj utils.obj

Since the main.obj object file depends on the source file main.cpp, the headers File1.h and File2.h, and the precompiled header utils.pch, the rule for this file will be as follows:

    main.obj : main.cpp file1.h file2.h utils.pch 
cl /c /Ehsc main.cpp /Yuutils.h

The compiler is called with the /c switch, which indicates that the code is compiled to an object file, but the compiler should not invoke the linker. The compiler is told to use the precompiled header file utils.pch through the header file utils.h with the /Yu switch. The rules for the other two source files will be similar.

The rule to make the precompiled header file is as follows:

    utils.pch : utils.cpp utils.h 
cl /c /EHsc utils.cpp /Ycutils.h

The /Yc switch tells the compiler to create the precompiled header using the header file, utils.h.

Makefiles are often much more complicated than this. They will contain macros, which group targets, dependents, or command switches. They will contain general rules for target types rather than the specific rules shown here, and they will have conditional tests. If you need to support or write a makefile, then you should look up all of the options in the manual for the tool.

You have been reading a chapter from
Beginning C++ Programming
Published in: Apr 2017
Publisher: Packt
ISBN-13: 9781787124943
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image