Implémentation de vecteurs en C++

Jinku Hu 12 octobre 2023
Implémentation de vecteurs en C++

Cet article montrera comment implémenter une classe similaire à std::vector en C++.

Utilisez les fonctions malloc et realloc pour implémenter la classe vector personnalisée en C++

Le conteneur std::vector est connu comme un tableau dynamique de style C qui stocke ses éléments de manière contiguë. Même si l’implémentation exacte n’est pas appliquée, certaines fonctionnalités du conteneur sont requises par la spécification. A savoir, un vector doit être une structure de données ordonnée et fournir un accès aléatoire à ses éléments. D’autres fonctionnalités peuvent être exprimées sous forme de fonctions membres publiques exposées sous forme d’interface, dont certaines seront implémentées dans les exemples suivants. Notez que l’implémentation réelle de std::vector peut devenir assez étendue, nous démontrons donc simplement un point de départ à partir duquel le lecteur pourrait ajouter plus de fonctionnalités.

Dans un premier temps, nous devrons définir une classe MyVector comme modèle de fonction pouvant stocker n’importe quel type générique. Ensuite, nous pouvons inclure des membres de données de base comme le pointeur vers le tableau d’éléments et des objets entiers pour stocker la taille/capacité en conséquence. N’oubliez pas que std::vector peut allouer des morceaux de mémoire plus importants que les éléments stockés n’en ont besoin pour de meilleures performances. Cette taille mémoire allouée est appelée capacité du vecteur, et nous la stockons dans une donnée membre cap. Nous attribuons un montant fixe (objets 20) une fois qu’un nouvel objet MyVector est construit et définissons les éléments actuels à zéro.

Notez que nous utilisons la fonction malloc, qui peut être considérée comme nuisible dans le C++ contemporain, mais elle offre de la flexibilité et de bonnes performances si elle est utilisée avec prudence. De plus, la mémoire allouée avec malloc peut être étendue avec la fonction realloc, dont nous aurons besoin pour gérer le redimensionnement du tableau.

Au total, notre classe contient trois fonctions membres et l’opérateur [], qui constituent la base d’un vecteur dynamique. L’exemple de code suivant illustre les implémentations des fonctions push_back, size et operator[]. Les deux derniers sont assez intuitifs et simples à comprendre, tandis que push_back peut nécessiter quelques explications.

Notez que nous n’avons besoin d’allouer de nouvelles régions mémoire qu’une fois l’objet construit lorsque le nombre d’éléments dépasse la capacité. Ainsi, nous avons besoin de deux chemins séparés pour chaque scénario dans la fonction push_back, dont l’un invoquera la fonction realloc pour étendre le bloc mémoire courant. realloc accepte deux paramètres : le pointeur vers la région mémoire précédente et la taille de la nouvelle région en octets. Si l’appel réussit, un pointeur valide est renvoyé, soit le même que le pointeur précédent ou un nouveau selon si suffisamment de mémoire a pu être trouvée dans le même bloc. Sinon, le pointeur NULL est retourné pour indiquer que la requête a échoué.

#include <iostream>
#include <string>

using std::cin;
using std::cout;
using std::endl;
using std::string;

template <typename T>
class MyVector {
 public:
  MyVector() {
    cap = alloc;
    vector = (T *)malloc(sizeof(T) * alloc);
    elem_num = 0;
  };

  void push_back(const T &data);
  void pop_back();

  [[nodiscard]] bool empty() const;
  [[nodiscard]] size_t size() const;
  [[nodiscard]] size_t capacity() const;
  T &operator[](size_t pos);

  ~MyVector();

 private:
  T *vector = nullptr;
  size_t cap;
  size_t elem_num;
  const int alloc = 20;
};

template <typename T>
MyVector<T>::~MyVector() {
  free(vector);
}

template <typename T>
void MyVector<T>::push_back(const T &data) {
  if (elem_num < cap) {
    *(vector + elem_num) = data;
    elem_num++;
  } else {
    vector = (T *)realloc(vector, sizeof(T) * cap * 2);
    cap *= 2;

    if (vector) {
      *(vector + elem_num) = data;
      elem_num++;
    }
  }
}

template <typename T>
void MyVector<T>::pop_back() {
  if (empty()) return;
  elem_num--;
}

template <typename T>
T &MyVector<T>::operator[](size_t pos) {
  if (pos >= 0 && pos <= elems) return *(this->arr + pos);
  throw std::out_of_range("Out of bounds element access");
}

template <typename T>
size_t MyVector<T>::capacity() const {
  return cap;
}

template <typename T>
bool MyVector<T>::empty() const {
  return elem_num == 0;
}

