Aller au contenu

Exercices de programmation

Afin de compléter les compétences en C++ qui sont requises pour ce cours, la leçon ‘C++’ a été conçue pour être suivie de façon autonome par les étudiants. Les exercises de cette leçon sont regroupés dans cette page.

Ces leçons sont également mises à disposition sous forme de leçons EduTools a travers CLion. Cour EduTools : Embedded C++.

Certains exercices seront revus en classe, mais il est attendu que les étudiants suivent ces leçons de façon autonome.

Points importants concernant la programmation orientée objet en C++

En comparaison au langage Java pratiqué au cours de la première année d’enseignement, il est intéressant de noter les points suivants :

  • Tout langage orienté objet a comme objectif de permettre de créer des objets ou instances de classe dont l’état est toujours cohérent. En particulier, l’état d’un objet doit être cohérent à sa création et tous les attributs de l’objet doivent être initialisés. C++, comme Java, réalise le chaînage des constructeurs afin d’assurer que les attributs hérités des classes parentes soient proprement initialisés. En C++, le chaînage des constructeurs par défaut est implicite et le chaînage des autres constructeurs est réalisé dans l’ initializer list.
  • En C++ comme en Java, un constructeur est appelé à la création d’un objet. Ce constructeur ne contient aucune instruction return.
  • Les constructeurs peuvent être surchargés, comme les méthodes. Un constructeur couramment surchargé est le copy constructor défini comme class(const class& other).

Contrairement à Java :

  • C++ ne garantit pas une initialisation par défaut des attributs d’une classe. Il faut noter que les dernières versions de C++ ont introduit des mécanismes d’initialisation par défaut, mais que ceux-ci sont restrictifs et que la bonne pratique veut donc que tous les attributs soient initialisés à la construction.
  • En C++, les opérateurs peuvent être surchargés. Il est donc par exemple possible de surcharger l’opérateur d’affectation qui est défini comme class& operator=(const class& other).

Preprocessor directives and macros

Exercice 1

Complete following code snippets in the main.cpp and definitions.h files. Execute the program to see the results.

definitions.h

/* Write the right preprocessor directive */ EMBEDDED_C__DEFINITIONS_H
/* Write the right preprocessor directive */ EMBEDDED_C__DEFINITIONS_H

/* Write the right preprocessor directive */ DEBUG_ON 1

/* Write the right preprocessor directive */ //EMBEDDED_C__DEFINITIONS_H
main.cpp

#include <iostream>
/* Write the right preprocessor directive */ "definitions.h"

int main() {
/* Write the right preprocessor directive */ DEBUG_ON
    std::cout << "Debug print is on" << std::endl;
/* Write the right preprocessor directive */
    std::cout << "Debug print is off" << std::endl;
/* Write the right preprocessor directive */
    return 0;
}
Solution

defintinons.h

#ifdef EMBEDDED_C__DEFINITIONS_H
#define EMBEDDED_C__DEFINITIONS_H

#define DEBUG_ON 1

#endif // EMBEDDED_C__DEFINITIONS_H

main.cpp

#include <iostream>
#include "definitions.h"

int main() {
#ifdef DEBUG_ON
    std::cout << "Debug print is on" << std::endl;
#else
    std::cout << "Debug print is off" << std::endl;
#endif
    return 0;
}

Namespace

Exercice 2

Which statements are correct ?

  • It is a good practice to split the code between header and source files.
  • The header file contains the implementation of the code.
  • Other files import the header files with the #include keyword to access to the public implementation.
  • The expression Class::method() is used in the .cpp file to define the constructors and methods.
  • Namespaces are useful to avoid name conflicts.
  • Namespaces can’t be nested.
  • std::cout means that cout is in the std namespace.
  • namespace A { } means that all classes and methods defined inside the brackets are in the namespace A.
Solution
  • It is a good practice to split the code between header and source files.
  • Other files import the header files with the #include keyword to access to the public implementation.
  • The expression Class::method() is used in the .cpp file to define the constructors and methods.
  • Namespaces are useful to avoid name conflicts.
  • std::cout means that cout is in the std namespace.
  • namespace A { } means that all classes and methods defined inside the brackets are in the namespace A.

Fundamental Data Types

Exercice 3

Observe the given code snippet and answer the questions below.

#include <iostream>

int main() {
    long x = 0;
    std::cout << "Number of bytes for long : " << sizeof(x) << std::endl;
    return 0;
}

How many bytes has the long data type ?

  • 1
  • 2
  • 3
  • 4
  • 8
  • It is machine dependent.
Solution
  • It is machine dependent.

Exercice 4

Observe the given code snippet and answer the questions below.

