DEV Community

Cover image for Intermediate C++ tutorial: Strings, maps, memory, and more
Hunter Johnson for Educative

Posted on • Originally published at educative.io

Intermediate C++ tutorial: Strings, maps, memory, and more

C++ is one of the oldest programming languages, created back in 1979. It has remained a crucial language for modern developers for its uses in large system performance for fields like video game development, operating systems, browsers, and both office and medical software.

It’s easy to see why developers want to learn C++. If you’re already learning C++, you’ve probably noticed that it can be challenging to pick up.

Worry not! C++ is still a valuable language to learn, so today we’ll walk you through some intermediate C++ concepts and examples to get you one step closer to mastering this challenging but sought-after language.

Here are the topics we’ll cover today:

Using object-oriented programming in C++

C++ is an object-oriented language. This paradigm is one of its defining differences from C and C#. In C++, we accomplish this by using objects and classes to store and manipulate data. Through this object-oriented approach, developers can implement the strategies of inheritance, polymorphism, abstraction, and encapsulation.

For a refresher on object-oriented concepts and terms, see What is Object Oriented Programming? OOP Explained in Depth.

When learning C++, it’s important to understand how to make the most of its OOP capabilities. Below, we’ll explore some examples of each of the core OOP strategies: inheritance, polymorphism, abstraction, and encapsulation.

Using Inheritance in C++

Setting up inheritance relationships in C++ is easy; we simply append a class declaration with : [parent name](). This allows us to share both public variables and methods from parent class to child.

Below, you’ll see how we can use this to create the BankAccount class, pulling from the Account class.

#include <iostream>

class Account{

public:
   Account(double b): balance(b){}

   void deposit(double amt){
     balance += amt;
   }

   void withdraw(double amt){
     balance -= amt;
   }

   double getBalance() const {
     return balance;
   }

private:
   double balance;

};

class BankAccount: public Account{

public:
  // using Account::Account;
  BankAccount(double b): Account(b){}

  void addInterest(){
    deposit( getBalance()*0.05 );
  }
};

int main(){

  std::cout << std::endl;

  BankAccount bankAcc(100.0);
  bankAcc.deposit(50.0);
  bankAcc.deposit(25.15);
  bankAcc.withdraw(30);
  bankAcc.addInterest();

  std::cout << "bankAcc.getBalance(): " << bankAcc.getBalance() << std::endl;

  std::cout << std::endl;

}

-->
bankAcc.getBalance(): 152.407
Enter fullscreen mode Exit fullscreen mode

Above, we create two classes: the parent Account and the child BankAccount. We defined the public functions deposit, withdraw, and getBalance in Account as well as addInterest in BankAccount.

In our main section, we see how we use all of these functions on our new BankAccount object, bankAcc, regardless of if the function is from the parent or child class.

Using Polymorphism in C++

One of the most common applications of polymorphism in C++ is through function overriding. By having two functions of the same name in different classes, we can create code that completes different behavior depending on the inheritance of the selected object.

Below, we’ll see an example of how polymorphism can be used to implement a sample Draw function across different shape classes:

To focus on overridden functions, we’ve just filled each Draw with a simple, unique count statement. However, this could be replaced by a drawing algorithm and behave in the same way.

#include <iostream>

class Shape {
public:
  Shape() {}
  //defining a virtual function called Draw for shape class
  virtual void Draw() { std::cout << "Drawing a Shape" << std::endl; }
};

class Rectangle : public Shape {
public:
  Rectangle() {}
  //Draw function defined for Rectangle class
  virtual void Draw() { std::cout << "Drawing a Rectangle" << std::endl; }
};

class Triangle : public Shape {
public:
  Triangle() {}
  //Draw function defined for Triangle class
  virtual void Draw() { std::cout << "Drawing a Triangle" << std::endl; }
};

class Circle : public Shape {
public:
  Circle() {}
  //Draw function defined for Circle class
  virtual void Draw() { std::cout << "Drawing a Circle" << std::endl; }
};

int main()
{
  Shape* s;

  Triangle  tri;
  Rectangle rec;
  Circle    circ;

  // store the address of Rectangle
  s = &rec;
  // call Rectangle Draw function
  s->Draw();

  // store the address of Triangle
  s = &tri;
  // call Triangle Draw function
  s->Draw();

  // store the address of Circle
  s = &circ;
  // call Circle Draw function
  s->Draw();

  return 0;
}

-->
Drawing a Rectangle
Drawing a Triangle
Drawing a Circle
Enter fullscreen mode Exit fullscreen mode

