قراءة موجّهة · الملاحظات

الأسبوع 4: الذاكرة

اقرأ للفهم، ثم دوّن المصطلحات الإنجليزية التي تحتاج إلى تذكّرها أثناء التطبيق.

جلسة دراسة مقترحة: حتى 55 دقيقةWeek 4
محاور هذه الخطوة
  • المؤشرات
  • العناوين
  • malloc
  • الملفات

ترجمة آلية تحت المراجعة

الأسبوع 4: الذاكرة

المصدر الأصلي على CS50

المحاضرة الرابعة (Lecture 4)

مرحبًا! (Welcome!)

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

فن البكسل (Pixel Art)

  • وحدات البكسل عبارة عن مربعات، ونقاط فردية، ذات ألوان مرتبة على شبكة من الأعلى إلى الأسفل ومن اليسار إلى اليمين.
  • يمكنك تخيل الصورة كخريطة من البتات، حيث تمثل الأصفار اللون الأسود والآحاد تمثل اللون الأبيض.

    Zeros and ones being converted to a black and white smiley

سداسي عشري (Hexadecimal)

  • RGB، أو الأحمر، الأخضر، الأزرق، هي أرقام تمثل مقدار كل لون من هذه الألوان. في Adobe Photoshop، يمكنك رؤية هذه الإعدادات كما يلي:

    A photoshop panel with RGB values and hexadecimal input

    لاحظ كيف أن كمية اللون الأحمر والأزرق والأخضر تغير اللون المحدد.

  • يمكنك أن ترى من الصورة أعلاه أن اللون لا يتم تمثيله بثلاث قيم فقط. توجد في أسفل النافذة قيمة خاصة مكونة من أرقام وأحرف. يتم تمثيل 255 كـ FF. لماذا قد يكون هذا؟
  • سداسي عشري هو نظام عد يحتوي على 16 قيمة عد. وهم على النحو التالي:

      0 1 2 3 4 5 6 7 8 9 A B C D E F
    

    لاحظ ذلك تم الآن إنشاء F يمثل 15.

  • يُعرف أيضًا بالنظام الست عشري القاعدة 16.
  • عند العد بالنظام الست عشري، يكون كل عمود قوة للرقم 16.
  • الرقم يتم تمثيل 0 كـ 00.
  • الرقم يتم تمثيل 1 كـ 01.
  • الرقم 9 يمثله 09.
  • الرقم يتم تمثيل 10 كـ 0A.
  • الرقم يتم تمثيل 15 كـ 0F.
  • الرقم يتم تمثيل 16 كـ 10.
  • الرقم يتم تمثيل 255 كـ FF، لأن 16 × 15 (أو F) هو 240. أضف 15 أخرى لتحصل على 255. هذا هو أعلى رقم يمكنك حسابه باستخدام نظام سداسي عشري مكون من رقمين.
  • يعتبر النظام الست عشري مفيدًا لأنه يمكن تمثيله باستخدام أرقام أقل. يسمح لنا النظام الست عشري بتمثيل المعلومات بشكل أكثر إيجازًا.

الذاكرة (Memory)

  • في الأسابيع الماضية، ربما تتذكر عرض الفنان الخاص بنا لكتل ​​الذاكرة المتزامنة. وبتطبيق الترقيم السداسي العشري على كل كتلة من كتل الذاكرة هذه، يمكنك تصورها على النحو التالي:

    Blocks of memory numbered in hex

  • يمكنك أن تتخيل كيف قد يكون هناك ارتباك بشأن ما إذا كان قد تمثل الكتلة 10 أعلاه موقعًا في الذاكرة أو القيمة 10. وفقًا لذلك، وفقًا للاتفاقية، غالبًا ما يتم تمثيل جميع الأرقام السداسية العشرية بـ 0x البادئة كما يلي:

    blocks of memory numbered in hex with 0x

  • في نافذتك الطرفية، اكتب code addresses.c واكتب الشيفرة الخاص بك كما يلي: يتم تخزين

    // Prints an integer
    
    #include <stdio.h>
    
    int main(void)
    {
        int n = 50;
        printf("%i\n", n);
    }
    

    لاحظ كيف n في الذاكرة بالقيمة 50.

  • يمكنك تصور كيفية تخزين هذا البرنامج لهذه القيمة كما يلي:

    the value 50 stored in a memory location with hex

