Menu
flag Ar iconالعربيةdown icon

printf في لغة C: سلاسل التنسيق والمحدّدات وطباعة الأعداد العشرية

كيف تعمل printf فعليًا في لغة C - نموذج سلسلة التنسيق، والمحدّدات التي تستخدمها يوميًا، والعرض والدقّة لإخراج مصطفّ، وطباعة الأعداد العشرية بعقل، والقيمة المُرجَعة، ولماذا تكون printf(user_input) ثغرة أمنية.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

printf أول دالة يتعلّمها كل مبرمج C وآخر دالة يقرأ معظمهم دليلها كاملًا. وهي تستحق عشر دقائق، لأن سلسلة تنسيقها لغة صغيرة قائمة بذاتها - ولأن الخطأ في محدّد ليس خطأً مطبعيًا بل سلوكًا غير معرَّف.

وهي تعيش في <stdio.h>، فكل برنامج يستخدمها يبدأ بـ #include <stdio.h>.

نموذج سلسلة التنسيق

تمشي printf على سلسلة التنسيق محرفًا واحدًا في كل مرة. وكل ما ليس % يُطبع كما كُتب تمامًا. أما % فتبدأ مواصفة تحويل تلتهم الوسيط التالي وتطبعه بالصيغة المطلوبة.

تُطابَق الوسائط بالمحدّدات بحسب الموضع حصرًا: فأول % تأخذ count، والثانية تأخذ name، وهكذا. ولا تملك printf وسيلة للتحقّق من أنك مرّرت ما وعدت به - إذ لا تستطيع رؤية الأنواع - فسلسلة التنسيق عقد عليك الوفاء به. وهذا ما يجعل -Wall جديرة بالتفعيل: فـ GCC يقرأ سلسلة التنسيق الحرفية في وقت الترجمة ويحذّر حين لا يطابق وسيط.

ولا شيء يفرض سطرًا جديدًا. فـ printf("Hello") تترك المؤشّر بعد الحرف o، ولهذا تكاد كل سلسلة تنسيق تنتهي بـ \n.

المحدّدات التي تستخدمها يوميًا

خمسة تغطّي معظم الشيفرة:

المحدّديطبعمثال استدعاءالخرج
%dعدد int موقّعprintf("%d", -42)-42
%fعدد doubleprintf("%f", 2.5)2.500000
%sسلسلة نصية (char *)printf("%s", "hi")hi
%cمحرف واحدprintf("%c", 'A')A
%pعنوان مؤشّرprintf("%p", (void*)&x)0x7ffd...

وتختبئ قاعدتان في تلك القصاصة. تُكتب علامة % الحرفية %% - فعلامة % وحيدة في آخر سلسلة تنسيق سلوك غير معرَّف. و%p تتطلّب void *، فحوّل المؤشّر؛ وتمرير int * مباشرةً غير معرَّف تقنيًا وإن طُبع بشكل سليم عمليًا.

والجدول الكامل - كل محدّد وكل معدِّل طول وعمود scanf - في صفحة محدّدات التنسيق. وتبقى هذه الصفحة مع ما تلجأ إليه فعلًا.

العرض والدقّة والمحاذاة

بين % والحرف يمكنك وضع رايات وعرض ودقّة. وهذا كيف تحوّل حلقة إلى جدول.

العرض حدّ أدنى لا حدّ أقصى أبدًا: فـ %3d مع القيمة 123456 تطبع الأرقام الستة كلها. أما الدقّة فتعني أشياء مختلفة بحسب النوع - أرقامًا بعد الفاصلة العشرية لـ %f، وأقصى عدد محارف لـ %s، وأدنى عدد أرقام لـ %d.

ويمكن توفير كليهما في وقت التشغيل بـ *، التي تقرأ وسيطًا من نوع int:

وتغيير width يعيد توزيع الجدول كله. وهذه هي الطريقة المعيارية لطباعة أعمدة مصطفّة في C - دون حاجة إلى دالة حشو نصوص.

طباعة الأعداد العشرية بعقل

%f بلا دقّة تطبع دائمًا ستة أرقام بعد النقطة، وهو ما نادرًا ما يريده أحد:

ثلاثة خيارات، ولكلٍّ منها قاعدة إرشادية:

  • %.Nf للنقود والقياسات وكل ما له عدد منازل طبيعي. و%.2f هي ما تريده للعملات.
  • %e للترميز العلمي، دائمًا d.dddddde±dd.
  • %g حين لا تعرف رتبة المقدار مسبقًا. فهي تختار %f أو %e أيّهما أقصر وتزيل الأصفار الختامية - وهي الافتراضي الأعقل لتسجيل قيمة لم ترها.

وفخّ يستحق القول بوضوح: لا يوجد %lf في printf. فوسائط float تُرقّى إلى double قبل أن تراها printf أصلًا، فتتولّى %f الاثنين، و%lf لا معنى لها إلا في scanf حيث يهمّ حجم الوجهة. (تقبل C99 فعلًا %lf في printf كمرادف، لكن عادة كتابتها تقود الناس إلى كتابة %f في scanf، وهو ما يعطب فعلًا.)

الأعداد الصحيحة بعروض أخرى