Above we see a parent class, shape, and three child classes, Triangle, Rectangle, and Circle. The process of drawing each shape will differ depending on the shape itself. This leads us to use polymorphism to define a Draw function for each of the classes.

C++ will prioritize the local function over the parent Draw function by default. This means that we can call Draw without worrying about which shape class the object is. If it is a rectangle, the output will be “Drawing a rectangle”. If it's a triangle, it will output “Drawing a triangle”, and so on.

Abstraction and Encapsulation

Abstraction works similarly in C++ as other hard-coded languages through the use of the private and public keywords. This is similar to abstraction, as abstraction acts to hide non-essential information. Encapsulation can be used to achieve abstraction via private getter and setter functions.

Below, we’ll see how to achieve both abstraction and encapsulation in C++:

#include <iostream>
using namespace std;

class abstraction
{
    private:
        int a, b;

    public:

        // public setter function
        void set(int x, int y)
        {
            a = x;
            b = y;
        }

        // public getter function  
        void display()
        {
            cout<<"a = " <<a << endl;
            cout<<"b = " << b << endl;
        }
};

int main() 
{
    abstraction obj;

    obj.set(1, 2);
    obj.display();

    return 0;
}

-->
a = 1
b = 2
Enter fullscreen mode Exit fullscreen mode

Here we first create an abstraction class that initializes private variables a and b. We also define two public functions, set and display. This achieves encapsulation by separating variables from the outside world, so we can only access them via public functions. Using set, we then change the values from inside main. Finally, we print the variables using display.

This ensures that users cannot directly change variables but can use them through the getter and setter functions, achieving abstraction by keeping the variables themselves secure and hidden from users.

How do Strings work in C++?

Strings in C++ are similar to those of other languages in that they’re a collection of ordered characters. However, in C++, there are two ways to create strings, either using C-style strings or the C++ string class.

C style strings are the old-fashioned way of creating strings in C++. Rather than a standard string object, C-style strings consist of a null-terminated array of characters ending with the special character \0. Due to this hidden character, C-style strings are always a length of one more character than the visible number of characters.

This size can either be unspecified to be set automatically to the required size or manually to any desired size.

char str[] = "Educative"; // automatic length set to 10
char str[10] = "Educative"; // manual set length to 10
Enter fullscreen mode Exit fullscreen mode

We can also create strings in C++ using the C++ string class that is built into the standard C++ library. This is the more popular method, as all memory management, allocation, and null termination are internally handled by the class. Another advantage is that the length of the string can be changed at runtime thanks to the dynamic allocation of memory.

Overall, these changes make the string class more resistant to errors and provide many built-in functions like append(), which adds to the end of the string, and length(), which returns the number of characters in the string.

Below, we’ll learn how to use functions like these to complete common manipulations of strings.

How to print a string

We can print strings in C++ using the cout global object along with the << operator, which precedes the printed content. We also include the endl global object, which is used to skip a line after the operation for better readability. As both endl and cout are predefined objects of the global class ostream, we must make sure to include the <iostream> header in programs where they are needed.

Below we can see how we’d initialize and print str1:

#include <iostream>
#include <string>
using namespace std;
int main() {
   string str1 ("printed string"); //initializes the string
   cout << str1 << endl;  //prints string
   return 0;
}

-->
printed string
Enter fullscreen mode Exit fullscreen mode

How to calculate the length of a string

To calculate the length of a string, we can use either the length() or size() functions. Each performs identically, and each exists to aid in readability. These functions can also be used to measure the length of STL containers, like maps and vectors.

The synonymous functions are included to increase readability, while the length is intuitive to use for string. It would be more intuitive to refer to an array’s size than its length.

Below we’ll see how to print the length of string str1 with both the length and size functions.

#include <iostream>
#include <string>
using namespace std;

int main() {
  string str1 = "Hello"; //initilization

  //calculate length
  cout << "myStr's length is " << str1.length() << endl;
  cout << "myStr's size is " << str1.size() << endl;

  return 0;
}

-->
myStr's length is 5
myStr's size is 5
Enter fullscreen mode Exit fullscreen mode

How to concatenate a string

This final manipulation is a fancy way of saying “stick together two strings”. When doing this in C++ we can use either the + operator or the predefined append function which each achieve the same effect.

For simple test code, there is close to no difference between the two options. When used in larger, more complicated programs, however, append will perform significantly faster than +.

#include <iostream>
#include <string>
using namespace std;
int main() {

string str1= "combined ";
string str2 = "strings";
string str3 = str1 + str2;
cout << str3 << endl;

//OR

string str4 = str1.append(str2);
cout << str4;

}

