Elementary C++ : A Tutorial For Absolute Beginners In C++ Programming
Introduction
If anyone spots a mistake in this tutorial, I would appreciate it if you could email me at sgolodetz@dial.pipex.com, and I’ll endeavour to fix it. I’ve tried to make it as Standard-compliant and accurate as possible, but I’m sure the comp.lang.c++ regulars would be able to pick holes in it (partly a reflection of how much I still have to learn and partly a reflection of how much they themselves know). While we’re on the subject, I recommend visiting the newsgroup if you want to improve your C++ programming; it’s full of expert regulars who are more than willing to help out fellow programmers of all standards.
1.1 The main() function
Your console program starts and ends in the main function. The main function can be defined in a variety of ways, depending on both your personal preferences, the C++ standard and whether or not you intend to parse command line options. A fairly standard main function can be defined as follows:
int main()
{
...
return
0;
}
Technical Section |
It is very important to note here that main must return an int according to the C++ Standard. Whether or not your compiler wrongly accepts void main() as valid, if you use that then your program is non-conforming. A further point to note is that return 0 is technically not required according to the Standard, although it is generally considered good programming practice to include it. |
Note that some of the words are coloured blue: these are C++ keywords. The simple definition of the main function naturally leads on to several more complicated topics which are vital to understanding C++. You may find these next sections a little difficult at first; if so keep re-reading them until you feel comfortable with the concepts presented.
1.2 Variables
There are many intrinsic (built-in) variable types in C++, many of which you don’t need to know yet (if ever). This table summarises some of the common ones you are likely to encounter. Note that anything written in [] is optional.
char (Character) |
bool (Boolean : true / false) |
double (Double-Precision) |
float (Single-Precison) |
int (Integer) |
unsigned [int] (Unsigned Integer) |
long [int] (Long Integer) |
unsigned long [int] (Unsigned Long) |
As you can see these will need some explaining. Firstly, a floating-point number is any number which is not an integer. For example 3.14 is a floating-point number. Clearly computers cannot store such numbers to infinite decimal places, hence we have both single-precision floating-point numbers and double-precision floating-point numbers which can store numbers to varying degrees of accuracy. Using doubles is preferable on the whole, since they store more significant figures and are just as fast as floats. The only time when using floats is preferable is when memory is at a premium. The second important concept is that of signed and unsigned numbers. At this point I could go into a section about how numbers are stored in binary form but it would bore you silly, so suffice to say that signed numbers are interpreted as either positive or negative whereas unsigned numbers are interpreted as positive. This means that if we use unsigned numbers for variables which can only be positive we can store numbers roughly twice as big as if half of our “allowance” was wasted on negative numbers. To make things simple, just use signed variables for now.
Now that’s out of the way (phew!) we can get on to how to actually declare variables. Well, actually we can’t, because I need to explain the difference between declare and define first or you will get it wrong forever (I did). Basically declaring something is telling the compiler it exists, defining it is declaring it and actually initializing it with a value as well. This means that all definitions are actually declarations as well. It’s quite difficult to show the difference with variables, so I’ll return to the theme when I cover functions. For now, to define (and simultaneously declare) a variable we do something like this:
int a;
Note the semi-colon at the end of the line. It has to be there because it’s the end of a statement.
1.3 Commenting
Returning to our original program, we could now add some variables in and maybe even try changing their values with mathematical operators. Firstly, I want to introduce commenting. There are two ways to comment in C++:
1) The old C way
/* This is a comment */
/*
So is this
*/
2) The new C++ way
// This is a one-line only commenting
method
// The following statement is illegal
//
This is not a way of commenting (this statement will be read and the compiler will attempt to compile it : an error will occur)
Comments appear in green by default.
1.4 Mathematical
Operators
Here’s a program which fiddles around with a few numbers. It is heavily commented (for heavily read unnecessarily) and uses some mathematical operators which we will cover straight away afterwards:
int main()
{
int a=5; // a contains 5
int b=10; // b contains 10
int c=a*b; // c contains 50
int d=a/b; // d contains 0 (rounded down)
double
e=(double)a/(double)b; // e contains
0.5
return
0;
}
The mathematical operators which you will make most use of are +, -, * and / (where * is multiply and / is divide). You should understand most of the last program with the possible exception of the line int d=a/b; where d contains 0 despite the fact that 5 divided by 10 is ½. Simply put, integers can only contain whole numbers. If you forget this you will be in big trouble because you will get lots of weird logic errors which shouldn’t be there at all. Even if we had made d a double it would still have contained 0. The reason is that the compiler evaluates the expression and then stores it in d. It takes simply the integer part of 0.5 (i.e. 0) and puts it in d. Oh dear, we are screwed, you think. But in fact, what we actually have to do is simply convert one or more of the numerator and denominator to doubles before dividing; the result of the expression is then a double which contains the correct answer, this is then stored in e.
1.5 Displaying Text
Disclaimer: The usage of the word string in the following passage might be misleading. In this context I am referring to null-terminated C-style “strings,” rather than instances of the std::string class.
Manipulating numbers is great, don’t get me wrong, but what the heck’s the point if you can’t see them? Text in C++ can be stored in null-terminated arrays of characters (ok, nowadays most people use the Standard Library’s string class but that’s not the point here). What is an array, you ask? Examine this line of code:
int a[5];
What do you think it does? If you guessed that it creates 5 integers which can be accessed using the name a, you are perfectly right. Arrays contain a certain number of elements (however many you choose ignoring memory limitations) which are numbered from 0 onwards. For example, in the five-element array we see above, there are elements from 0 to 4. If we wanted the 3rd element, therefore, we would write a[2]. Returning to our text-based theme, if we wanted to store some text (called a string), we would use a character array which was as long as the string we wanted to put in it plus one additional byte, since as I said above strings are null-terminated which means they end in 0 (also seen as ‘\0’, the escape sequence for the null-terminator), telling the compiler that the string is finished. So, we could write the following:
char test[5] = "Test";
The compiler will add the terminating null-character for us. Now if we want to display this text on the screen, we could write:
printf("%s", test);
You can accept this at face value, but if you’re a questioning sort of person like me you’ll be wondering what the hell’s going on here.
Technical Section |
Basically printf is a function which is defined as follows: int printf(const char *format [, argument]...); If you don’t know what a function is, don’t worry, read the next section first and come back here! Just to explain, format is a string which specifies exactly what else is going to be given to the function (known as passing to the function as a parameter / argument) since printf can take any number of optional arguments as we can tell from the fact that it has an ellipsis (…) as part of its function declaration. Don’t worry how it does it (all will be explained later), just accept that you need to specify the type of the arguments you’re passing to it so that it can display them correctly. In our case, %s is the format code for a string, which is what it’s getting. |
Returning to our original example which used numbers, if we wanted to print out all our values from the program we wrote and then move onto a new line, we could simply write:
printf("%i %i %i %i %f\n", a, b, c, d, e);
As you can see, the function is nice and flexible, but also rather complex. Later on in the course we will cover stream input/output (the C++ method) which makes your life so much easier. Note that the \n is what is called the escape sequence for a line feed (ASCII character 10).
1.6 Functions
Functions are distinct subsections of a program which perform specific tasks. They can be passed variables as parameters and use them, and they can return a value. Alternatively, they can choose not to return any value, in which case they are declared as follows:
void MyFunc([parameters]);
The void keyword is useful for several things, but they are not something I’m going to deal with now. If functions return a value, they are declared thus:
type-specifier MyFunc([parameters]);
It’s easier to demonstrate than to explain, so without more ado here is a function which returns the number passed in + 5:
int AddFive(int num)
{
return
(num+5);
}
The function takes one parameter (num) of integer type and returns an integer. The return statement returns whatever follows it from the function, in this case the original number we passed in + 5. Now, say for example we wanted to write a function which added two numbers together and returned the result. How would we go about doing that? First, consider what information the function needs to be given and what information we want back from it. It needs two numbers, which we will call a and b, and it returns the sum of the two. For our example, we will impose the restriction that a and b are integers, and that therefore the function will return an integer. We will also assume for now that the result will always be within the range which can be stored by an integer (INT_MIN <= result <= INT_MAX). This function does the job:
int Add(int a, int b)
{
return
(a+b);
}
1.A Practice Tasks
You could read for a day about programming and learn nothing. To get to grips with C++, you’re going to have to write some actual code. Try these; if you get stuck look back at the above sections:
i) Write a function which multiplies two numbers together and returns the result. You are given that the two numbers are integers and therefore that is all the result needs to be.
ii) Write a function which takes two integer parameters, divides the first by the second, and returns the result. You should take account of the possibility that the result of the division will not be a whole number and adjust your code accordingly. Whilst you do not as yet know how to handle such a case, you should also realise that since dividing by zero is an error; there is a possibility that someone using your function could theoretically cause the entire program to crash. We will cover how to deal with this eventuality in later sections.
iii) Write a function which takes an integer parameter and prints it to the screen.
2.1 Pointers
Sooner or later you’re going to have to come to grips with pointers, and since I’m feeling kind you’ve just earned the right to learn about them now. Read this statement carefully: pointers are a way of indirectly accessing a variable. So, you say, that sounds cool (hmm), but how do I use them and why do they work? Examine this sample code:
int a = 5;
int *b = &a; // b contains the address of a
*b+=5; // *b = *b + 5
Weird, huh? The thing which will get you is the fact that * does not mean the same thing all the way through this code. Let’s examine each line at a time, or you’ll get really confused! The first line is really self-explanatory, so starting with the second:
int *b = &a;
The * before b tells us that it is a pointer to an integer (* in declarations means pointer). The & operator gives us the address of a in this instance. If you have no idea what I’m talking about, that’s not really surprising : pointers are damn hard to get the hang of to start with. Basically, each variable is stored internally within the computer (pointers included, they are a type of variable) in what is called a VTABLE (variable table), similar to this:
Address |
Variable
Name |
Value |
0x0123 |
a |
5 |
… |
… |
… |
0xABCD |
b |
0x0123 |
Now, address is simply where the variable is stored in the computer’s memory. So b points to a since it is a pointer to an integer (which a is) and it contains the address of a. Anyway, let’s now deal with the final line of code:
*b+=5;
There are two issues which need explaining here, the first being that x+=y is a shorthand way of writing x = x + y. The second is the meaning of * in this instance. Here *b means whatever is pointed to by b (in other words a). This is called dereferencing b since b as a pointer is referring to a indirectly and we are doing the reverse process to get the original variable. What actually happens here is that the compiler looks up the value of b and looks for the variable with that address.
2.2 Arrays Decaying to Pointers (pre-cursor to Dynamic Memory Allocation)
You may have wondered (or then again you may not) how exactly arrays work. Either way, you’re going to find out now. For example, using our earlier array, test, which if you remember was declared as follows:
char test[5];
This is where it gets a little complex. In many situations, such as when passing arrays to functions, the name of the array decays to a pointer to the first array element. So the above array would be “passed” to a function via a pointer to test[0]. This has important ramifications, especially when we consider the use of sizeof. If we work out the size of an array in the scope in which it was declared, we get its size (the number of elements multiplied by the size of an individual element), but if we pass it to a function and try to find its size using sizeof, we will end up instead calculating the size of the pointer to which the array name decayed. The important point to take away from all of this is that:
test == &test[0];
in many situations. We’ll deal with the exceptions later on.
Technical Section |
This leads us to another important point: when we write (for example) test[1] what we actually mean is *(test+1) The reasoning for this goes as follows: if test is equal to &test[0], then test[0] is equal to *test (in other words dereferencing test). If we dereference test+1, we get the next element in the array. The reason this works is because the original creators of C designed pointer addition so that incrementing a pointer by one (adding one to it) increased the pointer by the size of one element of whichever type it pointed to. So for example if we had a pointer to an integer, adding one to the pointer would increase it by the size of an integer. If you don’t quite follow this, don’t worry, it’s not necessary yet. A further interesting point is that just as we can write test[1], we can also write 1[test]. The reasoning behind this is as follows: 1[test] == *(1+test) == *(test+1) == test[1] |
2.3 Arrays of Pointers (just to be confusing)
There are occasions (for an example of this see the second parameter of the main() function right at the very beginning) where you might want to use an array of pointers. We declare an array of pointers like this:
type-specifier *array[elements];
For example, if we wanted to declare an array of ten pointers to integers, we would write the following:
int *array[10];
2.4 The Right-Left Rule
When we start getting more complicated declarations like the one above, we need some way to easily find out what’s going on. For example, of what type is array in the following example?
int *(*array)[10];
I’ll bet you don’t have a clue, because neither would I unless I knew this rule which I’m about to teach you. array is in fact a pointer to an array of ten pointers to integers. So, how do I know? Follow these steps:
1) Start with the identifier (array)
2) Read right until you encounter a bracket or reach the end of the line
3) Then read left until you encounter a bracket or reach the beginning of the line (if the latter happens you have finished)
4) Repeat steps 2 and 3 until you are done
Note that [x] is read as an array of x elements. This whole sequence is known as the right-left rule since you go first right, then left, then…you get the picture. It’s one of the most damn useful things you will ever learn in C++, so remember it well.
2.5 Dynamic Memory Allocation
Hold on to your seats! This is the interesting bit you’ve been waiting for all this time. Firstly, you need to understand something about all the variables you’ve been creating so far. They are what is known as statically-allocated, that is they were created by you at the start of the program (at “design-time”). Sometimes, you don’t know how big you want something to be at this stage, you only find out when you’re in the program (at “run-time”). This is the problem which dynamic memory allocation solves. For example, say we wished to read in a load of integers from a file (I’m not going to show you how to do that yet, but it’s a good example), then we would need an array of integers to hold those we read in. Ah, you say, that’s easy, I know exactly how to make arrays because we covered them above. Hold on Sherlock! Say you create your array of five integers (feeling very proud as you do so). Now I proceed to write six integers into a file and try and feed it into your program. Oh dear, it’s a cock-up… Even if your program doesn’t crash, the results may not be pretty. What we need is an array which we can allocate at run-time. We do this as follows:
int *dynamic_array = new int[num_elements];
This means that dynamic_array now points to the first element of a dynamically-allocated array of num_elements elements. But this is not enough for the master sleuth. If we do this too much, we’ll run out of memory. We need a way of cleaning up after ourselves. This is provided by the delete function:
delete [] dynamic_array;
Notice the use of the square brackets. This tells the compiler that we are deleting an array.
Technical Section |
In C, memory was allocated using the commands malloc()
and free(). The malloc() function had a return type of void * (this is a
pointer which can point to anything whatsoever) and then had to be typecast
(more on this later) to the required type. You are heading for serious
trouble if you try and allocate memory with malloc() and release it with
delete or allocate it with new and release it with free(). Never use these
obsolete functions and you will be fine; if you’re feeling tempted to try
them out, don’t. |
It’s about time I showed you a complete program illustrating some of the concepts I’ve covered so far. Once again, I’ve commented it heavily, hopefully it should clarify anything you’re having difficulty understanding. Note that to use the printf() function we need to include a C header file (we use the C++ Standard header but the functions are inherited from C): we will cover the use of header files later in the course.
#include <cstdio> // makes printf() available – IMPORTANT!
using std::printf;
/*
===========================
Add
Adds two numbers together
and returns the result.
===========================
*/
int Add(int a, int b)
{
return
(a+b);
}
/*
===========================
main
Entry and exit point for
our program.
===========================
*/
int main()
{
// Allocate
a new array of three integers.
int *array = new int[3];
// Fill the array with values.
array[0] = 5;
array[1] = 10;
// Call the
add function to add the two elements.
array[2] = Add(array[0], array[1]);
// Print out
the three array values.
printf("%i %i %i\n", array[0], array[1], array[2]);
// Clear up
the memory which the array is taking up.
delete [] array;
// End the
program.
return
0;
}
Notice that the Add() function appeared above the main() function so that the latter knows it exists. If we had placed it below main(), we would have had to place what is known as a function prototype (also known as a declaration) above main(). A prototype for Add() would look as follows:
int Add(int a, int b);
As you can see this is exactly the same as the way we defined Add() above, but without the function body (the bit enclosed in braces) and with a semi-colon tagged onto the end. The more observant among you will notice that this is simply the way we said that functions were declared (above).
2.6 Pointers to Functions
As well as declaring pointers which can point to variables, you can also declare pointers to functions. Say you wanted to create a pointer to a function which took no parameters and returned an integer. This is *not* the correct way to do it:
int *func();
If you thought it would be this, congratulations, you have just learned how to make a function returning a pointer to an integer. As you can tell if you evaluate it using the Right-Left Rule (above), this is not what we want. Instead, we want this:
int (*func)();
As you can see, func is now a pointer to a function returning an integer, which is what we wanted. Frankly, you won’t need pointers to functions most of the time, but it just shows you what can be done. Later in the course, we’ll also cover pointers to member functions, which require a slightly different syntax.
2.A Practice Tasks
i) Write a complete program which creates a dynamic array of size 3 of integers, fills in the first two elements with values you choose, calls a function (which takes two integer parameters and returns an integer result) to multiply the two elements and stores the result in the third element. This result should then be output and the program terminated.
ii) Determine the type of variables a-c (hint: they are closely interrelated):
int (*a)();
int (*b())();
int (*(*c)())();
iii) Write the declaration of d, an array of size 10 of pointers to functions taking no parameters and returning integers. This is not easy, and using the Right-Left Rule is essential. If you can’t do it, don’t worry, it’s only an exercise.
iv) (Bonus Task) Write a program which has two functions, one which adds two integers and returns an integer result and one which does the same for subtracting. Create a function pointer to point to either of these functions, choose one which it will point to and call it by dereferencing the pointer (note this needs to be in brackets) and then calling the function pointed to. Because this is such a difficult task, I have provided the following solution. Try to understand it but don’t worry if you can’t.
#include <cstdio> // makes
printf() available
using std::printf;
int Add(int a, int b)
{
return (a+b);
}
int Subtract(int a, int b)
{
return (a-b);
}
int main()
{
// func: pointer to function taking two ints and returning
int
int (*func)(int,int);
// func points to Subtract (this is just an example here)
func = &Subtract;
// iResult is value returned from the function pFunc points
to
int result = (*func)(5, 10);
printf("%i\n", result);
return 0;
}
Interestingly, you can simply say func = Subtract and int result = func(5, 10) since they are identical to the above forms as far as the compiler is concerned. These latter two are easier to read but tend to make you treat them as aliases to the functions rather than pointers, which has the potential to lead to nasty bugs. In general, the way shown in the code sample is the more logical way of going about things. Also, when we come to use pointers to member functions, we’ll have to do it this way in any case.
3.1 Header Files – The Basics
I promised I’d come back to header files and this is the section where they finally get the brief treatment they deserve at this stage. Basically, you can place function prototypes in header files rather than at the top of your source file, and then include them with the pre-processor directive #include. Unsurprisingly, this includes the contents of the header file in your source file. All you need to know at this stage is that the standard header files which come with the compiler should be enclosed in <> in the #include command to tell the compiler that they are located in one of its standard directories; any header files that you create should simply be enclosed in "".
3.2 Decision-Making – The Basics
Often we need to make decisions based on the value of a particular variable. This section is not going to cover many aspects of decision-making, but it will give you as much information as you need for the moment. As an example, say we had an integer variable num, and we wanted to check whether it’s value was between 0 and 4, or between 5 and 10. If it was between 0 and 5, we would display, “num is between 0 and 4” and likewise for the other case. We would express this as follows:
if ((num >= 0) && (num <= 4))
printf("num is between 0 and 4");
else if ((num >= 5) && (num <= 10))
printf("num is between 5 and 10");
The only complicated thing about this is to determine what the && operator does. In this case it means “and,” so what we are really saying is if the first condition and the second condition are true then the whole expression is true, and if the whole expression is true then execute the following statement.
3.3 Passing Pointers as Parameters to Functions
Up till now, we have simply been writing functions which return a value. What happens if we wanted to change the value passed into the function without specifically returning a value? As an example, you might think we could simply write the following to increment num by increment:
void IncrementWrong(int num, int increment)
{
num+=increment;
}
Now, there is something wrong with this which you may not be able to spot unless you have some previous programming knowledge. When values are passed to functions, a copy of them is first made on what is known as the stack and it is this copy which is then passed to the function (this is known as passing by value). Although you change the copy within the function, the original remains unchanged so the function doesn’t work as expected. Instead, you need to pass a pointer to the variable you want to change, and change it indirectly within the function. That way, although a copy gets made of the pointer (which obviously you can’t modify within the function), its contents remain the same so we’re all good and can access the variable it points to:
void Increment(int *num, int increment)
{
*num+=increment;
}
Of course, when calling the function, we need to pass in the address of the integer we want to change, otherwise we end up trying to change an address somewhere else in memory (often jovially known as writing to la-la land).
Technical Section |
Ever since C++ came along, there has been a better way to do this, which closely models other languages’ ability to pass values by reference instead of by value. This effectively means that you pass the variable as a pointer, but without the inherently time-consuming nature of constantly dereferencing parameters. This fantastic new method is called a reference. Basically this is another variable which you can define to be an alias of a variable. It works like this: int num; int &ref_num = num; Two important restrictions are that a) references such as ref_num must be initialised as soon as they are declared and that b) once initialised they cannot be changed to point to another variable, they are fixed. Any change now to ref_num will change num itself. So, how is this useful for passing variables to functions? Look at this: void Increment(int &num, int increment) { num+=increment; As you can see, num is now an alias to any variable being passed in; it is the equivalent of a pointer managed internally by the compiler. Notice that we don’t need to dereference it this time. |
3.4 C-Style Structures
Imagine we want to model an address book. We have lots of entries, each consisting of the person’s name and their address (and maybe even their telephone number). We could make a whole two arrays, one called Names and one called Addresses, but wouldn’t it be better to group the two together? They are, after all, related in our example. The way we do this is by using a structure as follows:
struct entry
{
char name[32];
char address[128];
};
Now is the time to do something cunning. We have a whole lot of entries in an address book, but we don’t have an address book. Let’s create an address book structure to hold our entries:
struct address_book
{
entry entries[32];
};
As you can see, this address_book structure can hold 32 entries. It is often better to define a value somewhere else and use it in place of an arbitrary value (“magic number”) chosen at random. For example:
// somewhere else in the program
#define MAX_ENTRIES 32
// back in the structure
entry entries[MAX_ENTRIES];
This makes it easier for readers of our program to understand what exactly our intention is. Now, say we wanted to immediately create a variable of the type of the structure we have just created (called an instance of the structure). How would we go about doing this? We could keep it simple:
// straight after structure
definition
address_book AddressBook;
Or we could use the more compact approach:
struct address_book
{
entry entries[MAX_ENTRIES];
} AddressBook;
So, we have an address book and some entries, but how do we go about accessing them? Say we wanted to access the name associated with the first entry in the address book, we would use the following:
AddressBook.entries[0].name
The . (dot) operator is used to access members of structures. We can declare pointers to structures too, as shown by the following code:
address_book *AddressBook_ptr = new address_book;
Now, however, we appear to have a problem. How do we access members of a structure through a pointer? Well, those of you who are more perceptive may have spotted that:
(*AddressBook_ptr).entries[0].szName
would work as expected. However, this is far too much of a pain to use on a regular basis, so the creators of C decided that it would be much simpler to create a new operator (the structure pointer dereference operator or so I am reliably informed <g>). The operator -> is used for this purpose. Hence:
Structure_ptr->member == (*Structure_ptr).member
Note that the equality operator is ==, not simply = (which is the assignment operator). If you write = when you mean == in for example an if statement, the compiler will happily change the value of the variable for you despite the fact that you only wanted to compare the two:
if (i = 1)
{
// Oh dear, it’s 1...
}
What you meant to write here was:
if (i == 1)
{
//
Comparison happens as expected.
}
It is a good idea to turn the warning level on your compiler up to the highest possible level: that way the compiler will warn you of the possibility that you’ve made an error when writing a statement like the above. Returning to our original theme, if we want to free the memory associated with the above address book, we would simply write:
delete AddressBook_ptr;
Notice that this time we must not use the square brackets since AddressBook_ptr does not point to an array.