%d لأجل int. أما الأنواع الأكبر أو غير الموقّعة فتحتاج معدِّل طول، وعدم تطابقها سلوك غير معرَّف لا خطأ تقريب:

و%zu لأجل size_t هي ما يخطئ فيه الناس أكثر - فـ sizeof تُرجِع size_t لا int، فـ printf("%d", sizeof x) خطأ يصادف أن يعمل على أنظمة 32 بتًا ويسيء التصرّف على أنظمة 64 بتًا.

القيمة المُرجَعة

تُرجِع printf عدد المحارف التي كتبتها، أو عددًا سالبًا عند الفشل. وهي تُتجاهَل عادةً، وهذا مقبول في الغالب - لكنها مفيدة فعلًا حين تحتاج إلى معرفة عرض الخرج:

وتكتب sprintf وsnprintf القريبتان في مخزن بدل الشاشة، وتُرجِعان الطول الذي أنتجتاه. وفضّل دائمًا snprintf، التي تأخذ حجم المخزن ولا يمكن أن تفيض:

وتُرجِع snprintf الطول الذي كانت ستحتاجه السلسلة الكاملة، فقيمة مُرجَعة تساوي حجم المخزن أو تزيد عليها تخبرك بأن الخرج اقتُطع.

ثغرة سلسلة التنسيق

هذه هي المشكلة الأمنية الوحيدة في printf، وهي خطيرة. لا تمرّر أبدًا بيانات لم تكتبها كسلسلة تنسيق:

/* DANGEROUS - never do this */
printf(user_input);

/* Correct */
printf("%s", user_input);

إن احتوت user_input على %s، قرأت printf وسيطًا لم يُمرَّر قط واتّبعت أي قمامة على هيئة مؤشّر تجدها، فتنهار عادةً. و%x مكرّرة تُفرغ محتويات المكدّس على الشاشة، وقد تتضمّن كلمات مرور أو مفاتيح. و%n، التي تكتب عدد المحارف في وسيط، حوّلت هذا تاريخيًا إلى إفساد اعتباطي للذاكرة وتنفيذ شيفرة عن بُعد.

والقاعدة مطلقة ولا تكلّف شيئًا: سلسلة التنسيق دائمًا ثابت نصّي كتبته أنت. وأي شيء متغيّر يدخل كوسيط. وراية -Wformat-security في GCC تنبّه إلى الخطأ، وهي متضمَّنة في -Wall -Wextra.

التخزين المؤقّت: لماذا يظهر الخرج متأخّرًا أحيانًا

لا تكتب printf إلى الطرفية فورًا. فالخرج مخزَّن مؤقّتًا - ويُفرَّغ حين يمتلئ المخزن، أو حين يظهر سطر جديد على طرفية، أو حين ينتهي البرنامج بشكل طبيعي. ونتيجتان لذلك:

  • قد يفقد البرنامج الذي ينهار خرجًا "طُبع سلفًا". فإن كنت تنقّح بـ printf ولم تظهر الرسالة الأخيرة، فقد وقع الانهيار بعد ذلك السطر لا قبله. أضف fflush(stdout) أو أرسل خرج التنقيح إلى stderr غير المخزَّن مؤقّتًا.
  • وfprintf(stderr, ...) هي الطريقة الصحيحة لطباعة الأخطاء. فهي غير مخزَّنة مؤقّتًا، وتذهب إلى تيار منفصل يستطيع المستخدمون إعادة توجيهه مستقلًّا عن الخرج العادي.

الأسئلة الشائعة

كيف تعمل printf في لغة C؟

تأخذ printf سلسلة تنسيق وتمسحها محرفًا محرفًا. فالمحارف العادية تُطبع كما هي؛ وكل % تبدأ مواصفة تحويل تستهلك أحد الوسائط التالية وتطبعه بالصيغة المطلوبة. فـ printf("%d apples\n", 5) تطبع 5 apples وسطرًا جديدًا.

كيف أطبع عددًا عشريًا بمنزلتين في لغة C؟

استخدم دقّة: printf("%.2f\n", 3.14159) تطبع 3.14. والعدد بعد النقطة هو كم رقمًا يلي الفاصلة العشرية. و%f بلا دقّة تطبع دائمًا ستة أرقام، ولهذا تبدو الأعداد العشرية غير المنسّقة مزعجة إلى هذا الحد.

لماذا تطبع printf كلامًا مهملًا أو تنهار؟

في كل الأحوال تقريبًا بسبب عدم تطابق بين المحدّد والوسيط. فـ printf("%d", 3.5) أو printf("%s", 42) سلوك غير معرَّف - إذ لا تستطيع printf رؤية الأنواع الحقيقية، بل تثق بسلسلة التنسيق. ترجم بـ -Wall، فيجعل GCC يفحص التنسيق مقابل الوسائط.

لماذا تكون printf(user_input) خطيرة؟

إن احتوى نصّ المستخدم على متتاليات %، فستطيعها printf وتقرأ وسائط لم تُمرَّر قط - فتسرّب ذاكرة المكدّس، ومع %n تكتب في الذاكرة أيضًا. وهذه هي ثغرة سلسلة التنسيق الكلاسيكية. اكتب دائمًا printf("%s", user_input) بدلًا من ذلك.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن