notes / Programmation système
Implémentation de sortie formatée en C
Notes sur la reconstruction d’une fonction minimale de type printf en C : arguments variadiques, analyse de format, conversions, sortie, valeurs de retour et cas limites.
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.hva_listva_startva_argva_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
NULLsi le comportement est défini - longue corde
Nombre entier
%d%i- zéro
- nombre positif
- nombre négatif
INT_MAXINT_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
NULLsi 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
_printfpersonnalisé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
printfstandard - 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
printfpour 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:
printfsemble 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.