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

التعامل مع المحارف والسلاسل النصية في لغة سي C


Naser Dakhel

تُستخدم لغة سي على نطاق واسع في تطبيقات المعالجة والتعامل بالمحارف characters والسلاسل النصية strings، وهذا الأمر غريب بعض الشيء لأن اللغة لا تحتوي على مزايا موجهة لهذا الغرض بالتحديد،؛ وإذا كنت معتادًا على لغات البرمجة التي تحتوي على مزايا موجهة للتعامل مع المحارف والسلاسل النصية، فستجد التعامل مع لغة سي بهذا الخصوص مضجرًا على أقل تقدير.

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

التعامل مع المحارف

يجري التعامل مع محارف السلسلة النصية في لغة سي عن طريق التصريح عن مصفوفات، أو حجزهم ديناميكيًا، والتعامل مع المحارف وتحريكها "يدويًّا". إليك مثالًا عن برنامج يقرأ نصًّا سطرًا بسطر من دخل البرنامج القياسي، ويتوقف البرنامج عن القراءة إذا كان السطر مكوّنًا من السلسلة النصية "stop"، ويُطبع طول السطر فيما عدا ذلك. يعتمد البرنامج على تقنية تُستخدم في معظم برامج سي ألا وهي: يقرأ البرنامج المحارف ويحوّلها إلى مصفوفة ويحدّد نهاية المصفوفة بمحرفٍ إضافي له القيمة صفر؛ كما يستخدم هذا المثال دالة strcmp للمقارنة بين سلسلتين نصيتين من خلال تضمين المكتبة string.h.

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

#define LINELNG 100     /* الطول الأعظمي لسطر الدخل الواحد */

main(){
      char in_line[LINELNG];
      char *cp;
      int c;

      cp = in_line;
      while((c = getc(stdin)) != EOF){
              if(cp == &in_line[LINELNG-1] || c == '\n'){
                      /*إدخال ما يدل على نهاية السطر*/
                      *cp = 0;
                      if(strcmp(in_line, "stop") == 0 )
                              exit(EXIT_SUCCESS);
                      else
                              printf("line was %d characters long\n",
                                      (int)(cp-in_line));
                      cp = in_line;
              }
              else
                      *cp++ = c;
      }
      exit(EXIT_SUCCESS);
}

[مثال 1]

يوضح لنا هذا المثال مزيدًا من مزايا وطرق لغة سي المُستخدمة في برامجها، وأهمها هو طريقة تمثيل السلاسل النصية ومعالجتها.

إليك تطبيقًا عمليًّا عن الدالة strcmp التي تقارن بين سلسلتين نصيتين وتعيد القيمة صفر إن تساوت قيمتهما، وللدالة هذه في الحقيقة تطبيقات أكثر ولكننا سنتجاهل المعلومات التي قد تزيد من تعقيد شرحنا. لاحظ استخدام الكلمة المفتاحية const في التصريح عن الوسطاء، والذي يوضِّح أن الدالة لن تعدل على محتويات السلسلة النصية بل ستفحص قيم محتوياتها فقط، ونلاحظ استخدام هذه الطريقة في التعريف عن العديد من دوال المكتبة القياسية.

/*
* برنامج يختبر مساواة سلسلتين نصيتين
* يُعيد القيمة "خطأ" إذا تساوت السلسلتين
*/
int
str_eq(const char *s1, const char *s2){
      while(*s1 == *s2){
              /*
               * إعادة 0 عند نهاية السلسلة النصية
               */
              if(*s1 == 0)
                      return(0);
              s1++; s2++;
      }
      /* عُثر على فرق بين السلسلتين */
      return(1);
}

[مثال 2]

السلاسل النصية

يعرف كل مبرمجٍ للغة سي معنى السلسلة النصية، فهي مصفوفةٌ من متغيرات من نوع "char"، ويكون المحرف الأخير لهذه السلسلة النصية متبوعًا بمحرف فراغ null. ربما تصرخ الآن وتقول "ولكنني اعتقدت أن السلسلة النصية هي نصٌ محتوًى داخل إشارتي تنصيص!" أنت محقّ، إذ تُعد السلسلة التالية في لغة سي مصفوفةً من المحارف:

