C++ is a high-level, general purpose programming language providing facilities for data abstraction, object-oriented and generic programming. C++ was designed by Bjarne Stroustrup and the first release came in 1985. C++ is compatible with the C programming language, as C is a subset of C++. C++ has been standardized by the International Organization for Standardization (ISO); the first ISO standard on C++ was published in 1998 (C++98), and there was a major update in 2011 (C++11). C++ has been a top programming language since the 1990s.
Table of Contents
1. Establishing a development environment
The best way to learn a programming language is to write programs in it. And, for that, we need a development environment. We need the means to write the code and, then, compile and build the executable file. The development environment depends on the operating system of the computer and also the personal preferences of the programmer. You can use a text editor like vim, or an Integrated Development Environment (IDE), like Eclipse, or VS Code. For building the executable file, g++ compiler can be used. We can install the g++ compiler in Ubuntu Linux using the commands,
$ sudo apt-get update $ sudo apt-get install build-essential
Also, the make utility should, preferably, be used for building the executable file.
2. Hello, World!
The first program in a programming language is Hello, World! Here is a Hello World! program in C++.
// helloworld.cpp #include <iostream> int main () { std::cout << "Hello, World!\n"; }
The first line starting with a double slash is a comment. It is for human use and is ignored by the compiler. Next, we have the include for the iostream system header file, which is for the standard Input/Output stream facilities. It provides stream classes and objects for I/O operations. There are three standard stream objects provided by iostream. The first is cin, the standard input stream, which by default, is the keyboard. The second is cout, the standard output stream, which by default, is the video display. And, the third is cerr, the standard error stream. This is also, by default, the video display. The operator << puts “Hello, World!\n” on the standard output stream, cout.
We can compile and run our first Hello, World! program as below.
$ g++ helloworld.cpp -o helloworld $ ./helloworld Hello, World!
Consider the line,
std::cout << "Hello, World!\n";
in the above program. It says, “put Hello, World!\n“, on cout, which is a part of the std, standard library namespace. Rather than qualifying the names from std for each use, we can specify “using namespace std” once, and the names from std become visible in the entire scope. And, our Hello, World program is modified as below.
// helloworld.cpp #include <iostream> using namespace std; int main () { cout << "Hello, World!\n"; }
We need to compile our modified Hello, World! program and it's time to use the make program. So, we can just say, “make helloworld”, and the helloworld executable is built.
$ make helloworld g++ helloworld.cpp -o helloworld $ ls -ls total 20 16 -rwxrwxr-x 1 user1 user1 16384 Aug 22 21:09 helloworld 4 -rw-rw-r-- 1 user1 user1 111 Aug 22 21:05 helloworld.cpp
make has some implicit rules. When we say, “make helloworld”, it looks for a source file with the stem, “helloworld’ in the current directory. It finds “helloworld.cpp”, which has the “.cpp” extension. So, make figures out that it has a C++ source file to compile and it chooses the g++ compiler. And, make builds the helloworld executable file.
In general, programs have multiple source files and might need special libraries to generate the final executable file. A .cpp file is compiled into an object file using the make implicit rule,
$(CXX) -c $(CXXFLAGS) $(CPPFLAGS) foo.cpp
The value of $(CXX) is g++. $(CXXFLAGS) are extra flags for the C++ compiler and these, if required, can be set in the makefile. If set, these are used automatically by the implicit compilation command. Similarly, $(CPPFLAGS) are extra flags for the C preprocessor.
After all the source files are compiled into objects files, the object files need to be linked with relevant libraries to generate the final executable file. The command for generating the executable file (the recipe, as it called) is like,
$(CXX) $(LDFLAGS) $(OBJECTS) $(LOADLIBES) $(LDLIBS) -o foo
$(LDFLAGS) are extra flags for the compiler for invoking the linker. $(LDLIBS) indicate the libraries. $(LOADLIBES) is also for the libraries, but is deprecated. $(OBJECTS) are the object file names separated by spaces. Our makefile for building helloworld is,
# # Makefile # .PHONY: clean LDLIBS := LDFLAGS := OBJECTS := helloworld.o helloworld: $(OBJECTS) $(CXX) $(LDFLAGS) $(OBJECTS) $(LDLIBS) -o helloworld clean: rm -f *.o rm -f helloworld
In makefiles, the first character in lines containing commands must be the tab character. So, the lines “$(CXX) $(LDFLAGS) $(OBJECTS) $(LDLIBS) -o helloworld” and the ones containing the rm command must start with the (invisible) tab character. Otherwise, make aborts processing and gives an error. Using the above makefile, we can build helloworld as below.
$ make clean rm -f *.o rm -f helloworld $ $ ls -ls total 8 4 -rw-rw-r-- 1 user1 user1 111 Aug 22 21:05 helloworld.cpp 4 -rw-rw-r-- 1 user1 user1 193 Aug 22 23:09 Makefile $ $ make g++ -c -o helloworld.o helloworld.cpp g++ helloworld.o -o helloworld $ $ ./helloworld Hello, World!
3. Types and Variables
The basic types in C++ are char, bool, int and double. A type tells about the representation of an object of its type in memory and the operations that can be done with it. A variable is a named object. The type char is the most basic type in a computer. It represents a character. The bool type represents boolean, and can hold either a false, or a true value. Similarly, the types, int and double represent integers and double precision floating point numbers. The following program prints the sizes of objects of different types.
#include <iostream> using namespace std; int main () { cout << "Size of char is " << sizeof (char) << " byte.\n"; cout << "Size of bool is " << sizeof (bool) << " byte.\n"; cout << "Size of short is " << sizeof (short) << " bytes. \n"; cout << "Size of int is " << sizeof (int) << " bytes.\n"; cout << "Size of long is " << sizeof (long) << " bytes.\n"; cout << "Size of long long is " << sizeof (long long) << " bytes.\n"; cout << "Size of float is " << sizeof (float) << " bytes.\n"; cout << "Size of double is " << sizeof (double) << " bytes.\n"; cout << "Size of long double is " << sizeof (long double) << " bytes.\n"; }
The above program gives the following result on a x86_64 based system.
$ make sizes g++ sizes.cpp -o sizes $ ./sizes Size of char is 1 byte. Size of bool is 1 byte. Size of short is 2 bytes. Size of int is 4 bytes. Size of long is 8 bytes. Size of long long is 8 bytes. Size of float is 4 bytes. Size of double is 8 bytes. Size of long double is 16 bytes.
The types short, long and long long are variations of the basic type int. Similarly, float and long double are variations of the basic type double.
Variables are introduced in a program via a declaration like,
extern int x;
It tells the compiler that there is an integer variable named x, and x becomes available for use in statements in the scope. There are two similar sounding terms, declaration, and definition for variables. A declaration just tells about a variable, as in the case of x, above and it becomes available for use in statements. Definition does more than that. Consider the definition of a variable y.
int y;
Here, y is defined as an integer. An object y is created, and space is allocated. Since we have not initialized it, y gets the default initialization. All the global and static variables are initialized to zero. Local variables are not initialized; they contain junk values. However, it is considered a good practice to initialize variables explicitly at the time of definition. So, we could have written the definition of y as,
int y = 0; // y is initialized to 0
Instead of assignment, we can use braces around the values for initialization, as in the examples below.
int p {7}; double q {2.33}; char a {'c'}; bool b {true};
And, instead of specifying the type in the definition, we can say auto, and the compiler makes a guess about the type of the variable from the value of initialization. The auto type can be used when the type of a variable is not important.
auto p = 7;
4. Arithmetic and Relational Operators
C++ provides plus (+), minus (-), multiply (*), divide (/) arithmetic operators. It provides remainder, or modulus (%) operator for integers. It also provides unary plus and unary minus operators. For example,
x + y // x plus y +x // unary plus x x - y // x minus y -x // unary minus x x * y // x multiplied by y x / y // x divided by y x % y // if x and y are integers, remainder of x divided by y
There are increment (++) and decrement (--) operators. The increment operator adds 1 to the operand and the decrement operator subtracts 1 from the operand. Both the increment and decrement operators can be used as prefix or postfix to the operand. Prefix usage increments or decrements the operand before use, whereas in the case of postfix, the operand is first used in the expression and then the increment or decrement operation is done. For example,
++i; // add 1 to i, prefix i--; // subtract 1 from i, postfix x = a [++i]; // prefix: First add 1 to i and then assign a [i] to x x = *ptr++; // postfix: First, assign *ptr to x and then increment ptr
The relational operators are less than (<), greater than (>), less than or equal to (<=), greater than or equal to (>=), equal to (==) and not equal to (!=). The precedence of first four operators is higher than the last two and the precedence of relational operators is less than the binary arithmetic operators. For example,
x < y // x is less than y x > y // x is greater than y x <= y // x is less than or equal to y x >= y // x is greater than or equal to y x + y == a + b // x plus y is equal to a + b x != -1 // x is not equal to -1
A single equal to sign (=) is the assignment operator. The arithmetic operators can be combined with the assignment operator, like,
<variable> <arithmetic-operator>= <expression>
It is a concise notation for this.
<variable> = <variable> <arithmetic-operator> <expression>
For example,
p = a + b; // compute a + b and assign the result to p p += a + b; // compute a + b and add the result to p p -= a + b; // compute a + b and subtract the result from p p *= a + b; // compute a + b and multiply the result to p p /= a + b; // compute a + b and divide the result into p p %= a + b; // If p, a and b are integers, compute a + b // and set p to remainder after dividing (a + b) into p
5. Bitwise operators
C++ provides following bitwise operators, which can be applied to only char, int, short, long and long long types. These are mostly applied to bit masks and flags, in which case, they are applied to unsigned int or insigned long types.
- &, for bitwise AND,
- |, for bitwise inclusive OR,
- ^, for bitwise exclusive OR (XOR),
- <<, for left shift,
- >>, for right shift,
- ~, for unary 1s complement, where bits that are set are reset and vice-versa.
A common use of bitwise operators is in maintenance of flags, where flag is an unsigned integer, and, individual bits reflect status of certain boolean settings. In the example below, some of the bits of an unsigned int flag are set and, also, reset. Also, we check whether certain bits are on.
#include <iostream> using namespace std; int main () { unsigned short flag = 0; unsigned short mask = 0; cout << hex; // print integers in hexadecimal // set bits 0 and 4 mask = 0x11; flag |= mask; // flag is 0x11 cout << "flag = " << flag << '\n'; // set bits 2 and 3 mask = 0xc; flag |= mask; // flag is 0x1d cout << "flag = " << flag << '\n'; // reset bits 1 and 2 mask = ~0x6; flag &= mask; // flag is now 0x19 cout << "flag = " << flag << '\n'; // check whether bits 3 and 4 are on mask = 0x18; if ((flag & mask) == mask) cout << "Bits 3 and 4 are on\n"; else cout << "Bits 3 and 4 are not on\n"; // reset bit 4 flag &= ~0x10; // flag is 0x9 cout << "flag = " << flag << '\n'; // check whether bits 3 and 4 are on if ((flag & mask) == mask) cout << "Bits 3 and 4 are on\n"; else cout << "Bits 3 and 4 are not on\n"; return 0; }
6. Constants
A constant is something that does not change. There are two types of constants in C++. First, there are constant variables. The qualifier const is prefixed to a variable definition and the variable must be initialized. However, a constant variable is evaluated at runtime. The const qualifier means that this variable is constant and, once defined, cannot be modified during its lifetime.
const int day_hours = 24; // day_hours is a constant double r = 2.303; // r is not a constant const double q = r; // OK, q is a constant, evaluation is at runtime q = day_hours; // ERROR: q cannot be modified
Constant variables are often used in function interfaces. The parameters are often passed as pointers or reference to objects defined elsewhere and, most of the time, we do not want the called function to modify data for which a pointer or a reference has been passed. So, in those cases, the parameters are qualified as constant variables. Also, the function return value, especially if it is a pointer or reference and should not be modified, can be qualified as const.
C++ also provides the constexpr keyword. A variable with qualifier constexpr must be initialized and evaluated at compile-time. For example,
#include <iostream> #include <cmath> using namespace std; constexpr double cube (const double x) { return pow (x, 3); } double square (const double x) { return x * x; } int main () { constexpr double p = 2.3; const double q = 2.4; double r = 2.303; constexpr double s = p; // OK, p is constexpr constexpr double t = 1.55 * cube (q); // ERROR, q is not constexpr constexpr double u = 1.55 * cube (r); // ERROR, r is not constexpr constexpr double v = square (p); // ERROR, square does not return constexpr double w = cube (p); // OK, can use constexpr function in non-constexpr contexts // .... }
The variable p is a constexpr; so, s can also be a constexpr. Both p and s are evaluated at compile time. t cannot be a constexpr because q is not a constexpr. Similarly, u cannot be a constexpr because r is not a constexpr. Also, v cannot be a constexpr because the function square does not return a constexpr. We can remove the first two errors by making q and r constexpr's. Also, the function double square () should be made constexpr double square () to make v a constexpr. Finally, although cube returns a constexpr, it can be used in non-constexpr contexts. Any constexpr function can be used non-constexpr context, but, not vice-versa.
7. Arrays and Pointers
C++ provides arrays and pointers. An array is a sequence of objects of a base type called elements, with the elements occupying consecutive memory locations. The number of elements in an array is prefixed at compile-time. An array is defined as,
int v [10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Here, v is an array of 10 integers, which are the elements of the array. An element in an array can be accessed as array-name [index]. The array index starts from zero. So, the array v has 10 elements, v [0], v [1], ..., v [9]. The index of the last element is 1 less than the array size.
A pointer variable points to an object of a base type. It stores the address of an object; so it is said to be pointing to that object. Using a pointer variable, we can access the object it points to. For example, a pointer to an integer is defined as,
int *ptr; // ptr is a pointer to an int
[] and * are declaration operators. [] means array of, and * means pointer to.
The unary & operator, in an expression, gives the address of a variable. So, we can assign the address of v [5] as below.
ptr = &v[5];
Now, ptr points to v [5]. We can access the object a pointer points using the star operator. So, in this case, we can now access v [5] as *ptr.
The unary & is also a declaration operator. It gives a reference to a variable, which is a new name for a variable. It is like a pointer, but you do not need to put the star (*) operator to use the object. Also, once a name refers to an object, it is bound to that object and it can not be made to refer to another object.
int i = 77; int& j = i; cout << "j = " << j << '\n'; // prints "j = 77"
8. Loops
Like C, C++ provides the while, for, and do-while loops. Let's look at an example of using the for loop. We use the for loop for finding the sum of the elements of an array storing ten integers.
#include <iostream> using namespace std; int main () { int v [10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int sum {0}; for (auto i = 0; i < 10; i++) { cout << v [i] << '\n'; sum += v [i]; } cout << "The sum is " << sum << '\n'; }
It prints the array elements, one per line, and, then, the sum. In C++, we use an alternate form of for statement, called the range-for statement, which is like,
for (type elem : range-expression)
...
The range-expression is a sequence like an array or a vector, or a list of comma separated values in braces. For each iteration of the for loop, the next element of the sequence specified by the range-expression is copied in elem. It is preferable to use type of elem as auto.
#include <iostream> using namespace std; int main () { int v [] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int sum {0}; for (auto x : v) { cout << x << '\n'; sum += x; } cout << "The sum is " << sum << '\n'; }
Instead of using a copy of each element of the sequence in the loop control variable, elem, we can use the reference, as in the example below.
#include <iostream> using namespace std; int main () { int v [] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int sum {0}; for (auto& x : v) { cout << x << '\n'; sum += x; } cout << "The sum is " << sum << '\n'; }
9. User-Defined Types
We have seen the basic built-in types - char, bool, int and double. These reflect the basic capabilities of the underlying hardware. These help in writing expressions in a program. In addition to these, C++ provides abstraction mechanisms for making higher level user-defined types that can capture and represent the attributes and behavior of real-life entities. The user-defined types are classes, structures, unions, and enumerations.
9.1 Classes
The two important characteristics of object-oriented programming are encapsulation and inheritance. Encapsulation involves "putting together" data and code for an object. A class is a user-defined type for an object. Inheritance involves building a hierarchy of classes where a derived class inherits the properties of a base class. For example, consider the class Queue.
class Queue { public: Queue (); void push (const std::string&); std::string pop (); std::string peekHead () const; std::string peekTail () const; bool isEmpty () const; void printQ () const; private: struct element* tail; };
A class comprises of members, which can be data, or attributes, and functions, which are also called methods. The members are classified as public and private. The public members are for the users of the class, and can be accessed from outside the class. The private members are internal to the class; these cannot be accessed from outside the class. The public members define the class for the users. The users of the class do not have to worry about the implementation of the class. Then can just use the class as per the public members.
A queue is a first-in, first-out data structure. It has two ends, called the head and the tail. The elements enter queue from the tail end, progress towards the head, and eventually exit the queue from the head end. The elements progress and exit the queue in the order of entrance to the queue, making the queue a first-in, first-out data structure.
There are multiple ways to implement the queue data structure. One common and effective way to implement the queue is using a linked list of elements. The linked list is managed using two pointers to the elements, the head and the tail. There are two major operations that can be done on a queue and these are push and pop. An element enters the queue at the tail using the push operation and it leaves the queue at the head when there is a pop operation done on the queue. Then, there are auxiliary operations, peek for the head and tail of queue, which tell elements that are currently at the head and tail of the queue respectively. We also want to know whether the queue is empty and, also, we wish to print the queue at any time. We will look at the program for implementing the class Queue in the later part of this post.
9.2 Structures
A structure is a class with public members by default. Apart from that, the structures provide the same functionality as the classes. For example, consider the struct Employee in the program below.
#include <iostream> using namespace std; int main () { struct Employee { Employee (string s, string t, string u) : empId {s}, name {t}, address {u} {} void setname (string s) { name = s; } string getname () { return name; } private: string empId; string name; string address; }; Employee e1 ("ABC123", "John Dow", "DEF456"); cout << e1.getname () << endl; e1.setname ("Joe Gargery"); string s = e1.getname (); cout << s << endl; }
By default, all members of a struct are public. Here, we have declared the data members, empId, name and address as private. Apart from these, the constructor, Employee () and methods, setname () and getname () are public. As an example, we have created an object e1 of type Employee by calling the constructor. The data members are private. So, we have modified the member name using the setname method and queried name for printing using the getname method.
9.3 Unions
A union is a struct that holds only one of the data members at a time. All the data members have the same address and the space allocated for a union is the space required by the largest data member in the union. It is for the programmer to keep track of the data member actually present in the union at any time. For example, in the structure Employee, we want to add the data of birth of the employee. But, for some employees, date of birth is not available and the best we have is the approximate age of the employee. So, we can have a union, with two data members, dob and approxAge.
union DobOrAge { string dob; int approxAge; }; struct Employee { // ... private: string empId; string name; string address; union DobOrAge dobOrAge; bool dobAvaliable; };
We keep a flag dobAvailable in the structure, which, if true indicates that the date of birth is kept in the union. If dobAvailable is false, it means that the approximate age of the employee is kept in the union. Unions help in conserving the memory. Only one data member out of many is relevant at any time, which needs to be kept in the memory for that duration of time.
Unions are sometimes misused for finding how a certain type looks when viewed as another type. For example, if we want to see the individual bytes of an integer, we can do something like this.
// print bytes of an integer #include <iostream> using namespace std; int main () { union IntOrChar { IntOrChar (int i) : intVal {i} {} int intVal; char charVal [4]; }; int i = 999779; // an arbitrary number IntOrChar ioc (i); cout << hex; // print integers in hexadecimal for (int i = sizeof (ioc.charVal) - 1; i >= 0; i--) { cout << (unsigned int) ioc.charVal [i] << ' ' ; } cout << endl; }
However, there are better ways of achieving the same result and this is best avoided.
9.4 Enumerations
C++ provides enumeration types, and there are two of them, the "plain" enum's and the enum class. A "plain" enum type defines a set of values which a variable of that type can take. For example,
enum PrimaryColors { Red, Green, Blue };
PrimaryColors is a "plain" enum type whose variables can take values from the set { Red, Green, Blue}. However, the names "Red", "Green" and "Blue" are "unscoped"; they are visible in the entire scope where enum PrimaryColors is declared. So, we cannot declare another enum that has the same names.
enum PrimaryColors { Red, Green, Blue }; enum ArtColors { Red, Yellow, Green }; // Error
A better way is to use enum class instead of plain enum's. Then the values are available only in the scope of the class. You cannot use unqualified values like "Red" or "Green"; you have use something like "ArtColors::Red". Now you get the benefit of type safety. The value PrimaryColors::Red is completely different from ArtColors::Red. For example,
enum class PrimaryColors { Red, Green, Blue }; enum class ArtColors { Red, Yellow, Green }; ... PrimaryColors pcolor = Red; // error, "Red" is not available in this scope PrimaryColors pcolor = ArtColors::Red; // error, pcolor is of type PrimaryColors, // not ArtColors PrimaryColors pcolor = PrimaryColors::Red; // OK
The default operators for enum class are assignment, initialization and comparison. We can use static_cast
int i = pcolor; //error, cannot convert PrimaryColors to int int i = static_cast<int> (pcolor); // OK
10. Namespaces
Real world programs tend to be large; they need be broken down into logical entities so that the development can be managed. Also, we need to use libraries in our programs, the most common case being the standard library, std. The parts are compiled separately, but they need to be linked together to make the final executable file. Somehow, we need to ensure that the global names do not clash. To this effect, C++ has the mechanism of namespaces. A namespace defines a scope where classes, functions, enumerations, etc, which belong together can be put. After that, we can access a name by qualifying it with a namespace, like std::cout. Or, we could declare "using namespace std;" to access the names in the std namespace.
11. Error Handling
A program may have syntax and/or logical errors. Syntax errors are like a misspelled name, missing semicolon, mismatch in braces, etc. These are caught by the compiler and have to be corrected before the program can run and produce results. Then, there are exceptions. An exception might occur because of certain conditions that interrupt the normal control flow. For example, the situation where the caller function calls for the next element in the queue and the queue is empty. The exceptions can be runtime error, parameter out of range, invalid argument, etc. C++ provides a mechanism for handling exceptions.
Some functions have conditions where exceptions can occur. If a function gets an exception during execution, information about the exception needs to be communicated to the calling function. It does that by "throwing" the exception. The calling function knows that the called function can throw an exception. It puts the code for one or more function calls that can have exceptions in a try block. The try block has one or more associated catch blocks that catch exceptions. For example,
#include <iostream> int f1 (int i) // Example function: Always throws an exception { if (i > 50) throw std::runtime_error ("Testing error 41"); else throw std::invalid_argument ("Invalid argument"); return 0; } int main () { int i = 50; try { f1 (i); } catch (std::runtime_error) { std::cerr << "Runtime error" << std::endl; } catch (...) { std::cerr << "An exception occurred" << std::endl; } return 0; }
The catch (...) block is for "catch all" exceptions, that is, all exceptions that have not been caught by previous catch (...) blocks, if any. If the called function throws an error and it is not caught by the calling function, the program terminates.
12. Example Program: An implementation of Queue using a linked list
As an example for the concepts introduced so far, we have a program for queue, implemented using a linked-list. The program is organized in three files Queue.h, Queue.cpp, and userq.cpp. Queue.h is the header file, which needs to be included by any program that wishes to use our queue. It contains the declarations for the Queue class, and the structure used by the elements in the queue. Queue.cpp is the program implementing the queue as a linked-list. There is a pointer, called tail, which points to the last element in the queue. The last element's next pointer points to the first element of the queue. So, the head of the queue is tail -> next, and we use that expression, instead of a separate variable, for the head of the queue. Finally, the userq.cpp is a user program, using the queue. The schematic of the queue looks like this.
The source code for the header file Queue.h is,
// Queue.h class Queue { public: Queue (); ~Queue (); void push (const std::string&); std::string pop (); std::string peekHead () const; std::string peekTail () const; bool isEmpty () const; void printQ () const; private: struct Element { std::string *name; Element* next; }; Element* tail; };
The Queue.cpp file is as follows.
// Queue.cpp #include <iostream> #include <string> #include "Queue.h" using namespace std; Queue::Queue () : tail {nullptr} {} Queue::~Queue () { // delete the existing queue while (tail != nullptr) { Element* ptr = tail -> next; // head if (ptr == tail) tail = nullptr; else tail -> next = ptr -> next; cout << "Deleting " << *(ptr -> name) << endl; delete (ptr -> name); delete (ptr); } } void Queue::push (const string& s) { // Put at the end of queue string *s1 = new string (s); Element* ptr = new (Element); ptr -> name = s1; if (tail == nullptr) ptr -> next = ptr; else { ptr -> next = tail -> next; tail -> next = ptr; } tail = ptr; cout << "Added " << *s1 << endl; } string Queue::pop () { // pop the head of the queue if (tail == nullptr) throw runtime_error ("Queue is empty"); Element *ptr = tail -> next; // head of the queue string s = *(ptr -> name); if (ptr == tail) tail = nullptr; else tail -> next = ptr -> next; delete (ptr -> name); delete (ptr); return s; } string Queue::peekHead () const { string s; if (tail == nullptr) throw runtime_error ("Queue is empty!"); else { s = *((tail -> next) -> name); return s; } } string Queue::peekTail () const { string s; if (tail == nullptr) throw runtime_error ("Queue is empty!"); else { s = *(tail -> name); return s; } } bool Queue::isEmpty () const { // returns true if the queue is empty return tail == nullptr; } void Queue::printQ () const { // print persons in the queue if (isEmpty ()) { cout << "Queue is empty" << endl; return; } int num = 0; Element* ptr = tail -> next; // head while (true) { cout << ++num << ". " << *(ptr -> name) << endl; if (ptr == tail) break; else ptr = ptr -> next; } }
And the user program, userq.cpp is,
// userq.cpp #include <iostream> #include <string> #include "Queue.h" using namespace std; enum class Command { Push = 1, Pop, PeekHead, PeekTail, IsQueueEmpty, PrintQ, Quit = 9 }; int main () { Queue q; int input = 0; Command command; bool over = false; string s; while (!over) { cout << "\nQueue Management\n\n"; cout << "1. Push: Add to queue\n"; cout << "2. Pop: Delete head of queue\n"; cout << "3. Peek: Head of queue\n"; cout << "4. Peek: Tail of queue\n"; cout << "5. Is the queue empty?\n"; cout << "6. Print queue\n"; cout << "9. QUIT\n\n"; cout << "Your option: "; getline (cin, s); try { input = stoi (s); // Convert the string to an integer } catch (std::invalid_argument& e) { cerr << "Invalid input: Not a number! Press ENTER to continue "; getline (cin, s); continue; } catch (std::out_of_range& e) { cerr << "Invalid input: Number out of range! Press ENTER to continue "; getline (cin, s); continue; } if (input < 1 || input > 9 || input == 7 or input == 8) { cerr << "Error in input. Please enter value 1-6 or 9 for Quit. Press ENTER to continue "; getline (cin, s); continue; } command = static_cast<Command> (input); string name; switch (command) { case Command::Push : cout << "Enter name: "; getline (cin, name); q.push (name); break; case Command::Pop : try { name = q.pop (); cout << name << " exited the queue.\n"; } catch (runtime_error) { cerr << "The queue is empty!\n"; } break; case Command::PeekHead : try { name = q.peekHead (); cout << name << " is at the beginning of queue.\n"; } catch (runtime_error) { cerr << "The queue is empty!\n"; } break; case Command::PeekTail : try { name = q.peekTail (); cout << name << " is at the end of the queue.\n"; } catch (runtime_error) { cerr << "The queue is empty!\n"; } break; case Command::IsQueueEmpty : cout << "Is the queue empty? " << ((q.isEmpty ()) ? "Yes." : "No.") << '\n'; break; case Command::PrintQ : cout << "The queue is: \n\n"; q.printQ (); break; case Command::Quit : over = true; break; } if (!over) { cout << "Press ENTER to continue "; getline (cin, s); } } cout << "\nEnding the program...\n"; return 0; }
The Makefile is,
# # Makefile # .PHONY: clean LDLIBS := LDFLAGS := OBJECTS := Queue.o userq.o userq: $(OBJECTS) $(CXX) $(LDFLAGS) $(OBJECTS) $(LDLIBS) -o userq Queue.o: Queue.cpp Queue.h userq.o: userq.cpp Queue.h clean: rm -f *.o rm -f userq
We can compile and build the above program,
$ make g++ -c -o Queue.o Queue.cpp g++ -c -o userq.o userq.cpp g++ Queue.o userq.o -o userq $ $ ./userq Queue Management 1. Push: Add to queue 2. Pop: Delete head of queue 3. Peek: Head of queue 4. Peek: Tail of queue 5. Is the queue empty? 6. Print queue 9. QUIT Your option: 1 Enter name: Jane Doe Added Jane Doe Press ENTER to continue ...