المؤشرات (Pointers)

  • تحتوي لغة C على عاملين قويين يتعلقان بالذاكرة:

      & Provides the address of something stored in memory.
      * Instructs the compiler to go to a location in memory.
    
  • يمكننا الاستفادة من هذه المعرفة عن طريق تعديل الشيفرة الخاص بنا على النحو التالي:

    // Prints an integer's address
    
    #include <stdio.h>
    
    int main(void)
    {
        int n = 50;
        printf("%p\n", &n);
    }
    

    لاحظ %p، والذي يسمح لنا بعرض عنوان موقع ما في الذاكرة. &n يمكن ترجمتها حرفيًا على أنها "عنوان n." سيؤدي تنفيذ هذا الرمز إلى إرجاع عنوان الذاكرة الذي يبدأ بـ 0x. يمكنك تنزيل هذا الرمز هنا.

  • A المؤشر هو متغير يقوم بتخزين عنوان شيء ما. باختصار، المؤشر هو عنوان موجود في ذاكرة جهاز الكمبيوتر الخاص بك.
  • خذ بعين الاعتبار الشيفرة التالي:

    int n = 50;
    int *p = &n;
    

    لاحظ ذلك تم الآن إنشاء p هو مؤشر يحتوي على عنوان عدد صحيح n.

  • قم بتعديل الشيفرة الخاص بك كما يلي:

    // Stores and prints an integer's address
    
    #include <stdio.h>
    
    int main(void)
    {
        int n = 50;
        int *p = &n;
        printf("%p\n", p);
    }
    

    لاحظ أن هذا الشيفرة له نفس تأثير الشيفرة السابق. لقد استفدنا ببساطة من معرفتنا الجديدة بالـ & و * عوامل التشغيل. يمكنك تنزيل هذا الرمز هنا.

  • لتوضيح استخدام عامل تشغيل * ، ضع في اعتبارك ما يلي: يقوم سطر

    // Stores and prints an integer's address
    
    #include <stdio.h>
    
    int main(void)
    {
        int n = 50;
        int *p = &n;
        printf("%p\n", p);
    }
    

    لاحظ أن يتم استخدام كتلة printf بطباعة عنوان العدد الصحيح. يقوم int *p بإنشاء مؤشر مهمته تخزين عنوان الذاكرة لعدد صحيح. يمكنك تنزيل هذا الرمز هنا.

  • يمكنك تصور الشيفرة الخاص بنا كما يلي:

    Same value of 50 in a memory location with a pointer value stored elsewhere

    لاحظ أن المؤشر يبدو كبيرًا نوعًا ما. في الواقع، عادةً ما يتم تخزين المؤشر كقيمة 8 بايت. يقوم p بتخزين عنوان 50.

  • يمكنك تصور المؤشر بشكل أكثر دقة كعنوان يشير إلى عنوان آخر:

    A pointer as an arrow, pointing from one location of memory to another