#include <iostream>

int main() {
    int32_t x = 0;
    std::cout << "Number of bytes for i32 : " << sizeof(x) << std::endl;
    return 0;
}

How many bytes has the int32_t data type ?

  • 1
  • 2
  • 3
  • 4
  • 8
  • It depends of the machine.
Solution
  • 4

Exercice 5

Observe the given code snippet and answer the questions below.

#include <string>

int main() {
    std::string s1 = "Hello";
    std::string s2 = "Hello";

    bool x = false;
    if (s1 == s2) {
        x = true;
    }
    return 0;
}

What is the value of x at the end of the program ?

  • true
  • false
Solution
  • true

Manipulation of std::string instances

Exercice 6

This code snippet checks if a string is a palindrome or not. For this, a method to delete all spaces in a string (eraseSpaces()) and to lower all characters (toLowerCase()) are written as well.

Observe how toLowerCase() uses the iterator std::string::iterator to access each character. Read the official documentation of the std::string and complete the code snippet.

The main() method should be able to run without errors and produce the expected results on the console.

#include <iostream>
#include <string>

std::string toLowerCase(std::string s) {
    for (std::string::iterator it = s.begin(); it != s.end(); it++) {
        // '*it' permits to access the character at the address pointed by the
        // iterator
        // use std::tolower for converting the given character to lower case
        // based on the current locale
        *it = std::tolower(*it);
    }
    return s;
}

std::string eraseSpaces(std::string s) {
/* Erase all spaces */
    return s;
}

bool palindrome(std::string s) {
/* Check if the string is a palindrom */
    return true;
}

int main() {
    std::string sPal = "madam";
    sPal = eraseSpaces(sPal);
    sPal = toLowerCase(sPal);
    std::cout << palindrome(sPal) << std::endl;

    sPal = "Do geese see God";
    sPal = eraseSpaces(sPal);
    sPal = toLowerCase(sPal);
    std::cout << palindrome(sPal) << std::endl;

    sPal = "This is not a palindrome";
    sPal = eraseSpaces(sPal);
    sPal = toLowerCase(sPal);
    std::cout << palindrome(sPal) << std::endl;

    return 0;
}
Solution

Complete the two functions eraseSpaces() and palindrome():

#include <iostream>
#include <string>

std::string toLowerCase(std::string s) {
    for (std::string::iterator it = s.begin(); it != s.end(); it++) {
        // '*it' permits to access the character at the address pointed by the
        // iterator
        // use std::tolower for converting the given character to lower case
        // based on the current locale
        *it = std::tolower(*it);
    }
    return s;
}

std::string eraseSpaces(std::string s) {
    std::string without_space = "";
    for (std::string::iterator it = s.begin(); it != s.end(); it++) {
        if(*it != ' '){
            without_space += *it;
        }
    }
    return without_space;
}

bool palindrome(std::string s) {
    std::string reversed(s.rbegin(), s.rend());
    return s == reversed;
}

int main() {
    std::string sPal = "madam";
    sPal = eraseSpaces(sPal);
    sPal = toLowerCase(sPal);
    std::cout << palindrome(sPal) << std::endl;

    sPal = "Do geese see God";
    sPal = eraseSpaces(sPal);
    sPal = toLowerCase(sPal);
    std::cout << palindrome(sPal) << std::endl;

    sPal = "This is not a palindrome";
    sPal = eraseSpaces(sPal);
    sPal = toLowerCase(sPal);
    std::cout << palindrome(sPal) << std::endl;

    return 0;
}

Expected output:

1
1
0

Variables and types

Exercice 7

Which statements are correct ?

  • C++ is a strongly typed language like Java.
  • In C++, variables are always stored on the stack.
  • In C++, global variables are stored on the heap.
  • In C++, it’s better to declare global variables with the const or constexpr keywords than the define keyword.
Solution
  • C++ is a strongly typed language like Java.
  • In C++, it’s better to declare global variables with the const or constexpr keywords than the define keyword.

Explore memory layout

Exercice 8

Write a program that explores the memory layout of a C++ program.

  • Declare a uninitialized global variable and print its address.
  • Declare an initialized global variable and print its address.

1/ Observe the addresses of the two global variables and determine which one is located in the .bss section and which one is located in the .data section.

Note that you can use the readelf tool to inspect the memory layout of the compiled program and identify the sections where the variables are located.

  • Write a recursive function like below and call it from the main function.
  • Inside the function, declare a local variable and print its address.

2/ Where is this local variable located in memory? What happens to the address of this local variable when the function is called recursively? Can you explain why?

