اذهب إلى المحتوى

التعامل مع الدخل والخرج I/O وتنسيقه في لغة سي C


Naser Dakhel

تكلّمنا سابقًا عن الدخل والخرج في لغة سي بالاستعانة بالمكتبات القياسية، وحان الوقت لنتعلم الآن مختلف الدوال الموجودة في هذه المكتبات التي تضمن لنا أساليب مختلفة في القراءة والكتابة.

الدخل والخرج المنسق

هناك عدد من الدوال المُستخدمة لتنسيق الدخل والخرج، وتحدد كلًا من هذه الدوال التنسيق المتبع للدخل والخرج باستخدام سلسلة التنسيق النصية format string، وتتألف سلسلة التنسيق النصية في حالة الخرج من نص عادي plain text يمثّل الخرج كما هو، إضافةً إلى مواصفات التنسيق format specifications التي تتطلب معالجة خاصة لواحد من الوسطاء المتبقية في الدالة، بينما تتألف سلسلة التنسيق النصية في حالة الخرج من نص عادي يطابق مجرى الدخل وتحدد هنا مواصفات التنسيق معنى الوسطاء المتبقية.

يُشار إلى كل واحدة من مواصفات التنسيق باستخدام المحرف "%" متبوعًا ببقية التوصيف.

الخرج: دوال printf

تتخذ مواصفات التنسيق في دوال الخرج الشكل التالي، ونشير إلى الأجزاء الاختيارية بوضعها بين قوسين:

%<flags><field width><precision><length>conversion

نشرح معنى كل من الراية flag وعرض الحقل field width والدقة precision والطول length والتحويل conversion أدناه، إلا أنه من الأفضل النظر إلى وصف المعيار إذا أردت وصفًا مطولًا ودقيقًا.

الرايات

يمكن ألا تأخذ الرايات أي قيمة أو أن تأخذ أحد القيم التالية:

قيمة الراية الشرح
- ملئ سطر التحويل من اليسار ضمن حقوله.
+ يبدأ التحويل ذو الإشارة بإشارة زائد أو ناقص دائمًا.
مسافة فارغة space إذا كان المحرف الأول من تحويل ذو إشارة ليس بإشارة، أضف مسافةً فارغة، ويمكن تجاوز الراية باستخدام "+" إذا وجدت.
# يُجبر استخدام تنسيق مختلف للخرج، مثل: الخانة الأولى لأي تحويل ثماني لها القيمة "0"، وإضافة "0x" أمام أي تحويل ست عشري لا تساوي قيمته الصفر، ويُجبِر الفاصلة العشرية في جميع تحويلات الفاصلة العائمة حتى إن لم تكن ضرورية، ولا يُزيل أي صفر لاحق من تحويلات g و G.
0 يُضيف إلى تحويلات d و i و o و u و x و X و e و E و f و F و G أصفارًا إلى يسارها بملئ عرض الحقل، ويمكن تجاوزه باستخدام الراية "-"، وتُتجاهل الراية إذا كان هناك أي دقة محددة لتحويلات d، أو i، أو o، أو u، أو x، أو X، ونحصل على سلوك غير معرف للتعريفات الأخرى.
عرض الحقل field width عدد صحيح عشري يحدد عرض حقل الخرج الأدنى ويمكن تجاوزه إن لزم الأمر، يُحوّل الوسيط التالي إلى عدد صحيح ويُستخدم مثل قيمة لعرض الحقل إن استُخدمت علامة النجمة "*"، وتُعامل هذه القيمة إذا كانت سالبة كأنها راية "-" متبوعة بعرض حقل ذي قيمة موجبة. يُملأ الخرج ذو الطول الأقصر من عرض الحقل بالمسافات الفارغة (أو بأصفار إذا كان العدد الصحيح المعبر عن عرض الحقل يبدأ بالصفر)، ويُملأ الخرج من الجهة اليسرى إلا إذا حُدّدَت راية تعديل اليسار left-adjustment.
الدقة precision تبدأ قيمة الدقة بالنقطة '.'، وهي تحدد عدد الخانات الدنيا لتحويلات d، أو i، أو o، أو u، أو x، أو X، أو عدد الخانات التي تلي الفاصلة العشرية في تحويلات e، أو E، أو f، أو العدد الأعظمي لخانات تحويلات g وG، أو عدد المحارف المطبوعة من سلسلة نصية في تحويلات s. يتسبب تحديد كمية حشو الحقل padding بتجاهل قيمة field width. يُحوَّل الوسيط التالي في حال استخدامنا لعلامة النجمة "*" إلى عدد صحيح ويُستخدم بمثابة قيمة لعرض الحقل، وتعامل القيمة كأنها مفقودة إذا كانت سالبة، وتكون الدقة صفر إذا وجدت النقطة فقط.
الطول length وهي h تسبق محدد specifier لطباعة نوع عدد صحيح integral ويتسبب ذلك في معاملتها وكأنها من النوع "short" (لاحظ أن الأنواع المختلفة القصيرة shorts تُرقّى إلى واحدة من أنواع القيم الصحيحة int عندما تُمرّر مثل وسيط). تعمل l مثل عمل h إلا أنها تُطبّق على وسيط عدد صحيح من نوع "long"، وتُستخدم L للدلالة على أنه يجب طباعة وسيط من نوع "long double"، ويطبَّق ذلك فقط على محددات الفاصلة العائمة. يتسبب استخدام هذا في سلوك غير معرف إذا كانت باستخدام النوع الخاطئ من التحويلات.

يوضح الجدول التالي أنواع التحويلات:

المحدد التأثير الدقة الافتراضية
d عدد عشري ذو إشارة 1
i عدد عشري ذو إشارة 1
u عدد عشري عديم الإشارة 1
o عدد ثماني عديم الإشارة 1
x عدد ست عشري عديم الإشارة من 0 إلى f 1
X عدد ست عشري عديم الإشارة من 0 إلى F 1
  تحدد الدقة عدد خانات الأدنى المُستبدل بأصفار إن لزم الأمر، ونحصل على خرج دون أي محارف عند استخدام الدقة صفر لطباعة القيمة صفر  
f يطبع قيمة من النوع double بعدد خانات الدقة (المقربة) بعد الفاصلة العشرية. استخدم دقة بقيمة صفر للحد من الفاصلة العشرية، وإلا فستظهر خانة واحدة على الأقل بعد الفاصلة العشرية 6
e, E يطبع قيمة من نوع double بالتنسيق الأسي مُقرّبًا بخانة واحدة قبل الفاصلة العشرية، وعدد من الخانات يبلغ الدقة المحددة بعده، وتُلغى الفاصلة العشرية عند استخدام الدقة صفر، وللأس خانتان على الأقل تطبع بالشكل 1.23e15 في تنسيق e أو 1.23E15 في حالة التنسيق E 6
g, G تستخدم أسلوب التنسيق f، أو e (E مع G) بحسب الأس، ولا يُستخدم التنسيق f إذا كان الأس أصغر من "‎-4" أو أكبر أو يساوي الدقة. تُحدّ الأصفار التي تتبع القيمة وتُطبع الفاصلة العشرية فقط في حال وجود خانات تابعة. غير محدد
c يُحوّل الوسيط من نوع عدد صحيح إلى محرف عديم الإشارة ويُطبع المحرف الناتج عن التحويل  
s تُطبع سلسلة نصية بطول خانات الدقة، ويجب إنهاء السلسلة النصية باستخدام NUL إذا لم تُحدّد الدقة أو كانت أكبر من طول السلسلة النصية لا نهائي
p إظهار قيمة مؤشر من نوع (void *‎) بطريقة تعتمد على النظام  
n يجب أن يكون الوسيط مؤشرًا يشير إلى عدد صحيح، ويكون عدد محارف الخرج باستخدام هذا الاستدعاء مُسندًا إلى العدد الصحيح  
% علامة "%" _

[جدول 1 التحويلات]

تجد وصف الدوال التي تستخدم هذه التنسيقات في الجدول التالي، وجميع الدوال المُستخدمة مُضمّنة في المكتبة <stdio.h>، إليك تصاريح هذه الدوال:

#include <stdio.h>

int fprintf(FILE *stream, const char *format, ...);
int printf(const char *format, ...);
int sprintf(char *s, const char *format, ...);