سلاسل (Strings)

  • الآن بعد أن أصبح لدينا نموذج عقلي للمؤشرات، يمكننا استعادة مستوى التبسيط الذي تم تقديمه سابقًا في هذه الدورة.
  • قم بتعديل الشيفرة الخاص بك كما يلي:

    // Prints a string
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        string s = "HI!";
        printf("%s\n", s);
    }
    

    لاحظ أن السلسلة تتم طباعة s . يمكنك تنزيل هذا الرمز هنا.

  • تذكر أن السلسلة هي مجرد مصفوفة من الأحرف. على سبيل المثال، يمكن تمثيل string s = "HI!" على النحو التالي:

    The string HI with an exclamation point stored in memory

  • ومع ذلك، ما هو s حقا؟ أين هو s مخزنة في الذاكرة؟ كما يمكنك أن تتخيل، يجب تخزين s في مكان ما. يمكنك تصور العلاقة s إلى السلسلة كما يلي:

    Same string HI with a pointer pointing to it

    لاحظ كيف يتم استدعاء المؤشر s يخبر المترجم بمكان وجود البايت الأول من السلسلة في الذاكرة.

  • قم بتعديل الشيفرة الخاص بك كما يلي:

    // Prints a string's address as well the addresses of its chars
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        string s = "HI!";
        printf("%p\n", s);
        printf("%p\n", &s[0]);
        printf("%p\n", &s[1]);
        printf("%p\n", &s[2]);
        printf("%p\n", &s[3]);
    }
    

    لاحظ أن ما ورد أعلاه يطبع مواقع الذاكرة لكل حرف في السلسلة s. ال يُستخدم الرمز & لإظهار عنوان كل عنصر من عناصر السلسلة. عند تشغيل هذا الشيفرة، لاحظ أن العناصر 0, 1, 2و 3 بجوار بعضها البعض في الذاكرة. يمكنك تنزيل هذا الرمز هنا.

  • وبالمثل، يمكنك تعديل الشيفرة الخاص بك كما يلي:

    // Declares a string with CS50 Library
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        string s = "HI!";
        printf("%s\n", s);
    }
    

    لاحظ أن هذا الرمز يقوم بإنشاء سلسلة باستخدام cs50.h . يمكنك تنزيل هذا الرمز هنا.

  • بخلع عجلات التدريب، يمكنك تعديل الشيفرة الخاص بك مرة أخرى: تمت إزالة

    // Declares a string without CS50 Library
    
    #include <stdio.h>
    
    int main(void)
    {
        char *s = "HI!";
        printf("%s\n", s);
    }
    

    لاحظ ذلك تم الآن إنشاء cs50.h . يتم تنفيذ السلسلة كـ char *. سيقدم هذا الرمز السلسلة التي تبدأ من موقع s. يزيل هذا الشيفرة بشكل فعال عجلات التدريب الخاصة بـ string نوع البيانات المقدم بواسطة cs50.h. هذا كود C خام، بدون سقالات مكتبة cs50. يمكنك تنزيل هذا الرمز هنا.

  • يمكنك تخيل كيفية إنشاء سلسلة، كنوع بيانات.
  • تعلمنا الأسبوع الماضي كيفية إنشاء نوع البيانات الخاص بك كبنية.
  • تتضمن مكتبة cs50 تعريفًا للنوع كما يلي: typedef char *string
  • تعريف النوع هذا، عند استخدام مكتبة cs50، هو تمثيل مبسط يسمح للشخص باستخدام نوع بيانات مخصص يسمى string.

المؤشر الحسابي (Pointer Arithmetic)

  • حساب المؤشر هو القدرة على إجراء العمليات الحسابية على مواقع الذاكرة.
  • يمكنك تعديل الشيفرة الخاص بك لطباعة كل موقع ذاكرة في السلسلة كما يلي:

    // Prints a string's chars
    
    #include <stdio.h>
    
    int main(void)
    {
        char *s = "HI!";
        printf("%c\n", s[0]);
        printf("%c\n", s[1]);
        printf("%c\n", s[2]);
    }
    

    لاحظ أننا نقوم بطباعة كل حرف في موقعه s. يمكنك تنزيل هذا الرمز هنا.

  • علاوة على ذلك، يمكنك تعديل الشيفرة الخاص بك كما يلي:

    // Prints a string's chars via pointer arithmetic
    
    #include <stdio.h>
    
    int main(void)
    {
        char *s = "HI!";
        printf("%c\n", *s);
        printf("%c\n", *(s + 1));
        printf("%c\n", *(s + 2));
    }
    

    لاحظ أن الحرف الأول في موقع تتم طباعة s . ثم الشخصية في الموقع تتم طباعة s + 1 ، وهكذا. يمكنك تنزيل هذا الرمز هنا.

  • وبالمثل، ضع في اعتبارك ما يلي:

    // Prints substrings via pointer arithmetic
    
    #include <stdio.h>
    
    int main(void)
    {
        char *s = "HI!";
        printf("%s\n", s);
        printf("%s\n", s + 1);
        printf("%s\n", s + 2);
    }
    

    لاحظ أن هذا الشيفرة يطبع القيم المخزنة في مواقع الذاكرة المختلفة بدءًا من s. يمكنك تنزيل هذا الرمز هنا.