void recursiveFunction(int n) {
    int localVar[16]; // Declare a local variable (array of 16 integers)
    ...

    if (n > 0) {
        recursiveFunction(n - 1);
    }
}
  • Declare a static local variable inside the function and print its address.

3/ Where is this static local variable located in memory? What happens to the address of this static local variable when the function is called recursively? Can you explain why?

  • Add a dynamic allocation of an array of 16 integers inside the function and print its address.

4/ Where is this dynamically allocated array located in memory? What happens to the address of this dynamically allocated array when the function is called recursively? Can you explain why?

5/ What happen if you call the recursive function with a large value of n?

6/ How we configure the stack size of a C++ program? What are the implications of increasing or decreasing the stack size?

Solution

memory_layout.cpp (solution)
#include <stdio.h>
#include <stdlib.h>

#include "mbed.h"
#include "mbed_trace.h"
#if defined(MBED_CONF_MBED_TRACE_ENABLE)
#define TRACE_GROUP "main"
#endif  // MBED_CONF_MBED_TRACE_ENABLE

// globals to force placement in .bss and .data
static int g_bss_var;
static int g_data_var            = 0xBEEF;
static const char g_rodata_var[] = "Hello from .rodata";

void recursiveFunction(int n)
{
    // stack allocation
    int localVar[16];  // Declare a local variable (array of 16 integers)

    // heap allocation
    const int heap_size = 16;
    int* heap_val       = (int*)malloc(sizeof(int) * heap_size);
    assert(heap_val != NULL);

    printf("--- recursion %d ---\n", n);
    printf("stack : &localVar = %p-%p\n",
           (void*)&localVar,
           (void*)((char*)&localVar + sizeof(localVar)));
    printf("heap  : heap_val  = %p-%p\n",
           (void*)heap_val,
           (void*)((char*)heap_val + sizeof(int) * heap_size));
    printf("-----------------------------\n");

    if (n > 0) {
        recursiveFunction(n - 1);
    }

    // free dynamically allocated memory
    free(heap_val);
}

int main(void)
{
#if defined(MBED_CONF_MBED_TRACE_ENABLE)
    if (mbed_trace_init() != 0) {
        tr_error("Failed to initialize mbed trace");
        return -1;
    }
#endif

    // .bss is zero-initialized in C/C++
    assert(0 == g_bss_var);

    // .data holds initialized globals
    assert(0xBEEF == g_data_var);

    printf("##########################################################################\n");
    printf("bss var      : &g_bss_var  = %p\n", (void*)&g_bss_var);
    printf("data var     : &g_data_var = %p\n", (void*)&g_data_var);
    printf("rodata var   : g_rodata_var = %p\n", (void*)g_rodata_var);
    printf("function addr: recursiveFunction = %p\n", (void*)recursiveFunction);
    recursiveFunction(4);

    // Let's call it a second time to see the stack and heap addresses again
    // Did you expect that ?
    recursiveFunction(4);

    while (1) {
        asm volatile("nop");
    }
}
1/ The uninitialized global variable is located in the .bss section, while the initialized global variable is located in the .data section. readelf -a firmware.elf is used to inspect the memory layout of the compiled program and identify the sections where the variables are located.

2/ The local variable is located on the stack. When the function is called recursively, a new instance of the local variable is created on the stack for each function call. The address of the local variable changes with each recursive call because each call creates a new stack frame. Note that the Mbed stack grows downwards, so the address of the local variable will decrease with each recursive call.

3/ The static local variable is located in the .data section (or .bss if uninitialized). When the function is called recursively, the address of the static local variable remains the same because it is shared across all instances of the function.

4/ The dynamically allocated array is located on the heap. When the function is called recursively, a new instance of the dynamically allocated array is created on the heap for each function call. The address of the dynamically allocated array changes with each recursive call because each call creates a new allocation on the heap. Note that the heap grows upwards in Mbed, so the address of the dynamically allocated array will increase with each recursive call.

5/ If you call the recursive function with a large value of n, it may lead to a stack overflow due to too many recursive calls consuming the stack space.

6/ The stack size of a C++ program in Mbed can be configured by editing the mbed_app.json file and setting the stack_size parameter. Increasing the stack size allows for deeper recursion and more local variables, but it also consumes more memory. Decreasing the stack size can save memory but may lead to stack overflow if the program requires more stack space than allocated. Note that each thread in Mbed has its own stack, so the stack size configuration applies to each thread individually.

Arrays

Exercice 9

Observe the given code snippet and answer the questions below.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3};
    // insert 6 at the position v.begin()
    v.insert(v.begin(), 6);
    // insert twice 5 at the position v.begin()
    v.insert(v.begin(), 2, 5);
    // append 4 at the end of the vector
    v.emplace_back(4);
    // remove the element specified at position --v.end()
    v.erase(--(--v.end()));
    return 0;
}

