La gestion des tableaux en C++ peut être délicate, surtout lorsque leur taille doit varier pendant l'exécution du programme. Bien que la bibliothèque standard offre des conteneurs comme std::vector, comprendre les mécanismes sous-jacents est fondamental pour maîtriser la programmation système et la gestion de la mémoire. Cet article explore la création d'une classe de tableau dynamique personnalisée, similaire à std::vector dans ses fonctionnalités de base, en mettant l'accent sur les concepts de gestion de la mémoire, de constructeurs, de destructeurs et de surcharge d'opérateurs.
Structure de la classe de tableau dynamique
Pour construire notre propre tableau redimensionnable, nommons-le DynArray, nous aurons besoin de plusieurs composants clés. La classe devra encapsuler la logique de stockage des éléments, de gestion de sa taille actuellle et de sa capacité d'allocation. Voici les opérations essentielles que notre DynArray devra prendre en charge, illustrées par un exemple d'utilisation:
#include <iostream>
#include <algorithm> // Pour std::copy
#include <cstddef> // Pour size_t
// Déclaration de la classe DynArray (l'implémentation suivra)
class DynArray {
public:
DynArray(size_t initial_capacity = 0);
DynArray(const DynArray& other);
~DynArray();
DynArray& operator=(const DynArray& other);
int& operator[](size_t index);
const int& operator[](size_t index) const; // Version const pour lecture seule
void append(int value);
size_t getSize() const;
size_t getCapacity() const;
private:
size_t element_count; // Nombre d'éléments actuellement présents
size_t allocated_capacity; // Capacité totale allouée en mémoire
int* data_ptr; // Pointeur vers le bloc de mémoire des éléments
};
int main() {
DynArray arr1; // Tableau initialisé vide
for (int i = 0; i < 5; ++i) {
arr1.append(i * 10); // Ajout d'éléments
}
DynArray arr2;
arr2 = arr1; // Surcharge de l'opérateur d'affectation
std::cout << "Contenu de arr2: ";
for (size_t i = 0; i < arr2.getSize(); ++i) {
std::cout << arr2[i] << " ";
}
std::cout << std::endl; // Attend: 0 10 20 30 40
arr1[2] = 200; // Surcharge de l'opérateur []
DynArray arr3(arr1); // Appel du constructeur de copie
std::cout << "Contenu de arr3 (après modification de arr1): ";
for (size_t i = 0; i < arr3.getSize(); ++i) {
std::cout << arr3[i] << " ";
}
std::cout << std::endl; // Attend: 0 10 200 30 40
DynArray arr4;
arr4.append(1);
arr4.append(2);
arr4 = arr2; // arr4 devient une copie de arr2 (0 10 20 30 40)
std::cout << "Contenu de arr4 (après affectation de arr2): ";
for (size_t i = 0; i < arr4.getSize(); ++i) {
std::cout << arr4[i] << " ";
}
std::cout << std::endl; // Attend: 0 10 20 30 40
return 0;
}
L'exécution de ce programme devrait produire le résultat suivant :
Contenu de arr2: 0 10 20 30 40
Contenu de arr3 (après modification de arr1): 0 10 200 30 40
Contenu de arr4 (après affectation de arr2): 0 10 20 30 40
Implémentation des membres de la classe DynArray
Abordons l'implémentation de chaque fonction membre de notre classe DynArray.
Constructeur (DynArray::DynArray(size_t initial_capacity))
Le constructeur est responsable de l'initialisation de l'objet DynArray. Il alloue un bloc de mémoire si une capacité initiale est spécifiée.
DynArray::DynArray(size_t initial_capacity) :
element_count(0),
allocated_capacity(initial_capacity),
data_ptr(nullptr)
{
if (initial_capacity > 0) {
data_ptr = new int[initial_capacity];
}
}
Constructeur de copie (DynArray::DynArray(const DynArray& other))
Le constructeur de copie est essentiel pour garantir la sémantique de "copie profonde" (deep copy). Sans cela, une copie superficielle (shallow copy) entraînerait deux objets partageant le même pointeur de données, ce qui est une source fréquente d'erreurs et de corruption de mémoire.
DynArray::DynArray(const DynArray& other) :
element_count(other.element_count),
allocated_capacity(other.allocated_capacity),
data_ptr(nullptr)
{
if (other.data_ptr != nullptr && other.element_count > 0) {
data_ptr = new int[allocated_capacity];
std::copy(other.data_ptr, other.data_ptr + other.element_count, data_ptr);
}
}
Destructeur (DynArray::~DynArray())
Le destructeur est chargé de libérer la mémoire allouée dynamiquement pour éviter les fuites de mémoire. Il est appelé automatiquement lorsque l'objet DynArray est détruit.
DynArray::~DynArray() {
delete[] data_ptr;
data_ptr = nullptr; // Bonne pratique
element_count = 0;
allocated_capacity = 0;
}
Opérateur d'affectation (DynArray& DynArray::operator=(const DynArray& other))
L'opérateur d'affectation permet de copier le contenu d'un DynArray dans un autre. Il doit gérer les cas d'auto-affectation et s'assurer que les ressources existantes sont correctement libérées avant la copie.
DynArray& DynArray::operator=(const DynArray& other) {
if (this == &other) { // Vérifie l'auto-affectation (ex: arr = arr;)
return *this;
}
// Libère les ressources actuelles
delete[] data_ptr;
data_ptr = nullptr;
element_count = other.element_count;
allocated_capacity = other.allocated_capacity;
if (other.data_ptr != nullptr && other.element_count > 0) {
data_ptr = new int[allocated_capacity];
std::copy(other.data_ptr, other.data_ptr + other.element_count, data_ptr);
}
return *this;
}
Opérateur d'accès (int& DynArray::operator[](size_t index))
Cet opérateur permet d'accéder aux éléments du tableau en utilisant la syntaxe d'indexation (par exemple, arr[i]). Une version const est ajoutée pour permettre l'accès en lecture seule sur des objets constants.
int& DynArray::operator[](size_t index) {
// Il serait judicieux d'ajouter une vérification de débordement ici (assert ou throw)
return data_ptr[index];
}
const int& DynArray::operator[](size_t index) const {
// Version const
return data_ptr[index];
}
Ajout d'un élément (void DynArray::append(int value))
La fonction append ajoute un nouvel élément à la fin du tableau. Pour optimiser les performances, elle utilise une stratégie de réallocation qui double la capacité lorsque la capacité existante est pleine, réduisant ainsi le nombre total de réallocations coûteuses.
void DynArray::append(int value) {
if (element_count == allocated_capacity) { // La capacité est pleine, il faut réallouer
size_t new_capacity = (allocated_capacity == 0) ? 1 : allocated_capacity * 2; // Double la capacité ou initialise à 1
int* new_elements = new int[new_capacity];
if (data_ptr) { // S'il y avait des éléments, les copier
std::copy(data_ptr, data_ptr + element_count, new_elements);
delete[] data_ptr;
}
data_ptr = new_elements;
allocated_capacity = new_capacity;
}
data_ptr[element_count++] = value; // Ajoute la valeur et incrémente le compteur
}
Obtention de la taille (size_t DynArray::getSize() const)
Cette fonction retourne le nombre actuel d'éléments stockés dans le tableau.
size_t DynArray::getSize() const {
return element_count;
}
Obtention de la capacité (size_t DynArray::getCapacity() const)
Cette fonction retourne la capacité totale de mémoire allouée, c'est-à-dire le nombre maximum d'éléments pouvant être stockés sans réallocation.
size_t DynArray::getCapacity() const {
return allocated_capacity;
}
Synthèse de l'implémentation
Voici la classe DynArray complète, intégrant toutes les fonctions membres discutées :
#include <iostream>
#include <algorithm> // Pour std::copy
#include <cstddef> // Pour size_t
class DynArray {
public:
// Constructeur: initialise avec une capacité donnée (par défaut 0)
DynArray(size_t initial_capacity = 0) :
element_count(0),
allocated_capacity(initial_capacity),
data_ptr(nullptr)
{
if (initial_capacity > 0) {
data_ptr = new int[initial_capacity];
}
}
// Constructeur de copie: effectue une copie profonde
DynArray(const DynArray& other) :
element_count(other.element_count),
allocated_capacity(other.allocated_capacity),
data_ptr(nullptr)
{
if (other.data_ptr != nullptr && other.element_count > 0) {
data_ptr = new int[allocated_capacity];
std::copy(other.data_ptr, other.data_ptr + other.element_count, data_ptr);
}
}
// Destructeur: libère la mémoire allouée
~DynArray() {
delete[] data_ptr;
data_ptr = nullptr;
element_count = 0;
allocated_capacity = 0;
}
// Opérateur d'affectation: gère l'auto-affectation et la copie profonde
DynArray& operator=(const DynArray& other) {
if (this == &other) {
return *this;
}
delete[] data_ptr;
data_ptr = nullptr;
element_count = other.element_count;
allocated_capacity = other.allocated_capacity;
if (other.data_ptr != nullptr && other.element_count > 0) {
data_ptr = new int[allocated_capacity];
std::copy(other.data_ptr, other.data_ptr + other.element_count, data_ptr);
}
return *this;
}
// Opérateur []: accès en lecture/écriture
int& operator[](size_t index) {
// Ajouter ici une vérification de borne si nécessaire, par exemple assert(index < element_count);
return data_ptr[index];
}
// Opérateur []: accès en lecture seule pour objets constants
const int& operator[](size_t index) const {
return data_ptr[index];
}
// Ajoute un élément à la fin du tableau, réalloue si nécessaire
void append(int value) {
if (element_count == allocated_capacity) {
size_t new_capacity = (allocated_capacity == 0) ? 1 : allocated_capacity * 2;
int* new_elements = new int[new_capacity];
if (data_ptr) {
std::copy(data_ptr, data_ptr + element_count, new_elements);
delete[] data_ptr;
}
data_ptr = new_elements;
allocated_capacity = new_capacity;
}
data_ptr[element_count++] = value;
}
// Retourne le nombre d'éléments actuels
size_t getSize() const {
return element_count;
}
// Retourne la capacité totale allouée
size_t getCapacity() const {
return allocated_capacity;
}
private:
size_t element_count; // Nombre d'éléments effectivement stockés
size_t allocated_capacity; // Capacité du tableau (taille du bloc mémoire)
int* data_ptr; // Pointeur vers le tableau d'entiers
};
Cette implémentation couvre les aspects fondamentaux d'un tableau dynamique. Pour une robustesse accrue, on pourrait envisager d'ajouter des fonctions comme la suppression d'éléments, l'insertion à une position spécifique, ou des mécanismes de gestion des erreurs (comme des exceptions pour l'accès hors limites). La stratégie de réallocation par doublement de la capacité, telle qu'implémentée dans append, est une technique courante qui offre de bonnes performances en moyenne.