template <typename T>
size_t MyVector<T>::size() const {
  return elem_num;
}

struct MyClass {
  int num;
  double num2;
};

int main() {
  MyVector<MyClass> m1;

  m1.push_back({1, 1.1});
  m1.push_back({1, 1.2});
  m1.push_back({1, 1.3});
  m1.push_back({1, 1.4});

  for (size_t i = 0; i < m1.size(); ++i) {
    cout << m1[i].num << ", " << m1[i].num2 << endl;
  }

  cout << "/ ------------------- /" << endl;
  m1.pop_back();
  m1.pop_back();

  for (size_t i = 0; i < m1.size(); ++i) {
    cout << m1[i].num << ", " << m1[i].num2 << endl;
  }

  return EXIT_SUCCESS;
}

Production:

1, 1.1
1, 1.2
1, 1.3
1, 1.4
/ ------------------- /
1, 1.1
1, 1.2

Afin de tester la classe MyVector, nous incluons du code de pilote dans la fonction main et définissons l’objet personnalisé MyClass à stocker en tant qu’éléments. Enfin, nous ajoutons la fonction pop_back qui supprime un élément à l’arrière du vecteur. La fonction pop_back n’a pas besoin de désallouer ou de supprimer le contenu de l’élément. Il peut simplement décrémenter le membre elem_num de un à chaque invocation, et le prochain appel push_back réécrira la position de l’élément supprimé. Il est également important de désallouer les blocs de mémoire une fois que l’objet est hors de portée. Ainsi, nous devons inclure un appel à la fonction free dans un destructeur de la classe.

#include <iostream>
#include <string>

using std::cin;
using std::cout;
using std::endl;
using std::string;

template <typename T>
class MyVector {
 public:
  MyVector() {
    arr = new T[default_capacity];
    cap = default_capacity;
    elems = 0;
  };

  void push_back(const T &data);
  void pop_back();

  [[nodiscard]] bool empty() const;
  [[nodiscard]] size_t size() const;
  [[nodiscard]] size_t capacity() const;
  T &operator[](size_t pos);

  ~MyVector();

 private:
  T *arr = nullptr;
  size_t cap;
  size_t elems;
  const int default_capacity = 20;
};

template <typename T>
MyVector<T>::~MyVector() {
  delete[] arr;
}

template <typename T>
void MyVector<T>::push_back(const T &data) {
  if (elems < cap) {
    *(arr + elems) = data;
    elems++;
  } else {
    auto tmp_arr = new T[cap * 2];
    cap *= 2;
    for (int i = 0; i < elems; i++) {
      tmp_arr[i] = arr[i];
    }
    delete[] arr;
    arr = tmp_arr;

    *(arr + elems) = data;
    elems++;
  }
}

template <typename T>
T &MyVector<T>::operator[](size_t pos) {
  if (pos >= 0 && pos <= elems) return *(this->arr + pos);
  throw std::out_of_range("Out of bounds element access");
}

template <typename T>
size_t MyVector<T>::size() const {
  return elems;
}

template <typename T>
size_t MyVector<T>::capacity() const {
  return cap;
}

template <typename T>
void MyVector<T>::pop_back() {
  if (empty()) return;
  elems--;
}

template <typename T>
bool MyVector<T>::empty() const {
  return elems == 0;
}

struct MyClass {
  string name;
  double num;
};

int main() {
  MyVector<MyClass> m1;

  m1.push_back({"one", 1.1});
  m1.push_back({"two", 1.2});
  m1.push_back({"three", 1.3});
  m1.push_back({"four", 1.4});

  for (size_t i = 0; i < m1.size(); ++i) {
    cout << m1[i].name << ", " << m1[i].num << endl;
  }

  cout << "/ ------------------- /" << endl;
  m1.pop_back();
  m1.pop_back();

  for (size_t i = 0; i < m1.size(); ++i) {
    cout << m1[i].name << ", " << m1[i].num << endl;
  }

  return EXIT_SUCCESS;
}

Production:

one, 1.1
two, 1.2
three, 1.3
four, 1.4
/ ------------------- /
one, 1.1
two, 1.2
Auteur: Jinku Hu
Jinku Hu avatar Jinku Hu avatar

Founder of DelftStack.com. Jinku has worked in the robotics and automotive industries for over 8 years. He sharpened his coding skills when he needed to do the automatic testing, data collection from remote servers and report creation from the endurance test. He is from an electrical/electronics engineering background but has expanded his interest to embedded electronics, embedded programming and front-/back-end programming.

LinkedIn Facebook

Article connexe - C++ Vector