مقارنة السلسلة (String Comparison)

  • سلسلة الأحرف هي ببساطة مجموعة من الأحرف يتم تحديدها بواسطة موقع البايت الأول الخاص بها.
  • في وقت سابق من الدورة، تناولنا المقارنة بين الأعداد الصحيحة. يمكننا تمثيل هذا في الشيفرة عن طريق الكتابة code compare.c في النافذة الطرفية كما يلي:

    // Compares two integers
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Get two integers
        int i = get_int("i: ");
        int j = get_int("j: ");
    
        // Compare integers
        if (i == j)
        {
            printf("Same\n");
        }
        else
        {
            printf("Different\n");
        }
    }
    

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

  • في حالة السلاسل، لا يمكن مقارنة سلسلتين باستخدام عامل التشغيل == .
  • الاستفادة من سيحاول عامل التشغيل == في محاولة لمقارنة السلاسل مقارنة مواقع الذاكرة الخاصة بالسلاسل بدلاً من الأحرف الموجودة فيها. وبناء على ذلك، نوصي باستخدام strcmp.
  • لتوضيح ذلك، قم بتعديل الشيفرة الخاص بك كما يلي:

    // Compares two strings' addresses
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Get two strings
        char *s = get_string("s: ");
        char *t = get_string("t: ");
    
        // Compare strings' addresses
        if (s == t)
        {
            printf("Same\n");
        }
        else
        {
            printf("Different\n");
        }
    }
    

    ملاحظة الكتابة HI! لكلا السلسلتين لا يزال يؤدي إلى إخراج Different. يمكنك تنزيل هذا الرمز هنا.

  • لماذا تبدو هذه السلاسل مختلفة؟ يمكنك استخدام ما يلي لتصور السبب:

    two strings stored separately in memory

  • لذلك الشيفرة الخاص بـ يحاول compare.c أعلاه معرفة ما إذا كانت عناوين الذاكرة مختلفة، وليس السلاسل نفسها.
  • باستخدام strcmp، يمكننا تصحيح الشيفرة الخاص بنا: يمكن ل

    // Compares two strings using strcmp
    
    #include <cs50.h>
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        // Get two strings
        char *s = get_string("s: ");
        char *t = get_string("t: ");
    
        // Compare strings
        if (strcmp(s, t) == 0)
        {
            printf("Same\n");
        }
        else
        {
            printf("Different\n");
        }
    }
    

    لاحظ ذلك تم الآن إنشاء strcmp العودة 0 إذا كانت السلاسل متماثلة. يمكنك تنزيل هذا الرمز هنا.

  • لتوضيح كيفية تواجد هاتين السلسلتين في موقعين بشكل أكبر، قم بتعديل الشيفرة الخاص بك كما يلي:

    // Prints two strings
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Get two strings
        char *s = get_string("s: ");
        char *t = get_string("t: ");
    
        // Print strings
        printf("%s\n", s);
        printf("%s\n", t);
    }
    

    لاحظ كيف أنه لدينا الآن سلسلتين منفصلتين مخزنتين، على الأرجح في موقعين منفصلين. يمكنك تنزيل هذا الرمز هنا.

  • يمكنك رؤية مواقع هاتين السلسلتين المخزنتين مع تعديل بسيط: تم تغيير

    // Prints two strings' addresses
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Get two strings
        char *s = get_string("s: ");
        char *t = get_string("t: ");
    
        // Print strings' addresses
        printf("%p\n", s);
        printf("%p\n", t);
    }
    

    لاحظ أن يتم استخدام كتلة %s إلى %p في بيان الطباعة. يمكنك تنزيل هذا الرمز هنا.