What is the content of the vector at the end of the program ?

  • 5, 5, 1, 2, 3, 4
  • 5, 5, 6, 1, 2, 3, 4
  • 5, 5, 6, 1, 2, 4
  • 5, 5, 6, 1, 3, 4
  • 5, 5, 6, 1, 2, 3
Solution
  • 5, 5, 6, 1, 2, 4

Exercice 10

Here is a small exercise to work with the array in C++.

  • The min() method searches the min value of the array. A fixed sized array on the stack is used.
  • The max() method searches the max value of the array. A fixed sized array on the heap is used. Remember how the array’s space on the heap must be freed.
  • The swap() method reverse the array values into a new array. For example, { 1, 2, 3, 4} becomes { 4, 3, 2, 1} and { 2, 4, 3} becomes {3, 4, 2}. You may of course use vector’s methods for implementing the method.

The main() method should be able to run without errors and produce the expected results.

#include <iostream>
#include <stdexcept>
#include <vector>

int min(const int myArray[], int size) {
    if (size <= 0)
        throw std::invalid_argument("Array is empty.");
/* Search the min value of the array */
}

int max(const int *myArray, int size) {
    if (size <= 0)
        throw std::invalid_argument("Array is empty.");
/* Search the max value of the array */
}

std::vector<int> swap(std::vector<int> v) {
    int size = v.size();
    std::vector<int> v2(0, size);
/* Swap the content of the given array into v2 */
    return v2;
}

int main() {
    // === Using min() with array on the stack
    int myArray1[] = {-1, 2, -3, 4, -5, 6};
    int size1 = /* Compute the size of the array */
    std::cout << min(myArray1, size1) << std::endl;

    // === Using max() with array on the heap
    int* myArray2 = new int[] {-1, 2, -3, 4, -5, 6};
    int size2 = 6; // The size can't be computed
    std::cout << max(myArray2, size2) << std::endl;
    /* Delete the array from the heap */
    myArray2 = nullptr;

    // === Using swap with vector
    std::vector<int> v1 = {1, 2, 3, 4, 5, 6};
    std::vector<int> v2 = swap(v1);
    for (int i: v2) {
        std::cout << i;
    }

    return 0;
}
Solution

Complete the three functions and main:

#include <iostream>
#include <stdexcept>
#include <vector>

int min(const int myArray[], int size) {
    if (size <= 0)
        throw std::invalid_argument("Array is empty.");
    int minValue = myArray[0];
    for (int i = 0; i < size; i++) {
        if (myArray[i] < minValue) {
            minValue = myArray[i];
        }
    }
    return minValue;
}

int max(const int *myArray, int size) {
    if (size <= 0)
        throw std::invalid_argument("Array is empty.");
    int maxValue = myArray[0];
    for (int i = 0; i < size; i++) {
        if (myArray[i] > maxValue) {
            maxValue = myArray[i];
        }
    }
    return maxValue;
}

std::vector<int> swap(std::vector<int> v) {
    int size = v.size();
    std::vector<int> v2(0, size);
    std::reverse(v.begin(), v.end());
    return v;
}

int main() {
    // === Using min() with array on the stack
    int myArray1[] = {-1, 2, -3, 4, -5, 6};
    int size1 = sizeof(myArray1) / sizeof(myArray1[0]);
    std::cout << min(myArray1, size1) << std::endl;

    // === Using max() with array on the heap
    int* myArray2 = new int[] {-1, 2, -3, 4, -5, 6};
    int size2 = 6; // The size can't be computed
    std::cout << max(myArray2, size2) << std::endl;
    delete[] myArray2;
    myArray2 = nullptr;

    // === Using swap with vector
    std::vector<int> v1 = {1, 2, 3, 4, 5, 6};
    std::vector<int> v2 = swap(v1);
    for (int i: v2) {
        std::cout << i;
    }

    return 0;
}

Expected output:

-5
6
654321

Exercice 11

The code should display the following result for each call to starsArrays and starsVector:

*****
****
***
**
*

For implementing both methods, use a multidimensional array. The base number of stars is given in advance and permits to allocate the first dimension. The second dimension is dynamically allocated during runtime so that the array’s size corresponds to the needs.

The first method uses the standard array whereas the second method uses vectors.

The main() method should be able to run without errors and produce the expected results.

#include <iostream>
#include <vector>