"a string"

وهذا الشيء الوحيد الذي يمكنك التصريح عنه لحظة استخدامه، أي دون التصريح عن مصفوفة المحارف وتحديد حجمها.

احذر: كانت السلاسل النصية في لغة سي القديمة تُخزّن مثل أي سلسلة محارف اعتيادية، وكانت قابلةً للتعديل. إلا أن المعيار ينص على أن محاولة التعديل على سلسلة نصية سيتسبب بسلوك غير محدد على الرغم من كون السلاسل النصية مصفوفةً من نوع "char" وليس "const char".

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

char secret[9];
secret[0] = 'a';
secret[1] = ' ';
secret[2] = 's';
secret[3] = 't';
secret[4] = 'r';
secret[5] = 'i';
secret[6] = 'n';
secret[7] = 'g';
secret[8] = 0;

وهي مصفوفةٌ من المحارف متبوعةٌ بقيمة صفر، وتحتوي جميع قيم المحارف بداخلها، لكنها عديمة الاسم إذا صُرِّح عنها باستخدام طريقة السلسلة النصية المحاطة بعلامتي تنصيص، فكيف نستطيع استخدامها؟

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

"a string"

النوع مؤشر طبعًا، إذ أن السلسلة النصية المكتوبة بالشكل السابق تكافئ مؤشرًا إلى أول عناصر المصفوفة الخفية عديمة الاسم، وهي مصفوفةٌ من نوع char، فالمؤشر من نوع "مؤشر إلى char"، ويوضح الشكل 1 هذه الحالة.

EffectString.png

شكل 1 أثر استخدام السلسلة النصية

للبرهان على السابق، ألقِ نظرةً على البرنامج التالي:

#include <stdio.h>
#include <stdlib.h>
main(){
      int i;
      char *cp;

      cp = "a string";
      while(*cp != 0){
              putchar(*cp);
              cp++;
      }
      putchar('\n');

      for(i = 0; i < 8; i++)
              putchar("a string"[i]);
      putchar('\n');
      exit(EXIT_SUCCESS);
}

[مثال 3]

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

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

if(*s1 == 0):

وإذا وصل المؤشر للنهاية فإنها تُعيد 0 للدلالة على أن السلسلتين متساويتين، يمكن إجراء الاختبار ذاته باستخدام المؤشر s2* دون أي فارق؛ أما في حالة الفرق بين المحرفين تُعاد القيمة 1 للدلالة على فشل المساواة.

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

المؤشرات وعامل الزيادة

ذكرنا سابقًا التعبير التالي، وقلنا أنّنا ستعيد النظر فيه لاحقًا:

(*p)++;

حان الوقت الآن للكلام عن ذلك؛ إذ تُستخدم المؤشرات بكثرة مع المصفوفات والتمرير بينها، لذلك من الطبيعي استخدام العاملين -- و ++ معها. يوضح المثال التالي إسناد القيمة صفر إلى مصفوفة باستخدام المؤشر وعامل الزيادة:

#define ARLEN 10

int ar[ARLEN], *ip;

ip = ar;
while(ip < &ar[ARLEN])
      *(ip++) = 0;

[مثال 4]

يُضبط المؤشر ip ليبدأ من بداية المصفوفة، وعلى الرغم من إشارة المؤشر إلى داخل المصفوفة إلا أن القيمة التي يشير إليها تساوي الصفر، وعند وصولنا للحركة التكرارية ينجز عامل الزيادة ما هو متوقع ويُنقل المؤشر للعنصر الذي يليه داخل المصفوفة، وبالتالي استخدام العامل ++ عامل إلحاق postfix مفيدٌ في هذه الحالة.

معظم الأشياء التي ناقشناها شائعة الوجود، وستجدها في معظم البرامج (استخدام عامل الزيادة والمؤشرات بالشكل الموضح في المثال السابق) ليس مرةً واحدةً أو مرتين بل تقريبًا كل عدّة أسطر ضمن الشيفرة البرمجية، وستعتقد أنك تراها بصورةٍ أكثر إذا كنت تجد استخدامها صعب الفهم. لكن ما هي التشكيلات التي يمكننا الحصول عليها؟ بالنظر إلى أن * تعني التأشير و ++ تعني الزيادة و -- تعني النقصان، كما أنه لدينا خياري وضع العاملين السابقين مثل عامل إلحاق postfix أو عامل إسباق prefix، نحصل على الاحتمالات التالية (بغض النظر عن عامل الزيادة أو النقصان) مع التركيز على موضع الأقواس:

(p*)++ زيادة سابقة للشيء الذي يشير إليه المؤشر
++(p*) زيادة لاحقة للشيء الذي يشير إليه المؤشر
(++p)* زيادة لاحقة على المؤشر
*(p++)* زيادة سابقة على المؤشر

[جدول 1 معاني المؤشرات]

اقرأ الجدول السابق بحرص وتأكد أنك تفهم جميع التركيبات التي ذُكرت.

يمكن فهم محتوى الجدول السابق بعد تفكير بسيط، ولكن هل يمكنك توقع ما الذي سيحدث عند إزالة الأقواس بالنظر إلى أن الأسبقية للعوامل الثلاث * و -- و ++ متساوية؟ تتوقع حدوث أخطاء كارثية، أليس كذلك؟ يوضّح الجدول 1 أن هناك حالةٌ واحدةٌ يجب أن تحافظ فيها على الأقواس.

مع أقواس دون أقواس إن أمكن
(p*)++ p*++
++(p*) ++(p*)
(++p)* ++p*
(p++)* p++*

[جدول 2 المزيد من معاني المؤشرات]

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

المؤشرات عديمة النوع

من المهم في بعض الأحيان تحويل نوعٍ من المؤشرات إلى نوع آخر بمساعدة التحويل بين الأنواع مثل التعبير التالي:

(type *) expression

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

هناك بعض الحالات التي ستحتاج فيها لاستخدام مؤشر "معمّم generic"، وأبرز مثال على ذلك هو تطبيق لدالة المكتبة القياسية malloc التي تُستخدم لحجز المساحة على الذاكرة للكائن الذي لم يصرّح عنه بعد، ويجري تزويد الحجم المراد حجزه عن طريق تزويد مثل وسيطٍ سواءٌ كان الكائن متغيرًا من نوع float أو مصفوفة من نوع int أو أي شيء آخر. تعيد الدالة مؤشرًا إلى عنوان التخزين المحجوز التي تختاره بطريقتها الخاصة (والتي لن نتطرق إليها) من مجموعةٍ من عناوين الذاكرة الفارغة، ومن ثم يُحوَّل المؤشر إلى النوع المناسب. على سبيل المثال، تحتاج القيمة من نوع float إلى 4 بايتات من الذاكرة، وبالتالي نكتب ما يلي لحجز مساحة للقيمة:

float *fp;

fp = (float *)malloc(4);

تعثر الدالة malloc على 4 بايتات من الذاكرة الفارغة، ويُحوَّل عنوان الذاكرة إلى مؤشر من نوع "مؤشر إلى float"، ثم تُسند القيمة إلى المؤشر (fp في حالة مثالنا السابق).

لكن ما هو نوع المؤشر الذي ستُسند قيمة malloc إليه؟ نحن بحاجة نوع يمكن أن يحتوي جميع أنواع المؤشرات فنحن لا نعلم نوع المؤشر الذي ستعيده الدالة malloc.

الحل هو باستخدام نوع المؤشر * void الذي تكلمنا عنه سابقًا، إليك المثال السابق مع إضافة تصريح للدالة malloc:

void *malloc();
float *fp;

fp = (float *)malloc(4);

لا حاجة لاستخدام تحويل الأنواع على القيمة المُعادة من الدالة malloc حسب قوانين الإسناد للمؤشرات، ولكن استُخدم تحويل الأنواع لممارسة الأمر لا أكثر.

لا بد من طريقة لمعرفة قيمة وسيط malloc الدقيقة في نهاية المطاف، ولكن القيمة ستكون مختلفةً على أجهزة بمعماريات مختلفة، لذا لا يمكنك الاكتفاء باستخدام القيمة الثابتة 4 فقط، بل يجب علينا استخدام عامل sizeof.

ترجمة -وبتصرف- لقسم من الفصل Arrays and Pointers من كتاب 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.


×
×
  • أضف...