النسخ والمالوك (Copying and malloc)

  • هناك حاجة شائعة في البرمجة وهي نسخ سلسلة إلى أخرى.
  • في نافذتك الطرفية، اكتب code copy.c واكتب الشيفرة كما يلي:

    // Capitalizes a string
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        string s = get_string("s: ");
    
        // Copy string's address
        string t = s;
    
        // Capitalize first letter in string
        t[0] = toupper(t[0]);
    
        // Print string twice
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }
    

    لاحظ ذلك تم الآن إنشاء string t = s ينسخ عنوان s إلى t. وهذا لا يحقق ما نرغب فيه. لم يتم نسخ السلسلة - تم نسخ العنوان فقط. وعلاوة على ذلك، لاحظ إدراج ctype.h. يمكنك تنزيل هذا الرمز هنا.

  • يمكنك تصور الشيفرة أعلاه كما يلي:

    two pointers pointing at the same memory location with a string

    لاحظ ذلك تم الآن إنشاء s و t ما زالوا يشيرون إلى نفس كتل الذاكرة. هذه ليست نسخة أصلية من السلسلة. بدلاً من ذلك، هذان مؤشران يشيران إلى نفس السلسلة.

  • قبل أن نتناول هذا التحدي، من المهم التأكد من أننا لا نواجه أي مشكلة خطأ في التجزئة من خلال الشيفرة الخاص بنا، حيث نحاول النسخ string s إلى string t، حيث string t غير موجود. يمكننا توظيف تعمل strlen على النحو التالي للمساعدة في ذلك: يتم استخدام

    // Capitalizes a string, checking length first
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        string s = get_string("s: ");
    
        // Copy string's address
        string t = s;
    
        // Capitalize first letter in string
        if (strlen(t) > 0)
        {
            t[0] = toupper(t[0]);
        }
    
        // Print string twice
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }
    

    لاحظ ذلك تم الآن إنشاء strlen للتأكد string t لديه محتوى. إذا لم يحدث ذلك، فلن يتم نسخ أي شيء. يمكنك تنزيل هذا الرمز هنا.

  • لكي نتمكن من عمل نسخة أصلية من السلسلة، سنحتاج إلى تقديم كتلتين أساسيتين جديدتين. أولا، malloc يسمح لك، كمبرمج، بتخصيص كتلة بحجم معين من الذاكرة. ثانيا، free يسمح لك بإخبار المترجم بذلك حرر تلك الكتلة من الذاكرة التي قمت بتخصيصها مسبقًا.

  • يمكننا تعديل الشيفرة الخاص بنا لإنشاء نسخة أصلية من سلسلتنا كما يلي: يقوم

    // Capitalizes a copy of a string
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        char *s = get_string("s: ");
    
        // Allocate memory for another string
        char *t = malloc(strlen(s) + 1);
    
        // Copy string into memory, including '\0'
        for (int i = 0; i <= strlen(s); i++)
        {
            t[i] = s[i];
        }
    
        // Capitalize copy
        t[0] = toupper(t[0]);
    
        // Print strings
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }
    

    لاحظ ذلك تم الآن إنشاء malloc(strlen(s) + 1) بإنشاء كتلة من الذاكرة بطول السلسلة s زائد واحد. وهذا يسمح بإدراج فارغة \0 الحرف في السلسلة النهائية المنسوخة. ثم، تمر حلقة for عبر السلسلة s ويقوم بتعيين كل قيمة لنفس الموقع على السلسلة t. يمكنك تنزيل هذا الرمز هنا.

  • اتضح أن الشيفرة الخاص بنا غير فعال. قم بتعديل الشيفرة الخاص بك كما يلي: يتم تعريف

    // Capitalizes a copy of a string, defining n in loop too
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        char *s = get_string("s: ");
    
        // Allocate memory for another string
        char *t = malloc(strlen(s) + 1);
    
        // Copy string into memory, including '\0'
        for (int i = 0, n = strlen(s); i <= n; i++)
        {
            t[i] = s[i];
        }
    
        // Capitalize copy
        t[0] = toupper(t[0]);
    
        // Print strings
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }
    

    لاحظ ذلك تم الآن إنشاء n = strlen(s) الآن في الجانب الأيسر من for loop. من الأفضل عدم استدعاء الوظائف غير الضرورية في الحالة المتوسطة لـ for ، حيث سيتم تشغيلها مرارًا وتكرارًا. عند التحرك n = strlen(s) على الجانب الأيسر، الوظيفة strlen يعمل مرة واحدة فقط. يمكنك تنزيل هذا الرمز هنا.

  • ال يتم استخدام الدالة C تحتوي اللغة على وظيفة مضمنة لنسخ السلاسل التي تسمى strcpy. ويمكن تنفيذها على النحو التالي:

    // Capitalizes a copy of a string using strcpy
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        char *s = get_string("s: ");
    
        // Allocate memory for another string
        char *t = malloc(strlen(s) + 1);
    
        // Copy string into memory
        strcpy(t, s);
    
        // Capitalize copy
        t[0] = toupper(t[0]);
    
        // Print strings
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    }
    

    لاحظ ذلك تم الآن إنشاء strcpy يقوم بنفس العمل الذي نقوم به تم تنفيذ حلقة for سابقًا. يمكنك تنزيل هذا الرمز هنا.

  • كلاهما get_string و malloc العودة NULL، قيمة خاصة في الذاكرة، في حالة حدوث خطأ ما. يمكنك كتابة رمز يمكنه التحقق من ذلك شرط NULL كالتالي:

    // Capitalizes a copy of a string without memory errors
    
    #include <cs50.h>
    #include <ctype.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
        // Get a string
        char *s = get_string("s: ");
        if (s == NULL)
        {
            return 1;
        }
    
        // Allocate memory for another string
        char *t = malloc(strlen(s) + 1);
        if (t == NULL)
        {
            return 1;
        }
    
        // Copy string into memory
        strcpy(t, s);
    
        // Capitalize copy
        if (strlen(t) > 0)
        {
            t[0] = toupper(t[0]);
        }
    
        // Print strings
        printf("s: %s\n", s);
        printf("t: %s\n", t);
    
        // Free memory
        free(t);
        return 0;
    }
    

    لاحظ أنه إذا كانت السلسلة التي تم الحصول عليها ذات طول 0 أو فشل malloc، تم إرجاع NULL . علاوة على ذلك، لاحظ ذلك free يتيح للكمبيوتر معرفة أنك انتهيت من كتلة الذاكرة هذه التي قمت بإنشائها عبرها malloc. يمكنك تنزيل هذا الرمز هنا.

