Beginners Guide to C++ (Udemy)

June 1st, 2015

With Udemy’s permission (and kind invitation), I am reposting a great article for those just getting started with C++ programming. Not only is it an excellent resource for C++ beginners (and beginning programmers in general), its a concise and well-worded overview of the things I learned during my first year of computer science studies.

What this article makes clear is that C++ is designed for building software that works closely with the low-level components of the computer. Unlike higher-level languages like Java, C++ allows the programmer to directly manipulate memory via pointers. Learning C++ is a good idea if you want to understand how computers work on a low level.

As a first language, I found C++ to be challenging, but not frustratingly difficult to master. When I was first learning the language, well-written and well-illustrated learning materials were scant on the Internet, which is what motivated me to start creating computer science tutorials on my first YouTube channel, KarBytes. The Udemy article is something I wish I had when I started my adventure as a computer science student a few years ago.

Without further ado, here is the lovely article. Thanks Tiffany!


Table of Contents

  1. Introduction
  2. Getting Started
  3. Hello World
  4. The Language
  5. An Application
  6. Conclusion

1: Introduction

C++ is a general purpose programming language. Its powerful and offers its developers a wide range of programming paradigms. The language is often favored by those who are building software that interacts with low-level resources or the hardware of a computing device; even a large number of desktop applications prefer C++ due to its light weight runtime and flexibility. The language provides facilities that many other languages simply refuse to possibly in order to keep things sane and simple. For example, with C++ it is possible to do low-level memory manipulation, which makes it an ideal choice for building hardware drivers, operating system kernels, software that runs on embedded systems, etc.

Image taken from the original Udemy article.

The wide array of features that C++ provides and the numerous ways of doing anything in C++ makes it a difficult task to put everything about this language in one post. In this article, we make an attempt to briefly touch on some aspects of the language that should let a programmer with some experience in other object oriented languages get started with C++.


2: Getting Started

Getting started with C++ is quite easy on most platforms. All you need to do is install a compiler suite of choice, and you are good to go. Unless, of course, you prefer a particular integrated development environment (IDE). In which case, you can simply download one of the popular IDEs for C++ that is available for your platform, and use it.

On Linux, you may want to use the GNU Compiler Collection (GCC) for most of your C++ needs. On most Linux distributions, installing this compiler collection usually requires a single line of command, unless the compilers are already installed on your system. For example, in Arch Linux all you have to execute in your terminal is:

sudo    pacman    -S    gcc

On OS X, a C++ compiler (variation of LLVM) is already included, which should be sufficient for this beginners tutorial.

On Windows, however, installing GCC might be a little more difficult. There are many IDEs available (such as CodeBlocks with MinGW) that you can possibly download as they come bundled with a set of compilers. Or, you can install MinGW separately. Otherwise, you can always use Microsofts version of C++ by installing Visual Studio which can often be a better choice given the ease of getting started.


Hello World

What purpose does a programming language serve if it doesnt allow you to print the text Hello, world! to your terminal? The very first C++ program that we will look at is one that prints this simple text to the terminal. Create the following file and name it hello.cpp:

#include <iostream>
using namespace std;

int main()    
{
      cout << "Hello, world!" << endl;
      return 0;
}

To run this program, you must first compile it using a C++ compiler of your choice. We will be using the C++ compiler in the GNU Compiler Collection from the terminal throughout the article. If you are also using that, this is what you should be executing in your terminal to compile the program:

g++ hello.cpp

If all goes well, you should notice an executable file named a.out next to hello.cpp (on Windows, the file will be named a.exe). You may run the compiled program now from your terminal on *nix systems by issuing the following command:

./a.out

On Windows:

a.exe

This should print the following line of text on your terminal:

Hello, World!


Directives

The way C or C++ compilers work is it turns your source code into executable programs in three high-level phases (preprocess, compile, and link). The first phase involves a preprocessor that handles all the directives every line of your code beginning with a hash symbol. In this case, #include instructs the preprocessor to include the header file associated to the standard library iostream. This, in essence, enables you to import functionalities into your code from external implementations; just like the import statements in Python, Go, etc., or the require function in Node.js. The underlying behavior of the #include directive is different, a bit more simple: it merely copies the contents of the named files in the spots where these directives appear.