#include <stdarg.h>     // بالإضافة إلى‫ stdio.h
int vfprintf(FILE *stream, const char *format, va list arg);
int vprintf(const char *format, va list arg);
int vsprintf(char *s, const char *format, va list arg);
الاسم الاستخدام
fprintf نحصل على الخرج المنسق العام بواسطتها كما وصفنا سابقًا، ويكتب الخرج إلى الملف المُحدد باستخدام المجرى stream
printf دالة مُطابقة لعمل الدالة fprintf إلا أن وسيطها الأول هو stdout
sprintf دالة مُطابقة لعمل الدالة fprintf باستثناء أن خرجها لا يُكتب إلى ملف، بل يُكتب إلى مصفوفة محارف يُشار إليها باستخدام المؤشر s
vfprintf خرج مُنسَّق مشابه لخرج الدالة fprintf إلا أن لائحة الوسطاء المتغيرة تُستبدل بالوسيط arg الذي يجب أن يُهيَّأ باستخدام va_start، ولا تُستدعى va_end باستخدام هذه الدالة
vprintf مطابقة للدالة vfprintf باستثناء أن الوسيط الأول يساوي إلى stdout
vsprintf خرج مُنسَّق مشابه لخرج الدالة sprintf إلا أن لائحة الوسطاء المتغيرة تُستبدل بالوسيط arg الذي يجب أن يُهيَّأ باستخدام va_start، ولا تُستدعى va_end باستخدام هذه الدالة

[جدول 2 الدوال التي تطبع خرجًا مُنسَّقًا]

تُعيد كل الدوال السابقة عدد المحارف المطبوعة أو قيمة سالبة للدلالة على حصول خطأ، ولا يُحسب المحرف الفارغ الزائد بواسطة دالة sprintf و vsprintf.

يجب أن تسمح التنفيذات بالحصول على 509 محارف على الأقل عند استخدام أي تحويل.

الدخل: دوال scanf

هناك عدة دوال مشابهة لمجموعة دوال printf بهدف الحصول على الدخل، والفارق الواضح بين مجموعتي الدوال هذه هو أن مجموعة دوال scanf تأخذ وسيطًا متمثلًا بمؤشر حتى تُسند القيم المقروءة إلى وجهتها المناسبة، ويُعد نسيان تمرير المؤشر خطأ شائع الحدوث ولا يمكن للمصرّف أن يكتشف حدوثه، إذ يمنع استخدام لائحة وسطاء متغيرة من ذلك.

تُستخدم سلسلة التنسيق النصية للتحكم بتفسير المجرى للبيانات المُدخلة التي تحتوي غالبًا على قيم تُسند إلى كائنات يُشار إليها باستخدام وسطاء دالة scanf المتبقية، وقد تتألف سلسلة التنسيق النصية من:

  • مساحة فارغة white space: تتسبب بقراءة مجرى الدخل إلى المحرف التالي الذي لا يمثّل محرف مسافة فارغة.
  • محرف اعتيادي ordinary character: ويمثل المحرف أي محرف عدا محارف السلسلة الفارغة أو "%"، ويجب أن يطابق المحرف التالي في مجرى الدخل هذا المحرف المُحدّد.
  • توصيف التحويل conversion specification: وهو محرف "%" متبوع بمحرف "*" اختياري (الذي يكبح التحويل)، ويُتبع بعدد عشري صحيح لا يساوي الصفر يحدد عرض الحقل الأعظمي، ومحرف "h"، أو "l"، أو "L" اختياري للتحكم بطول التحويل، وأخيرًا محدد تحويل إجباري. لاحظ أن استخدام "h"، أو "l"، أو "L" سيؤثر على على نوع المؤشر الواجب استخدامه.

حقل الدخل -باستثناء المحددات "c" و "n" و "]"- هو سلسلة من المحارف التي لا تمثل مسافة فارغة وتبدأ من أول محرف في الدخل (بشرط ألا يكون المحرف مسافة فارغة)، وتُنهى السلسلة عند أول محرف متعارض أو عند الوصول إلى عرض الحقل المُحدّد.

