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

عامل sizeof وحجز مساحات التخزين في لغة سي C


Naser Dakhel

يُعيد العامل "sizeof" حجم المُعامل operator بالبايتات، وتعتمد نتيجة العامل "sizeof" بكونها عددًا صحيحًا عديم الإشارة "unsigned int" أو عددًا كبيرًا عديم الإشارة "unsigned long" على التطبيق implementation، وهذا هو السبب في تفادينا لأي مشكلات في المثال السابق (المقال السابق) عند التصريح عن دالة malloc على الرغم من عدم تزويد التصريح بأي تفاصيل عن معاملاتها؛ إذ يجب استخدام ملف الترويسة stdlib.h عوضًا عن ذلك عادةً للتصريح عن malloc على النحو الصحيح. إليك المثال ذاته ولكن بتركيز على جعله قابلًا للتنقل portable عبر مختلف الأجهزة:

#include <stdlib.h>     /* malloc() يتضمن ملف الترويسة تصريحًا عن */
float *fp;

fp = (float *)malloc(sizeof(float));

يجب أن يُكتب معامل sizeof داخل قوسين إذا كان فقط اسمًا لنوع بيانات (وهي الحالة في مثالنا السابق)، بينما يمكنك التخلي عن القوسين إذا كنت تستخدم اسم كائن بيانات عوضًا عن ذلك، ولكن هذه الحالة نادرة الحدوث.

#include <stdlib.h>

int *ip, ar[100];
ip = (int *)malloc(sizeof ar);

لدينا في المثال السابق مصفوفة باسم ar مكونةٌ من 100 عنصر من نوع عدد صحيح int، ويشير ip إلى مساحة التخزين الخاصة بهذه المصفوفة (مساحةٌ لمئة قيمة من نوع int) بعد استدعاء malloc (بفرض أن الاستدعاء كان ناجحًا).

تعدّ char (محرف وهي اختصارٌ إلى character) وحدة القياس الأساسية للتخزين في لغة سي، وتساوي بايتًا واحدًا، جرّب نتيجة التعليمة الآتية:

sizeof(char)

وبناءً على ذلك، يمكنك حجز مساحة لعشرة قيم من نوع char على النحو التالي:

malloc(10)

ولحجز مساحة لمصفوفة بحجم عشرة قيم من نوع int، نكتب:

malloc(sizeof(int[10]))

تُعيد الدالة malloc مؤشرًا إلى الفراغ null pointer في حال لم تتوفر المساحة الكافية للإشارة إلى خطأ ما. يحتوي ملف الترويسة stdio.h ثابتًا معرّفًا باسم NULL، والذي يُستخدم عادةً للتحقق من القيمة المُعادة من الدالة malloc ودوال أخرى من المكتبة القياسية، وتُعد القيمة 0 أو ‎(void *)0 مساويةً لهذا الثابت ويمكن استخدامها.

إليك المثال التالي لتوضيح استخدام الدالة malloc، إذ يقرأ البرنامج في المثال سلاسلًا نصيةً بعدد MAXSTRING من الدخل، ثمّ يرتب السلاسل النصية أبجديًّا باستخدام الدالة strcmp. يُشار إلى نهاية السلسلة النصية بمحرف الهروب escape character التالي n\، وتُرتَّب السلاسل باستخدام مصفوفة من المؤشرات تُشير إلى السلسلة النصية وتبديل مواضع المؤشرات حتى الوصول إلى الترتيب الصحيح، مما يجنُّبنا عناء نسخ السلاسل النصية ويحسّن من سرعة تنفيذ البرنامج ويحد من هدر الموارد إلى حدٍّ ما.

استخدمنا في الإصدار الأول من المثال مصفوفةً ثابتة الحجم، ثم استخدمنا في الإصدار الثاني حجز المساحة باستخدام malloc لكل سلسلة نصية عند وقت التشغيل run-time، بينما بقيت مصفوفة المؤشرات -لسوء الحظ- ثابتة الحجم، إلا أنه يمكننا تطبيق حلّ أفضل باستخدام قائمة مترابطة Linked list، أو أي هيكل بيانات مشابه لتخزين المؤشرات دون الحاجة لاستخدام المصفوفات ثابتة الحجم إطلاقًا، ولكننا لم نتكلم عن هياكل البيانات بعد.