Namespaces

C++ provides a mechanism namespaces that allows a bunch of functions and classes to be grouped under a named scope. This helps avoid name conflicts in large projects. Everything inside the standard library of C++ is declared within the std namespace. Without declaring using namespace std; in our program, we would have to prepend references to every element of the standard library with std::. For example, instead of writing cout << …”, we would have to write “std::cout << …”.

Entry

In C++, the entry point of any program is the main function. The signature of the main function is int main(). However, int main(int, char*[]) is also acceptable which we will eventually take a look at later in this tutorial. The small bootstrap code that invokes your main function expects it to return the exit status of the program as an integer. Ideally, a program that runs and exits successfully yields an exit status of zero. Non-zero exit statuses are used to indicate different types of failure. In this program, we always return a zero.

Streams

As you might have guessed, the line of code that actually prints Hello, world! to your terminal is cout << “Hello, world!” << endl;”. The line itself may look self explanatory – you are _pushing_ (<<) a piece of text into “cout”, that somehow prints it to the terminal. In this case, “cout” is an object that represents the standard output. In C world, it corresponds to the stream stdout. Finally, “endl” is what puts a new-line character at the end of the stream and forces the content of the stream to be flushed to the destination, which, in the case of stdout, is the terminal.

The Language

C++, in terms of syntax, has some similarities to C. In many ways, C++ is a superset of what C offers. The following are some brief outlines about a few aspects of C++, which is hardly comprehensive, but should help us cover some general concepts and get used terminologies essential for the rest of the article. Unlike most other languages, C++ doesnt limit you to a single paradigm of programming. Other aspects, such as templating and exception handling, are being completely left out for some more advanced tutorials of C++.

Variables

C++, being a strongly-typed language, requires you to explicitly declare variables. A declaration typically involves indicating the name and type of a variable. An integer variable named num can be declared as follows:

int num;

To declare and initialize a variable at the same time, you can do this:

int num = 5;

C++ also allows you to declare arrays by simply appending a pair of square brackets after the variable name and putting the size of the array in between them:

int arr[10];

This declares an array by the name arr that can hold 10 integers. However, declaring arrays like this require you to know the size while you are writing the code. This is often not desirable, and that is where you need dynamic allocation of arrays. In C++, it is possible to dynamically allocate an array using the keyword new[]:

int size = 10;
int arr[] = new int[size];

You may be wondering, how is this any different from the previous method of declaring arrays? After all both, are declaring an array of size 10. The difference is that in this case, size is a variable. And size doesnt always need to have the value 10. In fact, it can be any number, as long as your system memory is large enough to contain that array.

Speaking of memory, it is important to understand that in C++ not all memory allocations are treated equal. When you are declaring an array, the C++ runtime is allocating some memory for it. However, depending on how you allocated that array (or object), the memory may not be deallocated automatically. This is where C++ is significantly different from other programming languages that are garbage collected. In languages such as Python or Go, memory is deallocated by the garbage collector when it is no longer in use. In C++ not all allocated memory is deallocated automatically.

Whenever you use the new or new[] keyword, the memory allocated for that has a dynamic lifespan. And hence, it is only deallocated through an explicit use of the delete or delete[] keyword. From the example above, the array created through invocation of the new[] keyword will only be deallocated if the following code is executed:

delete[] arr;

Types

Like most typed languages, C++ comes with a handful of primitive types: bool, char, int, long, float, double, etc. Most of these types differ from one another based on the size data type that it can hold on to.

Most of these were already available in C, but C++ takes it a step further with things like the standard string library. By including the string library, you get better ways of managing strings of characters, compared to handling arrays of characters:

#include <string>
using namespace std; 
int main() {
     string s = "an important string";
     return 0;
}

Operators

Variables are just part of a programming language, and incomplete without operators. For all primitive types in C++ there is a large number of operators available. Ranging from simple assignment operators to a plethora of arithmetic, logical, and bitwise operators. C++ even lets you define custom operator behaviors by letting you overload them. A standard library and its custom types make really good use of this feature.

  • Arithmetic operators: +, -, *, /

  • Comparison operators: ==, !=, >, <, >=, <=

  • Logical operators: !, , ||

  • Bitwise operators: , |, ^, ~, <<, ≶≶

  • Pointer operators: *, , ->

  • And more

