revs412@portfolio:~/notes/implementing-formatted-output-in-c$cat implementing-formatted-output-in-c.md

Pourquoi cette note existe

Cette note documente le processus de mise en œuvre d'une fonction de style printf minimale en C.

La valeur de ce projet n'est pas de copier la bibliothèque standard. La partie utile est de comprendre ce que la sortie formatée fait réellement derrière un simple appel comme:

printf("Hello %s, you have %d messages
", name, count);

Un utilisateur normal voit du texte.

Une implémentation printf doit :

read the format string
detect conversion markers
pull the correct argument type
convert values into printable characters
write the result to output
track the number of printed characters
handle errors and edge cases

Cela en fait un projet utile de bas niveau C et d'analyse.

Contexte du projet

Le projet a été construit en C comme un projet fondamental axé sur le formatage des sorties et les fonctions variades.

Il fait partie du portefeuille en tant que note de programmation de systèmes, et non en tant qu'affectation de formation générique.

La valeur est de montrer la compréhension de:

  • Interfaces de fonctions C
  • arguments variades
  • analyse du format
  • envoi de conversion
  • conversion numérique
  • sortie de caractères
  • Valeurs de retour
  • comportement en cas de bord
  • conception de petites bibliothèques

Il complète la note shell minimale Unix parce que les deux projets traitent de C, le comportement Linux, et la manipulation soigneuse des détails de bas niveau.

Ce que ce projet veut prouver

  • les fonctions simples de bibliothèque masquent la logique d'analyse réelle
  • Les fonctions variades exigent une discipline de type stricte
  • les chaînes de format sont de petites langues
  • chaque spéculateur de conversion a besoin d'un comportement clair
  • la sortie entière est un problème de conversion, pas seulement l'impression de chiffres
  • Valeur de retour
  • les cas bord définissent si la mise en œuvre est fiable
  • Les projets C doivent être soigneusement séparés entre l'analyse, la conversion et la production

Pioche et outils utilisés

Langue et temps d'exécution

  • C
  • GCC
  • Direction standard de la bibliothèque C
  • Direction de sortie de type POSIX
  • Sortie du terminal Linux

C Concepts

  • stdarg.h
  • va_list
  • va_start
  • va_arg
  • va_end
  • pointeurs
  • ficelles
  • conversion entière
  • sortie numérique itérative ou récursion
  • pointeurs de fonction ou direction des tables d'expédition
  • Gestion des erreurs

Concepts de présentation

  • spécifiants de conversion
  • caractères littéraux
  • Pourcentage d'évasion
  • Nombre de retours
  • entiers signés
  • entiers non signés
  • caractères
  • ficelles
  • hexadécimal en cas de mise en œuvre
  • drapeaux/largeur/direction de la précision si elle est prolongée

Construction prévue

La construction prévue est une fonction de style printf minimale qui peut imprimer des valeurs formatées communes.

Une première version forte devrait soutenir:

%c  character
%s  string
%d  signed decimal integer
%i  signed decimal integer
%u  unsigned decimal integer
%%  literal percent sign

Conversions prolongées possibles :

%x  lowercase hexadecimal
%X  uppercase hexadecimal
%o  octal
%p  pointer address

Une version plus avancée peut prendre en charge:

field width
precision
left alignment
zero padding
plus sign
space flag
hash flag
length modifiers

La première version devrait prioriser l'exactitude sur trop de fonctionnalités.

Modèle mental de base

Une fonction de style printf est principalement une boucle sur la chaîne de format.

Débit simplifié:

for each character in format string:
    if normal character:
        print it
        increment count

    if '%' is found:
        read next format specifier
        fetch matching argument
        convert argument to output
        increment count

La partie dure rend cette fiabilité lorsque l'entrée est incomplète, invalide ou utilise différents types d'arguments.

Arguments variades

printf accepte un nombre variable d'arguments.

Cela signifie que la fonction ne peut pas connaître les types d'arguments de la seule signature de la fonction.

Une implémentation personnalisée utilise stdarg.h.

Débit théorique:

va_list args;

va_start(args, format);
value = va_arg(args, expected_type);
va_end(args);

La chaîne de format indique à la fonction quel type tirer ensuite.

Cela crée une règle importante:

The parser and argument extraction must agree.

Si le format indique %d, l'implémentation devrait tirer un int.

Si le format dit %s, il devrait tirer un char *.

Tirer sur le mauvais type provoque un comportement non défini.

Parsing de chaîne de format

La chaîne de format est le jeu d'instructions.

Exemple :

"Name: %s | Score: %d%%"

L'analyseur voit :

literal text: "Name: "
specifier: %s
literal text: " | Score: "
specifier: %d
specifier: %%

L'analyseur doit distinguer:

  • caractères normaux
  • spécifiants de conversion valides
  • Pourcentage littéral de signes
  • non valides ou incomplets
  • drapeaux/largeur/précision facultatifs si mis en œuvre

Même un petit analyseur a besoin d'un comportement prévisible.

Conversion Expédition

Une implémentation propre devrait éviter de mettre chaque conversion en une seule fonction massive.

Une meilleure structure:

format parser
  ↓
specifier dispatcher
  ↓
conversion handler
  ↓
output function

Exemple de gestionnaire :

print_char
print_string
print_signed_int
print_unsigned_int
print_hex
print_pointer

Cela facilite l'extension et le débogage du code.

Sortie de caractères

Au niveau le plus bas, la sortie formatée devient la sortie de caractères.

Une implémentation minimale peut écrire en utilisant:

write(1, &c, 1);

ou un autre assistant de sortie.

Une fonction d'aide peut suivre le nombre imprimé :

print one character
if success:
    count += 1
else:
    mark error

Ceci est utile car printf retourne le nombre de caractères imprimés.

Valeur de retour

La valeur de retour est importante.

Standard printf retourne le nombre de caractères imprimés, ou une valeur négative en cas d'erreur.

Une implémentation personnalisée devrait suivre attentivement les caractères imprimés.

Exemple :

len = _printf("Hi %s", "Ochy");

Nombre prévu:

length of "Hi Ochy"

Le dénombrement devrait comprendre :

  • caractères littéraux
  • caractères convertis
  • Pourcentage de panneaux imprimés par %%
  • nouvelles lignes
  • espaces

Il ne devrait pas compter les caractères qui n'ont pas été écrits avec succès.

Conversion des chaînes

La conversion %s imprime une chaîne.

Cas importants:

  • Chaîne normale
  • chaîne vide
  • Direction du pointeur NULL
  • longue corde
  • précision si mise en œuvre

Un comportement sécuritaire commun est d'imprimer quelque chose comme:

(null)

lorsque le pointeur de chaîne est NULL, selon les exigences choisies.

La clé est de définir clairement le comportement et de le tester.

Conversion des caractères

La conversion %c imprime un seul caractère.

Même s'il imprime un char, la promotion de l'argument variadic signifie qu'il est généralement récupéré sous la forme d'un int.

Détails importants:

char arguments are promoted to int in variadic functions

Ainsi, la mise en œuvre devrait utiliser:

va_arg(args, int)

puis lancer ou sortir en tant que personnage.

Conversion entière signée

Les conversions %d et %i impriment des entiers signés.

Cas importants:

  • zéro
  • nombres positifs
  • nombres négatifs
  • Valeur entière minimale
  • grand nombre
  • Nombre de retours

Le cas le plus difficile est souvent :

INT_MIN

parce que sa valeur absolue peut ne pas correspondre à un int signé.

Une implémentation sûre doit gérer cela sans débordement.

Conversion entière non signée

La conversion %u imprime des entiers décimals non signés.

Cela évite les signes négatifs, mais nécessite toujours l'extraction des chiffres.

Débit théorique:

if value is 0:
    print '0'

else:
    repeatedly divide by 10
    collect digits
    print digits in correct order

L'implémentation peut utiliser une récursion, un tampon temporaire ou un stockage à chiffres inversés.

Conversion hexadécimal

Si %x et %X sont implémentés, la fonction convertit les numéros en base 16.

Chiffres :

0123456789abcdef
0123456789ABCDEF

Ceci enseigne un concept général:

number conversion = repeated division by base

Décimal, octal et hexadécimal sont toutes des variations de la même logique.

Conversion des pointeurs

Si %p est implémenté, la fonction imprime une valeur de type adresse.

Habituellement, la direction de sortie est :

0x...

Cela nécessite:

  • récupérer un pointeur
  • coulée vers un type entier adapté aux adresses
  • conversion en hexadécimal
  • maniement direction pointeur nul

Le formatage Pointer est une bonne extension mais pas nécessaire pour la première version.

Pourcentage d'évasion

La séquence %% imprime un signe de pourcentage littéral.

Exemple :

_printf("Progress: 100%%
");

Produit :

Progress: 100%

Cela est important car % est normalement le début d'un spéculateur de conversion.

L'analyseur doit reconnaître l'évasion spéciale.

Specifiants non valides

Une chaîne de format peut contenir des conversions non prises en charge.

Exemple :

_printf("Value: %q
", value);

Une implémentation minimale nécessite un comportement défini.

Choix possibles:

print '%' and the unknown specifier
ignore it
return an error

L'important, c'est la cohérence.

Un comportement indéfini ou aléatoire rend le débogage plus difficile.

Pourcentage incomplet à la fin

Un boîtier de bord commun:

_printf("hello %");

La chaîne de format se termine après %.

La mise en œuvre doit décider de ce qui se passe.

Comportement possible:

  • Erreur de retour
  • imprimer %
  • ignorer les spécifications incomplètes

Pour une mise en œuvre personnalisée, cela devrait être documenté et testé.

Drapeaux, largeur et direction de précision

Une implémentation de base peut sauter le formatage avancé.

Une version étendue plus forte peut supporter:

-   left align
+   force sign
0   zero padding
#   alternate form
space sign
width
precision

Exemple :

printf("%08d", 42);

Direction des produits:

00000042

Ces caractéristiques transforment l'analyseur d'un simple détecteur de spécifications en un moteur de formatage plus sérieux.

Pour une première version, il est préférable de mettre en œuvre moins de fonctionnalités correctement.

Buffer vs Direct Write

Il existe deux stratégies communes de production.

Écrire directement

Imprimez chaque personnage dès qu'il est prêt.

Avantages:

  • plus simple
  • moins de gestion de la mémoire
  • facile à comprendre

Inconvénients

  • beaucoup d'appels d'écriture
  • plus difficile de revenir sur l'erreur

Buffer en premier

Construire la sortie en mémoire, puis écrire.

Avantages:

  • moins écrit
  • plus facile à gérer la sortie finale
  • plus près de la direction de performance

Inconvénients

  • plus de gestion de mémoire
  • redimensionnement du tampon
  • complexité du traitement des erreurs

Une version minimale peut utiliser des écritures directes.

Gestion de la mémoire

Un printf minimum peut ne pas avoir besoin d'allocation dynamique lourde si elle imprime directement.

Mais la mémoire peut apparaître dans:

  • tampons à chiffres temporaires
  • données de format copiées
  • tampons d'aide à la conversion
  • manipulation dynamique de la chaîne
  • largeur avancée/formatage de précision

En C, chaque allocation doit être nettoyée.

Pour une petite implémentation, évitez l'allocation inutile lorsque de simples tampons de pile ou une sortie récursive suffisent.

Liste de vérification

Produit de base

  • ficelle simple
  • chaîne vide
  • nouvelle ligne
  • espaces
  • plusieurs caractères littéraux

Caractère

  • %c
  • direction de caractère nul si testé
  • caractères multiples

Chaîne

  • %s
  • chaîne vide
  • Chaîne NULL si le comportement est défini
  • longue corde

Nombre entier

  • %d
  • %i
  • zéro
  • nombre positif
  • nombre négatif
  • INT_MAX
  • INT_MIN

Non signé

  • %u
  • zéro
  • grande valeur non signée

Pourcentage

  • %%
  • pourcentage à côté du texte
  • signes multiples pour cent

Format mixte

  • %s %d %c
  • texte avant et après les conversions
  • conversions répétées

Affaires invalides

  • spécifiant non pris en charge
  • Suivi %
  • direction des arguments manquants
  • chaîne de format est NULL si le comportement est défini

Comparaison avec l'impression standardf

Une stratégie de test utile consiste à comparer les valeurs de sortie et de retour avec la norme printf pour les conversions supportées.

Exemple de direction d'essai:

custom output == printf output
custom return == printf return

Comparer uniquement les caractéristiques que la mise en œuvre personnalisée prétend soutenir.

Si les drapeaux ou la précision ne sont pas implémentés, ils ne devraient pas être testés comme comportement supporté.

Bogues courantes

Mauvais type va_arg

Cause:

format parser expects one type but va_arg retrieves another

Résultat :

undefined behavior

Correction :

match each specifier to the correct promoted type

Hypothèse de terminateur NULL manquante

Cause:

string handler assumes every pointer is valid

Résultat :

segmentation fault

Correction :

define and handle NULL string behavior

Nombre de retours incorrect

Cause:

converted characters are printed but not counted correctly

Correction :

centralize output/count logic

INT_MIN Dépassement

Cause:

trying to convert INT_MIN with normal negative-to-positive logic

Correction :

handle minimum value safely using wider type or unsigned logic

Pourcentage de manipulation brisée

Cause:

parser treats %% like invalid format

Correction :

special-case %% as literal percent

Fonction massive

Cause:

all parsing and conversion lives in one function

Correction :

split parser, dispatcher, and handlers

Décisions pratiques

Mettre en place moins de spécifications d'abord

Une %c correcte, %s, %d, %i, %u et %% est meilleure que de nombreuses conversions interrompues.

Nombre de pistes en un seul endroit

Les bogues de comptage de caractères sont plus faciles à éviter lorsque toutes les sorties passent par un helper.

Gardez l'analyseur prévisible

Les spéculateurs non soutenus doivent se comporter de façon cohérente.

Regarder les promotions variades

char et short sont promus à int dans les appels variades.

Entier des bords d'essai

0, valeurs négatives, INT_MAX et INT_MIN révèlent de nombreux bogues.

Éviter toute affectation inutile

Une sortie directe simple permet de garder la première version plus sûre.

Ce qu'une version terminée devrait montrer

Une version terminée forte devrait montrer:

  • fonction _printf personnalisée
  • analyseur de chaîne de format
  • Gestion des arguments variadiques
  • expéditeur de conversion
  • aide à la sortie de caractères
  • nombre de caractères imprimés
  • %c
  • %s
  • %d
  • %i
  • %u
  • %%
  • option %x, %X, %o, %p
  • comportement d'erreur constant
  • essais avec printf standard
  • README avec des spécifications prises en charge
  • aucune fuite de mémoire pour les chemins supportés

Preuves à retenir

Voici quelques éléments de preuve utiles à cette note :

  • arbre source
  • Interface de fonction _printf
  • parser/dispatcher code extrait
  • entier aide à la conversion
  • sortie d'essai par rapport à printf
  • Essais sur cas bord
  • Essais de valeur de retour
  • Sortie Valgrind si disponible
  • Tableau de spécifications supporté par README
  • exemples de comportement non soutenu

Hypothèses techniques

Cette note suppose que la mise en œuvre est écrite en C.

Il suppose que l'objectif est de construire une fonction de type printf minimale pour l'apprentissage et la programmation de systèmes fondamentaux.

Il suppose également que l'implémentation prend en charge un sous-ensemble documenté de printf standard, pas le comportement complet de la bibliothèque standard.

Principaux risques

  • Tirer de mauvais types de va_arg
  • retour du mauvais nombre de caractères
  • s'écraser sur les chaînes NULL
  • Mauvais traitement INT_MIN
  • Pourcentage d'évasion
  • comportement de format non pris en charge non documenté
  • trop de fonctionnalités ajoutées avant que le noyau fonctionne
  • fuites de mémoire des tampons temporaires
  • aide à la sortie incohérente
  • comparant avec la norme printf pour les fonctionnalités non implémentées

État actuel

Cette note représente un projet de faible niveau C axé sur la production formatée.

Il est plus ancien/fondamental par rapport à l'infrastructure et les notes du système d'affaires, mais il ajoute encore de la valeur parce qu'il montre:

parsing
type handling
conversion logic
output control
edge-case discipline

Cela en fait une bonne note de programmation de systèmes.

Ce que la présente note ne prétend pas

La présente note ne prétend pas remplacer la bibliothèque C standard.

Il ne prétend pas implémenter complètement chaque drapeau printf, largeur, précision, modificateur de longueur, comportement local ou détail spécifique à la plateforme.

Il documente une implémentation personnalisée minimale utilisée pour comprendre comment la sortie formatée fonctionne en interne.

À emporter pratique

La leçon utile est:

printf semble simple car le travail difficile d'analyse et de conversion est caché.

Pour reconstruire même une petite version, vous devez comprendre :

  • arguments variades
  • au format des chaînes
  • Extraction spécifique au type
  • conversion nombre-texte
  • Nombre de sorties
  • Gestion des erreurs
  • cas bord
  • discipline de la mémoire

C'est ainsi que le projet devient une note de programmation de systèmes C au lieu d'un exercice de formation de base.