Le char en C

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

Bonsoir,

J’apprend le C, on dit qu’un signed char va entre -128 et 127 mais j’ai testé le code suivant et il compile, même avec l’option -Wall.

#include <stdio.h>

int main()
{
    signed char m1 = -128;
    signed char m2 =  200; // On s'attend à une erreur ici
    
    printf("m1 : %c\n", m1);
    printf("m2 : %c\n", m2);
    
    return 0;
}

Comment expliquer cette différence entre la pratique et la théorie ?

+0 -0

Il n’y a pas de différence entre la pratique et la théorie :D

Le C est un langage faiblement typé. Par là, j’entends qu’il est capable de faire des convertions sans qu’elles soit explicites ni évidente. Ici, 200 ou -128 sont des valeurs de type int qui sont converties à l’initialisation en char.

Pour t’en convaincre tu peux essayer d’affichier directement les valeurs avec printf et le format %d

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    signed char m1 = -128;
    signed char m2 =  200; // On s'attend à une conversion implicite ici
    
    printf("m1 : %d\n", m1);
    printf("m2 : %d\n", m2);
    
    return EXIT_SUCCESS;
}

Ce comportement n’est pas universel d’autres langages demandent d’expliciter les conversions, d’autres langages sont encore plus souple que le C. En C, les conversions implicites se font entre les types numériques (int, usize, …) et les addresses (void, char, int*, …). Pour le C++, c’est uniquement entre les types numériques.

+0 -0

Salut,

En C, un dépassement de capacité ne conduit pas à une erreur de compilation, même s’il est détecté par le compilateur. Il s’agit d’un comportement indéfini, c’est-à-dire que la norme du langage ne précise pas le comportement qui est obtenu dans cette situation. Dans les faits, il y a trois possibilités :

  1. Dans 99% des cas (parce que quasiment tous les processeurs modernes fonctionnent ainsi), la valeur boucle, c’est-à-dire qu’une fois le maximum atteint, on retombe au minimum et inversement. Dans ton exemple, la valeur 200 va devenir -56.
  2. La valeur sature (elle reste bloquée au maximum ou au minimum).
  3. Le processeur émet une exception et l’exécution du programme est arrêtée.

C’est probablement un peu tôt si tu commences l’apprentissage du C, mais c’est expliqué dans ce chapitre du cours C. ;)

+1 -0
  1. Dans le cas des expressions le compilateur fusionne la valeur qui dépasse avec une autre qui fait revenir dans les bornes. Exemple bidon: dans le cas des signés, le compilateur est autorisé à transformer (2 * x + 10) / 2 en x+5 — bon après, les calculs se font sur des int, du coup ça se teste avec INT_MAX-5 p.ex.

(Le compilateur est autorisé parce que les dépassements ne sont justement pas définis.)

Merci pour vos réponses, ça m’aide à en apprendre plus sur le C.
Existe-t-il un site officiel qui référence les fonctions en C (documentation) ?
J’ai beau chercher, je tombe sur des sites bizarres ou peu fiables avec Google.

Effectivement le nombre renvoie -56.

Par ailleurs un autre cas limite similaire en jouant avec INT_MAX :

#include <stdio.h>
#include <limits.h>

int somme(int a, int b)
{
    return a+b;
}

int main()
{
    int res = somme(INT_MAX, 1);
    printf("Résultat : %i\n", res);       // Résultat : -2147483648   
    
    return 0;
}

@ache à quoi sert le void en tant qu’argument de la fonction main ?

+0 -0

Existe-t-il un site officiel qui référence les fonctions en C (documentation) ?

cppreference?

@ache à quoi sert le void en tant qu’argument de la fonction main ?

En C, à dire que la fonction ne prend aucun paramètres. Si on ne met rien (en C; le C++ a changé pour le choix plus intuitif précédent), cela veut dire: infinité de paramètres.

Existe-t-il un site officiel qui référence les fonctions en C (documentation) ?

info-matique

Comme l’a cité @lmghs, cppreference est un bon site de référence et t’évitera de te farcir la lecture de la norme. Maintenant, pour le côté « officiel », le seul document (’fin les seuls documents) à l’être est la norme. La dernière en date est la C18, (il y a un lien vers un PDF dans les référence de l’article, c’est un « brouillon », pas le document final validé, mais cela revient quasiment au même) et la partie qui t’intéresse est la 7 qui décrit la bibliothèque standard.

+0 -0

@ache à quoi sert le void en tant qu’argument de la fonction main ?

info-matique

Ahah ! Tu remarques tout ^^
C’est très bien.

En C, il n’y a que deux prototypes normalisé de la fonction main.

int main(void);
// et 
int main(int argc, char *argv[]);

En C++, ne rien mettre est équivalent à mettre void en C. Bref, si une fonction ne prend pas d’argument, il faut mettre void.

En C, l’overflow d’un unsigned est correctement défini par la norme du langage. Ce n’est pas le cas pour l’overflow d’un signed, comme expliqué par Taurre, c’est un comportement indéfini.

+0 -0

Bref, si une fonction ne prend pas d’argument, il faut mettre void.

Par curiosité, que se passe-t-il dans mon code lorsque je ne mets pas de void comme argument ? Le compilateur semble accepter des comportements non standard aux normes, du coup comment coder proprement et éviter les erreurs ?

Si tu ne mets rien alors la fonction accepte un nombre indéfini d’argument.

Exemple:

#include<stdlib.h>

int foo() {
  return 11;
}
int main(void) {
  foo(12, 13, "qsdqd");
  return EXIT_SUCCESS;
}

C’est totalement standard mais pas très utile. Par contre, pour la fonction main, effectivement, le compilateur accepte des points d’entrés non standard. :/

+0 -0

Petite questions toujours avec le char en C et les pointeurs.
Lorsqu’on déclare un pointeur, pourquoi a-t-on besoin de préciser le type du pointeur ?

Par exemple, l’exemple suivant fonctionne très bien et compile :

#include <stdio.h>

int main()
{
    char  c  =  'E';
    int*  p  =  &c;
    
    printf("%c", *p);  // Affiche : E
    return 0;
}

Même résultat en inversant les types char et int.
Dans ma tête, un pointeur contient une adresse mémoire, peut importe le type du contenu adressé.

Rigolo.

Déjà, tu devrais avoir au moins un avertissement. Dans la norme, il n’est pas écrit que les pointeurs sont compatibles entre eux (sauf pour char* et void*). Tu dois caster en void* pour signifier que tu souhaites réellement faire cette conversion.

Bref, peu importe ce que dit la norme, les compilo laissent un warning et compilent. C’est un peu la philosophie du C.

Ici, le résultat est vraiment dépendant de là où tu exécutes le code. Tu es très certainement sur une architecture little-endian. C’est pour ça que ça marche. Sur une autre architecture, ça ne devrait pas marcher.

L’idée, c’est que 'E' est stocké sur le premier octet du int et donc quand le pointeur sur char* accède à cet endroit, il accède à la bonne valeur.

Dans le sens contraire, quand c est de type char, quand le pointeur sur int déréférence ce pointeur, il déréférence le int stocké ici. C’est-à-dire, autant d’octets dont il a besoin (certainement 4 octets). Mais seul le premier est finalement affiché car tu utilises %c.

Dans les deux cas, en big endian, tu aurais eu un comportement différent (la première fois, ça aurait affiché 0, la seconde un nombre au hasard).

Dans tous les cas, dans le second cas, tu accèdes à de la mémoire non allouée donc il est tout à fait possible que le programme segfault.


Pour répondre à ta question, le type d’un pointeur est nécessaire pour plusieurs choses.

  • Au déréférencement. Un char* ne va déréférencer que 1 octet alors qu’un double en déréférencera certainement 8.
  • Pour l’arithmétique des pointeurs. Incrémenté un char* va augmenter sa valeur de 1. Alors qu’incrémenté un int* va augmenter sa valeur de sizeof(int). C’est cohérent, quand j’incrémente un pointeur de char, je veux le prochain char. Quand j’incrémente un pointeur de int*, je veux le prochain int, pas un truc bizarre qui est entre les deux.

En espérant t’avoir éclairé.e

+0 -0

Déjà, tu devrais avoir au moins un avertissement. Dans la norme, il n’est pas écrit que les pointeurs sont compatibles entre eux (sauf pour char* et void*). Tu dois caster en void* pour signifier que tu souhaites réellement faire cette conversion.

ache

Mmm… Convertir ou non vers un pointeur sur void ou char entre les deux ne change a priori rien au problème, tu ne fais qu’ajouter une étape. Faire char * -> void * -> int * est identique à char * -> int *, puisque la conversion vers et depuis le type void * garantit simplement que ton pointeur restera identique.

A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.

ISO/IEC 9899:2017, doc. N2176, § 6.3.2.3, al. 1, p. 41.

Par ailleurs, la norme permet un petit peu plus en ce qui concerne les conversions entre pointeurs.

A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer.

ISO/IEC 9899:2017, doc. N2176, § 6.3.2.3, al. 7, p. 41.

Toutefois, ça c’est pour la conversion, l’accès à l’objet référencé est en revanche une autre question.

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

  • a type compatible with the effective type of the object,
  • a qualified version of a type compatible with the effective type of the object,
  • a type that is the signed or unsigned type corresponding to the effective type of the object,
  • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
  • a character type.
ISO/IEC 9899:2017, doc. N2176, § 6.5, al. 7, p. 55.

Pour faire bref, la norme proscrit l’accès à un objet via un pointeur d’un type différent, sauf exceptions (la plus emblématique étant l’emploi d’un pointeur sur char).

Dans le sens contraire, quand c est de type char, quand le pointeur sur int déréférence ce pointeur, il déréférence le int stocké ici. C’est-à-dire, autant d’octets dont il a besoin (certainement 4 octets). Mais seul le premier est finalement affiché, car tu utilises %c.

ache

N’oublie pas que printf() est une fonction à nombre variable d’arguments et qu’elle reçoit un int en argument, pas une adresse. Elle n’affiche donc pas « le premier byte » de ce int, mais converti cette valeur en unsigned char pour l’afficher. Étant donné que le pointeur p référence en fait un char, on peut donc observer un affichage hasardeux sur une architecture littel endian également (sans compter qu’il s’agit d’un comportement indéfini).

c: If no l length modifier is present, the int argument is converted to an unsigned char, and the resulting character is written.

ISO/IEC 9899:2017, doc. N2176, § 7.21.6.1, al. 8, p. 229.
+0 -0
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