ahmed.o.mohamed

[C] المؤشرات, المصفوفات الحرفية و التراكيب

1 مشاركة في هذا الموضوع

فهرس الأسئلة :


  1. المؤشرات (Pointers) :
    • ما هو المؤشر ؟
    • متى تُستخدم المؤشرات ؟
    • ما معنى NULL ؟
    • مالفرق بين الــ Null Pointer و Uninitialized Pointer ؟
    • ما معنى stack, heap, buffer ؟
    • مالفرق بين تحرير و تصفير الذاكرة ؟
    • إذا كان المؤشر يشير إلى ذاكرة عنوانها صفر ألا يعني هذا أن قيمة الخانة المُؤشر عليها تساوي صفر أيضا ؟
    • ماهو المؤشر الثابت ؟
    • ماهو المؤشر إلى ثابت ؟
    • ماهو المؤشر الثابت إلى ثابت ؟
    • ما هو المؤشر العائم (Void Pointer) و فيم يُستخدم ؟
    • ماهو الــ Pointer to Pointer ؟
    • مالفرق بين char a[] and char * a ؟
    • باعتبار أن t مصفوفة, مالفرق بين الكتابات التالية t, &t, &(t[0]) ؟
    • باعتبار أن t مصفوفة, ما معنى الكتابة التالية *(t + 3) ؟
    • ما معنى هذه الكتابة *p++ ؟
    • ما هو دور المعامل [] ؟
    • ماذا تعني الكتابة التالية int (*p)[4] ؟
    • ما هي فائدة الدالة malloc ؟
    • ما هي فائدة الدالة calloc ؟
    • ما الفائدة من استخدام calloc إذا كانت malloc تفي بالغرض !؟
    • ما هي فائدة الدالة realloc ؟
    • كيف أنشئ مصفوفة ديناميكية ثنائية البعد ؟
    • مالفرق بين الدالة malloc و المعامل new ؟
    • p و q يُشيران إلى متغيرين يحملان نفس القيمة و مع ذلك فإن p==q تُعيد false دائما !, مالسبب ؟
    • كيف نعرف نوع المتغير الذي يُشير إليه مؤشر من النوع void ؟

[*]المصفوفات الحرفية (Strings) :

  • ما هو NUL ؟
  • لماذا يحتاج المترجم إلى وضع NUL في نهاية كل مصفوفة حرفية ؟
  • ما معنى الكتابة التالية char * p = "Bonjour"; ؟
  • كيف نُحول حروف سلسلة حرفية إلى حروف صغيرة/كبيرة ؟
  • كيف نقوم بتحويل عدد إلى سلسلة حرفية ؟
  • كيف نقوم بتحويل سلسلة حرفية إلى عدد ؟
  • كيف ندمج السلاسل الحرفية ؟
  • كيف نقارن السلاسل الحرفية ؟
  • لماذا لا أستطيع مقارنة السلاسل الحرفية المقروءة بالدالة fgets ؟
  • كيف ننشئ مصفوفة من السلاسل الحرفية ؟
  • لماذا يجب كتابة الرمز \ هكذا \\ ؟
  • كيف نقوم بنسخ مصفوفة ؟

[*]التراكيب (Structures) :

  • كيف أنشئ اسما مستعارا لــ Structure ؟
  • مالفرق بين structure و union ؟
  • لماذا حجم البنية لا يُساوي بالضرورة مجموع أحجام العناصر ؟
  • كيف نقوم بنسخ structure ؟
  • كيف نقارن بين بـُـنْـيـَـتـَـيـْـن ؟
  • ماذا تعني الكتابة التالية unsigned int i : <n>; ؟
  • كيف نستخدم مؤشر يُشير إلى بنية ؟
  • مالفرق بين الكتابتين sizeof(struct data) and sizeof(struct data *) ؟
  • كيف يتم الإعلان عن بنية تشير إلى نفسها ؟
  • كيف نُحسن تهيئة المتغيرات ؟

1. المؤشرات (Pointers) :

ما هو المؤشر ؟

هناك 3 أشياء أساسية لفهم تعريف المؤشر :

  • كل متغير يملك عنوان في الذاكرة, من خلال هذا العنوان يُمكن للمعالج أن يصل إلى قيمة المتغير و يُجري عليها بعض العمليات (القراءة, الكتابة, إلخ).
  • المؤشر هو متغير يحوي عنوان في الذاكرة (عنوان متغير, عنوان دالة, عنوان بنية, إلخ).
  • هناك خطأ شائع عند البعض و هو استخدام كلمة "مؤشر" من أجل وصف "عنوان".

متى تُستخدم المؤشرات ؟

من الصعب حصر جميع المجالات التي تُستخدم فيها المؤشرات ولكن إليك بعض الأمثلة:

  • عندما نستخدم التمرير بواسطة المرجع (Pass by reference).
  • عندما نريد تمرير كائن ذو حجم كبير, التمرير بواسطة المرجع سيكون أقل تكلفة من حيث الوقت و الذاكرة أيضا. تمرير نسخة من الكائن يتطلب نسخ محتويات الكائن من جديد.
  • عند الحاجة إلى حجز ديناميكي للذاكرة.