Some of these operators can be combined with the assignment operator to form much more concise expressions in some cases. For example:

a += b // equivalent to a = a + b

Flow Control

C++, just like C, comes with a bunch of conditional and loop constructs. It includes, but are not limited to, if-else statements, for and while loops, and more. Most of these behave almost exactly as you would expect them to, except in some cases.

if (month == 0) { 
    cout << "January"; 
} else if(month == 1) { 
    cout << "February"; 
// ... 
} else if(month == 11) { 
    cout << "December"; 
} else { 
    cout << "not a month"; 
}

The construct above can be replaced with a switch statement.

switch(month) { 
     case 0: 
         cout << "January"; 
         break; 
     case 1: 
         cout << "February"; 
         break; 
     // ... 
     default: 
         cout << "not a month"; 
}

However, switch statements are only limited to integer (int, long long int, etc.) and character (char) values and variables, and will not quite work with strings, which you may have seen being possible in languages like Go and JavaScript.

In C++ you have multiple ways of writing a loop statement. There is while, do-while, and for.

while(n > 0) { 
     // ... 
}

The code inside these curly braces will be executed repeatedly, as long as the condition (in this case n > 0) evaluates to true.

Functions

Functions in C++ have two parts: the signature and the body. As you would expect in any strongly-typed language, the arguments and the return type of the function must be explicitly mentioned. The following is the signature of a function that takes two int arguments and returns an int:

int add(int a, int b);

The implementation of this function can look something like this:

int add(int a, int b) { 
     return a+b; 
}

It is important to remember that the order in which these functions appear in a source code file is very important. If you want to use a function, it must be declared before it is called. For example, the following program will simply refuse to be compiled:

int sub(int a, int b) { 
     return add(a, -b); 
} 
int add(int a, int b) { 
     return a+b; 
}

One thing that we can do to fix this is to move the function add above the function sub. However, that may not work in situations where there is a circular dependency. What if add depended on sub, and somehow sub depended on add? In most cases, the way we fix this is through declaring the functions first and then implementing them later on. In C++ you can declare a function just by stating its signature, and later on complete the implementation by defining its body:

int add(int a, int b); // Prototype of function add 
int sub(int a, int b) { 
     return add(a, -b); 
} 
int add(int a, int b) { 
     return a+b; 
}

Classes

Classes in C++, are meant to give data structure within your application. It is similar to struct used in C, with the added benefit of being able to define methods on them, as well as polymorphism. In fact, a minimal class looks very much like a struct in C:

class Fruit { 
public: 
     bool isCitric; 
     double weight; 
private: 
     bool isEaten; 
};

Here, the name of the class is Fruit, and it has two public attributes (isCitric and weight) and one private attribute (isEaten). Adding methods to the class is a two step process. The method signature is declared inside the class definition, and their implementation is added afterwards.

class Fruit { 
public: 
     // ... 
     void eat(); 
     // ... 
}; 
void Fruit::eat() { 
     this->isEaten = true; 
}

Notice how this is similar to declaring function signatures and implementing their behavior later on. We can instantiate an object of this class by simply declaring a variable of this type:

Fruit f;

Here f is a variable of type Fruit. To access an attribute or a method of this object, we can use the structure reference operator:

f.isCitric; 
f.eat();

When it comes to C++, these are separated into two files, usually for every class. A .h file which contains the class definition and method signatures and a .cpp file which contains the implementation of the methods. For example, for our Fruit class we could create two files named fruit.h and fruit.cpp. The first file fruit.h would contain the definition of the Fruit class. The second file fruit.cpp would contain the implementation of the Fruit::eat method. Classes in C++, as you would expect from any polymorphic programming language, can extend other classes through a simple syntax:

class Orange : public Fruit {};

Classes can also have one or more constructors and a destructor. These elements of a class are defined and declared in ways similar to that of methods, with some minor differences:

class Orange : public Fruit { 
     Orange(); // Constructor 
     ~Orange(); // Destructor 
}; 
Orange::Orange() { 
     this -≶ isCitric = true; 
     // ... 
} 
Orange::~Orange() { 
     // No-op 
}