void starsArrays(int base) {
    // Creation of the array
    char* array[base];
    for (int i = 0; i < base; i++) {
/* Fill the array */
        array[i][base - i] = '\0';
    }
    // Display
    for (int i = 0; i < base; i ++) {
        char* index = array[i];
        // Like in C, '*' is used to access the value at the index address
        // Like in C, index++ increments the address to the next address
        while(*index != '\0') {
            std::cout << *index++;
        }
        std::cout << std::endl;
    }
    // Deletion of the resources (for each new, we must have a delete)
    for (int i = 0; i < base; i++) {
/* Delete the dynamically allocated arrays */
    }
}

void starsVector(int base) {
    std::vector<std::vector<char>> array(base);
/* Fill the array */
    // Display
    // Notice how we know the array sizes with the vector library
    int nbr_rows = array.size();
    for (int i = 0; i < nbr_rows; i++) {
        int nbr_cols = array[i].size();
        for (int j = 0; j < nbr_cols; j++) {
            std::cout << array[i][j];
        }
        std::cout << std::endl;
    }
    // No need to delete, the vector library takes care of it
}

int main() {
    constexpr int BASE = 5;
    starsArrays(BASE);
    starsVector(BASE);
    return 0;
}
Solution

Complete the fill and delete functions:

#include <iostream>
#include <vector>

void starsArrays(int base) {
    // Creation of the array
    char* array[base];
    for (int i = 0; i < base; i++) {
        array[i] = new char[base - i + 1];
        for (int j = 0; j < base - i; j++) {
            array[i][j] = '*';
        }
        array[i][base - i] = '\0';
    }
    // Display
    for (int i = 0; i < base; i ++) {
        char* index = array[i];
        while(*index != '\0') {
            std::cout << *index++;
        }
        std::cout << std::endl;
    }
    // Deletion of the resources
    for (int i = 0; i < base; i++) {
        delete[] array[i];
    }
}

void starsVector(int base) {
    std::vector<std::vector<char>> array(base);
    for (int i = 0; i < base; i++) {
        for (int j = 0; j < base - i; j++) {
            array[i].push_back('*');
        }
    }
    // Display
    int nbr_rows = array.size();
    for (int i = 0; i < nbr_rows; i++) {
        int nbr_cols = array[i].size();
        for (int j = 0; j < nbr_cols; j++) {
            std::cout << array[i][j];
        }
        std::cout << std::endl;
    }
}

int main() {
    constexpr int BASE = 5;
    starsArrays(BASE);
    starsVector(BASE);
    return 0;
}

Classes

Exercice 12

Which statements are correct ?

  • C++ is an object-oriented language.
  • The usage of access modifiers like public or private are the same as in Java.
  • It’s possible to define several constructors in C++.
  • In C++, the class’s attributes can’t be modified in a method tagged with the keyword const
Solution
  • C++ is an object-oriented language.
  • It’s possible to define several constructors in C++.
  • In C++, the class’s attributes can’t be modified in a method tagged with the keyword const

Exercice 13

Observe the code snippet and answer the question below. What are the correct statements ?

#include <string>

class Dummy {
public:
    // constructors
    Dummy() : _number(-1), _s("a") { }
    Dummy(int number) : _number(number), _s("b") { }
    Dummy(std::string s) : _number(1), _s(s) { }

private:
    // private data fields
    std::string _s;
    int _number;
};

int main() {
    Dummy d1;
    Dummy d2(2);
    Dummy d3("Toto");
    return 0;
}
  • The attribute values of d1 are number = -1, s = “a”.
  • The attribute values of d1 are number = -1, s = “b”.
  • The attribute values of d1 are number = 1, s = “Toto”.
  • The attribute values of d2 are number = 2, s = “a”.
  • The attribute values of d2 are number = 2, s = “b”.
  • The attribute values of d2 are number = 1, s = “b”.
  • The attribute values of d3 are number = -1, s = “a”.
  • The attribute values of d3 are number = 1, s = “b”.
  • The attribute values of d3 are number = 1, s = “Toto”.
Solution
  • The attribute values of d1 are number = -1, s = “a”.
  • The attribute values of d2 are number = 2, s = “b”.
  • The attribute values of d3 are number = 1, s = “Toto”.

Exercice 14

Writing a C++ class

The Student class must be completed. The main() method should run without errors and produce the expected results.

Observe the following points for the Student class:

  • It has a first_name (std::string), a last_name (std::string) and an age (int) as attributes.
  • It has a constructor without parameters that initialize the student to the following values ; “Toto” (first_name), “Titi” (last_name) and 18 (age).
  • It has another constructor with the first name, last name and age as parameters. It initializes a Student instance to the given parameters.
  • The public method isMajor() returns true if the age is bigger or equal to 18, false otherwise.
  • The public method name() returns the concatenation of the first name and the last name with a space in between.