-->
combined strings
combined strings
Enter fullscreen mode Exit fullscreen mode

What is a Pointer in C++?

In C++, all variables must be stored somewhere within the host computer’s memory. To help programs find these variables without searching the memory, C++ allows us to use the special variable, pointers, to explicitly give the variable’s address.

Pointers carry two pieces of information:

  • The memory address stored as the value of the pointer
  • The data type indicating the type of variable it points to

Declaring a pointer is similar to declaring a standard variable, except that the pointer’s name is preceded by an asterisk.

int *ptr; 
struct coord *pCrd; 
void *vp;
Enter fullscreen mode Exit fullscreen mode

Let's see how this can be used below:

#include <iostream>
using namespace std;

int main ()
{
  int val1, val2;
  int * mypointer;

  mypointer = &val1;
  *mypointer = 10;
  mypointer = &val2;
  *mypointer = 20;
  cout << "firstvalue is " << val1 << '\n';
  cout << "secondvalue is " << val2 << '\n';
  return 0;
}

-->
firstvalue is 10
secondvalue is 20
Enter fullscreen mode Exit fullscreen mode

First, we initialize two int variables, val1, and val2 as well as an int pointer, mypointer. We then set mypointer to the address of val1 using the & operator. Then the value mypointer points to is set to 10. Since mypointer currently points to the address of val1, this operation changes the value of val1.

We then repeat that process, setting mypointer to the address of val2 and the value at that location to 20.

Pointers have two main advantages in C++: speed and memory use. Using pointers reduces runtime, as programs can access values more quickly when they are given direct memory addresses.

Pointer cheat sheet

As pointers are one of the more unique elements of C++, it can be tricky to remember all you can do with them. For help, here's our quick guide on basic pointer syntax:

Cpp cheatsheet

What are Arrays in C++?

C++ arrays are a collection of similar data types stored under the same name. They’re often visualized as a row of i boxes that can be selected by calling the box’s index to access the value held within.

Tip: Array index values begin at 0, meaning the first element in the array would be accessed by calling the element 0 rather than 1

The length of an array is set (either explicitly or implicitly) at declaration and cannot change without remaking the array entirely. This, however, makes the array a very memory-efficient structure compared to the vector, as no memory is used after the array is initialized.

#include <iostream>
using namespace std;

int main() {
  int arr[5] = {19, 10, 5, 6, 14}; //initializing the array with 5 values
  cout << "The value of arrr[0], that is, the first value in the array is: " << arr[0] << endl;
  cout<< "The value of arrr[1], that is, the second value in the array is: " << arr[1] << endl;
  cout<< "The value of arrr[2], that is, the third value in the array is: " << arr[2] << endl;
  cout<< "The value of arrr[3], that is, the fourth value in the array is: " << arr[3] << endl;
  cout<< "The value of arrr[4], that is, the fifth value in the array is: " << arr[4] << endl;
  int arr2[] = {1,2,3,4}; //we don't specify the size and the compiler assumes a size of 4
}

-->
The value of arrr[0], that is, the first value in the array is: 19
The value of arrr[1], that is, the second value in the array is: 10
The value of arrr[2], that is, the third value in the array is: 5
The value of arrr[3], that is, the fourth value in the array is: 6
The value of arrr[4], that is, the fifth value in the array is: 14
Enter fullscreen mode Exit fullscreen mode

How to find the length of an array in C++

Unlike STL containers and strings, we cannot find the length of an array using length or size. Instead, we use either the sizeof() operator or by using pointer arithmetic.

Let's see how we can use each, starting with sizeof:

#include <iostream>
using namespace std;

int main() {
  int arr[] = {1,2,3,4,5,6};
  int arrSize = sizeof(arr)/sizeof(arr[0]);
  cout << "The size of the array is: " << arrSize;
  return 0;
}

-->
The size of the array is: 6
Enter fullscreen mode Exit fullscreen mode

Unlike the length function, sizeof actually returns the number of bytes the selected object takes up in memory. The size of each element of an array varies from array to array, so we cannot assume it is only one byte.

To get around this, we use each element within the same array and will use the same amount of memory. Therefore, if we divide the total bytes used by the array, sizeof(arr), by the number of bytes used by the first element, sizeof(arr[0]), we get the number of elements in the array.

Another way to find the size is using pointer arithmetic:

#include <iostream>
using namespace std;

int main() {
  int arr[] = {1,2,3,4,5,6};
  int arrSize = *(&arr + 1) - arr;
  cout << "The size of the array is: " << arrSize;
  return 0;
}

-->
The size of the array is: 6
Enter fullscreen mode Exit fullscreen mode