فالجريند (Valgrind)

  • فالجريند هي أداة يمكنها التحقق لمعرفة ما إذا كانت هناك مشكلات متعلقة بالذاكرة في برامجك التي استخدمتها malloc. على وجه التحديد، فإنه يتحقق لمعرفة ما إذا كنت free كل الذاكرة التي قمت بتخصيصها.
  • خذ بعين الاعتبار الشيفرة التالي لـ memory.c:

    // Demonstrates memory errors via valgrind
    
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int *x = malloc(3 * sizeof(int));
        x[1] = 72;
        x[2] = 73;
        x[3] = 33;
    }
    

    لاحظ أن تشغيل هذا البرنامج لا يسبب أية أخطاء. بينما يتم استخدام malloc لتخصيص ذاكرة كافية لمصفوفة، ويفشل الشيفرة في ذلك free التي خصصت الذاكرة. يمكنك تنزيل هذا الرمز هنا.

  • إذا كتبت make memory متبوعة بـ valgrind ./memory، سوف تحصل على تقرير من valgrind that will report where memory has been lost as a result of your program. One error that valgrind reveals is that we attempted to assign the value of 33 في x من 3، حيث قمنا بتخصيص مصفوفة من الحجم فقط 3 (x[0], x[1]و x[2]). خطأ آخر هو أننا لم نتحرر أبدًا x.
  • يمكنك تعديل الشيفرة الخاص بك لتحرير ذاكرة x على النحو التالي:

    // Demonstrates memory errors via valgrind
    
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int *x = malloc(3 * sizeof(int));
        x[0] = 72;
        x[1] = 73;
        x[2] = 33;
        free(x);
    }
    

    لاحظ أن تشغيل valgrind again now results in no memory leaks or errors

القيم المهملة (Garbage Values)

  • عندما تطلب من المترجم كتلة من الذاكرة، ليس هناك ما يضمن أن هذه الذاكرة ستكون فارغة.
  • من المحتمل جدًا أن الذاكرة التي قمت بتخصيصها قد تم استخدامها مسبقًا بواسطة الكمبيوتر. وبناء على ذلك، قد ترى غير المرغوب فيه أو القيم المهملة. هذا نتيجة حصولك على كتلة من الذاكرة ولكنك لم تقم بتهيئتها. على سبيل المثال، ضع في اعتبارك التعليمة البرمجية التالية لـ garbage.c:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int scores[1024];
        for (int i = 0; i < 1024; i++)
        {
            printf("%i\n", scores[i]);
        }
    }
    

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

متعة المؤشر مع بينكي (Pointer Fun with Binky)