#include <iostream>
#include <string>

class Student {
public:
    // constructor
/* Write the constructor without parameters */
/* Write the constructor with parameters  */

    // public methods
/* Write the name() method */
    int age() const { return _age; }
/* Write the isMajor() method */

private:
    // private data fields
    std::string _firstName;
    std::string _lastName;
    int _age;
};

int main() {
    Student s1;
    std::cout << s1.name() << " is major ? " << s1.isMajor() << std::endl;
    Student s2("Abc", "Def", 17);
    std::cout << s2.name() << " is major ? " << s2.isMajor() << std::endl;
    return 0;
}
Solution
#include <iostream>
#include <string>

class Student {
public:
    // constructor
    Student() : _firstName("Toto"), _lastName("Titi"), _age(18) { }
    Student(std::string firstName, std::string lastName, int age) : _firstName(firstName), _lastName(lastName), _age(age) { }

    // public methods
    std::string name() const { return _firstName + " " + _lastName; }
    int age() const { return _age; }
    bool isMajor() const { return _age >= 18; }

private:
    // private data fields
    std::string _firstName;
    std::string _lastName;
    int _age;
};

int main() {
    Student s1;
    std::cout << s1.name() << " is major ? " << s1.isMajor() << std::endl;
    Student s2("Abc", "Def", 17);
    std::cout << s2.name() << " is major ? " << s2.isMajor() << std::endl;
    return 0;
}

Expected output:

Toto Titi is major ? 1
Abc Def is major ? 0

Classes And Inheritance

Exercice 15

Which statements are correct ?

  • In C++, a class can only inherit one class like in Java.
  • Virtual methods are useful to define different behaviors in the derived classes by the method overridden.
  • Pure virtual methods have a defined syntax.
  • Pure virtual methods have a body, whereas virtual methods don’t have one.
Solution
  • Virtual methods are useful to define different behaviors in the derived classes by the method overridden.
  • Pure virtual methods have a defined syntax.

Exercice 16

Observe the code snippet and answer the question below. What is the output of this program ?

#include <iostream>

class A {
public:
    A() { std::cout << "A" << std::endl; }
    virtual void f() { std::cout << "Af" << std::endl; }
};

class B : public A {
public:
    B() { std::cout << "B" << std::endl; }
    void f() override { std::cout << "Bf" << std::endl; }
};

class C : public B {
public:
    C() { std::cout << "C" << std::endl; }
    void f() { std::cout << "Cf" << std::endl; }
};

int main() {
    A a;
    B b;
    C c;
    A& a2 = b;
    a2.f();
    A& a3 = c;
    a3.f();
    return 0;
}
  • A B C Af Af
  • A B C Bf Cf
  • A A B A B C Af Af
  • A A B A B C Bf Cf
Solution

The correct output is: A A B A B C Bf Cf

  • Constructor A is called when creating object a
  • When creating b, constructors are chained: A then B outputs
  • When creating c, constructors are chained: A then B then C outputs
  • a2.f() calls the virtual method override in B, outputting “Bf”
  • a3.f() calls the virtual method from C (which doesn’t override properly), outputting “Cf”

Exercice 17

Programming classes with inheritance

The code of this exercise defines 3 classes Form, Square and Triangle. Square and Triangle are both forms and thus both inherit from Form.

Complete the code so that the main() method runs without errors and produces the expected results.

#include <iostream>
#include <vector>

// Form class
class Form {
public:
    Form(int edgesNumber, int length) : _edgesNumber(edgesNumber), _length(length) { }

    int edgesNumber() const { return _edgesNumber; }
    int length() const { return _length; }

    virtual void print() { std::cout << "Form of " << edgesNumber() <<
        " edges with length " << length() << std::endl; }

private:
    int _edgesNumber;
    int _length;
};

// =============================================================================
// Square class
class Square : public Form {
public:
    Square(int length) : /* Add call to constructor of mother class */ { }
    /* Override the print() method for the expected result */
private:
    static constexpr int kNbrOfEdges = 4;
};

// =============================================================================
// Triangle class
class Triangle : public Form {
public:
    Triangle(int length) : /* Add call to constructor of mother class */ { }

    /* Add the declaration of the overriden print() method */
        int height = length() / 2 + 1;
        int width = 1;
        for (int i = 0; i < height; i++) {
            int space = (length() - width) / 2;
            for (int j = 0; j < space; j++) {
                std::cout << " ";
            }
            for (int k = 0; k < width; k++) {
                std::cout << "*";
            }
            std::cout << std::endl;
            width += 2;
        }
    }
private:
    static constexpr int kNbrOfEdges = 3;
};