ما معنى NULL ؟

هذا الثابت عبارة عن ماكرو مُعرف في المكتبة stddef.h, يُستخدم عادة في الدوال كقيمة مُعادة للدلالة على حدوث خلل في عمل الدالة.

هناك أيضا المؤشر NULL الذي يملك العنوان 0x00000000 وفي العادة يكون هذا العنوان invalid memory address في أغلب نظم التشغيل لكن ليس دائما وليس في كل الحالات, يُستخدم الـ null pointer للدلالة على أن المؤشر فارغ أي لم يتم حجز ذاكرة له بعد.

مالفرق بين الــ Null Pointer و Uninitialized Pointer ؟

المؤشر الغير مُهيأ (Uninitialized Pointer) هو المؤشر الذي لم نحجز له عنوانا محددا في الذاكرة, ربما يتسبب مثل هذا النوع من المؤشرات في تعطيل برنامجك إذا لم تقم بتهيأته لأنه و بكل بساطة يُشير إلى عنوان غير متوقع في الذاكرة. انظر المثال :

char *p1 = malloc(sizeof(char)); /* (undefined) value of some place on the heap */
char *p2; /* wild (uninitialized) pointer */
*p1 = 'a'; /* This is OK, assuming malloc() has not returned NULL. */
*p2 = 'b'; /* This invokes undefined behavior */

ربما تشير p2 إلى أي مكان في الذاكرة، لذا فإن هذه الخطوة :

*p2 = ‘b’

قد تُفسد منطقة غير معروفة من الذاكرة و ربما تحتوي على بيانات حساسة.

أحد الفروق بين الــ Null Pointer و Uninitialized Pointer, يكمن في المقارنة .. حيث يمكننا أن نقارن الــ null pointer مع عنوان أي مؤشر آخر بينما لا يمكننا فعل ذلك مع المؤشرات الغير مُهيئة.

ما معنى stack, heap, buffer ؟

  1. الـ Stack: هو مكان بالذاكرة لابد من تحديد المساحة التي سيتم حجزها فيه وقت ترجمة البرنامج، هذا الجزء من الذاكرة يحتوى على المتغيرات التي تم تعريفها داخل الكود أيا كان نوع هذه المتغيرات (ماعدا المتغيرات العامة) و أيضا المصفوفات التي تم تحديد حجم لها بشكل مباشر أو غير مباشر، أيضا تحتوى هذه الذاكرة على شجرة استدعاءات الدوال و قيم المعاملات. هذه الذاكرة يتم تحريرها بمجرد العودة للدالة التي قامت بالاستدعاء أو بمجرد الانتهاء من تنفيذ الدالة الحالية.
  2. الـ Heap: هو مكان بالذاكرة يمكن تحديد مساحته وقت ترجمة البرنامج أو وقت تشغيل البرنامج و لا يتم الحجز الفعلي للذاكرة إلا باستدعاء الدوال التي تقوم بتنفيذ عملية الحجز و حيث أنك تقوم بعملية الحجز بشكل يدوى فإنك ملزم بتنفيذ عملية تحرير الذاكرة بشكل يدوى أيضا و إن لم تقم بتحرير الذاكرة قد يحدث Memory Leak مما قد يؤدى إلى إنهاء برنامجك بشكل غير طبيعي أو أمور أسوء.
  3. الـ Buffer: هي ذاكرة مؤقتة لتخزين البيانات أثناء انتقالها من طرف إلى آخر بحيث تختلف السرعة بين كلا الطرفين, باعتبار أن وصول البيانات إلى الطرف الثاني أسرع .. فإن دور الـ buffer يكمن في حفظ البيانات ريثما يكتمل وصولها إلى الطرف الأول لتكمل طريقها إلى الطرف الثاني و الذي غالبا ما يكون المعالج. على سبيل المثال : يتم استخدام الـ buffer عند كل عملية إدخال لأن الإنسان (الطرف الأول) بطبيعة حاله أبطأ من المعالج (الطرف الثاني).
    يمكن أن يكون مكان الـ Buffer في Stack او الــ Heap.

مالفرق بين تحرير و تصفير الذاكرة ؟؟

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

بالنسبة لمصطلح التصفير فيجب التفريق فيه بين شيئين :

  • ذاكرة عنوانها صفر = العنوان الذي يشير اليه المؤشر يساوي صفر.
  • ذاكرة مصفرة = البيانات التي تحتويها قيمها أصفار .

لكن, إذا كان المؤشر يشير إلى ذاكرة عنوانها صفر ألا يعني هذا أن قيمة الخانة المُؤشر عليها تساوي صفر أيضا !؟

لا, في العادة الـnull pointer يشير الى ذاكرة غير صالحة للاستخدام invalid memory address.

ماهو المؤشر الثابت ؟

المؤشر الثابت هو المؤشر الذي لا يمكن تغيير عنوانه أثناء عمل البرنامج و يُعرف كالآتى :

Type *const Pointer=&VarType;

حيث Type نوع المؤشر والــ VarType متغير من نوع Type .

ماهو المؤشر إلى ثابت ؟