We can achieve a similar effect if we consider that the size of an array is equal to the difference between the address of the final element and the first element of the array.

Here’s a step-by-step breakdown:

  • (&arr + 1) points to the memory address right after the end of the array.
  • (&arr + 1) simply casts the above address to an int *.
  • Subtracting the address of the start of the array from the address of the end of the array​ gives the length of the array.

What is a Vector in C++?

C++ vectors are STL containers that act as a more refined version of string arrays. They simplify the process of inserting and deleting values at the cost of using more memory. Vectors store elements in a contiguous manner, so the elements are stored in memory side by side. Unlike arrays, vectors are dynamic, so their size can change on demand and can be traversed using iterators like begin() (for the beginning of the vector) and end() (for the end of the vector).

Vectors are best for situations where you will add or subtract values regularly, and memory is largely available.

First, let's see how to use the resize function to activate the vector’s handy dynamic capabilities:

#include <iostream>
#include <vector>
using namespace std;

int main() {
  vector<int> numbers;

  numbers.resize(7);
  cout<<numbers.size()<<endl;
  numbers.resize(4);
  cout<<numbers.size();
}

-->
7
4
Enter fullscreen mode Exit fullscreen mode

Resize sets the maximum number of elements in a vector to the specified number. Above, we first initialize the vector numbers, then set its size using resize(7). Say we then realize that we will cut 3 of the 7 elements. We would use resize(4) to trim the extra 3 elements to avoid unused elements cluttering the vector.

Below, we’ll see how to create a vector, use the push_back function to add values, and begin and end functions to print:

#include <iostream>
#include <vector>
using namespace std;

int main() {
  vector<int> v; // Vector's Implementation

    // Inserting Values in Vector
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);

    cout << "Output from begin to end: ";
    for (auto i = v.begin(); i != v.end(); ++i)
        cout << *i << " ";
}

-->
Output from begin to end: 1 2 3 4 5 
Enter fullscreen mode Exit fullscreen mode

Tip: Vectors resize automatically to fit new elements when using push_back or insert.

In C++, we must first initialize vector v and then populate it with values. We want the values to go on the end, not beginning, so we use the push_back function, which inserts the new element on the end. From there, we print each value in the vector between its beginning, selected with the begin function, and end, selected with the end function.

Using C++ Maps

Maps are a type of container that stores elements using key-value pairs. Each element on a map has a unique key as an identifier and a value that is retrieved when the key is called. Keys are automatically sorted from smallest to largest, which makes searching maps very quick.

When initialized, both the key and value data types must be set as well as the map object’s name.

Tip: Integers are the most common type of key used with maps; however, they can be other types as well.

The most important functions used with maps are insert(), which adds a new element to the map, and find(), which pulls the element with a corresponding key and returns its value.

Below, we see both of these functions in action with our Employees map:


#include <string.h>  
#include <iostream>  
#include <map>  
#include <utility>  
using namespace std; 

int main()  
{
  // Initializing a map with integer keys
  // and corresponding string values
  map<int, string> Employees; 

  //Inserting values in map using insert function
  Employees.insert ( std::pair<int, string>(101,"Aaron") );
  Employees.insert ( std::pair<int, string>(102,"Amanda") );
  Employees.insert ( std::pair<int, string>(105,"Ryan") );

  // Finding the value corresponding to the key '102'
  std::map<int, string>::iterator it = Employees.find(102);
  if (it != Employees.end()){
    std::cout <<endl<< "Value of key = 102 => " << Employees.find(102)->second << '\n';
  }
}

-->
Value of key = 102 => Amanda
Enter fullscreen mode Exit fullscreen mode

Maps are helpful for containing data that will be searched often and breakdown well into an associative structure, such as a company’s email registry.

How to sort a map by value in C++

As stated above, maps sort by key automatically. However, it may be beneficial to sort by value instead. We can do this by copying the elements to a vector of key-value pairs and then sorting the vector before finally printing.

Let's see that in action:

#include <iostream>
#include <map>
#include <vector>
#include <algorithm> // for sort function

using namespace std;

// utility comparator function to pass to the sort() module
bool sortByVal(const pair<string, int> &a, 
               const pair<string, int> &b) 
{ 
    return (a.second < b.second); 
} 