In this snippet of code above, what we are doing is declaring a class that extends the class Fruit with a constructor and a destructor. Constructors are declared in a way that is similar to declaring methods, except that they do not have any return type and have the same name as the class itself. Destructors are also declared similarly, with the additional tilde symbol appearing before the name.

If you are already familiar with some other object oriented programming language, picking up on classes and inheritance in C++ should be simple. But that should not be a reason to underestimate the power of polymorphism in C++. There are a number of relevant features that are not often seen in other languages (such as friend classes, operator overloading etc.), which when used properly, can result in very elegantly coded programs. However, what is essential for us to realize is that these flexibilities, when used irresponsibly result in mistakes that are both hard to find and understand.

The purpose of this section was to show a simple example of classes and inheritance, and not an absolute guide to polymorphism in C++, as that itself must be built on solid understanding of what exactly polymorphism is, which is too large to currently explain.


4: Pointers

Pointers are a whole new world. They are special kind of values that let you access and manipulate memory in a completely different way. Typically, a variable is a way for us to address a certain portion of the computers memory. However, you do not have much control over what portion of the computers memory you are referring to.

Image taken from the original Udemy article.

There are two pointer related operators that we must know about at the very least if we want to use pointers in C++: and *.

The ampersand is an address of operator. Lets say you have an integer variable named x. Any time you write x, it evaluates a pointer to variable x what it means is that it evaluates to the address of the memory location where the variable x stores its value. The asterisk, on the other hand, has two uses (apart from its role as an arithmetic operator multiplication). When you declare a variable, putting an asterisk in front of the name makes it a pointer variable.

int *ptrToX;

If you were to initialize this pointer variable with an address, for example:

int x = 5; 
int *ptrToX = &x;

and if you tried to inspect the value of p, what you would see is a number that represents the address of the memory location where x stored the value 5. This, would be of much use if you could not retrieve the actual value via a pointer. This is where the second use of the asterisk operator comes in: indirection. If you were to inspect the result of the expression *ptrToX, instead of just ptrToX you would actually get the value 5. When it comes to pointers to objects, especially accessing attributes and methods of objects, using pointers can lead to messy code. We may have a pointer to an instance of our Fruit class, and we want to access the method eat of that instance. To do that, we can use the indirection operator to first obtain the value, and then use the structure reference operator to access a member or method of the object:

Fruit f; 
Fruit *ptrToF = &f; 
(*ptrToF).isCitric; 
(*ptrToF).eat();

Or, we can use a special operator known as the structure dereference operator to do the same:

ptrToF -> isCitric; 
ptrToF -> eat();


5: An Application

To provide a more hands on learning experience, we will implement a small todo application in C++. The objective is to walk through the code of this application, and understanding how the language and the standard library play their parts in the implementation. A number of robust user interface toolkits are available that work really well with C++. Qt is such a toolkit that is cross-platform, and in fact one of the most well built. However, to keep things simple we will build our todo application to only work through a command line interface.

We will begin with an empty directory for our project. Next we create our first .cpp file ytodo.cpp. Let us create the file and populate it with an noop, but valid, C++ program:

int main() { 
     return 0; 
}

Let us build the application by executing the following command in the terminal:

g++ *.cpp

and run the compiled program:

./a.out

Remember, on Windows it will be a.exe instead of ./a.out. You will notice the program has been compiled and that running it produced no output in the terminal which is expected, given the program is empty.

Since we are building a todo application, we should at least define a simple class that would represent each individual task, including the tasks title and done state. Let us create another file, name it task.h and populate it with:

#include <string> 
using namespace std; 
class Task { 
public: 
    string title; 
    bool done; 
};

As you already know, .h files are where declarations reside. Since our Task class does not have any method yet, we can skip creating a .cpp file for it now.

Next, we need to be able to handle command line arguments. If you can recall, the entry function main has variant where its signature accepts an integer and an array of character arrays. These parameters of the main function will allow us to determine what command line flags or arguments were included when the command was run. This means our main function should now have the following signature:

int main(int argc, char* argv[])

Here, argc is an integer indicating the number of arguments (elements) to expect in the array argv. Each element in argv is an individual argument from the command line. For example, running the built program as:

./a.out add "Learn C++"

Will set argc to 3, and argv to an array of 3 elements: ./a.out, add and Learn C++. Initially we want to implement three subcommands for our todo application.

  • ls/list: list all tasks

  • dd: add a new task

  • mark: mark a task as done

But before we can do that, we must add a few lines of code to help us parse these command line arguments easily:

#include <iostream> 
#include <map>
#include <string> 

using namespace std; 

int main(int argc, char *argv[]) { 
     map <string, int> scmds; 
     scmds[""] = 1; 
     scmds["ls"] = 1; 
     scmds["list"] = 1; 
     scmds["add"] = 2; 
     scmds["mark"] = 3; 
     string arg1 = ""; 
     if(argc > 1) { 
         arg1 = string(argv[1]); 
     } 
     if(scmds.find(arg1) == scmds.end()) { 
         cout << "Invalid: " << arg1 << endl; 
         return 0; 
     } 
     switch(scmds[arg1]) { 
         case 1: 
             cout << "Unimplemented: ls" << endl; 
             break; 
         case 2: 
             cout << "Unimplemented: add" << endl; 
             break; 
         case 3: 
             cout << "Unimplemented: mark" << endl; 
             break; 
     } 
     return 0; 
}

There are a few things worth mentioning regarding this code. First of all, the standard library headers included. The first one, iostream, is something we are already familiar with from our Hello World example. The second and third headers are what provides us with a map data structure (similar to a dictionary or hash in other languages) and string implementation.

Inside the main function, the very first thing we are doing is creating a map named scmds. The map has string keys and int values. The purpose of this is to create a mapping of subcommand names (strings) to unique integers representing the action to perform. Notice how we assign the same number for ls and list, making them aliases to each other. Then, we determine if we have at least two arguments. If yes, we take the second element from the argv array and store it as a string in arg1. Otherwise, we set it to ls, making it the default action when a subcommand is not included. Next we determine if a valid subcommand was issued by checking our map for the keys existence. When you make an attempt to find a key that doesnt exist in the map, as per specifications, the map returns an iterator value equivalent to the one returned by the end method of the map.

Finally, we switch based on the numeric value obtained from the map for the specific subcommand name and perform a task for now we just indicate that the actions are not yet implemented.

You can compile and run the application by executing:

g++ *.cpp 
./a.out

Adding Tasks: Files and Vectors

Let us now implement the add action. To do this, we need to accomplish three things:

  • when the user tries to add something to the todo list, store it in a in-memory data structure

  • when the application exits, store the contents of that data structure in a file

  • when the application starts, read the contents of the file to restore the state of the data structure.

here is how we can do it:

#include <fstream>
#include <iostream>
#include <map>
#include <string> 
#include <vector>
#include "task.h" 

using namespace std; 

vector <Task> tasks; 
void load() { 
     ifstream ifs("tasks.ytd"); 
     string l; 
     while(getline(ifs, l)) { 
         Task t; 
         t.done = l[0] == '1'; 
         t.title = l.substr(2); 
         tasks.push_back(t); 
     } 
     ifs.close(); 
} 

void save() { 
     ofstream ofs("tasks.ytd"); 
     for(vector <Task&lgt;::iterator it = tasks.begin(); it != tasks.end(); ++it) { 
         ofs << (it -> done ? 1 : 0) << " " << it -> title << endl; 
     } 
     ofs.close(); 
} 

string clean(string s) { 
     string r = ""; 
     for(int i = 0, l = s.length(); i < l; ++i) { 
         if(s[i] == '\r' || s[i] == '\n') { 
             r += ' '; 
         } else { 
             r += s[i]; 
         } 
     } 
     return r; 
} 