المؤشر إلى ثابت هو المؤشر الذي يشير إلى قيمة لا نستطيع تغييرها من خلال المؤشر ولكن نستطيع تغيير العنوان الذى يشير إليه المؤشر, يمكن كذلك استخدام هذا النوع من المؤشرات في الإشارة للمتغيرات المعرفة على أنها ثابتة وفي هذه الحالة لن تستطيع تغيير القيمة الموجودة في المؤشر من خلال المؤشر أو من خلال المتغير, ويعرف على الشكل التالى :

const Type *Pointer = &ValType;
or
const Type *Pointer = &ConstValType;

ماهو المؤشر الثابت إلى ثابت ؟

المؤشر الثابت إلى ثابت هو المؤشر الثابت العنوان, الثابت القيمة أى لا نستطيع تغيير عنوانه أو قيمته ولكن إن كانت القيمة المسندة للمؤشر من متغير غير ثابت ففي هذه الحالة يمكن تغيير قيمة المؤشر ولكن من المتغير وليس المؤشر , ويعرف على الشكل التالى :

const Type *const Pointer=&ValType;
or
const Type *const Pointer=&ConstValType;

ما هو المؤشر العائم (Void Pointer) و فيم يُستخدم ؟

يمكنك اعتبار أن مؤشر الـ void يشير الى كائن غير معروف الطول او الهوية ، مجرد كتلة من الذاكرة .

انت من يحدد كيف يستخدمه, فمثلا يمكنك كتابة دالة تقوم بتصفير ذاكرة أي كائن, في هذه الحالة ستحتاج ان تمرر لها طول الكائن ومؤشر void لانك لا تعرف ان كنت ما ستقوم بتصفيره هو int أم char أم float أم structure, هنا تظهر أهمية مؤشر الـ void.

تُستخدم أيضا المؤشرات العائمة في تكوين مصفوفة لأنواع مختلطة أو تمرير متغيرات لدوال تتوقع عدة أنواع من البيانات.

ماهو الــ Pointer to Pointer ؟

هذا النوع من المؤشرات يحوي عنوان, و العنوان بدوره يحوي قيمة. فهو إذا لا يشير إلى قيمة بل إلى مؤشر و يُستخدم عادة للتعامل مع مصفوفة ديناميكية ثنائية البعد, يتم الإعلان عنه هكذا :

Type **Pointer;

باعتبار أن t مصفوفة, مالفرق بين الكتابات التالية t, &t, &(t[0]) ؟

الكتابة الأولى تمثل الكائن (المصفوفة), الكتابة الثانية عبارة عن عنوان المصفوفة, أما الثالثة فتمثل عنوان أول عناصر المصفوفة.

إذا, الكتابات الثلاثة مختلفة عن بعضها البعض و لا توجد بينها نقطة مشتركة.

الكتابة الثانية و الثالثة يملكان نفس العنوان و لكن يختلفان في النوع.

مالفرق بين char a[] and char * a ؟

تذكر جيدا أنه في لغة C, المصفوفة ليست مؤشر .. حتى ولو كان استخدامهما متقارب إلى حد كبير.

الكتابة char a[] تعني مصفوفة حرفية غير معروفة الحجم في البداية, و لكن عند استخدامها هكذا:

char a[] = "Hello";

تتحول إلى char[6] و تكون مصفوفة حرفية مُكونة من 6 حروف.

أما الكتابة char * a فتعني مؤشر يشير إلى متغير حرفي.

باعتبار أن t مصفوفة, ما معنى الكتابة *(t + 3) ؟

هذه الكتابة مُكافئة لـ t[3], لأن t يمثل عنوان أول عناصر المصفوفة و في حالتنا هذه ستتم إزاحته بمقدار ثلاث خانات إلى الأمام ثم يتم أخذ قيمة الذاكرة التي يشير إليها ذاك المؤشر. انظر المثال:

#include <stdio.h>

int main() {
int t[] = {10, 20, 30, 40};
int * p = t;
/* p pointe sur t[0] donc *p equivaut a t[0]. */
printf("t[0] = %d\n", *p);
/* p pointe sur t[0] donc p + 1 pointe sur t[1]. *(p + 1) equivaut donc a t[1]. */
printf("t[1] = %d\n", *(p + 1));
/* p pointe sur t[0] donc p + 2 pointe sur t[2]. *(p + 2) equivaut donc a t[2]. */
printf("t[2] = %d\n", *(p + 2));
/* p pointe sur t[0] donc p + 3 pointe sur t[3]. *(p + 3) equivaut donc a t[3]. */
printf("t[3] = %d\n", *(p + 3));
return 0;
}

أو بدون استخدام المؤشر p :

#include <stdio.h>

int main() {
int t[] = {10, 20, 30, 40};
printf("t[0] = %d\n", *t);
printf("t[1] = %d\n", *(t + 1));
printf("t[2] = %d\n", *(t + 2));
printf("t[3] = %d\n", *(t + 3));
return 0;
}

ما معنى هذه الكتابة *p++ ؟