المبادلة (Swapping)

  • في العالم الحقيقي، هناك حاجة شائعة في البرمجة وهي تبديل قيمتين. بطبيعة الحال، من الصعب تبديل متغيرين دون مساحة تخزين مؤقتة. في الممارسة العملية، يمكنك الكتابة code swap.c واكتب الشيفرة كما يلي لرؤية ذلك عمليًا:

    // Fails to swap two integers
    
    #include <stdio.h>
    
    void swap(int a, int b);
    
    int main(void)
    {
        int x = 1;
        int y = 2;
    
        printf("x is %i, y is %i\n", x, y);
        swap(x, y);
        printf("x is %i, y is %i\n", x, y);
    }
    
    void swap(int a, int b)
    {
        int tmp = a;
        a = b;
        b = tmp;
    }
    

    لاحظ أنه أثناء تشغيل هذا الرمز، فإنه لا يعمل. القيم، حتى بعد إرسالها إلى وظيفة swap ، لا تقم بالتبديل. لماذا؟ يمكنك تنزيل هذا الرمز هنا.

  • عندما تقوم بتمرير قيم إلى دالة، فإنك توفر نسخًا فقط. ال يقتصر من x و y على الوظيفة الرئيسية حيث تتم كتابة الشيفرة حاليًا. أي أن القيم x و y تم إنشاؤه في الشكل المجعد {} الأقواس من وظيفة main لها نطاق فقط وظيفة main . في الشيفرة الخاص بنا أعلاه، x و y يتم تمريرها قيمة .
  • خذ بعين الاعتبار الصورة التالية:

    a rectangle with شيفرة الآلة at top followed by globals heap and stack

    لاحظ ذلك تم الآن إنشاء عالمي المتغيرات، التي لم نستخدمها في هذه الدورة، تعيش في مكان واحد في الذاكرة. يتم تخزين وظائف مختلفة في stack في منطقة أخرى من الذاكرة.

  • الآن، تأمل الصورة التالية:

    a rectangle with main function at bottom and swap function directly above it

    لاحظ ذلك تم الآن إنشاء main و swap لديهما قسمان منفصلان إطارات أو مناطق الذاكرة. لذلك، لا يمكننا ببساطة تمرير القيم من دالة إلى أخرى لتغييرها.

  • قم بتعديل الشيفرة الخاص بك كما يلي:

    // Swaps two integers using pointers
    
    #include <stdio.h>
    
    void swap(int *a, int *b);
    
    int main(void)
    {
        int x = 1;
        int y = 2;
    
        printf("x is %i, y is %i\n", x, y);
        swap(&x, &y);
        printf("x is %i, y is %i\n", x, y);
    }
    
    void swap(int *a, int *b)
    {
        int tmp = *a;
        *a = *b;
        *b = tmp;
    }
    

    لاحظ أنه لا يتم تمرير المتغيرات قيمة لكن بواسطة مرجع. أي أن عناوين يتم توفير x و y للوظيفة. لذلك، يمكن لوظيفة swap معرفة مكان إجراء التغييرات على الفعل الفعلي x و y من الوظيفة الرئيسية. يمكنك تنزيل هذا الرمز هنا.

  • يمكنك تصور ذلك على النحو التالي:

    x and y stored in main function being passed by reference to the swap function

تجاوز السعة (Overflow)

  • A تجاوز سعة الكومة هو عندما تفيض الكومة، وتلمس مناطق من الذاكرة ليس من المفترض أن تفعلها.
  • A تجاوز سعة المكدس يحدث عندما يتم استدعاء عدد كبير جدًا من الوظائف، مما يؤدي إلى تجاوز حجم الذاكرة المتوفرة.
  • يتم أخذ كلا الأمرين في الاعتبار تجاوز سعة المخزن المؤقت.

scanf

  • في CS50، قمنا بإنشاء وظائف مثل get_int لتبسيط عملية الحصول على المدخلات من المستخدم.
  • scanf هي وظيفة مضمنة يمكنها الحصول على مدخلات المستخدم.
  • يمكننا إعادة التنفيذ get_int يمكن استخدامه بسهولة scanf على النحو التالي:

    // Gets an int from user using scanf
    
    #include <stdio.h>
    
    int main(void)
    {
        int n;
        printf("n: ");
        scanf("%i", &n);
        printf("n: %i\n", n);
    }
    

    لاحظ أن قيمة يتم تخزين n في موقع n في السطر scanf("%i", &n). يمكنك تنزيل هذا الرمز هنا.

  • ومع ذلك، تحاول إعادة التنفيذ get_string ليس بالأمر السهل. خذ بعين الاعتبار ما يلي:

    // Dangerously gets a string from user using scanf with array
    
    #include <stdio.h>
    
    int main(void)
    {
        char s[4];
        printf("s: ");
        scanf("%s", s);
        printf("s: %s\n", s);
    }
    

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

  • علاوة على ذلك، يمكن تعديل الشيفرة الخاصة بك على النحو التالي. ومع ذلك، يتعين علينا تخصيص قدر معين من الذاكرة مسبقًا لسلسلة ما:

    // Using malloc
    
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        char *s = malloc(4);
        if (s == NULL)
        {
            return 1;
        }
        printf("s: ");
        scanf("%s", s);
        printf("s: %s\n", s);
        free(s);
        return 0;
    }
    

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

  • من خلال تبسيط الشيفرة الخاص بنا على النحو التالي، يمكننا فهم هذه المشكلة الأساسية للتخصيص المسبق بشكل أكبر:

    #include <stdio.h>
    
    int main(void)
    {
        char s[4];
        printf("s: ");
        scanf("%s", s);
        printf("s: %s\n", s);
    }
    

    لاحظ أنه إذا قمنا بتخصيص مصفوفة من الحجم مسبقًا 4، يمكننا الكتابة cat ووظائف البرنامج. ومع ذلك، سلسلة أكبر من هذا يمكن إنشاء خطأ.

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