إليك ما يبدو عليه هيكل برنامجنا:

while(number of strings read < MAXSTRING
      && input still remains){

              read next string;
}
sort array of pointers;
print array of pointers;
exit;

سنستخدم بعض الدوال في برنامجنا أيضًا:

char *next_string(char *destination)

تقرأ الدالة السابقة سطرًا من المحارف بحيث ينتهي السطر بالمحرف n\ من دخل البرنامج، وتُسند المحارف البالغ عددها MAXLEN-1 إلى المصفوفة المُشار إليها بالمصفوفة الهدف destination، إذ يمثّل MAXLEN قيمةً ثابتةً لطول السلسلة النصية العظمى.

إذا كان المحرف الأول المقروء هو EOF (أي نهاية الملف)، أعِد مؤشرًا إلى الفراغ، وفيما عدا ذلك أعِد عنوان بداية السلسلة النصية (الهدف destination)، بحيث تحتوي السلسلة النصية الهدف دائمًا على المحرف n\، الذي يشير إلى نهاية السلسلة.

void sort_arr(const char *p_array[])

تمثل المصفوفة p_array[]‎ مصفوفة المؤشرات التي تشير للمحارف، ويمكن أن تكون المصفوفة كبيرة الحجم ولكن يُشار إلى نهايتها بأول عنصر يحتوي على مؤشر فراغ null pointer.

ترتّب الدالة sort_arr المؤشرات بحيث تُشير إلى السلاسل النصية المرتبة أبجديًا عند اجتياز مصفوفة المؤشرات بناءً على دليل index المؤشر.

void print_arr(const char *p_array[])

تُشابه دالة print_arr الدالة sort_arr ولكنها تطبع السلاسل النصية حسب ترتيبها الأبجدي.

تذكّر أنه يجري تحويل اسم المصفوفة إلى عنوانها وعنصرها الأول في أي تعبير يحتوي على اسمها، ومن شأن ذلك أن يساعدك في فهم الأمثلة على نحوٍ أفضل؛ والأمر مماثلٌ بالنسبة لمصفوفة ثنائية البعد، مثل مصفوفة strings في المثال التالي، فنوع التعبير strings[1][2]‎ هو char، ولكن للعنصر strings[1]‎ نوع "مصفوفة من char"، ولذلك يُحوَّل اسم المصفوفة إلى عنوان العنصر الأول ونحصل على ‎&strings[1][0]‎.

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

#define MAXSTRING       50      /* العدد الأعظمي للسلاسل النصية */
#define MAXLEN          80      /* الطول الأعظمي لكل سلسلة نصية */

void print_arr(const char *p_array[]);
void sort_arr(const char *p_array[]);
char *next_string(char *destination);

main(){
      /* نصرح عن المصفوفة مع إضافة عنصر فارغ في نهايتها */
      char *p_array[MAXSTRING+1];

      /* مصفوفة تخزين السلاسل النصية */
      char strings[MAXSTRING][MAXLEN];

      /* عدد السلاسل النصية المقروءة */
      int nstrings;

      nstrings = 0;
      while(nstrings < MAXSTRING &&
              next_string(strings[nstrings]) != 0){

              p_array[nstrings] = strings[nstrings];
              nstrings++;
      }
      /* إعدام قيمة المصفوفة */
      p_array[nstrings] = 0;

      sort_arr(p_array);
      print_arr(p_array);
      exit(EXIT_SUCCESS);
}

void print_arr(const char *p_array[]){
      int index;
      for(index = 0; p_array[index] != 0; index++)
              printf("%s\n", p_array[index]);
}


void sort_arr(const char *p_array[]){
      int comp_val, low_index, hi_index;
      const char *tmp;

      for(low_index = 0;
              p_array[low_index] != 0 &&
                              p_array[low_index+1] != 0;
                      low_index++){

              for(hi_index = low_index+1;
                      p_array[hi_index] != 0;
                              hi_index++){

                      comp_val=strcmp(p_array[hi_index],
                              p_array[low_index]);
                      if(comp_val >= 0)
                              continue;
                      /* التبديل بين السلسلتين النصيتين */
                      tmp = p_array[hi_index];
                      p_array[hi_index] = p_array[low_index];
                      p_array[low_index] = tmp;
              }
      }
}



