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 constructordéfini commeclass(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
#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
#includekeyword 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::coutmeans thatcoutis in thestdnamespace.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
#includekeyword 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::coutmeans thatcoutis in thestdnamespace.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
constorconstexprkeywords than thedefinekeyword.
Solution
- C++ is a strongly typed language like Java.
- In C++, it’s better to declare global variables with the
constorconstexprkeywords than thedefinekeyword.
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
#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");
}
}
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
publicorprivateare 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
Studentinstance to the given parameters. - The public method
isMajor()returnstrueif the age is bigger or equal to 18,falseotherwise. - 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 theprint()method.Switch: It has theturnOff()andturnOn()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.
iis passed by value, so modifications to the parameter insidef()don’t affect the original variablei(remains 2)dis passed by reference, so modifications tod.xaffect 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.
iis passed by pointer, so modifications through the pointer affect the original variable (becomes 3)dis passed by value, so modifications insidef()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.
iis passed by reference, so modifications affect the original variable (becomes 3)dis 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
deletecalls 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
deletecalls 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