IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Concepts en C

Cohérence, logique et fonctionnement du C


précédentsommairesuivant

IX. Programme C

IX-A. Structure d'un programme C

Un programme C est constitué :

  • d'objets globaux, c'est-à-dire d'objets déclarés en dehors de toute fonction.

    Ces objets sont en allocation statique. Ils sont potentiellement accessibles partout dans le programme. Ceci est leur avantage, mais également leur inconvénient : On ne sait que très difficilement où ils sont utilisés et surtout où, quand et par qui ils sont modifiés. Ils sont utilisés parfois (et à tort) pour éviter de passer des paramètres aux fonctions. Pourtant, ceci est également un inconvénient puisque les fonctions cessent de ce fait d'être des entités autonomes : elles dépendent alors d'éléments qui leur sont extérieurs. La seule lecture du code de la fonction ne permet plus de déterminer son action.

Ces inconvénients sont majeurs pour la maintenance et la compréhension du code. On considère que l'utilisation d'objets globaux est à proscrire et n'est justifiée que dans des cas très particuliers.

  • d'une collection de fonctions.

    Une et une seule d'entre elles doit s'appeler main. Exécuter un programme C consiste à invoquer la fonction main. Dans une fonction, on peut appeler les autres fonctions, y compris la fonction où on se trouve (appel récursif), toutefois un appel à main, bien que non interdit, est à éviter absolument. Dans une fonction, il est interdit de définir une fonction : les fonctions se trouvent donc définies les unes à la suite des autres.

IX-B. Nature du code exécutable