المؤثرات الأحادية (*,++,--) لهم أولوية قوية ويتم تنفيذهم من اليمين إلى اليسار, الكتابة *p++ تعني قيمة الخانة الموالية للخانة التي يشير إليها المؤشر p أما الكتابة (*p)++ فتعني زيادة قيمة المؤشر p بـ 1.

ما هو دور المعامل [] ؟

هذا المعامل يسمح بالوصول إلى عناصر المصفوفة, يتطلب وسيطين, الأول مؤشر و الثاني عدد صحيح, العبارة X[Y] مُكافئة لـ *( (X) + (Y) ). بعبارة أخرى, إذا كان p مؤشر فالكتابات التالية متكافئة p[1], 1[ p ] and *(p + 1) .

ماذا تعني الكتابة التالية int (*p)[4] ؟

هذه الكتابة تقوم بإنشاء متغير p بحيث يكون *p عبارة عن مصفوفة مكونة من 4 خانات, إذا p يشير إلى مصفوفة من 4 خانات, انظر المثال:

#include <stdio.h>

int main() {
int t[10][4];
int (*p)[4] = t;
/* p pointe sur t[0]. *p <=> t[0]. */;
(*p)[0] = 0; /* t[0][0] = 0 */
(*p)[1] = 0; /* t[0][1] = 1 */
(*(p + 1))[0] = 1; /* t[1][0] = 1 */
(*(p + 1))[1] = 1; /* t[1][1] = 1 */
printf("t[0][0] = %d\n", t[0][0]);
printf("t[0][1] = %d\n", t[0][1]);
printf("t[1][0] = %d\n", t[1][0]);
printf("t[1][1] = %d\n", t[1][1]);
return 0;
}

ما هي فائدة الدالة malloc ؟

الدالة malloc تستقبل عدد البايتات (نوعية حجم البيانات) و تعيد مؤشر void يشير إلى مكان الحجز مع العلم أنها لا تقوم بتصفير الذاكرة لذلك سنحتاج إلى الدالة memset (إن أردنا التصفير) التي تستقبل ثلاث معاملات, المعامل الأول هو المؤشر نفسه و الثاني هو القيمة المراد وضعها بداخل الخانة التي يشير إليها المؤشر و الثالث هو عدد البايتات أو كمية البيانات المُؤشر عليها انظر المثال:

char* ptr = malloc(sizeof (char));
if (ptr != NULL) {
memset(ptr, 0x00, sizeof (char));

free(ptr);
}

ما هي فائدة الدالة calloc ؟

الدالة calloc تستقبل معاملين, الأول هو عدد العناصر المراد حجزها، و المعامل الثاني هو حجم العنصر الواحد بالبايت, هذه الدالة تُعيد مؤشر void يشير إلى مكان أول خانة من هذه الخانات مع العلم أنها متتالية في الترتيب لذلك يمكننا الوصول إلى بقية الخانات عن طريقة تحريك المؤشر. مثلا اذا اردنا حجز 5 عناصر من نوع int اذا نقوم بالتالي :

malloc(5 * sizeof(int));

أو

calloc(5, sizeof(int));

مع اختلاف بسيط وهو أن الدالة calloc تقوم بتصفير البيانات التي تحجزها، وفي العادة calloc تقوم باستدعاء malloc داخلها .

ما الفائدة من استخدام calloc إذا كانت malloc تفي بالغرض !؟

عمليا ليس هناك فرق بينهما, لكن لجعل الكود اكثر وضوحا يفضل استخدام malloc عند حجز ذاكرة لكائن واحد و calloc عند حجز مصفوفة.

لتقريب الفكرة, يمكنك اعتبار أن malloc + memset = calloc.

ما هي فائدة الدالة realloc ؟

realloc تقوم باعادة حجز ذاكرة تم حجزها مسبقا عن طريق calloc/malloc بحجم مختلف وتقوم بنسخ البيانات من الذاكرة القديمة الى الاحدث اذا كان هذا ممكنا ثم تقوم بتحرير الذاكرة القديمة وارجاع عنوان للذاكرة الاحدث . (يمكن استخدامها عندما نريد حذف أو إضافة عناصر إلى المصفوفة)

كيف أنشئ مصفوفة ديناميكية ثنائية البعد ؟

توجد ثلاث طرق للإعلان عن المصفوفات الديناميكية :

  • الطريقة الأولى : نحجز مؤشر P هكذا :
    P = malloc(NBLIG* NBCOL* sizeof(élément))


    حيث NBLIG هو عدد الأسطر و NBCOL هو عدد الأعمدة و élément هو نوع عناصر المصفوفة.
    بهذه الطريقة يمكننا الوصول إلى العنصر الذي يوجد عند تقاطع السطر i و العمود j بالكتابة التالية :

    *( (P+j)+(i*NBCOL) )


  • الطريقة الثانية : إنشاء مصفوفة مؤشرات أحادية البعد, عدد عناصرها NBLIG, بهذه الطريقة يمكنك التعامل مع المصفوفات الغير ثابتة البعد (كل سطر له بعد مختلف - معالجة النصوص مثلا) كما تسمح لك هذه الطريقة بالتعامل السهل و السريع مع الأسطر (تبادل سطرين مع بعضها البعض دون نسخ محتوياتهما), هذه الطريقة أحسن و أسرع من السابقة, لأنه يمكننا الوصول إلى عنوان كل سطر دون أي حسابات.
  • الطريقة الثالثة : استخدام مؤشر لمؤشر, نفس الطريقة السابقة و لكن في هذه الطريقة سنستبدل جدول المؤشرات (حجمه مُعرف مُسبقا) بالحجز الديناميكي.

