تسلّمك معظم اللغات نوعًا string يعرف طوله. أما C فلا. ففي C السلسلة النصية ليست إلا مصفوفة من char مع قاعدة واحدة مرفقة: يتبع النصَّ بايتٌ قيمته صفر، يُكتب '\0' ويُسمّى المنهي الصفري. وكل ما عدا ذلك في سلاسل C يتبع من ذلك العرف الواحد.
السلسلة مصفوفة محارف زائد '\0'
عرّف سلسلة نصية بتهيئة مصفوفة char من ثابت نصّي:
تبدو المصفوفة أربعة بايتات لا ثلاثة:
name: +-----+-----+-----+------+
| 'A' | 'd' | 'a' | '\0' |
+-----+-----+-----+------+
index: 0 1 2 3
sizeof(name) تساوي 4. فقد عدّ المترجم المحارف، وأضاف واحدًا للمنهي، وحدّد حجم المصفوفة نيابةً عنك. وذلك البايت الإضافي هو سبب كون char name[3] = "Ada"; خطأً ينتظر الوقوع - فالنص يتّسع لكن المنهي لا، فلا يستطيع شيء لاحق معرفة أين تنتهي السلسلة.
وحين تريد مساحة لتغيير المحتوى لاحقًا، فحدّد الحجم بنفسك واترك فسحة:
char name[32] = "Ada"; /* 3 characters, a terminator, 28 bytes spare */
والبايتات غير المستخدمة يملؤها المهيّئ بأصفار، وهذا غير ضارّ.
لماذا يهمّ المنهي
لا تستقبل أي دالة C طولًا. فـ printf("%s", s) تُسلَّم عنوانًا واحدًا وتمشي إلى الأمام طابعةً البايتات حتى تلقى '\0'. إذًا فمصفوفة char بلا منهٍ ليست سلسلة نصية، ومعاملتها كذلك تقرأ أي ذاكرة تصادف أن تليها.
القاعدة الجديرة بالحفظ: sizeof تعطيك التخزين، وstrlen تعطيك النص. فلأجل char buf[32] = "Ada"، تكون sizeof buf هي 32 وstrlen(buf) هي 3. فهما تجيبان عن سؤالين مختلفين، والخلط بينهما أحد أشيع أخطاء C.
الثوابت النصّية مقابل مصفوفات المحارف
هذان السطران يبدوان متشابهين ويتصرّفان تصرّفًا مختلفًا جدًا:
char a[] = "hello"; /* an array you own, initialized from the literal */
char *p = "hello"; /* a pointer aimed at the literal itself */
a مصفوفة من ستة بايتات تحمل نسخة خاصة من النص. ويمكنك تعديلها. أما p فتشير إلى بيانات النصوص للقراءة فقط في البرنامج؛ والثابت النصّي مشترك، والكتابة عبر p سلوك غير معرَّف ينهار عادةً في وقت التشغيل لا في وقت الترجمة.
اكتب const char * كلما وُجّه مؤشّر إلى ثابت نصّي. عندئذٍ يرفض المترجم الكتابةَ في وقت الترجمة بدلًا من تركها تصبح انهيارًا. والاختيار بين الاثنين هو في الحقيقة سؤال عن المؤشّرات والمصفوفات، تغطّيه المؤشّرات والمصفوفات.
وفرق آخر يستحق المعرفة: sizeof a تساوي 6، بينما sizeof p هي حجم مؤشّر (8 على معظم الأنظمة) بصرف النظر عن طول النص.
طباعة السلاسل النصية وقراءتها
يطبع %s سلسلة نصية؛ ويطبع %c محرفًا واحدًا.
وللإدخال، تجنّب gets كليًا - فقد أُزيلت من اللغة لأنه لا يمكن استخدامها بأمان. استخدم fgets، التي تأخذ حجم المخزن ولن تفيض:
تحتفظ fgets بالسطر الجديد الذي ضغطته، وهو ما لا تريده في السلسلة تقريبًا أبدًا. وسطر strcspn يجد أول '\n' ويكتب فوقه منهيًا - وهي طريقة موجزة ومعيارية لاقتطاعه.
المرور على سلسلة محرفًا محرفًا
لأن النهاية مؤشَّر عليها لا معدودة، تفحص الحلقة الاصطلاحية المحرفَ نفسه:
كثيرًا ما يُختصر الشرط text[i] != '\0' إلى text[i] فقط، لأن '\0' صفر وبالتالي خاطئ. وكلاهما صحيح؛ والصيغة الصريحة أيسر قراءةً وأنت تتعلّم.
ويمكنك المرور بمؤشّر بدل الفهرس، وهو أسلوب C اصطلاحي بالقدر نفسه:
والتحويل إلى unsigned char قبل استدعاء toupper ليس زينة: فدوال <ctype.h> غير معرَّفة للقيم السالبة، وchar العادي قد يكون سالبًا للبايتات فوق 127.
لا تستطيع نسخ سلسلة بـ =
هذا هو الجدار الذي يصطدم به كل قادم جديد:
char a[10];
a = "hello"; /* error: assignment to expression with array type */
char b[10] = "hi";
char c[10];
c = b; /* error - same reason */
اسم المصفوفة ليس قيمة قابلة للتعديل، فليس لـ = ما تفعله. والموضع الوحيد الذي يعمل فيه الاختصار هو التهيئة عند التعريف، وهي عملية مختلفة يؤدّيها المترجم.
وللنسخ في وقت التشغيل، انسخ البايتات بـ strcpy من <string.h>:
والمنطق نفسه يفسّر لماذا لا تقارن == السلاسل النصية. فـ if (a == b) تقارن عنوانين، فهي خاطئة لمصفوفتين مختلفتين تحملان النص نفسه. استخدم strcmp(a, b) == 0 بدلًا منها. وكلتا الدالّتين وأقاربهما الأكثر أمانًا مغطّاة في دوال السلاسل النصية.
مصفوفات السلاسل النصية
قائمة السلاسل النصية إما مصفوفة مؤشّرات (لنصّ ثابت تقرؤه فقط) وإما مصفوفة char ثنائية الأبعاد (حين يجب أن تكون كل مدخلة قابلة للتعديل):
تخزّن مصفوفة المؤشّرات ثلاثة عناوين إلى ثوابت نصّية للقراءة فقط - موجزة لكنها غير قابلة للتعديل. أما صيغة char[3][16] فتحجز 48 بايتًا من ذاكرتك الخاصة، فيمكن إعادة كتابة كل صفّ.
أخطاء شائعة
- نسيان بايت المنهي. فـ
char s[5] = "hello"لا مساحة فيها لـ'\0'. حدّد الحجم بـstrlen + 1. - استخدام
sizeofحيث قصدتstrlen. فـsizeofعلى مؤشّر تعطي حجم المؤشّر لا طول النص. - الكتابة عبر
char *إلى ثابت نصّي. عرّف تلك المؤشّراتconst char *. - المقارنة بـ
==. فتلك تقارن عناوين. استخدمstrcmp. - القراءة بـ
scanf("%s", buf). فلا حدّ حجم لها وتفيض بسرور؛ فضّلfgets.
الأسئلة الشائعة
كيف تعرّف سلسلة نصية في لغة C؟
كمصفوفة char: char name[] = "Ada";. لا تملك C نوعًا مدمجًا للسلاسل النصية - فتلك المصفوفة تحمل 'A' و'd' و'a' وبايتًا رابعًا خفيًا هو '\0'، يؤشّر على النهاية. وإن أردت مساحة لتغيير النص لاحقًا، فامنح المصفوفة حجمًا صريحًا: char name[32] = "Ada";.
ما المنهي الصفري في لغة C؟
البايت '\0' (قيمته صفر) الذي يؤشّر على موضع انتهاء السلسلة. وكل دالة معيارية - printf("%s") وstrlen وstrcpy - تقرأ إلى الأمام حتى تجد ذلك البايت. ومصفوفة char بلا منهٍ ليست سلسلة نصية، وتمريرها إلى تلك الدوال يقرأ ما بعد نهاية ذاكرتك.
لماذا لا أستطيع إسناد سلسلة نصية بـ = في لغة C؟
لأن المصفوفة ليست قيمة يمكنك نسخها بـ =. فـ char a[10]; a = "hi"; لن تُترجَم. انسخ المحارف بدلًا من ذلك بـ strcpy(a, "hi") من <string.h>، أو هيّئ عند التعريف - char a[10] = "hi"; - وهو الموضع الوحيد الذي يعمل فيه الاختصار.
ما الفرق بين char * وchar [] في لغة C؟
char s[] = "hi" تُنشئ مصفوفة قابلة للتعديل تحمل نسخة خاصة من النص. وchar *p = "hi" تُنشئ مؤشّرًا إلى ثابت نصّي يعيش في ذاكرة للقراءة فقط - فقراءته مقبولة، والكتابة عبره تنهار. استخدم المصفوفة حين تنوي التعديل، والمؤشّر حين تقرأ فقط.