Remplir un tableau de pointeurs d'entiers

Le problème exposé dans ce sujet a été résolu.

Bonjour,

L’idée du code est de remplir un tableau de pointeurs d’entiers int* tab[5];.
Chaque pointeur du tableau va pointer vers un entier différent.

Cependant, le code n’affiche pas les nombres de 0 à 4.
Pourquoi la boucle (ligne 11) échoue-t-elle ?
Le code affiche 5 fois le nombre 4.

#define  SIZE  5
#include <iostream>

using std::cout;
using std::endl;

int main()
{
    int* tab[SIZE];

    for (int i = 0; i < SIZE; ++i)  {
        int k = i;
        tab[i] = &k;
    }

    for (int i = 0; i < SIZE; ++i) {
        cout << *(tab[i]) << endl;
    }

    return EXIT_SUCCESS;
}

Salut,

La variable k est locale à la boucle for. Ce serait pas du tout improbable que la même zone mémoire soit utilisée pour cette variable à chaque tour de boucle (ce qui expliquerait ton résultat). Regarder le code assembleur permet de valider cette hypothèse. :)

+1 -0

J’ai jamais utilise Geany et je sais pas comment il se configure. Mais j’imagine que tu utilises g++ ou clang comme compilateur. Ils ont une option -S qui permet d’obtenir le code assembleur. D’autres options peuvent également aider pour que le code ne soit pas trop optimisé par exemple (la documentation de ton compilateur et une petite recherche t’aideront plus que moi).

+0 -0

Dans tous les cas, c’est une erreur de programmation d’accéder à l’adresse d’une variable qui n’existe plus.

+5 -0

Effectivement, du coup ma seule option reste d’initialiser la variable k sur le tas.

Et ça fonctionne !  :-)

#define  SIZE  5
#include <iostream>

using std::cout;
using std::endl;

int main()
{
    int* tab[SIZE];

    for (int i = 0; i < SIZE; ++i)  {
        int* k = new int(i);
        tab[i] = k;

        // Encore plus court :
        // tab[i] = new int(i);
    }

    for (int i = 0; i < SIZE; ++i) {
        cout << *(tab[i]) << endl;
        delete tab[i];
    }

    return EXIT_SUCCESS;
}

Je dirais plutôt que c’est un problème de conception du langage que d’autoriser ça… :-°

Ce n’est certainement pas le crédo du C et du C++ que de s’interdire de prendre l’adresse de n’importe quelle variable. Cela a ses utilités. Si on restait local il pourrait y avoir des cas où ça sert.

Il faut apprendre à s’en servir. De la même façon que l’on peut commettre d’horrible erreurs de conception dans des langages qui ont des petites roulettes sur le côté (genre une sorted list qui dérive d’une liste…)

Ce n’est certainement pas le crédo du C et du C++ que de s’interdire de prendre l’adresse de n’importe quelle variable.

Et c’est bien pour ça qu’à part les cas d’utilisation de niche (et maintenance de code existant), rejeter la faute sur le développeur me parait contre productif. Mon message sous ces airs un peu trollesque pointe quand même ce que je considère être un vrai problème de philosophie. On a trop tendance à dire "c’est la faute du programmeur", quand clairement le vrai problème vient du design dépassé de ces langages (encore une fois, hors cas de niche). Si on prenait conscience de ces défauts plutôt que de se cacher derrière l’excuse "il faut apprendre à s’en servir" (voire infantiliser les utilisateurs de langages mieux conçus en parlant de "petites roulettes" :-° ) alors que les problèmes inhérent de gestion de la mémoire sont aujourd’hui bien gérés par les langages récents, peut être que l’industrie arriverait à s’en éloigner encore plus et on arrêterait d’enseigner ces langages à la fac hors formations liées aux applications de niche. C++ essaye de corriger le tir avec ses standards récents qui offrent des abstractions intéressantes, donc dans ce cas on pourrait arguer que le vrai problème est plutôt qu’il est difficile d’apprendre à s’en servir correctement. Cela dit, cette complexité gigantesque vient de son héritage historique. Peut être que c’est pas une si bonne idée de vouloir corriger les défauts d’un vieux langage et que passer à autre chose serait plus sage, mais c’est encore un débat différent.

+2 -0

adri1 : Je suis complètement d’accord avec toi, par contre personnellement je ne suis pas vraiment convaincu par les langages modernes actuels pour prendre la relève. De plus, souvent les abstractions notamment par rapport à la mémoire viennent avec un runtime dans lequel on reste enfermé pour l’écriture de librairies. Je ne sais pas si ces langages finiront par être massivement adoptés mais je pense que C/C++ mettra longtemps à décliner et pas que pour des raisons historiques.

+0 -0

Sortir encore l’argument des performances aujourd’hui alors que les performances au runtime de par exemple Rust (qui lui implémente carrément des abstractions sans coût) et Go sont compétitives avec celles de C ou C++ est absurde. Ça vient d’un type qui fait du HPC. Et c’est même sans discuter sur le temps gigantesque perdu au développement et à la maintenance des codes en s’obstinant à utiliser de vieilles technologies. Par ailleurs, étant donné que ces langages s’interfacent très bien avec C, ça veut dire qu’on n’a pas besoin de partir de zéro en terme de richesse d’écosystème.

La seule raison pour laquelle de nouveaux logiciels et bibliothèques sont encore développés en C ou C++ (hors niche bien sûr) est une pure question d’éducation, et cette espèce d’élitisme mal placé qui donne lieu à des arguments absurdes en faveur du C du genre "apprendre à se servir de C apprend la rigueur, c’est important" ou des pics gratuites comme celle des petites roulettes. Je demande aux fervents défenseurs du C de bien vouloir m’excuser de ne pas vouloir m’embarrasser à gérer des fuites de mémoire avec "mes petites roulettes" quand mon problème est d’implémenter un solveur itératif multigrille efficace qui fasse pas lui-même pédaler dans la semoule les itérations de mon algo de Newton-Raphson, lui même au-sein d’un solveur d’un problème physique hautement non-linéaire. Aka, j’ai d’autres chats à fouetter que des problèmes de gestion de mémoire qui sont réglés par le choix d’une technologie actuelle. Les problèmes actuels que l’on tente de résoudre informatiquement sont bien loin de ces considérations d’un autre temps, ou plutôt ils devraient l’être.

+0 -0

Je suis complètement d’accord avec toi. Il y a même des études qui montrent qu’une gestion automatique de la mémoire peut, pas si rarement que ça, apporter de meilleures performances qu’une gestion manuelle, même si on considère qu’il n’y a pas d’erreurs, car la gestion automatique peut faire du profiling et de l’adaptation automatique des (dés)allocations que peut difficilement faire une gestion manuelle. Et totalement d’accord on a vraiment autre chose à faire que gérer cet aspect qui s’automatise très bien.

Par contre dans mon message je ne parlais absolument pas de performances, je parlais du développement en lui-même. Les langages actuels ne me satisfont pas. C’est peut-être une question d’habitude, mais je trouve souvent leur syntaxe inélégante, je n’aime pas l’abandon de la programmation orientée objet, etc. Je n’ai pas encore trouvé, à mon goût, le digne successeur de C/C++.

Et quand tu dis que ces langages s’interfacent très bien avec le C, déjà ce n’est pas si simple que ça. Quand ton langage possède un runtime, tu ne peux pas écrire de librairies avec, à moins de rester enfermé dans ce langage. Donc soit il faut continuer de faire du C faute de mieux, soit il faut tout ré-écrire quinze fois. Donc généralement cette interface est uni-directionnelle ; et même dans le sens C -> langage moderne, souvent incomplète, peu pratique, voire expérimentale.

Toutes ces raisons font que je continue d’utiliser C pour mes librairies et C++ pour mes binaires, comme beaucoup de monde. Et ce n’est pas par plaisir car ces langages ont énormément de défauts et de sénilités. Les langages tels que Go ou Rust apportent énormément de choses très intéressantes, mais pour l’instant cela ne suffit pas selon moi.

Sortir encore l’argument des performances aujourd’hui alors que les performances au runtime de par exemple Rust (qui lui implémente carrément des abstractions sans coût)

adri1

Et en Rust on peut tout à fait faire l’erreur de conception décrite plus haut en utilisant les unsafe. C’est même le but des unsafe que de pouvoir faire ce genre de choses, et si on veut développer en Rust pour aller gratter des performances dans tous les coins, il y a fort à parier qu’on en aura besoin.

La présence du filet de sécurité de Rust est importante, en revanche, prétendre qu’on peut complètement s’en passer, niet. En tout cas pas tant qu’on n’aura pas des langages avec des systèmes de types complètement fou (genre bien plus fous que celui de Rust) associé à des compilateurs très optimisant.

(Et on parlera pas de l’absence de tout un tas d’outils qui font que Rust est encore parfaitement inutilisable dans certains domaines, parce que pas d’outil pour faire de la certif de code Rust).

Et en Rust on peut tout à fait faire l’erreur de conception décrite plus haut en utilisant les unsafe.

Et bien sûr, la différence intéressante par rapport à C étant qu’on peut réduire au maximum les zones concernées et les marquer comme telles, cet argument tombe complètement à l’eau. On peut écrire du code unsafe en Rust en le faisant exprès, mais on ne peut écrire du code safe en C qu’au prix d’efforts qu’il est absurde de porter sur le développeur.

+2 -0

Au-delà des problèmes de performance, est-ce que quelqu’un s’est intéressés à la proportion de failles de sécurité qui existent parce que le langage lui-même permet des comportement non sécurisés ?

J’ai l’impression que c’est courant, mais sans aucun chiffre sous la main, donc je ne sais pas si c’est juste une impression ou non.

Et bien sûr, la différence intéressante par rapport à C étant qu’on peut réduire au maximum les zones concernées et les marquer comme telles, cet argument tombe complètement à l’eau.

adri1

Note : à aucun moment je ne dis que Rust n’est pas mieux conçu que C. Juste que la présence de ces comportements est nécessaire si on veut un langage qui agit à bas niveau. Que le langage le permette par défaut est une faiblesse de conception (mais on a des outils qui nous aident à faire avec), mais certainement pas qu’on le permette tout court.

… mais on ne peut écrire du code safe en C qu’au prix d’efforts qu’il est absurde de porter sur le développeur.

L’effort, même en Rust est porté sur le développeur. La différence c’est le type de support que va avoir le développeur pour faire cet effort. En Rust, on a un système de types qui intègre ça et il faut bien garder en tête que faire typer un programme Rust correctement quand il fait des manipulation de mémoire complexe c’est difficile, et c’est un effort que va faire le développeur.

Quand on veut assurer la correction en C, et on sait le faire, notamment dans les domaines critiques (ferroviaire, avionique, etc), on utilise des outils d’analyse statique (interprétation abstraite, model checking, preuve déductive, preuve par raffinement depuis des modèles de plus haut niveau, etc). La différence elle est là : l’outillage n’est pas au niveau du langage, il est annexe, mais c’est la même chose pour plein d’autres sortes d’outils.

En l’occurrence, c’est aussi une raison pour laquelle Rust n’est pas du tout prêt pour ces domaines. Une grosse part de l’outillage pour des questions auquel le système de type ne sait pas répondre n’existe simplement pas (worst-case execution time, propriété fonctionnelles, etc). Pire encore, pour les code unsafe, tout ce qui existe pour l’instant, c’est des outils incomplets à base de test.

Au-delà des problèmes de performance, est-ce que quelqu’un s’est intéressés à la proportion de failles de sécurité qui existent parce que le langage lui-même permet des comportement non sécurisés ?

SpaceFox

C’est très courant :

Je mille-plussoie la réponse de Ksasss. (Je préviens, je suis un fan-boy du C mais : vous ne me ferez pas changer d’avis, et je ne m’attends pas à vous faire changer d’avis :) )

Au-delà des problèmes de performance, est-ce que quelqu’un s’est intéressés à la proportion de failles de sécurité qui existent parce que le langage lui-même permet des comportement non sécurisés ?

J’ai l’impression que c’est courant, mais sans aucun chiffre sous la main, donc je ne sais pas si c’est juste une impression ou non.

SpaceFox

Ne serait-ce que pour le C, valgrind fait une bonne partie du travail à ta place pour résoudre de nombreux problèmes.

Ensuite bah oui : les entrées utilisateurs sont toujours le bordel. Si c’est mal gérée, peu importe le langage, ton programme peut être attackables (je pense à des stack failles là, mais il y doit y en avoir bien d’autres).

Mais qu’on soit clair hein : que ce soit sous python, sous OCaml ou sous C tes entrées utilisateurs seront toujours chiantes à gérer si tu fais ça de manière non-safe.

A partir du moment où tu valgrind-checks ton programme et que tes entrées utilisateurs sont safes, tu réduits considérablement les probabilités de faille.

Quant au fait d’avoir besoin de valgrind-checks : si le programme est bien designée, tes allocs et tes frees se font naturellement. A partir du moment où le génie logiciel de ton logiciel est mal fichue, ça empire les problèmes initiaux que tu peux avoir.

Voilà ce que j’avais à dire. Et si je suis à côté de la plaque, conspuez-moi ! :) (gentillement quand même, je vous aime bien :3 )

Ne serait-ce que pour le C, valgrind fait une bonne partie du travail à ta place pour résoudre de nombreux problèmes.

lhp22

Hum, bof. C’est plutôt le minimum vital. Disons qu’un programme dont les tests ne passent pas dans un mode de debug avec instrumentation des accès mémoire, on sait déjà que c’est mort. De là à dire que l’on est en sécurité … Il y a du chemin.

Assez clairement, la majorité du code C qui est en production est insuffisamment testé ou vérifié. Cf la vidéo.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte