درس ۲۰ از ۴۹ · برنامه‌های بزرگ‌تر

مقدار، ارجاع و حافظه

سلام بدون زباله‌روب کار می‌کند. هیچ چیز پشت سر شما آزاد نمی‌شود و هیچ چیز بی‌آنکه دلیلش پیدا باشد بین دو متغیر شریک نمی‌شود. این درس نشان می‌دهد مقدار کی رونوشت می‌شود، کی شریک می‌ماند، و پس دادن حافظه با کیست.

ساختارها رونوشت می‌شوند

وقتی ساختاری را در متغیر دیگری می‌گذارید یا به تابع می‌فرستید، یک رونوشت ساخته می‌شود و تغییر رونوشت روی اصل اثری ندارد. اگر تابع باید مقدار صدازننده را تغییر دهد، ورودی را ارجاعی (&:) تعریف کنید. ورودی ارجاعی جلوی رونوشت گرفتن از ساختارهای بزرگ را هم می‌گیرد؛ درس تابع‌ها بیشتر درباره‌اش گفته است.

mem-copy.salam
ساختار حساب:
    همگانی موجودی: صحیح = ۰
پایان

تابع واریز(ح &: حساب، مبلغ: صحیح):
    ح.موجودی += مبلغ
پایان

تابع ریشه:
    ناپایا الف := حساب { موجودی = ۱۰۰ }
    ناپایا ب := الف
    ب.موجودی = ۵۰۰
    سرچاپ الف.موجودی، ب.موجودی
    واریز(الف، ۲۵)
    سرچاپ الف.موجودی
پایان
خروجی100 500 125

آرایه‌ها رونوشت می‌شوند، برش‌ها نما هستند

آرایه‌ی ثابتی مثل صحیح[۳] هم مقدار است: گذاشتنش در متغیر دیگر یا فرستادنش به ورودی‌ای از گونه‌ی صحیح[۳] همه‌ی عضوها را رونوشت می‌کند. ورودی برش، یعنی صحیح[]، نمایی از آرایه‌ی صدازننده است و نوشتن در آن اصل را تغییر می‌دهد. گونه‌ی ورودی را بر اساس کاری انتخاب کنید که تابع اجازه‌اش را دارد.

mem-array.salam
تابع صفر کن اولی(نمره‌ها: صحیح[۳]):
    نمره‌ها[۰] = ۰
پایان

تابع صفر کن اولی درجا(نمره‌ها: صحیح[]):
    نمره‌ها[۰] = ۰
پایان

تابع ریشه:
    هفته := [۷، ۵، ۹]
    ناپایا برنامه := هفته
    برنامه[۱] = ۱۰۰
    سرچاپ هفته، برنامه
    صفر کن اولی(هفته)
    سرچاپ هفته
    ناپایا زنده := [۷، ۵، ۹]
    صفر کن اولی درجا(زنده)
    سرچاپ زنده
پایان
خروجی[7, 5, 9] [7, 100, 9] [7, 5, 9] [0, 5, 9]

وکتور حافظه‌اش را شریک می‌شود

وکتور و نگاشت یک سرآیند کوچک‌اند (عضوها کجا هستند، چند تا هستند، چقدر جا مانده) که به حافظه‌ای روی هیپ اشاره می‌کند. گذاشتن آن‌ها در متغیر دیگر فقط سرآیند را رونوشت می‌کند. دو نام عضوهای یکسانی را می‌بینند، پس سبد.بنشان(۰، "برنج") در نما هم دیده می‌شود، ولی هر نام طول خودش را دارد، پس "چای"ی که به سبد افزوده شد در نما شمرده نمی‌شود. به این حالت نیمه‌شریک تکیه نکنید. هر جا مقدار مستقل لازم دارید، آن را با رونوشت() بخواهید و مثل هر وکتور دیگری آزادش کنید.

mem-share.salam
تابع ریشه:
    سبد := وکتور {} برگردان وکتور<رشته>
    دیرکن سبد.آزادکن()
    سبد.بیفزا("نان")
    سبد.بیفزا("شیر")
    نما := سبد
    سبد.بنشان(۰، "برنج")
    سبد.بیفزا("چای")
    سرچاپ سبد، نما

    ذخیره := سبد.رونوشت()
    دیرکن ذخیره.آزادکن()
    سبد.بنشان(۱، "تخم‌مرغ")
    ذخیره.بیفزا("نمک")
    سرچاپ سبد، ذخیره
پایان
خروجی["برنج", "شیر", "چای"] ["برنج", "شیر"] ["برنج", "تخم‌مرغ", "چای"] ["برنج", "شیر", "چای", "نمک"]

