Devriez-vous toujours utiliser ‘int’ pour les nombres en C, même s’ils ne sont pas négatifs?

J’utilise toujours unsigned int pour les valeurs qui ne devraient jamais être négatives. Mais aujourd’hui j’ai remarqué cette situation dans mon code:

void CreateRequestHeader( unsigned bitsAvailable, unsigned mandatoryDataSize, unsigned optionalDataSize ) { If ( bitsAvailable – mandatoryDataSize >= optionalDataSize ) { // Optional data fits, so add it to the header. } // BUG! The above includes the optional part even if // mandatoryDataSize > bitsAvailable. } 

Dois-je commencer à utiliser int au lieu de unsigned int pour les nombres, même s’ils ne peuvent pas être négatifs?

Devrais-je toujours …

La réponse à “Dois-je toujours …” est certainement “non”, il y a beaucoup de facteurs qui déterminent si vous devez utiliser un type de données. La cohérence est importante.

Mais, il s’agit d’une question hautement subjective, il est vraiment facile de gâcher les non signés:

 for (unsigned int i = 10; i >= 0; i--); 

se traduit par une boucle infinie.

C’est pourquoi certains guides de style, notamment le Guide de style C ++ de Google, découragent les types de données unsigned .

À mon avis, je n’ai pas rencontré beaucoup de bugs causés par ces problèmes de types de données non signés – je dirais que les assertions permettent de vérifier votre code et de les utiliser judicieusement (et moins lorsque vous effectuez des calculs).

Une chose qui n’a pas été mentionnée est que l’ échange de numéros signés / non signés peut conduire à des bogues de sécurité . Ceci est un gros problème, car de nombreuses fonctions de la bibliothèque C standard prennent / retournent des nombres non signés (fread, memcpy, malloc etc. prennent size_t parameters size_t )

Par exemple, prenez l’exemple inoffensif suivant (à partir du code réel):

 //Copy a user-defined structure into a buffer and process it char* processNext(char* data, short length) { char buffer[512]; if (length <= 512) { memcpy(buffer, data, length); process(buffer); return data + length; } else { return -1; } } 

Semble inoffensif, non? Le problème est que la length est signée, mais est convertie en unsigned lorsqu’elle est transmise à memcpy . Ainsi, définir une longueur sur SHRT_MIN valide le test <= 512 , mais memcpy à copier plus de 512 octets dans le tampon - cela permet à un attaquant de remplacer l'adresse de retour de la fonction et (après un peu de travail) ordinateur!

Vous pouvez naïvement dire: "Il est tellement évident que la longueur doit être size_t ou vérifiée pour être >= 0 , je ne pourrais jamais faire cette erreur" . Sauf que je vous garantis que si vous avez déjà écrit quelque chose de non sortingvial, vous l'avez. Ainsi que les auteurs de Windows , Linux , BSD , Solaris , Firefox , OpenSSL , Safari , MS Paint , Internet Explorer , Google Picasa , Opera , Flash , Open Office , Subversion , Apache , Python , PHP , Pidgin , Gimp , ... sur et sans relâche ... - et ce sont tous des gens shinys dont le travail est de connaître la sécurité.

En bref, utilisez toujours size_t pour les tailles.

Man, la programmation est difficile .

Dans certains cas, vous devez utiliser des types entiers non signés:

  • Vous devez traiter un datum comme une représentation binary pure.
  • Vous avez besoin de la sémantique de l’arithmétique modulo que vous obtenez avec des nombres non signés.
  • Vous devez vous size_t avec du code qui utilise des types non signés (par exemple, des routines de bibliothèque standard acceptant / size_t valeurs size_t .

Mais pour l’arithmétique générale, le fait est que lorsque vous dites que quelque chose “ne peut pas être négatif”, cela ne signifie pas nécessairement que vous devez utiliser un type non signé. Parce que vous pouvez mettre une valeur négative dans un non signé, c’est juste que cela deviendra une valeur très importante lorsque vous allez le sortir. Donc, si vous voulez dire que les valeurs négatives sont interdites, comme pour une fonction racine carrée de base, alors vous énoncez une condition préalable de la fonction, et vous devriez l’affirmer. Et vous ne pouvez pas affirmer que ce qui ne peut pas être est; vous avez besoin d’un moyen de conserver les valeurs hors bande pour pouvoir les tester (il s’agit de la même logique derrière getchar() renvoie un caractère int et not char ).

De plus, le choix entre signature et signature peut avoir des répercussions pratiques sur les performances. Jetez un oeil au code (artificiel) ci-dessous:

 #include  bool foo_i(int a) { return (a + 69) > a; } bool foo_u(unsigned int a) { return (a + 69u) > a; } 

Les deux foo sont les mêmes sauf pour le type de leur paramètre. Mais, compilé avec c99 -fomit-frame-pointer -O2 -S , vous obtenez:

         .file "try.c"
         .texte
         .p2align 4, 15
 .globl foo_i
         .type foo_i, @function
 foo_i:
         movl $ 1,% eax
         ret
         .size foo_i,.-foo_i
         .p2align 4, 15
 .globl foo_u
         .type foo_u, @function
 foo_u:
         movl 4 (% esp),% eax
         leal 69 (% eax),% edx
         cmpl% eax,% edx
         seta% al
         ret
         .size foo_u,.-foo_u
         .ident "GCC: (Debian 4.4.4-7) 4.4.4"
         .section .note.GNU-stack, "", @ progbits

Vous pouvez voir que foo_i() est plus efficace que foo_u() . C’est parce que le débordement arithmétique non signé est défini par le standard pour “boucler”, donc (a + 69u) peut très bien être plus petit que a si a est très grand, et donc il doit y avoir du code pour ce cas. Par contre, le débordement arithmétique signé n’est pas défini, donc GCC ira de l’avant et présumera que l’arithmétique signée ne déborde pas , et donc (a + 69) ne peut jamais être inférieur à a . Le choix de types non signés sans discrimination peut donc avoir un impact inutilement sur les performances.

Bjarne Stroustrup, créateur de C ++, met en garde contre l’utilisation de types non signés dans son livre Le langage de programmation C ++:

Les types entiers non signés sont idéaux pour les utilisations qui traitent le stockage comme un tableau de bits. Utiliser un non signé au lieu d’un int pour gagner encore un bit pour représenter des entiers positifs n’est presque jamais une bonne idée. Les tentatives visant à garantir que certaines valeurs sont positives en déclarant des variables non signées seront généralement annulées par les règles de conversion implicites.

La réponse est oui. Le type int “unsigned” de C et C ++ n’est pas un “nombre entier toujours positif”, quel que soit le nom du type. Le comportement des ints non signés C / C ++ n’a aucun sens si vous essayez de lire le type comme “non négatif” … par exemple:

  • La différence de deux non signés est un nombre non signé (cela n’a aucun sens si vous le lisez comme suit: “La différence entre deux nombres non négatifs est non négative”)
  • L’ajout d’un int et d’un int non signé est non signé
  • Il y a une conversion implicite de int en unsigned int (si vous lisez unsigned comme “non négatif” c’est la conversion inverse qui aurait du sens)
  • Si vous déclarez une fonction acceptant un paramètre non signé lorsque quelqu’un passe un entier négatif, vous obtenez simplement converti implicitement en une valeur positive énorme; En d’autres termes, l’utilisation d’un type de paramètre non signé ne vous aide pas à trouver des erreurs ni à la compilation ni à l’exécution.

En effet, les nombres non signés sont très utiles dans certains cas car ce sont des éléments de l’anneau “integers-modulo-N” avec N étant une puissance de deux. Ints non signés sont utiles lorsque vous souhaitez utiliser cette arithmétique modulo-n, ou en tant que bitmasks; ils ne sont PAS utiles en tant que quantités.

Malheureusement, en C et C ++, les non signés étaient également utilisés pour représenter des quantités non négatives afin de pouvoir utiliser tous les 16 bits lorsque les nombres entiers pouvant alors utiliser 32k ou 64k étaient considérés comme une grande différence. Je le classerais essentiellement comme un accident historique… vous ne devriez pas essayer de lire une logique car il n’y avait pas de logique.

Au fait, à mon avis, c’était une erreur … si 32k ne suffisait pas, alors 64k ne suffira pas non plus; abuser de l’entier modulo simplement à cause d’un bit supplémentaire à mon avis était un coût trop élevé à payer. Bien sûr, il aurait été raisonnable de faire si un type non négatif correct était présent ou défini … mais la sémantique non signée est tout simplement mauvaise pour l’utiliser comme non négatif.

Parfois, vous pouvez trouver qui dit que non signé est bon parce que cela “documente” que vous ne voulez que des valeurs non négatives … cependant, cette documentation n’a de valeur que pour les personnes qui ne savent pas vraiment comment fonctionne non signé pour C ou C ++. Pour moi, voir un type non signé utilisé pour des valeurs non négatives signifie simplement que l’auteur du code ne comprenait pas le langage de cette partie.

Si vous comprenez et souhaitez vraiment le comportement “wrapping” des ints non signés, alors ils sont le bon choix (par exemple, j’utilise presque toujours “unsigned char” lorsque je gère des octets); Si vous n’allez pas utiliser le comportement d’encapsulation (et que ce comportement va simplement vous poser problème dans le cas de la différence que vous avez montrée), cela indique clairement que le type non signé est un mauvais choix et que vous devrait coller avec les plaines.

Est-ce que cela signifie que le type de retour C ++ std::vector<>::size() est un mauvais choix? Oui … c’est une erreur. Mais si vous dites, soyez prêt à être appelé de mauvais noms par qui ne comprend pas que le nom “unsigned” est juste un nom … ce qui compte, c’est le comportement et c’est un comportement “modulo-n” (et non on considérerait un type “modulo-n” pour la taille d’un conteneur un choix judicieux).

Je semble être en désaccord avec la plupart des gens ici, mais je trouve les types unsigned très utiles, mais pas sous leur forme historique brute .

Si, par conséquent, vous vous en tenez à la sémantique que représente un type pour vous, il ne devrait y avoir aucun problème: utilisez size_t (unsigned) pour les index de tableau, les offsets de données, etc. off_t (signed) pour les offsets de fichiers. Utilisez ptrdiff_t (signé) pour les différences de pointeurs. Utilisez uint8_t pour les petits entiers non signés et int8_t pour les entiers signés. Et vous évitez au moins 80% des problèmes de portabilité.

Et n’utilisez pas int , long , unsigned , char si vous ne devez pas. Ils appartiennent aux livres d’histoire. (Parfois, vous devez, les erreurs de retour, les champs de bits, par exemple)

Et pour revenir à votre exemple:

bitsAvailable – mandatoryDataSize >= optionalDataSize

peut être facilement réécrit comme

bitsAvailable >= optionalDataSize + mandatoryDataSize

ce qui n’évite pas le problème d’un débordement potentiel ( assert c’est votre ami) mais je pense que vous vous rapprochez un peu de l’idée que vous voulez tester.

 if (bitsAvailable >= optionalDataSize + mandatoryDataSize) { // Optional data fits, so add it to the header. } 

Sans bogue, tant que obligatoireDataSize + optionalDataSize ne peut pas dépasser le type d’entier non signé – la dénomination de ces variables m’amène à penser que c’est probablement le cas.

Vous ne pouvez pas éviter complètement les types non signés dans le code portable, car de nombreux typedefs dans la bibliothèque standard sont non signés (notamment size_t ), et de nombreuses fonctions les renvoient (par exemple std::vector<>::size() ).

Cela dit, je préfère généralement m’en tenir aux types signés pour les raisons que vous avez décrites. Ce n’est pas seulement le cas que vous évoquez – en cas d’arithmétique signée / non signée, l’argument signé est discrètement promu en non signé.

D’après les commentaires sur l’un des articles du blog d’Eric Lipperts (voir ici ):

Jeffrey L. Whitledge

Une fois que j’ai développé un système dans lequel les valeurs négatives n’avaient aucun sens en tant que paramètre, plutôt que de valider que les valeurs des parameters étaient non négatives, je pensais que ce serait une bonne idée d’utiliser Uint à la place. J’ai rapidement découvert que chaque fois que j’utilisais ces valeurs pour quelque chose (comme appeler les méthodes BCL), elles étaient converties en entiers signés. Cela signifiait que je devais valider que les valeurs ne dépassaient pas la plage entière signée en haut, donc je n’ai rien gagné. De plus, chaque fois que le code était appelé, les ints utilisés (souvent reçus des fonctions BCL) devaient être convertis en uints. Il n’a pas fallu longtemps avant que je change toutes ces notes en ints et que je prenne toutes ces mesures inutiles. Je dois encore valider que les chiffres ne sont pas négatifs, mais le code est beaucoup plus propre!

Eric Lippert

Je n’aurais pas dit mieux moi même. Vous n’avez presque jamais besoin de la scope d’une uint, et ils ne sont pas compatibles CLS. La méthode standard pour représenter un petit entier est avec “int”, même s’il y a des valeurs qui sont hors limites. Une bonne règle de base: n’utilisez “uint” que dans les situations où vous interagissez avec du code non géré qui attend des uints, ou où l’entier en question est clairement utilisé comme un ensemble de bits, pas un nombre. Essayez toujours de l’éviter dans les interfaces publiques. – Eric

La situation où (bitsAvailable – mandatoryDataSize) produit un résultat “inattendu” lorsque les types sont unsigned et bitsAvailable < mandatoryDataSize est une raison pour laquelle des types parfois signés sont parfois utilisés même si les données ne sont jamais censées être négatives.

Je pense qu’il n’ya pas de règle absolue - j’utilise généralement des types non signés pour des données qui n’ont aucune raison d’être négatives, mais il faut s’assurer que l’encapsulation arithmétique n’expose pas de bogues.

Ensuite, si vous utilisez des types signés, vous devez parfois considérer le dépassement de capacité:

 MAX_INT + 1 

La clé est que vous devez faire attention lors de l'exécution de l'arithmétique pour ces types de bogues.

Non, vous devez utiliser le type qui convient à votre application. Il n’y a pas de règle d’or. Parfois, sur de petits microcontrôleurs, il est par exemple plus rapide et efficace d’utiliser de la mémoire, dans la mesure du possible, comme variables natives, mais c’est un cas très particulier. Je recommande également d’utiliser stdint.h dans la mesure du possible. Si vous utilisez Visual Studio, vous pouvez trouver des versions sous licence BSD.

S’il y a une possibilité de débordement, affectez les valeurs au type de données suivant lors du calcul, à savoir:

 void CreateRequestHeader( unsigned int bitsAvailable, unsigned int mandatoryDataSize, unsigned int optionalDataSize ) { signed __int64 available = bitsAvailable; signed __int64 mandatory = mandatoryDataSize; signed __int64 optional = optionalDataSize; if ( (mandatory + optional) <= available ) { // Optional data fits, so add it to the header. } } 

Sinon, vérifiez simplement les valeurs individuellement au lieu de calculer:

 void CreateRequestHeader( unsigned int bitsAvailable, unsigned int mandatoryDataSize, unsigned int optionalDataSize ) { if ( bitsAvailable < mandatoryDataSize ) { return; } bitsAvailable -= mandatoryDataSize; if ( bitsAvailable < optionalDataSize ) { return; } bitsAvailable -= optionalDataSize; // Optional data fits, so add it to the header. } 

Vous devrez examiner les résultats des opérations que vous effectuez sur les variables pour vérifier si vous pouvez dépasser / sous-traiter – dans votre cas, le résultat est potentiellement négatif. Dans ce cas, il vaut mieux utiliser les équivalents signés.

Je ne sais pas si c’est possible dans c, mais dans ce cas, je voudrais simplement lancer la chose XY dans un int.

Si vos nombres ne doivent jamais être inférieurs à zéro, mais ont une chance d’être <0, utilisez tous des entiers signés et saupoudrez des assertions ou d'autres vérifications à l'exécution. Si vous travaillez en réalité avec des valeurs 32 bits (ou 64 ou 16, selon votre architecture cible) où le bit le plus significatif signifie autre chose que "-", vous ne devez utiliser que des variables non signées pour les conserver. Il est plus facile de détecter les dépassements d'entiers où un nombre qui devrait toujours être positif est très négatif que lorsqu'il est nul, donc si vous n'avez pas besoin de ce bit, allez avec les signés.

Supposons que vous ayez besoin de compter de 1 à 50000. Vous pouvez le faire avec un entier non signé de deux octets, mais pas avec un entier signé de deux octets (si l’espace compte beaucoup).