char *next_string(char *destination){
      char *cp;
      int c;

      cp = destination;
      while((c = getchar()) != '\n' && c != EOF){
              if(cp-destination < MAXLEN-1)
                      *cp++ = c;
      }
      *cp = 0;
      if(c == EOF && cp == destination)
              return(0);
      return(destination);
}

[مثال 1]

إعادة الدالة next_string لمؤشر ليس من قبيل المصادفة، إذ أصبح بإمكاننا الآن الاستغناء عن استخدام مصفوفة السلاسل النصية واستخدام next_string لحجز مساحة التخزين الموافقة لها.

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

#define MAXSTRING       50      /* العدد الأعظمي للسلاسل النصية */
#define MAXLEN          80      /* الطول الأعظمي لكل سلسلة نصية  */

void print_arr(const char *p_array[]);
void sort_arr(const char *p_array[]);
char *next_string(void);

main(){
      char *p_array[MAXSTRING+1];
      int nstrings;

      nstrings = 0;
      while(nstrings < MAXSTRING &&
              (p_array[nstrings] = next_string()) != 0){

              nstrings++;
      }
      /* إعدام قيمة المصفوفة */
      p_array[nstrings] = 0;

      sort_arr(p_array);
      print_arr(p_array);
      exit(EXIT_SUCCESS);
}

void print_arr(const char *p_array[]){
      int index;
      for(index = 0; p_array[index] != 0; index++)
              printf("%s\n", p_array[index]);
}


void sort_arr(const char *p_array[]){
      int comp_val, low_index, hi_index;
      const char *tmp;

      for(low_index = 0;
              p_array[low_index] != 0 &&
                      p_array[low_index+1] != 0;
                      low_index++){

              for(hi_index = low_index+1;
                      p_array[hi_index] != 0;
                              hi_index++){

                      comp_val=strcmp(p_array[hi_index],
                              p_array[low_index]);
                      if(comp_val >= 0)
                              continue;
                      /* التبديل بين السلسلتين النصيتين */
                      tmp = p_array[hi_index];
                      p_array[hi_index] = p_array[low_index];
                      p_array[low_index] = tmp;
              }
      }
}

char *next_string(void){
      char *cp, *destination;
      int c;

      destination = (char *)malloc(MAXLEN);
      if(destination != 0){
              cp = destination;
              while((c = getchar()) != '\n' && c != EOF){
                      if(cp-destination < MAXLEN-1)
                              *cp++ = c;
              }
              *cp = 0;
              if(c == EOF && cp == destination)
                      return(0);
      }
      return(destination);
}

[مثال 2]

وأخيرًا إليك المثال كاملًا مع استخدام مصفوفة p_array للدالة malloc، ولاحظ إعادة كتابة معظم أدلة المصفوفة لتستخدم ترميز المؤشرات. إذا كنت تشعر بالإرهاق من جميع المعلومات التي قرأتها فتجاوز المثال التالي، فهو صعبٌ بعض الشيء.

شرح المثال: تعني char **p مؤشرًا يشير إلى المؤشر الذي يشير إلى محرف، ويجد معظم مبرمجو لغة سي هذه الطريقة في استخدام المؤشرات صعبة الفهم.

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

#define MAXSTRING       50      /* العدد الأعظمي للسلاسل النصية */
#define MAXLEN          80      /*الطول الأعظمي لكل سلسلة نصية  */

void print_arr(const char **p_array);
void sort_arr(const char **p_array);
char *next_string(void);

main(){
      char **p_array;
      int nstrings;   /* عدد السلاسل النصية المقروءة */

      p_array = (char **)malloc(
                      sizeof(char *[MAXSTRING+1]));
      if(p_array == 0){
              printf("No memory\n");
              exit(EXIT_FAILURE);
      }

      nstrings = 0;
      while(nstrings < MAXSTRING &&
              (p_array[nstrings] = next_string()) != 0){

              nstrings++;
      }
      /* إعدام قيمة المصفوفة */
      p_array[nstrings] = 0;

      sort_arr(p_array);
      print_arr(p_array);
      exit(EXIT_SUCCESS);
}

void print_arr(const char **p_array){
      while(*p_array)
              printf("%s\n", *p_array++);
}


void sort_arr(const char **p_array){
      const char **lo_p, **hi_p, *tmp;

      for(lo_p = p_array;
              *lo_p != 0 && *(lo_p+1) != 0;
                                      lo_p++){
              for(hi_p = lo_p+1; *hi_p != 0; hi_p++){

                      if(strcmp(*hi_p, *lo_p) >= 0)
                              continue;
                      /* التبديل بين السلسلتين النصيتين */
                      tmp = *hi_p;
                      *hi_p = *lo_p;
                      *lo_p = tmp;
              }
      }
}



char *next_string(void){
      char *cp, *destination;
      int c;

      destination = (char *)malloc(MAXLEN);
      if(destination != 0){
              cp = destination;
              while((c = getchar()) != '\n' && c != EOF){
                      if(cp-destination < MAXLEN-1)
                              *cp++ = c;
              }
              *cp = 0;
              if(c == EOF && cp == destination)
                      return(0);
      }
      return(destination);
}

[مثال 3]

سنستعرض مثالًا آخر لتوضيح استخدام دالة malloc وإمكاناتها في التعامل مع السلاسل النصية الطويلة؛ إذ يقرأ المثال السلاسل النصية من الدخل ويبحث عن محرف سطر جديد لتحديد نهاية السلسلة النصية (أي n\)، ثم يطبع السلسلة النصية إلى الخرج، ويتوقف البرنامج عن العمل عندما يصادف محرف نهاية الملف EOF. تُسنَد المحارف إلى مصفوفة، ويُدلّ على نهاية المصفوفة -كما هو معتاد- بالقيمة صفر، مع ملاحظة أن محرف السطر الجديد لا يُخزَّن بالمصفوفة بل يُستخدم فقط لتحديد سطر الدخل الواجب طباعته للخرج. لا يعلم البرنامج طول السلسلة النصية تحديدًا، ولذلك يبدأ بفحص كل عشرة محارف وحجز المساحة الخاصة بهم (الثابت GROW_BY).

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

تُستخدم الدالة free لتحرير المساحة القديمة المحجوزة من malloc مسبقًا، إذ يجب عليك تحرير المساحة غير المُستخدمة بعد الآن دوريًا قبل أن تتراكم، واستخدام free يحرّر المساحة ويسمح بإعادة استخدامها لاحقًا.

يستخدم البرنامج الدالة fprintf لعرض أي أخطاء، وهي دالةٌ مشابهة للدالة printf التي اعتدنا على رؤيتها، والفرق الوحيد بينهما هو أن الدالة fprintf تأخذ وسيطًا إضافيًّا يدل على وسيط الخرج الذي سيُطبع إليه، وهناك ثابتان لهذا الغرض معرّفان في ملف الترويسة stdio.h؛ إذ أن استخدام الثابت الأول stdout يعني استخدام خرج البرنامج القياسي، بينما يشير استخدام الثابت الثاني stderr إلى مجرى أخطاء البرنامج القياسي standard error stream، وقد يكون وسيطا الخرج متماثلين في بعض الأنظمة إلا أن بعض الأنظمة الأخرى تفصل بين الاثنين.

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

#define GROW_BY 10      /* يزداد حجم السلسلة النصية كل مرة بمقدار 10 */

main(){
      char *str_p, *next_p, *tmp_p;
      int ch, need, chars_read;

      if(GROW_BY < 2){
              fprintf(stderr,
                      "Growth constant too small\n");
              exit(EXIT_FAILURE);
      }

      str_p = (char *)malloc(GROW_BY);
      if(str_p == NULL){
              fprintf(stderr,"No initial store\n");
              exit(EXIT_FAILURE);
      }

      next_p = str_p;
      chars_read = 0;
      while((ch = getchar()) != EOF){
/* (*) */
              if(ch == '\n'){
                      /* الإشارة إلى نهاية السطر */
                      *next_p = 0;
                      printf("%s\n", str_p);
                      free(str_p);
                      chars_read = 0;
                      str_p = (char *)malloc(GROW_BY);
                      if(str_p == NULL){
                              fprintf(stderr,"No initial store\n");
                              exit(EXIT_FAILURE);
                      }
                      next_p = str_p;
                      continue;
              }
              /*
               * التحقق من وصولنا إلى نهاية المساحة المحجوزة
               */
              if(chars_read == GROW_BY-1){
                      *next_p = 0;    /* للدلالة على نهاية السلسلة النصية */
                      /* نستخدم الطرح بين المؤشرات لإيجاد طول السلسلة النصية الحالية*/
                      need = next_p - str_p +1;
                      tmp_p = (char *)malloc(need+GROW_BY);
                      if(tmp_p == NULL){
                              fprintf(stderr,"No more store\n");
                              exit(EXIT_FAILURE);
                      }
                      /*
                      ننسخ السلسلة النصية باستخدام دالة المكتبة
                       */
                      strcpy(tmp_p, str_p);
                      free(str_p);
                      str_p = tmp_p;
                      /*
                       * next_p إعادة ضبط
                       */
                      next_p = str_p + need-1;
                      chars_read = 0;
              }
              /*
               * إسناد المحرف إلى نهاية السلسلة النصية
               */
              *next_p++ = ch;
              chars_read++;
      }
      /*
       * عند وصولنا إلى نهاية الملف
       * هل توجد محارف غير مطبوعة؟
       */
      if(str_p - next_p){
              *next_p = 0;
              fprintf(stderr,"Incomplete last line\n");
              printf("%s\n", str_p);
      }
      exit(EXIT_SUCCESS);
}

[مثال 4]

(*) تُعاد الحلقة في الموضع المذكور عند كل سطر، وهناك مساحةٌ للعنصر صفر في نهاية السلسلة النصية دائمًا، لأننا نتحقق من أصغر من 2 وهو ما تحققنا منه سابقًا GROW_BY ذلك في الشرط التالي إلا في حال كان.

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

نستطيع استخدام القوائم المترابطة لطريقة أكثر تعقيدًا، مع استخدام الهياكل Structures التي سنتكلم عنها لاحقًا، إلا أن هذه الطريقة تأتي أيضًا ببعض المشكلات لأن دوال المكتبة القياسية لن تعمل عند استخدام طريقة مغايرة لتخزين السلاسل النصية.

ما الأشياء التي لا يستطيع العامل sizeof فعلها؟

يرتكب المبتدئون غالبًا الخطأ التالي عند استخدام العامل sizeof:

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

const char arr[] = "hello";
const char *cp = arr;
main(){

      printf("Size of arr %lu\n", (unsigned long)
                      sizeof(arr));
      printf("Size of *cp %lu\n", (unsigned long)
                      sizeof(*cp));
      exit(EXIT_SUCCESS);
}

[مثال 5]

لن تكون الأرقام ذاتها عند الطباعة، إذ سيعرف أولًا حجم arr بكونها 6 بصورةٍ صحيحة (خمسة محارف متبوعةٍ بمحرف الفراغ null)، بينما ستطبع التعليمة الثانية -على جميع الأنظمة- القيمة 1، لأن المؤشر cp* من نوع const char ذو الحجم 1 بايت، بينما arr مختلفةٌ فهي مصفوفةٌ من نوع const char. تسبب هذه المشكلة مصدرًا للحيرة، إذ أن هذه الحالة الوحيدة التي لا يجري فيها تحويل المصفوفة إلى مؤشر أولًا، فمن المستحيل استخدام sizeof لإيجاد طول مصفوفة باستخدام مؤشر يشير إليها، ويجب عليك استخدام اسم المصفوفة حصرًا.

نوع قيمة sizeof

لعلك تتساءل الآن عن نتيجة التالي:

sizeof ( sizeof (anything legal) )

فما هو نوع نتيجة عامل sizeof؟ الإجابة على هذا السؤال معرّفة بحسب التطبيق، وقد تكون unsigned long أو unsigned int بحسب تطبيقك، إلا أن هناك شيئان يمكن فعلهما للتأكد من أنك تستخدم القيمة بصورة صحيحة، وهما:

  • يمكنك استخدام تحويل الأنواع cast وتحويل القيمة إلى unsigned long قسريًا (كما فعلنا في المثال السابق).
  • يمكنك استخدام النوع المُعرّف size_t الموجود في ملف الترويسة stddef.h كما يوضح المثال التالي:
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

main(){
      size_t sz;
      sz = sizeof(sz);
      printf("size of sizeof is %lu\n",
              (unsigned long)sz);
      exit(EXIT_SUCCESS);
}

[مثال 6]

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


×
×
  • أضف...