نگاشت هم همین‌طور رفتار می‌کند، و وکتوری که درون یک ساختار است هم: رونوشت ساختار فقط سرآیند وکتورش را رونوشت می‌کند، نه عضوها را.

رونوشت و رونوشت عمیق

رونوشت() فقط برای ظرف بیرونی حافظه‌ی تازه می‌گیرد. وقتی عضوها خودشان ظرف‌اند، مثل وکتوری از ردیف‌ها، رونوشت هنوز در همه‌ی ردیف‌ها با اصل شریک است. رونوشت عمیق() تا ته می‌رود و ردیف‌ها را هم رونوشت می‌کند. در مثال زیر یک صندلی در اصل تغییر می‌کند: رونوشت سطحی آن را می‌بیند و رونوشت عمیق نه.

mem-deep.salam
تابع آزادکن جدول(ج: وکتور<وکتور<صحیح>>):
    هر ردیف در ج:
        ردیف.آزادکن()
    پایان
    ج.آزادکن()
پایان

تابع ریشه:
    صندلی‌ها := وکتور {} برگردان وکتور<وکتور<صحیح>>
    تکرار ۲ در ر:
        ردیف := وکتور {} برگردان وکتور<صحیح>
        تکرار ۳ در س:
            ردیف.بیفزا(ر * ۳ + س + ۱)
        پایان
        صندلی‌ها.بیفزا(ردیف)
    پایان
    سطحی := صندلی‌ها.رونوشت()
    عمیق := صندلی‌ها.رونوشت عمیق()
    صندلی‌ها.بگیر(۰).بنشان(۰، ۰)
    سرچاپ صندلی‌ها
    سرچاپ سطحی
    سرچاپ عمیق
    سطحی.آزادکن()
    آزادکن جدول(عمیق)
    آزادکن جدول(صندلی‌ها)
پایان
خروجی[[0, 2, 3], [4, 5, 6]] [[0, 2, 3], [4, 5, 6]] [[1, 2, 3], [4, 5, 6]]

هر رونوشت باید آزاد شود، و همین‌طور هر ظرفی که رونوشت عمیق ساخته است؛ برای همین آزادکن جدول اول هر ردیف را آزاد می‌کند و بعد وکتور بیرونی را، اما رونوشت سطحی، که ردیف‌هایش مال اصل‌اند، فقط حافظه‌ی خودش را آزاد می‌کند. ساختارها هم رونوشت() و رونوشت عمیق() دارند که مترجم از روی فیلدهایشان می‌سازد.

آزاد کردن: آزادکن و دیرکن

عددها، منطقی‌ها، ساختارهای ساده و آرایه‌های ثابت نیازی به آزاد کردن ندارند. هر چیزی که حافظه‌ی هیپ نگه دارد نیاز دارد: وکتور، نگاشت، جعبه‌ی پویا و رشته‌هایی که در بخش بعد می‌آیند. عادت خوب این است که درست در خط بعد از ساختن x بنویسید دیرکن x.آزادکن() تا هر وقت تابع به هر راهی تمام شد، آزاد شود.

تابعی که ظرفی می‌سازد و برمی‌گرداند آن را تحویل می‌دهد: از آن به بعد صدازننده صاحب آن است و خودش آزادش می‌کند. آزاد کردن ظرف حافظه‌ی خود ظرف را آزاد می‌کند، نه چیزهایی را که عضوها صاحبشان‌اند؛ پس نگاشتی از وکتورها اول تک‌تک وکتورها را آزاد می‌کند.

mem-free.salam
ساختار دانش‌آموز:
    همگانی نام: رشته = ""
    همگانی کلاس: رشته = ""
پایان

تابع گروه بندی(همه: وکتور<دانش‌آموز>): نگاشت<رشته، وکتور<رشته>>:
    گروه‌ها := نگاشت {} برگردان نگاشت<رشته، وکتور<رشته>>
    هر د در همه:
        نام‌ها := گروه‌ها.دارد(د.کلاس) ? گروه‌ها.بگیر(د.کلاس) : وکتور {} برگردان وکتور<رشته>
        نام‌ها.بیفزا(د.نام)
        گروه‌ها.درج(د.کلاس، نام‌ها)
    پایان
    برگشت گروه‌ها
پایان

تابع ریشه:
    شاگردان := وکتور {} برگردان وکتور<دانش‌آموز>
    دیرکن شاگردان.آزادکن()
    شاگردان.بیفزا(دانش‌آموز { نام = "سارا"، کلاس = "هفتم الف" })
    شاگردان.بیفزا(دانش‌آموز { نام = "رضا"، کلاس = "هفتم ب" })
    شاگردان.بیفزا(دانش‌آموز { نام = "مینا"، کلاس = "هفتم الف" })

    گروه‌ها := گروه بندی(شاگردان)
    دیرکن گروه‌ها.آزادکن()
    هر کلاس، نام‌ها در گروه‌ها:
        سرچاپ کلاس، نام‌ها
    پایان
    هر _، نام‌ها در گروه‌ها:
        نام‌ها.آزادکن()
    پایان
پایان
خروجیهفتم الف ["سارا", "مینا"] هفتم ب ["رضا"]

دقت کنید گروه بندی چطور نامی را اضافه می‌کند. گروه‌ها.بگیر(ک) رونوشتی از سرآیند وکتور برمی‌گرداند، پس اگر فقط روی آن بیفزا کنید، ممکن است نگاشت همان طول قدیمی را نگه دارد. بیرون آوردن وکتور، افزودن به آن و برگرداندنش با درج نگاشت را به‌روز نگه می‌دارد.

رشته را چه کسی آزاد می‌کند

رشته‌ی ثابتی مثل "سلام" بخشی از خود برنامه است و هرگز آزاد نمی‌شود. رشته‌ای که هنگام اجرا ساخته می‌شود روی هیپ است. قاعده‌ی کتابخانه‌ی استاندارد ساده است: تابعی که رشته برمی‌گرداند، در همه‌ی مسیرها رشته‌ی تازه‌ای برمی‌گرداند که صاحبش صدازننده است، پس وقتی کارتان تمام شد آن را با حافظه.آزادسازی(ر برگردان تهی*) آزاد کنید. در تابع‌های خودتان هم همین قاعده را رعایت کنید.

mem-str.salam
واردسازی رشته
واردسازی حافظه

تابع نامک(عنوان: رشته): رشته:
    برگشت رشته.جایگزینی(عنوان، " "، "-")
پایان

تابع برچسب(عنوان: رشته): رشته:
    ن := نامک(عنوان)
    دیرکن حافظه.آزادسازی(ن برگردان تهی*)
    برگشت "#" + ن
پایان

تابع ریشه:
    نشانی := نامک("نخستین نوشته‌ی من")
    دیرکن حافظه.آزادسازی(نشانی برگردان تهی*)
    سرچاپ نشانی

    ب := برچسب("خبرهای سلام")
    دیرکن حافظه.آزادسازی(ب برگردان تهی*)
    سرچاپ ب

    ناپایا بافر := رشته.سازنده()
    دیرکن رشته.آزادسازی بافر(بافر)
    تکرار ۳ در ی:
        اگر ی > ۰:
            رشته.افزودن به بافر(بافر، "، ")
        پایان
        رشته.افزودن به بافر(بافر، "ردیف ")
        رشته.افزودن عدد به بافر(بافر، (ی + ۱) برگردان صحیح۶۴)
    پایان
    گزارش := رشته.رشته بافر(بافر)
    دیرکن حافظه.آزادسازی(گزارش برگردان تهی*)
    سرچاپ گزارش
پایان
خروجینخستین-نوشته‌ی-من #خبرهای-سلام ردیف 1، ردیف 2، ردیف 3
  • برچسب نتیجه‌ی نامک را پس از استفاده آزاد می‌کند. اگر به‌جایش می‌نوشتید برگشت "#" + نامک(عنوان)، آن رشته صاحبی نداشت و هرگز آزاد نمی‌شد.
  • هر + روی رشته‌ها رشته‌ی تازه‌ای روی هیپ می‌سازد، پس زنجیره‌ای مثل الف + ب + ج نتیجه‌ی میانی را جا می‌گذارد. برای برنامه‌ای که زود تمام می‌شود مهم نیست؛ برای حلقه یا سروری که روزها کار می‌کند مهم است. متنی را که تکه‌تکه ساخته می‌شود با رشته.سازنده() بسازید، نتیجه را با رشته بافر بگیرید و بافر را با آزادسازی بافر آزاد کنید.

اشاره‌گر و حافظه

برای حافظه‌ی خام و برای کتابخانه‌های C، سلام اشاره‌گر دارد: صحیح۶۴* به یک صحیح۶۴ اشاره می‌کند، تهی* اشاره‌گر بی‌گونه است و پوچ اشاره‌گر خالی. حافظه.تخصیص(بایت‌ها) یک تکه حافظه می‌دهد، پ[ی] عضو ی‌ام را می‌خواند و می‌نویسد، و حافظه.آزادسازی آن تکه را پس می‌دهد. اندازه‌گونه(گونه) می‌گوید هر عضو چند بایت جا می‌گیرد.