int main(int argc, char *argv[]) { 
     load(); 

     map <string, int> scmds; 
     scmds[""] = 1; 
     scmds["ls"] = 1; 
     scmds["list"] = 1; 
     scmds["add"] = 2; 
     scmds["mark"] = 3; 

     string arg1 = ""; 
     if(argc >= 2) { 
         arg1 = string(argv[1]); 
     } 

     if(scmds.find(arg1) == scmds.end()) { 
         cout << "Invalid: " << arg1 << endl; 
         return 0; 
     } 

     switch(scmds[arg1]) { 
        case 1: 
             cout << "Unimplemented: ls" << endl; 
             break; 
         case 2: 
             if(argc != 3) { 
                   cout << "Missing task title" << endl; 
                   return 0; 
             } else { 
                   string arg2 = string(argv[2]); 
                   Task t; 
                   t.title = clean(arg2); 
                   tasks.push_back(t); 
             } 
             break; 
         case 3: 
             cout << "Unimplemented: mark" << endl; 
             break; 
     } 

     save(); 
     return 0; 
}

From a quick look, we can tell that three new functions have been added and some changes have been made to the second case of the switch statement in the main function. But at the same time, a couple of new headers have been included as well. Let us walk through the changes. First of all, the new standard library headers, namely fstream and vector provide the application with file reading/writing capabilities and a dynamic array-like data structure.

The fstream standard library header is similar to iostream, but instead of providing convenient stream access to the standard input/output, fstream provides similar ways of handling files. This time, we have included another header that belongs to our own project ./task.h. This is how we include the declaration of Task class in our application.

Just below the using namespace declaration, you can see a new variable declaration. Here, we have declared a vector named tasks. The vector will only store task objects, indicated by the mention of the class name within the angle brackets. So, a vector < int > type would actually be a vector of integers, just like vector < Task > is a vector of our class Task. Next, out of the three new functions, the third one is the simplest: it takes a string, replaces all the newline and carriage return characters with ordinary space, and returns the new string. This is something we will use to clean up the task title that we get as an argument.

In our load function, we instantiate an ifstream (input file stream) and point it to the file named tasks.ytd. We then use the standard getline function to fetch one line at a time from the file. We assume that each line is an individual task where the first character is either a 0 or a 1 (indicating not done and done), and the rest of the line after a space is the tasks title. If the file doesnt exist, getline will immediately return a value that will cause the while-loop to not run at all. For every line that we process, we create a task object, set the title and done state, and push it to the tasks vector.

The save function, as you would expect, does the reverse of the load function. It loops over every task stored in the tasks vector, and puts them in the tasks.ytd file. By default, ofstream (output file stream) opens the file and replaces its contents with what you write to it. The for-loop is a bit of a handful. First of all, the type of the value we iterate on looks a little alien. This is because, when looping over a vector, it is actually convenient to use an iterator object provided by the vector. The way it works is you call the vectors begin method to get an iterator pointed at the very beginning of the vector. At every step, you increment the iterator to move it to the next element. The iterator itself behaves like a pointer, so you need to dereference it to access the actual value. You continue the loop until the iterator reaches the end. You treat ofs as you would treat cout, by streaming data into it. Once all the data has been passed to ofs, you close the file.

Finally, in the second case that handles the add subcommand, we check if we have sufficient number of arguments. If yes, we create a task object and push the tasks vector. The load and save function invocation at the beginning and end of the main function handles the synchronization of the in-memory and file-based data storage for this application. You can now compile the application:

g++ *.cpp

and try adding a task by executing:

./bin/ytodo add "Learn C++"

You will notice that a file named tasks.ytd has been created. Opening it with your favorite text editor should show the task you have just added:

0 Learn C++

Listing Tasks

At this point, implementing the ls subcommand should be a simple feat:

int main(int argc, char *argv[]) { 
     // ... 
     switch() { 
         case 1: 
             for(int i = 0, l = tasks.size(); i < l; ++i) { 
                 Task t = tasks[i]; 
                 cout << (i+1) << ". " << t.title << " [" << 
                 (t.done ? "X" : " ") << "]" << endl; 
             } 
             break; 
         // ... 
     } 
     // ... 
}

Here, we loop from 0 to l, where l is the size of the vector tasks, and we print one line for every task. First we print a serial number, then the task’s title, and finally a pair of square brackets that would have an X in-between them for tasks that have been marked as done. You can now compile and run the program to print all the tasks that you have added to the list:

Image taken from the original Udemy article.

g++ *.cpp 
./a.out ls

