28 votes

Qu'y a-t-il de mal à utiliser des tableaux alloués dynamiquement en C++ ?

Comme le code suivant :

int size = myGetSize();
std::string* foo;
foo = new std::string[size];
//...
// using the table
//...
delete[] foo;

J'ai entendu dire qu'une telle utilisation (pas ce code précisément, mais l'allocation dynamique dans son ensemble) peut être dangereuse dans certains cas, et ne devrait être utilisée qu'avec RAII. Pourquoi ?

44voto

Kerrek SB Points 194696

Je vois trois problèmes principaux dans votre code :

  1. Utilisation de pointeurs nus et propriétaires.

  2. Utilisation du nu new .

  3. Utilisation de tableaux dynamiques.

Chacun est indésirable pour ses propres raisons. Je vais essayer d'expliquer chacune d'elles à tour de rôle.

(1) viole ce que j'aime appeler exactitude de la sous-expression et (2) viole exactitude de l'énoncé . L'idée ici est qu'aucune déclaration, et même pas toute sous-expression devrait être une erreur en soi. Je prends le terme "erreur" au sens large pour signifier "pourrait être un bug".

L'idée d'écrire un bon code est que si ça se passe mal, ce n'est pas de votre faute. Votre état d'esprit de base devrait être celui d'un lâche paranoïaque. Ne pas écrire de code du tout est une façon d'y parvenir, mais comme cela répond rarement aux exigences, la meilleure chose à faire est de s'assurer que quoi que vous fassiez, ce n'est pas de votre faute. La seule façon de prouver systématiquement que ce n'est pas votre faute est qu'aucun code n'a été écrit. partie de votre code est la cause première d'une erreur. Maintenant, regardons à nouveau le code :

  • new std::string[25] est une erreur, car elle crée un objet alloué dynamiquement qui est fui. Ce code ne peut que conditionnellement devenir une non-erreur si quelqu'un d'autre, quelque part ailleurs, et dans tous les cas, se souvient de nettoyer.

    Cela nécessite, tout d'abord, que la valeur de cette expression soit stockée quelque part. Cela se produit dans votre cas, mais dans des expressions plus complexes, il peut être difficile de prouver que cela se produira dans tous les cas (ordre d'évaluation non spécifié, je vous regarde).

  • foo = new std::string[125]; est une erreur parce qu'une fois de plus foo fuit une ressource, sauf si les étoiles s'alignent et quelqu'un se souvient, dans tous les cas et au bon moment, de faire le ménage.

La façon correcte d'écrire ce code jusqu'à présent serait :

std::unique_ptr<std::string[]> foo(std::make_unique<std::string[]>(25));

Notez que chaque sous-expression dans cette déclaration n'est pas la cause profonde d'un bug du programme. Ce n'est pas votre faute.

Enfin, en ce qui concerne le point (3), les tableaux dynamiques sont un défaut du C++ et ne devraient en principe jamais être utilisés. Il existe plusieurs défauts standard concernant uniquement les tableaux dynamiques (et qui ne sont pas considérés comme valant la peine d'être corrigés). L'argument simple est que vous ne pouvez pas utiliser les tableaux sans connaître leur taille. Vous pourriez dire que vous pourriez utiliser une valeur sentinelle ou une valeur de pierre tombale pour marquer la fin d'un tableau de façon dynamique, mais cela rendrait la correction de votre programme plus difficile. valor -dépendante, non type -et donc non vérifiable statiquement (la définition même de "unsafe"). Vous ne pouvez pas affirmer statiquement que Ce n'était pas votre faute.

Vous finissez donc par devoir maintenir un stockage séparé pour la taille du tableau de toute façon. Et devinez quoi, votre implémentation doit de toute façon dupliquer cette connaissance afin de pouvoir appeler les destructeurs lorsque vous dites delete[] donc c'est une duplication inutile. La bonne méthode, au contraire, est de ne pas utiliser de tableaux dynamiques, mais de séparer l'allocation de mémoire (et de la rendre personnalisable via des allocateurs, tant que nous y sommes) de la construction d'objets par éléments. Envelopper tout cela (allocateur, stockage, nombre d'éléments) dans une classe unique et pratique est la méthode C++.

Ainsi, la version finale de votre code est la suivante :