int main()
{
  // create the map
  map<string, int> mymap = {
    {"coconut", 10}, {"apple", 5}, {"peach", 30}, {"mango", 8}
  };

  cout << "The map, sorted by keys, is: " << endl;
  map<string, int> :: iterator it;
  for (it=mymap.begin(); it!=mymap.end(); it++) 
  { 
    cout << it->first << ": " << it->second << endl;
  }
  cout << endl;

  // create a empty vector of pairs
  vector<pair<string, int>> vec;

  // copy key-value pairs from the map to the vector
  map<string, int> :: iterator it2;
  for (it2=mymap.begin(); it2!=mymap.end(); it2++) 
  {
    vec.push_back(make_pair(it2->first, it2->second));
  }

  // // sort the vector by increasing order of its pair's second value
  sort(vec.begin(), vec.end(), sortByVal); 

  // print the vector
  cout << "The map, sorted by value is: " << endl;
  for (int i = 0; i < vec.size(); i++)
  {
    cout << vec[i].first << ": " << vec[i].second << endl;
  }
  return 0;
}

-->
The map, sorted by keys, is: 
apple: 5
coconut: 10
mango: 8
peach: 30

The map, sorted by value is: 
apple: 5
mango: 8
coconut: 10
peach: 30
Enter fullscreen mode Exit fullscreen mode

Memory Management in C++

One of the most unique facets of C++ is the need to explicitly allocate heap memory in runtime, a process called dynamic memory allocation. This is handled automatically by the compiler in other languages like Java and JavaScript, giving the programmer less control in favor of ease.

To allocate memory in C++, we use the new operator for a variable and new[] for an array. Each returns the memory address where that data will be stored. We can use this in conjunction with the pointers we discussed earlier to then assign a value to that address.

See how the new operator and pointers are used together below:

// declares an int pointer
int* var;

// allocate memory for variable
// using the new keyword 
var = new int;

// assign value to allocated memory
*var = 45;
Enter fullscreen mode Exit fullscreen mode

C++ does not have an automatic garbage collection system, meaning that you must manually deallocate memory from pointers once they’re no longer needed. To do this, we use the delete keyword, which frees up the memory for the given variable or container.

int* var;
var = new int;
delete var;
Enter fullscreen mode Exit fullscreen mode

If we forget to deallocate memory, we’ll get a memory leak due to unused pointers still holding allocated memory.

Tracking allocations and deallocations

Up until now, our example has one new and one delete, whereas practical programs may have dozens. When at a larger scale, it's harder to know if you’ve deleted all unneeded pointers or to find those unused pointers. To do a simple check, we can simply count the number of new allocations and compare that to the number of delete deallocations.

#include "myNew.hpp"
// #include "myNew2.hpp"
// #include "myNew3.hpp"

#include <iostream>
#include <string>

class MyClass{
  float* p= new float[100];
};

class MyClass2{
  int five= 5;
  std::string s= "hello";
};


int main(){

    int* myInt= new int(1998);
    double* myDouble= new double(3.14);
    double* myDoubleArray= new double[2]{1.1,1.2};

    MyClass* myClass= new MyClass;
    MyClass2* myClass2= new MyClass2;

    delete myDouble;
    delete [] myDoubleArray;
    delete myClass;
    delete myClass2;

  getInfo();

}

-->
Number of allocations: 6
Number of deallocations: 4
Enter fullscreen mode Exit fullscreen mode

Here we can see that we have 6 allocations but only 4 deallocations, meaning 2 of our pointers are still taking up memory. Though it does not tell us where these rogue pointers are, it does point us in the right direction if we’ve fully cleaned up after our program.

Attention to memory management is key to being successful as a C++ developer, as larger, complex programs have many more opportunities for mismanaged memory issues. If left unresolved, these issues can slow performance and even cause crashes in the program.

Tip: STL containers and C++ Strings automatically manage their memory allocation. Try using more of these to avoid memory leaks or having to micromanage every variable/container.

What to learn next

Congrats! You’ve now learned some of the intermediate C++ concepts that will make you a proficient and hire-able C++ developer.

As we discussed earlier, C++ is a difficult language to master, even for those with experience in other languages. However, once you get more familiar with its advanced capabilities, you’ll find that C++ grants you an unmatched level of control over often streamlined features like memory allocation.

As you continue your C++ learning journey, here are some advanced topics to check out next:

  • Smart Pointers
  • Move and copy semantics
  • Abstract Base classes
  • Virtual Methods
  • Templates

Educative’s new Grokking Coding Interview Patterns in C+ learning path walks you through these advanced topics and more, all with text-based interactive lessons and practice problems. These modules are career-focused, presenting the material you’ll need on the job, all written by expert C++ developers.

Happy learning!

Continue reading about C++ on Educative

Start a discussion

Why do you think learning C++ is a good idea for young developers? Was this article helpful? Let us know in the comments below!

Top comments (0)