إدخال/إخراج الملف (File I/O)

  • يمكنك القراءة من الملفات ومعالجتها. بينما ستتم مناقشة هذا الموضوع بشكل أكبر في الأسبوع المقبل، ضع في اعتبارك الشيفرة التالي لـ phonebook.c:

    // Saves names and numbers to a CSV file
    
    #include <cs50.h>
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        // Open CSV file
        FILE *file = fopen("phonebook.csv", "a");
    
        // Get name and number
        char *name = get_string("Name: ");
        char *number = get_string("Number: ");
    
        // Print to file
        fprintf(file, "%s,%s\n", name, number);
    
        // Close file
        fclose(file);
    }
    

    لاحظ أن هذا الرمز يستخدم مؤشرات للوصول إلى الملف. يمكنك تنزيل هذا الرمز هنا.

  • يمكنك إنشاء ملف يسمى phonebook.csv قبل تشغيل الشيفرة أعلاه أو التنزيل phonebook.csv. بعد تشغيل البرنامج أعلاه وإدخال الاسم ورقم الهاتف، ستلاحظ أن هذه البيانات موجودة في ملف CSV الخاص بك.
  • إذا أردنا التأكد من ذلك phonebook.csv موجود قبل تشغيل البرنامج، يمكننا تعديل الشيفرة الخاص بنا على النحو التالي:

    // Saves names and numbers to a CSV file, checking for NULL
    
    #include <cs50.h>
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        // Open CSV file
        FILE *file = fopen("phonebook.csv", "a");
        if (file == NULL)
        {
            return 1;
        }
    
        // Get name and number
        char *name = get_string("Name: ");
        char *number = get_string("Number: ");
    
        // Print to file
        fprintf(file, "%s,%s\n", name, number);
    
        // Close file
        fclose(file);
    }
    

    لاحظ أن هذا البرنامج يحمي من أ NULL المؤشر عن طريق الاستدعاء return 1. يمكنك تنزيل هذا الرمز هنا.

  • يمكننا تنفيذ برنامج النسخ الخاص بنا عن طريق الكتابة code cp.c وكتابة الشيفرة كالتالي:

    // Copies a file
    
    #include <stdio.h>
    
    typedef unsigned char BYTE;
    
    int main(int argc, char *argv[])
    {
        FILE *src = fopen(argv[1], "rb");
        FILE *dst = fopen(argv[2], "wb");
    
        BYTE b;
    
        while (fread(&b, sizeof(b), 1, src) != 0)
        {
            fwrite(&b, sizeof(b), 1, dst);
        }
    
        fclose(dst);
        fclose(src);
    }
    

    لاحظ أن هذا الملف يقوم بإنشاء نوع بيانات خاص بنا يسمى BYTE، وهو ما يعادل حجم uint8_t. ثم يقرأ الملف أ BYTE ويكتبه في ملف. يمكنك تنزيل هذا الرمز هنا.

  • تعتبر BMPs أيضًا مجموعة متنوعة من البيانات التي يمكننا فحصها ومعالجتها. هذا الأسبوع، ستفعل ذلك بالضبط في مجموعة المشكلات الخاصة بك!

التلخيص (Summing Up)

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

  • فن البكسل
  • سداسي عشري
  • الذاكرة
  • المؤشرات
  • سلاسل
  • المؤشر الحسابي
  • مقارنة السلسلة
  • النسخ
  • مالوك وفالجريند
  • القيم المهملة
  • المبادلة
  • تجاوز السعة
  • scanf
  • إدخال/إخراج الملف

نراكم في المرة القادمة!

أنهيت قراءة الملاحظات؟

يمكنك قراءة الدورة كاملة دون حساب. يصبح حفظ التقدم متاحًا بعد تفعيل الحساب، ويبقى محفوظًا بعد انتهاء العضوية.