The idea behind printing these serial number is to provide the user with an easy way to reference the tasks. For example, next we would like to implement the “mark” subcommand which will mark tasks as done. With these serial numbers, the user should be able to mark the tasks as done by referring to these numbers. To mark the second task from the list, the user can execute:

./a.out mark 2

Marking Tasks as Done

Implementing this “mark” subcommand may be easier than you think:

int main(int argc, char *argv[]) { 
     // ... 
     switch() { 
         // ... 
         case 3: 
             if(argc < 3) { 
                 cout << "Missing task number" << endl; 
                 return 0; 

             } else { 
                 string arg2 = string(argv[2]); 
                 int no = stoi(arg2); 
                 if(no < 1 || no > tasks.size()) { 
                     cout << "Invalid task number" << endl; 
                     return 0; 
                 } 
                 Task t = tasks[no-1]; 
                 t.done = true; 
             } 
             break; 
     } 
     // ... 
}

Just like “add”, for this subcommand we determine if the third argument is present or not. We extract the third argument as a string, convert it using the standard stoi function, and check if the number is within a valid range. We then find the appropriate task object in the vector and set its done attribute to true.

There is one small detail worth pointing out here. Notice the “&” symbol before the variable name “t”? That makes this variable a reference to the element of the vector, instead of making the program copy the element of the vector into a new memory location. References work in a way similar to that of pointers, except that they have a little more restrictions imposed on what you can do with them. Without this symbol, any changes made to “t” wouldn’t have any effect on the actual task element inside the vector. If “t” wasn’t a reference to the element, it would have been a copy, and changes made to the copy of an object wouldn’t have any effect on the original object.

Sorting Tasks: Overridden Operators and STL Algorithms

As you can see, we can now add small nifty features to this tool with small, incremental updates. Let us add another feature that would sort each task based on its status. This will let us move all the completed task to the bottom of the list. For this, we will use the “sort” algorithm that comes with the standard library.

Before we use the “sort” function, we need to modify our Task class a little for it to work with the function. There are many ways of using the function, one of which involves defining a custom less than operator for your custom data type or class in this case. Overriding an operator involves defining a method-like element for the class:

// task.h 
// ... 
class Task { 
     // ... 
     public operator < (const Task &b;) const; 
};

And we will provide the implementation of this overriden operator in our “task.cpp” file:

#include "task.h" 

bool Task::operator < (const Task &b;) const { 
     return !this -> done && b.done; 
}

This allows you to compare two task objects with the less than operator. Given two tasks a and b, a is less than b only if a is not done, but b is done. The const keywords before the argument type and after the function’s signature is there to assure the compiler that the object b passed as the argument will not be modified by the function and the function will also not modify the object at “this”.

Finally, we need modify our “ytodo.cpp” file to use the sort algorithm:

#include <algorithm>
// ... 
int main(int argc, char *argv[]) { 
     // ... 
     scmds["sort"] = 4; 
     // ... 
     switch(scmds[arg1]) { 
         // ... 
         case 4: 
             sort(tasks.begin(), tasks.end()); 
             break; 
     } 
     // ... 
}

And that is it. All you have to do to sort the vector of tasks is to:

  1. Define a less than operator for the class.

  2. Include the algorithm’s standard library.

  3. Invoke the sort function with the beginning and the ending iterators of the vector.

You can now compile and run the “sort” subcommand to organize the list by moving all the tasks that have been marked as done to the bottom.

g++ *.cpp 
./a.out sort


6: Conclusion

Learning C++ is usually quite a different experience from learning any other modern programming language. This is probably attributed to the age of the language, or may even be its less-than-high-level nature – even though C++ is in fact a high level language. C++ may not be the best choice for building your next web application server, but it is certainly a powerful language that can cover a lot of ground. With its performance characteristics and versatility, C++ is often a great language for building software that is closer to your bare machine (such as hardware drivers and operating systems), or software that would like to make the use of every bit of performance that the hardware can provide – such as games, real-time applications, embedded systems, missile thrust vector control systems, and more. Just like with any other language, mastering C++ is a matter of patience and practice. This article may have barely scratched the surface, but we hope it has given you an insight into what you can expect from C++ and a starting point from where you can continue to play with the language and continue to learn more about it.

This tutorial was made in collaboration with Toptal.


Categories: Reviewssciencetutorials