مالفرق بين الدالة malloc و المعامل new ؟

يكمن الفرق الأساسي في النقاط التالية:

  • malloc لاتستدعي المشيد كما يفعل new.
  • malloc لاتحتوي على آلية لمعالجة الأخطاء داخليا.
  • تشترط malloc تحديد حجم البايتات المراد حجزه حسب نوع المتغير.

p و q يُشيران إلى متغيرين يحملان نفس القيمة و مع ذلك فإن p==q تُعيد false دائما !, مالسبب ؟

هذا طبيعي جدا !, الشرط p==q يُقارن بين المؤشرين و ليس ما يُشيران إليه, في هذه الحالة تكون العناوين مختلفة و لكن المحتوى متساوي, لمقارنة محتوى الذاكرة التي يشير إليها كل مؤشر استخدم *p و *q أو قم باستدعاء الدالة memcmp, هكذا :

int * p, * q, n = 10;
/* On suppose ici que malloc ne peut pas échouer */
p = malloc(sizeof(int));
q = malloc(sizeof(int));
memcpy(p, amp;&n, sizeof(int)); /* Ou simplement *p = n; */
memcpy(q, amp;&n, sizeof(int)); /* Ou simplement *q = n; */
/* (*p == *q) mais (p != q) */
free(p);
p = q;
/* Maintenant (p == q) et donc naturellement (*p == *q) */
free(p); /* Ou free(q) */

كيف نعرف نوع المتغير الذي يُشير إليه مؤشر من النوع void ؟

لا يمكن معرفة نوع الكائن الذي يشير إليه مؤشر void, إذا كان المؤشر مستخدم لإنشاء generic function فإنه من الضروري أن نضيف إلى وسائط الدالة وسيط إضافي يُحدد حجم أو نوع المتغير المستعمل:

typedef enum {
TYPES_char,
TYPES_short,
TYPES_int,
TYPES_long,
TYPES_float,
TYPES_double
} TypePtr;
void MyFunction(void * Ptr, TypePtr Type);

2. المصفوفات الحرفية (Strings) :

ما هو NUL ؟

المحرف NUL (أو \0) هو الرمز الذي يُستخدم للدلالة على نهاية المصفوفة, جميع بتات هذا المحرف تساوي 0.

لماذا يحتاج المترجم إلى وضع الرمز NUL في نهاية كل مصفوفة حرفية ؟

تحتوي لغة C على أنواع بسيطة مثل short و long و float و char. المصفوفات التى يتم إنشائها من الأنواع السابقة إن كانت داخل الـ stack فيمكنك معرفة حجمها - بدون تحويلها لمؤشر - و ذلك عن طريق إحضار حجم المصفوفة باستخدام sizeof و قسمته على حجم نوع المصفوفة.

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

و حيث أننا نحتاج لاستخدام النصوص داخل لغة لا تدعم النصوص فلابد من وضع مفاهيم و تعاريف حتى نستطيع تمييز النص, هذه القواعد تتلخص في نقطتين:

  • النص هو مصفوفة من النوع char أو wchar_t
  • لابد و أن ينتهي النص بعلامة مميزة نستطيع من خلالها معرفة الموقع الذي يمثل نهاية النص و يتم هذا الأمر باستخدام الـ Null Character او Null Terminator.

إذا كنت ستستخدم المصفوفة الحرفية كنص فلابد و أن تنتهي بـ 0 و ذلك حتي تستطيع دوال التعامل مع النصوص معرفة نهاية النص أما إن كنت ستتعامل مع المصفوفة الحرفية كمصفوفة حروف فلابد أن تعلم عدد الحروف المكونة لها.

كما سبق و أوضحت أنه يمكنك معرفة حجم المصفوفة الموجودة داخل الـ Stack فقط إذا كنت ستتعامل معها داخل الـ scope الذي تم تعريفها فيه و لم تقم لتحويلها لمؤشر، أيضا المصفوفة التي تم تعريفها داخل الـ Heap يمكن معرفة حجمها داخل ويندوز كالتالي (لاحظ انه لابد من معرفة حجم نوع البيانات الذي تمثله المصفوفة)

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

int getSize(int* arr){
return HeapSize(GetProcessHeap(), 0, arr) / sizeof(int);
}

int main()
{
int* w = malloc(100 * sizeof (int));
int size = getSize(w);
printf("%d",size);
return 0;
}

الدالة getSize تعود لك بمساحة الدالة التي تم انشائها داخل الـ heap.

لا تقم بتجربة المثال السابق داخل Visual Studio لأنه يستخدم Debug Heap خاص به داخل وضع الـ Debug و أيضا الـ Release، إذا كنت تريد تجربة المثال فقم بتجربته داخل مساحة الذاكرة الخاصة به.