// =============================================================================
int main() {
    Form f(5, 7);
    f.print();

    Square s(4);
    s.print();

    Triangle t(5);
    t.print();

    return 0;
}
Solution

Complete the constructors using initializer lists and override the print methods:

#include <iostream>
#include <vector>

// Form class
class Form {
public:
    Form(int edgesNumber, int length) : _edgesNumber(edgesNumber), _length(length) { }

    int edgesNumber() const { return _edgesNumber; }
    int length() const { return _length; }

    virtual void print() { std::cout << "Form of " << edgesNumber() <<
        " edges with length " << length() << std::endl; }

private:
    int _edgesNumber;
    int _length;
};

// =============================================================================
// Square class
class Square : public Form {
public:
    Square(int length) : Form(kNbrOfEdges, length) { }
    virtual void print() override {
        for (int i = 0; i < length(); i++) {
            for (int j = 0; j < length(); j++) {
                std::cout << "*";
            }
            std::cout << std::endl;
        }
    }
private:
    static constexpr int kNbrOfEdges = 4;
};

// =============================================================================
// Triangle class
class Triangle : public Form {
public:
    Triangle(int length) : Form(kNbrOfEdges, length) { }

    virtual void print() override {
        int height = length() / 2 + 1;
        int width = 1;
        for (int i = 0; i < height; i++) {
            int space = (length() - width) / 2;
            for (int j = 0; j < space; j++) {
                std::cout << " ";
            }
            for (int k = 0; k < width; k++) {
                std::cout << "*";
            }
            std::cout << std::endl;
            width += 2;
        }
    }
private:
    static constexpr int kNbrOfEdges = 3;
};

// =============================================================================
int main() {
    Form f(5, 7);
    f.print();

    Square s(4);
    s.print();

    Triangle t(5);
    t.print();

    return 0;
}

Expected output:

Form of 5 edges with length 7
****
****
****
****
  *
 ***
*****

Exercice 18

Classes with pure virtual methods (interfaces)

In the code example, two interfaces are defined (as classes containing only pure virtual methods):

  • Printable : It has the print() method.
  • Switch : It has the turnOff() and turnOn() methods.

The base class Led represents a LED with a given intensity. It implements the two interfaces explained above so that it’s able to turn off and on, as well as print its current intensity.

You must complete the code so that the main() function runs without errors and produces the expected results.

#include <iostream>

class Printable {
public:
/* Write the print() method */
};

/* Write the Switch interface */

/* Write the Led class definition */ {
public:
    Led(int intensity): _current_intensity(intensity), _intensity(intensity) { }
    void print() override {
        std::cout << "Led has intensity of " << _current_intensity << std::endl;
    }
/* Write the turnOn() method */
/* Write the turnOff method */

private:
    int _current_intensity;
    const int _intensity;
};

int main() {
    Led l(200);
    l.print();
    l.turnOff();
    l.print();
    l.turnOn();
    l.print();

    Printable& p = l;

    Switch& s = l;
    s.turnOff();
    p.print();
    s.turnOn();
    p.print();

    return 0;
}
Solution

Implement the interfaces with pure virtual methods and the Led class:

#include <iostream>

class Printable {
public:
    virtual ~Printable() = default;
    virtual void print() = 0;
};

class Switch {
public:
    virtual ~Switch() = default;
    virtual void turnOn() = 0;
    virtual void turnOff() = 0;
};

class Led : public Printable, public Switch {
public:
    Led(int intensity): _current_intensity(intensity), _intensity(intensity) { }
    void print() override {
        std::cout << "Led has intensity of " << _current_intensity << std::endl;
    }
    void turnOn() override {
        _current_intensity = _intensity;
    }
    void turnOff() override {
        _current_intensity = 0;
    }

private:
    int _current_intensity;
    const int _intensity;
};

int main() {
    Led l(200);
    l.print();
    l.turnOff();
    l.print();
    l.turnOn();
    l.print();

    Printable& p = l;

    Switch& s = l;
    s.turnOff();
    p.print();
    s.turnOn();
    p.print();

    return 0;
}

Expected output:

Led has intensity of 200
Led has intensity of 0
Led has intensity of 200
Led has intensity of 0
Led has intensity of 200

Function And Method Arguments

Exercice 19

Passing arguments

Observe the program snippet and answer the question below. What are the values of i and d.x at the end of the program ?

class Dummy {
public:
    int x = 0;
};

void f(int i, Dummy& d) {
    i = i + 1;
    d.x++;
}

int main() {
    Dummy d;
    int i = 2;
    f(i, d);
    return 0;
}
  • i = 2, d.x = 0.
  • i = 3, d.x = 0.
  • i = 2, d.x = 1.
  • i = 3, d.x = 1.