mem-pointer.salam
واردسازی حافظه

تابع ریشه:
    ن := ۵
    مربع‌ها := حافظه.تخصیص((ن برگردان طبیعی۶۴) * اندازه‌گونه(صحیح۶۴)) برگردان صحیح۶۴*
    دیرکن حافظه.آزادسازی(مربع‌ها برگردان تهی*)
    تکرار ن در ی:
        مربع‌ها[ی] = (ی * ی) برگردان صحیح۶۴
    پایان
    ناپایا جمع := ۰ برگردان صحیح۶۴
    تکرار ن در ی:
        جمع += مربع‌ها[ی]
    پایان
    سرچاپ مربع‌ها[۴]، جمع
    هیچ := پوچ برگردان صحیح۶۴*
    سرچاپ هیچ == پوچ
پایان
خروجی16 30 true

هیچ چیز اشاره‌گر را برایتان بررسی نمی‌کند: خواندن بیرون از تکه‌ی گرفته‌شده، یا به کار بردن اشاره‌گر پس از حافظه.آزادسازی، اشکالی است که ممکن است همان لحظه خودش را نشان ندهد. کد معمولی سلام کمتر به اشاره‌گر نیاز دارد؛ اول سراغ وکتور بروید. درس کار با C جاهایی را نشان می‌دهد که اشاره‌گر لازم است.

آنچه مترجم پیشاپیش می‌گیرد

بخش بزرگی از این‌ها پیش از اجرای برنامه بررسی می‌شود. استفاده از حافظه پس از آزاد شدنش E139 است، و اگر فقط برخی مسیرها پیش از آن آزادش کنند W142؛ دوبار آزاد کردن E140 یا W143 است؛ برگرداندن مقداری که defer خود تابع آزادش می‌کند E141؛ و نگه داشتن رونوشتی از یک وکتور، یا اشاره‌گری از .ref()، پس از pushی که ممکن است حافظه‌اش را جابه‌جا کند W144. بلوکی از mem.Allocate که هیچ چیز آزادش نکند، برنگرداند، نگه ندارد یا به جای دیگری ندهد W158 است، و خواندن یا نوشتن پس از پایان چنین بلوکی E146. هر پیام خطی را نام می‌برد که حافظه در آن آزاد یا گرفته شده است.

پیدا کردن نشتی با memcheck

salam memcheck app.salam برنامه را با اطلاعات اشکال‌زدایی می‌سازد و زیر یک بررسی‌کننده‌ی حافظه اجرا می‌کند: اگر Valgrind نصب باشد با آن، وگرنه با AddressSanitizer. وقتی برنامه تمام شد، حافظه‌ای را که هرگز آزاد نشده و هر خواندن یا نوشتنی بیرون از یک تکه را گزارش می‌دهد، هر کدام با شماره‌ی خطی از کد سلام که آن حافظه را گرفته است. salam build app.salam --asan همین بررسی AddressSanitizer را درون یک برنامه‌ی معمولی می‌سازد.

مثال‌های رشته و اشاره‌گر بالا زیر salam memcheck بی‌هیچ خطایی اجرا می‌شوند. هر از گاهی برنامه‌های خودتان را هم با آن بررسی کنید، و برنامه‌ای را که قرار است مدت زیادی کار کند همیشه.

چند قاعده‌ی سرانگشتی

  • ساختارها و آرایه‌های ثابت مقدارند و با گذاشتن در متغیر دیگر رونوشت می‌شوند. اگر تابع باید مقدار صدازننده را تغییر دهد، از &: استفاده کنید.
  • گذاشتن وکتور یا نگاشت در متغیر دیگر عضوها را شریک می‌کند. هر جا مقدار مستقل لازم است، رونوشت() یا رونوشت عمیق() بگیرید.
  • هر کس حافظه‌ی هیپ می‌سازد آزادش می‌کند، یا به کسی می‌سپارد که آزادش کند. درست پس از ساختن x بنویسید دیرکن x.آزادکن().
  • رشتهای که تابعی برمی‌گرداند مال صدازننده است؛ با حافظه.آزادسازی آزادش کنید. رشته‌ی ثابت را هرگز آزاد نکنید.
  • اول چیزهای درون ظرف را آزاد کنید، بعد خود ظرف را.
  • برنامه‌هایی را که مدت زیادی کار می‌کنند با salam memcheck بررسی کنید.