ما معنى الكتابة التالية char * p = "Bonjour"; ؟

هذه الكتابة تقوم بإنشاء مؤشر يشير إلى السلسلة الحرفية Bonjour, مكان تخزين السلسلة يختلف باختلاف البيئة التي تعمل عليها, لغة C لا تضمن لك أن مساحة الذاكرة ستكون غير قابلة للقراءة.

char * p = "Bon*our";
p[3] = 'j';
/*Incorrect(mais accepte par le compilateur !).
* Il n'est pas garanti que p[3] soit modifiable. */
strcpy(p, "Salut"); /* Incorrect (mais accepte par le compilateur !).
* Pour la meme raison. */
p = "Salut"; /* Correct.
* p pointe maintenant sur une chaine de caracteres valant "Salut". */

لذا يجب اعتبار أن مساحة الذاكرة للقراءة فقط عن طريق تعريف المؤشر على أنه مؤشر إلى ثابت:

char const * p = "Bon*our";
p[3] = 'j'; /* Incorrect. Immediatement rejete par le compilateur.
* On ne peut pas modifier une 'constante' ! */
strcpy(p, "Salut"); /* Incorrect. Immediatement rejete par le compilateur.
* Pour la meme raison. */
p = "Salut"; /* Correct.
* p pointe maintenant sur une chaine de caracteres valant "Salut". */

كيف نُحول حروف سلسلة حرفية إلى حروف صغيرة/كبيرة ؟

يمكنك استخدام الدالة tolower لتصغير الأحرف أو toupper لتكبير الأحرف, الدالتين السابقتين يقومان بتحويل حرف واحد, لذا سيلزمك استخدام حلقة, في الويندوز يمكن استخدام الدالتين CharLower و CharLowerBuff لتصغير الحروف أو CharUpper و CharUpperBuff لتكبيرها.

كيف نقوم بتحويل عدد إلى سلسلة حرفية ؟

يمكنك استخدام الدالة sprintf الموجودة في الملف الرأسي stdio.h و التابعة للمعيار ANSI-C و بالتالي يمكن استخدامها تحت أي منصة, طريقة استخدامها تكون كالآتي:

#include <stdio.h>
char buf[32];
int n = 10;
sprintf(buf, "%d", n);

المعيار C99 أدخل الدالة snprintf و هي مشابهة لــ sptintf جدا, الفرق الوحيد هو أن snprintf تستقبل الحجم الأقصى للسلسلة الحرفية كوسيط إضافي.

كيف نقوم بتحويل سلسلة حرفية إلى عدد ؟

إذا كانت السلسلة الحرفية تحوي عدد, يمكننا إسناده لمتغير صحيح باستخدام الدالة strtol :

char buf[32] = "15";
long n;
n = strtol(buf, NULL, 10);

الوسيط الثالث للدالة عبارة عن قاعدة النظام المستعمل (في حالتنا هذه فإن النظام المستعمل هو النظام العشري), بعد تنفيذ الدالة سيحتوي الوسيط الثاني على عنوان السلسلة الحرفية بعد توقفها, إذا تم التحويل بنجاح فإن الوسيط الثاني سيشير إلى \0 :

char buf[32] = "15";
long n;
char * end
n = strtol(buf, &end, 10);
if (*end == '\0') {
/* La chaine a enteirement pu etre convertie */
} else {
/* La chaine contenait au moins un caractere ne pouvant pas etre converti en nombre */
}

كيف ندمج السلاسل الحرفية ؟

توجد عدة طرق, إذا أردنا دمج سلسلتين و تخزين الناتج في سلسلة ثالثة فمكننا استخدام الدالة sprintf هكذا :

#include <stdio.h>
char Chaine1[32], Chaine2[32], ChaineFinale[64];
sprintf(ChaineFinale, "%s%s", Chaine1, Chaine2);

يمكننا أيضا استدعاء الدالة strcat التي تقوم بدمج السلسلتين في السلسلة الأولى:

#include <string.h>
char Chaine1[64], Chaine2[32];
strcat(Chaine1, Chaine2); /* Chaine1 contient maintenant Chaine1 + Chaine2 */

طول السلسلة الثانية يجب أن يكون كافيا لإضافة السلسلة الأولى.

كيف نقارن السلاسل الحرفية ؟

لا تستخدم المعامل == في المقارنة. استخدم الدالة strcmp لمثل هذا الأمر, إذا كنت تريد مقارنة جزء معين من السلسلة الحرفية فيمكنك استخدام الدالة strncmp, توجد ثلاث حالات:

  • القيمة 0 تعني أن السلسلتين متساويتين.
  • القيمة السالبة تعني أن السلسلة الحرفية الأولى تحتوي على حروف ترميزها بالـ ASCII أصغر من ترميز حروف السلسلة الثانية.
  • القيمة الموجبة تعني أن السلسلة الحرفية الأولى تحتوي على حروف ترميزها بالـ ASCII أكبر من ترميز حروف السلسلة الثانية.