Solution

The correct answer is: i = 2, d.x = 1.

  • i is passed by value, so modifications to the parameter inside f() don’t affect the original variable i (remains 2)
  • d is passed by reference, so modifications to d.x affect the original object (becomes 1)

Exercice 20

Passing arguments

Observe the program snippet and answer the question below. What are the values of i and d.x at the end of the program ?

class Dummy {
public:
    int x = 0;
};

void f(int *i, Dummy d) {
    (*i) = (*i) + 1;
    d.x++;
}

int main() {
    Dummy d;
    int i = 2;
    f(&i, d);
    return 0;
}
  • i = 2, d.x = 0.
  • i = 3, d.x = 0.
  • i = 2, d.x = 1.
  • i = 3, d.x = 1.
Solution

The correct answer is: i = 3, d.x = 0.

  • i is passed by pointer, so modifications through the pointer affect the original variable (becomes 3)
  • d is passed by value, so modifications inside f() don’t affect the original object (remains 0)

Exercice 21

Passing arguments

Observe the program snippet and answer the question below. What are the values of i and d.x at the end of the program ?

#include <iostream>

class Dummy {
public:
    int x = 0;
};

void f(int &i, Dummy *d) {
    i = i + 1;
    d->x++;
}

int main() {
    Dummy d;
    int i = 2;
    f(i, &d);
    std::cout << d.x;
    return 0;
}
  • i = 2, d.x = 0.
  • i = 3, d.x = 0.
  • i = 2, d.x = 1.
  • i = 3, d.x = 1.
Solution

The correct answer is: i = 3, d.x = 1.

  • i is passed by reference, so modifications affect the original variable (becomes 3)
  • d is passed by pointer, so modifications through the pointer affect the original object (becomes 1)

Deallocation And Destructor

Exercice 22

Which statements are correct ?

  • C++ has a garbage collector like Java.
  • The delete calls explicitly the destructor of the object.
  • Each class has only one destructor.
  • The destructor is implicitly called at the end of the object’s lifetime when it is created on the heap.
Solution
  • The delete calls explicitly the destructor of the object.
  • Each class has only one destructor.

Exercice 23

Identify the memory management issue

Observe the following code and identify what problem it has.

#include <vector>

class Dummy {

};

int process() {
    std::vector<Dummy*> v;
    for (int i = 0; i < 5; i++) {
        v.push_back(new Dummy());
    }
    return 0;
}
Solution

This code has a memory leak. The objects allocated with new are never explicitly deallocated with delete. When the program ends, the dynamically allocated Dummy objects are not freed, causing a memory leak.

The corrected code with proper memory deallocation:

#include <vector>

class Dummy {

};

int process() {
    std::vector<Dummy*> v;
    for (int i = 0; i < 5; i++) {
        v.push_back(new Dummy());
    }

    // Properly deallocate memory
    for (int i = 0; i < v.size(); i++) {
        delete v[i];
    }
    v.clear();

    return 0;
}

Key Points: - Every new must be matched with a corresponding delete - Alternatively, use smart pointers like std::unique_ptr which automatically manage memory - In modern C++, std::vector<std::unique_ptr<Dummy>> would be the preferred approach

Template Classes

Exercice 24

C++ template functions

The max() function usually takes two arguments and returns the argument such that this argument is > to the other argument.

Given the max() function that takes two integer numbers as parameters:

int max(int a, int b) {
    return a >= b ? a : b;
}

Make this function generic by declaring a template function. As you can observe in the main program, this template function can be used for any type that defines/overloads the > operator.

#include <iostream>

namespace prog_1 {/* Define the template max() function */
}

int main() {
    std::cout << prog_1::max(5, 6) << std::endl;
    std::cout << prog_1::max(5.1, 5.9) << std::endl;
    std::cout << prog_1::max("abc", "abd") << std::endl;

    return 0;
}
Solution

Define the template max() function inside the namespace:

#include <iostream>

namespace prog_1 {
    template<typename T>
    T max(T a, T b) {
        return a >= b ? a : b;
    }
}

int main() {
    std::cout << prog_1::max(5, 6) << std::endl;
    std::cout << prog_1::max(5.1, 5.9) << std::endl;
    std::cout << prog_1::max("abc", "abd") << std::endl;

    return 0;
}

Expected output:

6
5.9
abd

Key Points: - The template function syntax is template<typename T> followed by the function signature - T is a placeholder for any type that supports the >= operator - Template functions work with int, float, double, std::string, C-strings, and any other type with operator overloading - The template is instantiated at compile-time for each type used in the program