std::vector<std::string> foo(25);

8voto

utnapistim Points 12060

J'ai entendu dire qu'une telle utilisation (pas ce code précisément, mais l'allocation dynamique dans son ensemble) peut être dangereuse dans certains cas, et ne devrait être utilisée qu'avec RAII. Pourquoi ?

Prenez cet exemple (similaire au vôtre) :

int f()
{
    char *local_buffer = new char[125];
    get_network_data(local_buffer);
    int x = make_computation(local_buffer);
    delete [] local_buffer;
    return x;
}

C'est trivial.

Même si vous écrivez le code ci-dessus correctement, quelqu'un peut venir un an plus tard, et ajouter une condition, ou dix ou vingt, dans votre fonction :

int f()
{
    char *local_buffer = new char[125];
    get_network_data(local_buffer);
    int x = make_computation(local_buffer);
    if(x == 25)
    {
        delete[] local_buffer;   
        return 2;
    }
    if(x < 0)
    {
        delete[] local_buffer; // oops: duplicated code
        return -x;
    }
    if(x || 4)
    {
        return x/4; // oops: developer forgot to add the delete line
    }
    delete[] local_buffer; // triplicated code
    return x;
}

Maintenant, s'assurer que le code n'a pas de fuite de mémoire est plus compliqué : vous avez plusieurs chemins de code et chacun d'entre eux doit répéter l'instruction delete (et j'ai introduit une fuite de mémoire exprès, pour vous donner un exemple).

C'est toujours un cas trivial, avec une seule ressource (local_buffer), et il suppose (naïvement) que le code ne lève aucune exception, entre l'allocation et la désallocation. Le problème conduit à un code non maintenable, lorsque votre fonction alloue ~10 ressources locales, peut lancer, et a de multiples chemins de retour.

De plus, la progression ci-dessus (cas simple et trivial étendu à une fonction plus complexe avec de multiples chemins de sortie, étendu à de multiples ressources et ainsi de suite) est une progression naturelle du code dans le développement de la plupart des projets. Ne pas utiliser le RAII, crée un moyen naturel pour les développeurs de mettre à jour le code, d'une manière qui diminuera la qualité, au cours de la durée de vie du projet ( C'est ce qu'on appelle le "cruft", et c'est une très mauvaise chose. ).

TLDR : L'utilisation de pointeurs bruts en C++ pour la gestion de la mémoire est une mauvaise pratique (bien que pour l'implémentation d'un rôle d'observateur, une implémentation avec des pointeurs bruts soit correcte). La gestion des ressources avec des pointeurs bruts viole SRP y SEC principes).

8voto

James Kanze Points 96599

Le code que vous proposez n'est pas sécurisé par les exceptions, et l'alternative :

std::vector<std::string> foo( 125 );
//  no delete necessary

est. Et bien sûr, le vector connaît la taille plus tard, et peut vérifier les limites en mode de débogage ; il peut être passé (par référence référence ou même par valeur) à une fonction, qui pourra alors l'utiliser l'utiliser, sans arguments supplémentaires. Array new suit les conventions C pour les tableaux, et les tableaux en C sont sérieusement cassés.

Pour autant que je puisse voir, il y a jamais un cas où un tableau nouveau est approprié.

2voto

activehigh Points 2289

Il y a deux inconvénients majeurs à cela -

  1. new ne garantit pas que la mémoire que vous allouez soit initialisée avec la fonction 0 ou null . Ils auront des valeurs indéfinies à moins que vous ne les initialisiez.

  2. Deuxièmement, la mémoire est allouée dynamiquement, ce qui signifie qu'elle est hébergée dans heap pas dans stack . La différence entre heap y stack c'est que les piles sont effacées quand la variable sort de sa portée mais heap ne sont pas effacés automatiquement et le C++ ne contient pas de collecteur d'ordures intégré. delete est manqué, vous vous retrouvez avec une fuite de mémoire.

2voto

Le pointeur brut est difficile à gérer correctement, par exemple en ce qui concerne la copie d'objets.

il est beaucoup plus simple et plus sûr d'utiliser une abstraction bien testée telle que std::vector .

en bref, ne réinventez pas inutilement la roue - d'autres ont déjà créé de superbes roues que vous ne pourrez probablement pas égaler en qualité ou en prix.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X