const char * chaine1 = "blabla";
const char * chaine2 = "blabla";
const char * chaine3 = "blablu";
if (strcmp(chaine1, chaine2) == 0) {
/* les chaines sont identiques */
}
if (strcmp(chaine2, chaine3) != 0) {
/* les chaines sont différentes */
}
if (strncmp(chaine2, chaine3, 3) == 0) {
/* les chaines sont identiques en se limitant a comparer les 3 premiers caracteres */
}

لماذا لا أستطيع مقارنة السلاسل الحرفية المقروءة بالدالة fgets ؟

عند قراءة سلسلة حرفية من ملف نصي باستخدام الدالة fgets يبقى رمز الرجوع إلى بداية السطر \n مُخزن في السلسلة و بالتالي يجب حذف الرمز قبل المقارنة:

char s[256];
FILE * fp;
...
if (fgets(s, sizeof s, fp) != NULL) {
char * p = strchr(s, '\n');
if (p != NULL)
*p = 0;
}

كيف ننشئ مصفوفة من السلاسل الحرفية ؟

يكفي إنشاء مصفوفة ثنائية البعد, إذا أردنا إنشاء مصفوفة تحتوي على 10 كلمات, كل كلمة تحتوي على 256 حرف كحد أقصى فيمكننا كتابة:

#include <string.h>
...
char Tableau[10][256];
...
strcpy(Tableau[0], "azerty");

في المثال السابق, قمنا بإسناد الكلمة azerty إلى المصفوفة رقم 0.

لماذا يجب كتابة الرمز \ هكذا \\ ؟

لأن \ يُمثل بداية الرموز الخاصة بلغة C, على سبيل المثال, الرمز \n يُمثل الرجوع إلى بداية السطر.

لكي يتم التعامل مع السلسلة السابقة باعتبار أنها الرمز \ متبوعا بالحرف n يكفي إضافة الرمز \ إلى العبارة لتصبح \\n.

نسيان الخطوة السابقة هو خطأ شائع, عادة ما يقع فيه المبتدئون في لغة C, انظر المثال:

remove("c:\arabteam\faqc\test.txt"); /* Ne fonctionne pas */
remove("c:\\arabteam\\faqc\\test.txt"); /* Fonctionne */

كيف نقوم بنسخ مصفوفة ؟

توجد طريقتان لفعل هذا الأمر, الطريقة الأولى تكمن في نسخ المصفوفة عنصرا بعنصر, هذه الطريقة معقدة جدا و بطيئة أيضا.

الطريقة الثانية تتم باستخدام الدالة memcpy, تستقبل هذه الدالة 3 وسائط, المصفوفة التي نريد نسخها و المصفوفة الجديدة و حجم البيانات المراد نسخها بالبايت, انظر المثال :

#include <string.h>
int Tab1[10], Tab2[10];
memcpy(Tab2, Tab1, sizeof Tab2);

3. التراكيب (Structures) :

كيف أنشئ اسما مستعارا لــ Structure ؟

يمكنك فعل ذلك بأكثر من طريقة:

typedef struct {
int x;
int y;
} POINT2D;

أو

struct point2d_s {
int x;
int y;
};
typedef struct point2d_s POINT2D;

أو

typedef struct point2d_s {
int x;
int y;
} POINT2D;

مالفرق بين structure و union ؟

البنية عبارة عن مجموعة من البيانات مختلفة النوع مُسجلة تحت اسم واحد:

#include <stdio.h>

struct point_s {
int x;
int y;
};

int main() {
struct point_s A;
A.x = 1;
A.y = 2;
printf("A = (%d, %d)\n", A.x, A.y);
return 0;
}

أما الـ union فهو عبارة عن قائمة من العناصر التي تستخدم نفس العنوان في الذاكرة:

#include <stdio.h>

union duo_u {
int n;
double x;
};

int main() {
union duo_u d;
d.n = 1;
printf("d.n = %d\n", d.n);
d.n = 2;
printf("d.n = %d\n", d.n);
d.x = 3.0;
printf("d.x = %f\n", d.x);
d.x = 4.0;
printf("d.x = %f\n", d.x);
return 0;
}

أهم الفوارق بين structure و union :


  • في أغلب الأحيان يكون حجم البنية مُساوي لمجموع أحجام العناصر الـمُكونة له أما حجم الـ union فيُساوي حجم أكبر العناصر.
  • قيمة أي متغير في الـ union تساوي قيمة آخر متغير تم استعماله لأن جميع العناصر تملك نفس العنوان بينما يملك كل عنصر في البنية عنوان مُختلف.
  • يمكن استخدام أكثر من عنصر في آن واحد في structure أما في الـ union فلا يمكن العمل على عنصرين في نفس الوقت.
  • الـ union تأخذ مساحة أقل من الذاكرة مُقارنة مع structure.

لماذا حجم البنية لا يُساوي بالضرورة مجموع أحجام العناصر ؟

لأن عناصر البنية ليست بالضرورة مُخزنة في أماكن متتالية في الذاكرة, يتعلق الأمر بالطريقة التي يعتمد عليها المعالج في الـ Alignment Constraints.

كيف نقوم بنسخ structure ؟

يتم ذلك عن طريق المعامل = أو الدالة memcpy الموجودة في الملف الرأسي string.h :

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