تُسند النتيجة إلى الشيء الذي يُشير إليه الوسيط إلا إذا كان الإسناد مكبوحًا باستخدام "*" المذكورة سابقًا، ويمكن استخدام محددات التحويل التالية:

  • المحددات d i o u x: تُحوِّل d عدد صحيح ذو إشارة، وتحوّل i عدد صحيح ذو إشارة وتنسيق ملائم لـstrtol، وتحوِّل o عدد صحيح ثماني، وتحوّل u عدد صحيح عديم الإشارة، وتحول x عدد صحيح ست عشري.
  • المحددات e f g: تحوِّل قيمة من نوع float (وليس double).
  • المحدد s: يقرأ سلسلة نصية ويُضيف محرف فارغ في نهايته، وتُنهى السلسلة النصية باستخدام مسافة فارغة عند الدخل (ولا تُقرأ هذه المسافة الفارغة على أنها جزء من الدخل).
  • المحدد ]: يقرأ سلسلة نصية، وتتبع ] لائحة من المحارف تُدعى مجموعة المسح scan set، ويُنهي المحرف [ هذه اللائحة. تُقرأ المحارف إلى (غير متضمنةً) المحرف الأول غير الموجود ضمن مجموعة المسح؛ فإذا كان المحرف الأول في اللائحة هو "^" فهذا يعني أن مجموعة القراءة تحتوي على أي محرف غير موجود في هذه القائمة، وإذا كانت السلسلة الأولية هي "[^]" أو "[]" فهذا يعني أن [ ليس محدّدًا بل جزءًا من السلسلة ويجب إضافة محرف "[" آخر لإنهاء اللائحة. إذا وجدت علامة ناقص "-" في اللائحة، يجب أن يكون موقعها المحرف الأول أو الأخير، وإلا فإن معناها معرف بحسب التنفيذ.
  • المحدد c: يقرأ محرفًا واحدًا متضمنًا محارف المسافات الفارغة، ولقراءة المحرف الأول باستثناء محارف المسافات الفارغة استخدم ‎%1s، ويحدد عرض الحقل مصفوفة المحارف التي يجب قراءتها.
  • المحدد p: يقرأ مؤشرًا من النوع (void *‎) والمكتوب سابقًا باستخدام المحدد ‎%p ضمن استدعاء سابق لمجموعة دوال printf.
  • المحدد %: المحرف "%" متوقّع في الدخل ولا يُجرى أي إسناد.
  • المحدد n: يُعاد عددًا صحيح يمثل عدد المحارف المقروءة باستخدام هذا الاستدعاء.

يوضح الجدول التالي تأثير محددات الحجم size specifiers:

المحدد يُحدِّد يُحوِّل
l d i o u x عدد صحيح كبير long int
h d i o u x عدد صحيح صغير short int
l e f عدد عشري مضاعف double
L e f عدد عشري مضاعف كبير long double

[جدول 3 محددات الحجم]

إليك وصف دوال مجموعة scanf مع تصاريحها:

#include <stdio.h>

int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *s, const char *format, ...);
int scanf(const char *format, ...);

تأخذ الدالة fscanf دخلها من المجرى المُحدد، وتُطابق الدالة scanf الدالة fscanf مع اختلاف أن الوسيط الأول هو المجرى stdin، بينما تأخذ sscanf دخلها من مصفوفة محارف مُحدّدة.

نحصل على القيمة EOF المُعادة في حال حدوث خطأ دخل قبل أي تحويلات، وإلا فنحصل على عدد التحويلات الناجحة الحاصلة وقد يكون هذا العدد صفر إن لم تُجرى أي تحويلات، ونحصل على خطأ دخل إذا قرأنا EOF، أو بوصولنا إلى نهاية سلسلة الدخل النصية، بينما نحصل على خطأ تحويل إذا فشل العثور على نمط مناسب يوافق التحويل المحدّد.

عمليات الإدخال والإخراج على المحارف

هناك عدد من الدوال التي تسمح لنا بإجراء عمليات الدخل والخرج على المحارف بصورةٍ منفردة، إليك تصاريحها:

#include <stdio.h>
/* دخل المحرف */
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
int ungetc(int c, FILE *stream);

/* خرج المحرف */
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);

/* دخل السلسلة النصية */
char *fgets(char *s, int n, FILE *stream);
char *gets(char *s);

/* خرج السلسلة النصية */
int fputs(const char *s, FILE *stream);
int puts(const char *s);

لنستعرض سويًّا كلًّا منها.

دخل المحرف

تقرأ مجموعة الدوال التي تنفذ هذه المهمة المحرف مثل قيمة من نوع "unsigned char" من مجرى الدخل المحدد أو من stdin، ونحصل على المحرف الذي يليه في كل حالة من مجرى الدخل. يُعامل المحرف مثل قيمة "unsigned char" ويُحوّل إلى "int" وهي القيمة المُعادة من الدالة. نحصل على الثابت EOF عند الوصول إلى نهاية الملف، ويُضبط مؤشر نهاية الملف end-of-file indicator إلى المجرى المحدد، كما نحصل على EOF في حالة الخطأ ويُضبط مؤشر الخطأ إلى المجرى المحدّد. نستطيع الحصول على المحارف بصورةٍ تتابعية باستدعاء الدالة تباعًا. قد نحصل على وسيط المجرى stream أكثر من مرة في حال استخدام هذه الدوال على أنها ماكرو، لذا لا تستخدم الآثار الجانبية هنا.

هناك برنامج "ungetc" الداعم أيضًا، الذي يُستخدم لإعادة محرف إلى المجرى مما يجعله المحرف التالي الذي سيُقرأ، إلا أن هذه ليست بعملية خرج ولن تتسبب بتغيير محتوى الملف، ولذا تتسبب عمليات fflush و fseek و rewind على المجرى بين عملية إعادة المحرف وقراءته بتجاهل هذا المحرف، ويمكن إعادة محرف واحد فقط وأي محاولات لإعادة EOF تُتجاهل، ولا يُعدّل على موضع مؤشر الملف في جميع حالات إعادة قراءة مجموعة من المحارف وإعادة قرائتها أو تجاهلها. يتسبب استدعاء ungetc الناجح على مجرى ثنائي بتناقص موضع مؤشر الملف إلا أن ذلك غير محدد عند استخدام مجرى نصي، أو مجرى ثنائي موجود في بداية الملف.

خرج المحرف

هذه الدوال مطابقة لدوال الدخل الموصوفة سابقًا إلا أنها تجري عمليات الخرج، إذ تعيد المحرف المكتوب أو EOF عند حدوث خطأ ما، ولا يوجد ما يعادل نهاية الملف End Of File في ملف الخرج.

خرج السلسلة النصية

تكتب هذه الدوال سلاسلًا نصيةً إلى ملف الخرج باستخدام المجرى stream إن ذُكر وإلا فإلى المجرى stdout، ولا يُكتب محرف الإنهاء الفارغ. نحصل على قيمة لا تساوي الصفر عند حدوث خطأ وإلا فالقيمة صفر.

تحذير: تضيف puts سطرًا جديدًا إلى سلسلة الخرج النصية بينما لا تفعل fputs ذلك.

دخل السلسلة النصية

تقرأ الدالة fgets السلسلة النصية إلى مصفوفة يُشار إليها باستخدام المؤشر s من المجرى stream، وتتوقف عن القراءة في حال الوصول إلى EOF أو عند أول سطر جديد (وتقرأ محرف السطر الجديد)، وتضيف محرفًا فارغًا null في النهاية. يُقرأ n-1 محرف على الأكثر (لترك حيز للمحرف الفارغ).

تعمل الدالة gets على نحوٍ مشابه لمجرى stdin إلا أنها تتجاهل محرف السطر الجديد.

تعيد كلا الدالتين s في حال نجاحهما وإلا فمؤشر فارغ، إذ نحصل على مؤشر فارغ عندما نصادف EOF قبل قراءة أي محرف ولا يطرأ أي تغيير على المصفوفة، بينما تصبح محتويات المصفوفة غير معرفة إذا ما واجهنا خطأ قراءة وسط السلسلة النصية بالإضافة إلى إعادة مؤشر فارغ.

الدخل والخرج غير المنسق

هذا الجزء بسيط، إذا هناك فقط دالتان تقدمان هذه الخاصية، واحدة منهما للقراءة والأخرى للكتابة ويصرَّح عنهما على النحو التالي:

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nelem, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nelem, FILE *stream);

تُجرى عملية القراءة أو الكتابة المناسبة للبيانات المُشار إليها بواسطة المؤشر ptr وذلك على nelem عنصر، وبحجم size، ويفشل نقل ذلك المقدار الكامل من العناصر فقط عند الكتابة، إذ قد تعيق نهاية الملف دخل العناصر بأكملها، وتُعيد الدالة عدد العناصر التي نُقلت فعليًّا. نستخدم feof أو ferror للتمييز بين نهاية الملف عند الدخل أو للإشارة على خطأ.

تُعيد الدالة القيمة صفر دون أي فعل إذا كانت قيمة size أو nelem تساوي إلى الصفر.

قد يساعدنا المثال الآتي في توضيح عمل الدالتين المذكورتين:

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

struct xx{
        int xx_int;
        float xx_float;
}ar[20];

main(){

        FILE *fp = fopen("testfile", "w");

        if(fwrite((const void *)ar,
                sizeof(ar[0]), 5, fp) != 5){

                fprintf(stderr,"Error writing\n");
                exit(EXIT_FAILURE);
        }

        rewind(fp);

        if(fread((void *)&ar[10],
                sizeof(ar[0]), 5, fp) != 5){

                if(ferror(fp)){
                        fprintf(stderr,"Error reading\n");
                        exit(EXIT_FAILURE);
                }
                if(feof(fp)){
                        fprintf(stderr,"End of File\n");
                        exit(EXIT_FAILURE);
                }
        }
        exit(EXIT_SUCCESS);
}

[مثال 1]

ترجمة -وبتصرف- لقسم من الفصل Libraries من كتاب The C Book.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...