Le code exécutable du programme (nous excluons de ce terme la définition et l'initialisation des objets globaux) se trouve intégralement dans les fonctions : il n'y a pas de code exécutable extérieur aux fonctions. Normalement, le déroulement de l'exécution d'un programme est séquentiel : après l'exécution d'un élément du code, on exécute l'élément du code écrit directement à la suite, mais on a des exceptions à ce processus : appel d'une fonction, sortie d'une fonction et surtout l'utilisation des instructions de contrôle qui permettent de modifier ce comportement.

Le code exécutable est construit à partir

  • d'instructions de contrôle
  • et d'expressions.

IX-B-1. Instructions de contrôle

Les instructions de contrôle modifient le déroulement séquentiel normal du programme. Elles peuvent permettre :

  • de n'exécuter un segment de code que si une condition est remplie :

if

if else

switch

  • de répéter un segment de code tant qu'une condition est remplie (boucles) :

for

while

do while

  • de réaliser inconditionnellement un saut directement à une autre partie du code :

goto

break

continue


Les instructions de saut inconditionnel brisent la structure du code et sont donc en général à éviter (sauf les break utilisés dans l'instruction switch dont la présence est le cas le plus fréquent et l'absence l'exception).

  • de sortir d'une fonction :

return

IX-B-2. Expressions

Tous les autres éléments du code exécutable sont des expressions terminées par un point-virgule.
L'exécution du programme n'est qu'une suite d'évaluation d'expressions, cette suite étant contrôlée par les instructions de contrôle.

  • Effets de bords :

    Au moment de l'exécution, ces expressions sont évaluées et l'évaluation fournit une valeur ou un objet selon son contexte . Pour que cette évaluation ait un effet, il faut qu'elle entraîne une modification de l'environnement propre du programme (modifie la valeur d'un objet ou lise la valeur d'un objet volatile) ou de l'environnement d'exécution du programme (par exemple, affiche quelque chose à la console) autrement dit qu'elle provoque une « action ». Dans ce cas, on dit que l'évaluation provoque des effets de bords.
  • Points de séquencement :

    L'interprétation des expressions est régie par la priorité et l'associativité des opérateurs. L'évaluation d'une expression est segmentée par des points de séquencement .

    Les points de séquencement délimitent les expressions élémentaires de l'expression (une expression est élémentaire si elle ne comporte pas de points de séquencement interne). On évalue complètement chaque expression élémentaire avant d'évaluer la suivante et l'ordre d'évaluation des opérandes de l'expression élémentaire n'est pas spécifié.
    Les effets de bords peuvent intervenir à tout moment pendant l'évaluation de l'expression élémentaire, mais doivent avoir été accomplis à la fin de l'évaluation.

    La plupart des expressions sont élémentaires. Les expressions construites avec

    - les opérateurs booléens && et ||

    - l'opérateur conditionnel ?:

    - l'opérateur , (virgule) (à ne pas confondre avec la virgule séparant les arguments d'une fonction)

    pour lesquelles un point de séquencement est situé après le premier opérande sont les exceptions. Par conséquent, pour ces opérateurs, l'opérande de gauche est toujours évalué en premier.
  • Expressions mal formées :

    Si la valeur d'une expression élémentaire peut dépendre de l'ordre d'évaluation de ses termes, l'expression est mal formée et conduit à un résultat indéfini. Une expression ayant une expression élémentaire mal formée est elle-même mal formée.

    - Une expression élémentaire peut être mal formée si l'évaluation des opérandes entraîne l'appel de plus d'une fonction possédant des effets de bords.

    - Elle est mal formée si deux opérandes (ou plus) font référence à un même objet alors que l'évaluation d'au moins l'un d'entre eux provoque un effet de bord sur cet objet.
Exemples d'expressions mal formées :
Sélectionnez
i = ++ i + 1
a[i++] = i
res = getchar()-getchar()
  • expressions arithmétiques :

    On a évoqué précédemment les types de grandeurs booléens et adresses ainsi que leurs opérateurs. On n'a pas envisagé le cas des nombreux types numériques entiers et réels. Lorsqu'un opérateur arithmétique binaire (à deux opérandes) Op combine deux valeurs V1 et V2 de types différents T1 et T2 :

V1 OpV2


le compilateur utilise un type commun T, pour lequel cet opérateur est défini, et pour lequel il existe une règle de transformation de T1 vers T et de T2 vers T (transtypage) .

V1 => TV2 => T


Il effectue alors l'opération entre les deux valeurs obtenues par transtypage de V1 et V2 en le type T et obtient comme résultat une valeur de type T.

Le principe est simple, son application l'est moins. Le premier critère de choix pour T sera logiquement d'essayer de conserver les valeurs lors du transtypage.

  1. Un opérande (au moins) est de type réel

    On peut donner un rang de classement aux types réels, avec par rang décroissant long double (C99) , double , float . Alors :

    - Si les deux types sont réels, le type T sera celui de T1 ou T2 qui a le rang le plus élevé. Les valeurs seront donc conservées.

    - Si un des types est réel et l'autre entier, le type T sera le type réel. Les valeurs seront donc (au moins approximativement) conservées.

  2. Les deux opérandes sont de type entier


    La difficulté arrive lorsque les deux types sont entiers.
    - Les types entiers possèdent un rang de classement, avec par rang décroissant, les types long long (en C99), long , int , short , signed char . Les types unsigned ont le même rang que le type signé correspondant.

    - La première étape est la « promotion des petits entiers » : les entiers de rang inférieur à celui de int sont promus, en conservant leur valeur, en int (ou si ce n'est pas possible en unsigned int. Ce sera, par exemple, le cas d'un unsigned short si l'étendue des valeurs représentées par un short est égale à celle d'un int ).
    Les deux opérandes ont maintenant un rang supérieur ou égal à celui de int .

    - Le type de rang le plus élevé est le type candidat à être le type commun. Le type final T sera ce type candidat, mais éventuellement dans sa version non signée.

    1. Si les valeurs peuvent être exactement représentées dans le type candidat, le type final T est le type candidat.

      C'est le cas si
      - le type des deux opérandes sont tout deux signés
      - le type des deux opérandes sont tout deux non signés
      - les valeurs du type non signé peuvent être représentées par les valeurs (positives) du type signé (le type signé est alors de rang plus élevé que le type non signé et est le type candidat).

    2. Sinon, le type final T est la version non signée du type candidat.

      Ce choix de la version non signée s'explique parce que le transtypage d'un entier signé en un entier non signé est parfaitement défini alors que l'inverse ne l'est pas.

    Quelques exemples :

type 1

type 2

type final T

int

long

long

unsigned int

unsigned long

unsigned long

int

unsigned int

unsigned int

int

unsigned long

unsigned long

unsigned int

long

long (?)
ou unsigned long (?)


précédentsommairesuivant

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.