struct personne_s {
char nom[21];
int age;
};

int main() {
struct personne_s A, B, C;
strcpy(A.nom, "A");
A.age = 26;
B = A;
memcpy(&C, &A, sizeof (C));
printf("A.nom = %s\n", A.nom);
printf("A.age = %d\n", A.age);
printf("B.nom = %s\n", B.nom);
printf("B.age = %d\n", B.age);
printf("C.nom = %s\n", C.nom);
printf("C.age = %d\n", C.age);
return 0;
}

كيف نقارن بين بـُـنْـيـَـتـَـيـْـن ؟

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

ماذا تعني الكتابة التالية unsigned int i : <n>; ؟

هذه الكتابة عبارة عن تصريح خاص بالتراكيب, هذا التصريح يسمح بإنشاء عنصر جديد اسمه i و حجمه n بت (bits), يُسمى هذا النوع من العناصر بــ Bit field :

struct test_s {
unsigned int i : 1;
unsigned int j : 2;
};

int main() {
struct test_s s;
s.i = 0; /* A la fin de l'instruction, on aura s.i = 0 en binaire soit 0 en decimal. */
s.i++; /* A la fin de l'instruction, on aura s.i = 1 en binaire soit 1 en decimal. */
s.i++; /* A la fin de l'instruction, on aura s.i = 0 en binaire soit 0 en decimal. */
s.j = 0; /* A la fin de l'instruction, on aura s.j = 00 en binaire soit 0 en decimal. */
s.j++; /* A la fin de l'instruction, on aura s.j = 01 en binaire soit 1 en decimal. */
s.j++; /* A la fin de l'instruction, on aura s.j = 10 en binaire soit 2 en decimal. */
s.j++; /* A la fin de l'instruction, on aura s.j = 11 en binaire soit 3 en decimal. */
s.j++; /* A la fin de l'instruction, on aura s.j = 00 en binaire soit 0 en decimal. */
return 0;
}

كيف نستخدم مؤشر يُشير إلى بنية ؟

يتم الإعلان عنه هكذا:

struct base {
int a;
double b;
};
struct base Elem;
struct base * p = &Elem;

لكي نستطيع الوصول إلى أحد أعضاء البنية نقوم بعمل dereference للمؤشر, هكذا:

(*p).a = 5;

يُمكن تبسيط الكتابة السابقة إلى:

p->a = 5;

الكتابة الأولى ليست مُكافئة للكتابة الثانية, لأن الأولى تقوم بعمل dereference للمؤشر قبل الدخول إلى محتواه أما الكتابة الثانية فتقوم بالذهاب مُباشرة إلى ذاكرة المتغير.

مالفرق بين الكتابتين sizeof(struct data) and sizeof(struct data *) ؟

الكتابة الأولى تُعيد حجم البنية أما الكتابة الثانية فتعيد حجم مؤشر البنية.

كيف يتم الإعلان عن بنية تشير إلى نفسها ؟

توجد عدة طرق لمثل هذا الأمر, المشكلة أنه عند الإعلان عن البنية باستخدام typedef سيكون النوع غير معرف بعد !.

هذه أحد أبسط الحلول, لحل المشكلة نقوم بإضافة tag إلى البنية :

typedef struct node {
char * item;
struct node * next;
} node_t;

هناك حل آخر يكمن في الإعلان عن نوع البنية قبل تعريفها عن طريق مؤشر:

typedef struct node * node_p;

typedef struct node {
char * item;
node_p next;
} node_t;

الإعلان السابق يُستخدم عادة من أجل الحصول على قوائم مترابطة أو بنى شجرية.

كيف نُحسن تهيئة المتغيرات ؟

المتغيرات العامة أو المتغيرات الساكنة (static) تكون مُهيئة تلقائيا عند الإعلان عنها, إذا لم يتم إسناد أي قيمة لها فالقيمة الافتراضية لها هي صفر (حسب نوع المتغير فقد تكون القيمة الافتراضية 0, 0.00, Or NULL)

أما بقية المتغيرات فيلزم أن نقوم بتهيئتها في الدالة الرئيسية main. المتغيرات المحجوزة ديناميكيا تدخل هي أيضا في الصنف الثاني, يمكن أن نستخدم الدالة calloc لتهيئة تلك المتغيرات, هذه الدالة تقوم بتصفير الذاكرة (تماما كما تفعل memset), هذه التهيئة تكون صالحة للأنواع الصحيحة (char, short, int, long) ولكنها لا تصلح مع المؤشرات و الأنواع الحقيقية (float).

الطريقة الأفضل لتهيئة المتغيرات حسب نوعها تكون كالتالي:

{
type a[10] = {0};
struct s x = {0};
}

و بالنسبة للمتغيرات الديناميكية:

[static] const struct s x0 = {0};
{
struct s *px = malloc(sizeof *px);
*px = x0;
}

تم تعديل بواسطه أحمد الشنقيطي
إضافة أسئلة جديدة.
4

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه

  • يستعرض القسم حالياً   0 members

    لا يوجد أعضاء مسجلين يشاهدون هذه الصفحة .