در قسمت قبل آموزش برنامه نویسی C به مدیریت حافظه در زبان C: چالشها و خطرات سرریز آرایه پرداختیم. در این قسمت به آشنایی با متغیرهای محلی و رویهها در زبان C می پردازیم.
تا کنون از الگوی طراحی “one big mess” استفاده کردهایم. تمام کدها درون main قرار داده شده و همه متغیرها در ابتدای برنامه تعریف میشوند. این روش زمانی که برنامه شما حدوداً 100 خط یا کمتر باشد به خوبی کار میکند، اما زمانی که با برنامهای 500,000 خطی سروکار دارید، برای سازماندهی نیازمند راهکاری هستید.
این فصل به روشهایی برای محدودکردن اسکوپ متغیرها و دستورات میپردازد تا بتوانید درک، مدیریت و نگهداری بلوکهای کد طولانی و غیرقابلمدیریت را آسانتر کنید.
بهعنوانمثال، شما میتوانید از یک متغیر سراسری (global) در هر جای برنامه استفاده کنید. اما برای اینکه بدانید در یک برنامه 500,000 خطی کجا و چگونه استفاده شده است، باید کل 500,000 خط را بررسی کنید. یک متغیر محلی اسکوپ محدودی دارد. برای اینکه بفهمید یک متغیر محلی کجا و چگونه استفاده میشود، تنها کافیست به 50 تا 200 خط کدی که در آن معتبر است نگاه کنید.
با طولانیتر شدن برنامهها، یاد خواهید گرفت که چگونه کد را به بخشهای قابل فهمی به نام رویه (procedure) تقسیم کنید. متغیرهای سراسری برای هر رویه در دسترس خواهند بود، اما میتوانید متغیرهای محلی تعریف کنید که فقط برای یکرویه خاص در دسترس باشند. همچنین نحوه سازماندهی داخلی متغیرهای محلی در
فریمهای پشته (stack frame) را فرا خواهید گرفت. باتوجهبه مقدار محدود حافظه در میکروکنترلر STM ، درک اینکه چقدر از حافظه پشته استفاده میکنیم بسیار مهم است.
در نهایت، با مفهوم بازگشت (recursion) آشنا خواهید شد، جایی که یک رویه به خودش اشاره میکند. بازگشت در آنچه میتواند انجام دهد پیچیده است، اما اگر قوانین را درک کرده و آنها را دنبال کنید، ساده خواهد بود.
توجه داشته باشید که هیچچیز در C مانع از نوشتن رویههای 30,000 خطی یا ایجاد متغیرهای محلی با اسکوپای به همان اندازه توسط برنامه نویسان بی دقت نمیشود. با این حال، فرض بر این است که شما فردی منطقی هستید و میخواهید به روشی کد بزنید که باعث کاهش سردرگمی به جای افزایش آن شود.
تا به این قسمت، فقط از متغیرهای سراسری (global) استفاده کردهایم که در کل برنامه (از خطی که اعلام میشوند تا انتهای برنامه) در دسترس هستند. متغیرهای محلی در بخش بسیار کوچکتری از برنامه یا همان «محل (Local)» در دسترس قرار میگیرند. محدودهای که یک متغیر در آن معتبر است، «اسکوپ» (scope) نامیده میشود. کد 16-1 اعلام متغیرهای محلی را نشان میدهد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /* برنامهی بیاستفاده برای نمایش متغیرهای محلی */ #include <stdio.h> int global = 5; // یک متغیر سراسری int main() { int localToProcedure = 3; // ... کاری انجام بده { 1 int local = 6; // یک متغیر محلی { int veryLocal = 7; // یک متغیر حتی محلیتر variable // ... کاری انجام بده 2} // veryLocal دیگر معتبر نیست 3} // local دیگر معتبر نیست return (0); } |
کد 16-1: متغیرهای محلی
اسکوپ یک متغیر محلی از جایی که اعلام میشود شروع شده و تا انتهای براکتهای چینشی {} همان بلاک ادامه پیدا میکند. متغیر localToProcedure برای کل تابع main معتبر است.
حالا بیایید به اسکوپهای کوچکتر نگاه کنیم که با اعلام متغیر محلی local شروع میشود. اسکوپ این متغیر در براکت ۲ که برای یک بلاک متفاوت (بخشی از کد محصور در براکتهای چینشی) است تمام نمیشود، بلکه تا براکت ۳ بلاکی که درست قبل از اعلام local شروع شده است ادامه پیدا میکند. متغیر veryLocal اسکوپ حتی کوچکتری دارد. این اسکوپ با اعلام int veryLocal =7; شروع شده و با پایان بلاک ۲ تمام میشود.
وقتی اسکوپ یک متغیر تمام میشود، برنامه دیگر نمیتواند از آن متغیر استفاده کند. برای مثال، تلاش برای برگرداندن مقدار veryLocal در انتهای main با استفاده از دستور return(veryLocal); کارساز نخواهد بود.
در مثال قبلی، تمام متغیرهای محلی علاوه بر داشتن اسکوپ متفاوت، نامهای متفاوتی هم داشتند. بااینحال، متغیرها میتوانند در اسکوپهای مختلف، نام یکسانی داشته باشند. اگر چندین متغیر نام یکسانی داشته باشند، زبان C از مقدار متغیری که در اسکوپ جاری قرار دارد استفاده میکند و سایر متغیرها را پنهان میکند. (لطفاً این کار را انجام ندهید، چون باعث سردرگمی کد میشود. در اینجا فقط به این موضوع اشاره شده است تا بدانید از چه چیزی باید اجتناب کنید.) بیایید به کد 16-2 نگاهی بیندازیم که برنامه نوشتهشده بسیار بدی را نشان میدهد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /* * برنامهی بیاستفاده برای نمایش متغیرهای پنهان */ #include <stdio.h> 1 int var = 7; // یک متغیر int main() { // ... کاری انجام بده { 2 int var = 13; // را پنهان میکند var=7 { 3 int var = 16; // را پنهان میکند var=7 و var=13 // ... کاری انجام بده } // ... کاری انجام بده } // ... کاری انجام بده return (0); } |
کد 16-2: متغیرهای پنهان
در این برنامه، سه متغیر تعریف میکنیم که همه نامشان var است. هنگامی که متغیر 2 تعریف میشود، متغیر 1 را پنهان میکند. به همین ترتیب، دستور int var = 16; متغیر 2 که 1 را پنهان کرده بود، پنهان میکند.
فرض کنید بخواهیم بعد از تعریف سوم عبارت زیر را اضافه کنیم:
1 | var = 42; |
کدام var را مقداردهی میکنیم؟ varای که در شمارهی 1، 2 یا 3 تعریف شده است؟ این حقیقت که مجبوریم این سؤال را بپرسیم، نشان میدهد که کد گیجکننده است. من این موضوع را بهعنوان تمرین به خواننده واگذار نمیکنم، چرا که راهحل درست این است که از ابتدا چنین کاری را انجام ندهیم.
یکرویه راهی برای تعریف کد است تا بتوان دوباره از آن استفاده کرد. بیایید به کد 16-3 نگاه کنیم که یک مثال ساده ارائه میدهد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /** سه بار «Hello» و یکبار «world!» چاپ میکند. */ #include <stdio.h> /** * Tell the world hello. */ void sayHello(void) { puts("Hello"); } int main() { sayHello(); sayHello(); sayHello(); puts("world!"); return (0); } |
کد 16-3: نمایش یک رویه
این برنامه سه بار «Hello» و سپس «world!» چاپ میکند. یکرویه با یک بلوک کامنت شروع میشود که الزاماً ضروری نیست، اما اگر میخواهید کد باکیفیت بنویسید، باید قبل از هر رویه یکی از آنها را قرار دهید. ابتدای بلوک کامنت (/**) نشان میدهد که ابزار مستندسازی Doxygen باید آن را پردازش کند. برای سازگاری با فرمت کتابخانههای STM، از همان قرارداد کامنتنویسی استفاده میکنیم.
عبارت void sayHello(void) به زبان C میگوید که نام رویه ما sayHello است. این رویه هیچچیزی برنمیگرداند (اولین void) و هیچ پارامتری را دریافت نمیکند (دومین void). بلوک {} که به دنبال این عبارت میآید، بدنه رویه را تعریف میکند و حاوی تمام دستورالعملهای اجرا شده توسط رویه است.
sayHello() ; فراخوانیهایی به رویه sayHello هستند. آنها به پردازنده دستور میدهند محل دستور بعدی (یا فراخوانی دیگری به sayHello یا فراخوانی به puts) را ذخیره کند و سپس برای اجرا با خط اول sayHello شروع کند. هنگامی که رویه تمام میشود (یا به یک دستور return برخورد میکند)، اجرا در نقطهای که در طول فراخوانی ذخیره شده ادامه مییابد.
در برنامهنویسی، هر رویه (procedure) دارای متغیرهای محلی خاص خود است. وظیفه کامپایلر سازماندهی حافظه برای نگهداری این متغیرها است. برای متغیرهای سراسری (global variables) که خارج از یکرویه قرار دارند، کامپایلر میگوید: «برای نگهداری عدد صحیحی به نام ‘Total’ به ۴ بایت حافظه نیاز دارم.» سپس لینکر (linker) این نیاز را مشاهده کرده و یک مکان فیزیکی در حافظه (برای مثال، 0xffffec04) را به متغیر اختصاص میدهد. متغیرهای سراسری بهصورت ایستا (statically) در زمان کامپایل انتساب داده میشوند، به این معنی که کامپایلر فضایی برای متغیرها در نظر میگیرد و این فضا دیگر تغییر نمیکند. این متغیرها هرگز از بین نمیروند و حافظهی آنها مجدداً تخصیص داده نمیشود.
متغیرهای محلی یکرویه پیچیدگی بیشتری دارند. این متغیرها باید بهصورت پویا (dynamically) در زمان اجرا تخصیص داده شوند. هنگامی که یکرویه شروع به کار میکند، تمام متغیرهای محلی آن رویه انتساب داده میشوند. (توجه داشته باشید که نوع خاصی از متغیر محلی به نام «متغیر محلی ایستا» وجود دارد که در زمان کامپایل تخصیص داده میشود، اما در این بحث فعلاً به آن نمیپردازیم.) هنگامی که رویه به پایان میرسد، این متغیرها از حافظه آزاد میشوند. کامپایلر با ایجاد یک قاب پشته (stack frame) در زمان شروع رویه و حذف آن در زمان پایان رویه، این کار را انجام میدهد. قاب پشته تمام اطلاعات موقت موردنیاز رویه را نگه میدارد.
بیایید به لیست 16-4 نگاه کنیم که یک برنامهی نمونه را نشان میدهد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /** * @brief برنامهای برای نشاندادن رویهها و متغیرهای محلی */ /** * تابعی که از تابع دیگری فراخوانده میشود */ void inner(void) { int i = 5; // یک متغیر int k = 3; // یک متغیر دیگر i = i + k; // انجام کاری با متغیرها } /** * تابع سطح بالا */ void outer(void) { int i = 6; // یک متغیر int j = 2; // یک متغیر دیگر i = j + i; // استفاده از متغیرها inner(); } int main() { outer(); return (0); |
اجازه دهید پروژهای برای این برنامه ایجاد کرده و دیباگ را آغاز کنیم. برنامه را در دیباگر اجرا کنید، سپس با استفاده از دستور «Step Into» (معمولاً کلید F5) بهصورت گامبهگام جلو بروید تا به خط ۱ برسید. صفحهی شما باید شبیه به شکل 16-1 باشد.
هنگامی که یک برنامه بارگذاری میشود، تمام متغیرهای تخصیصدادهشده بهصورت ایستا، مکانهای حافظهی مخصوص به خود را دریافت میکنند. در تراشهی STM32، این متغیرها به بخش پایینی حافظه (دسترسی تصادفی (RAM)) اختصاص داده میشوند. باقیماندهی حافظه برای انتساب پویا رزرو میشود. به طور خاص، از دو ناحیهی حافظه بهصورت پویا استفاده میشود:
برای اینکه ببینید پشته فراخوان در هر رویه کجا قرار دارد، روی تب Registers در پنل بالا سمت راست کلیک کنید و به پایین اسکرول کنید تا زمانی که رجیستر rsp را ببینید. شکل 16-2 نشان میدهد که حاوی مقدار 0x7fffffffd0e0 است.
بسته به ماشین، ممکن است از یک آدرس حافظه پایین شروع شود و به سمت بالا رشد کند یا از یک آدرس حافظه بالا شروع شود و به سمت پایین رشد کند. روی این ماشین (x86)، از بالا شروع میشود و به پایین میرود.
پشته فراخوان از فریم بیرونی در آدرس 0x7fffffffd0f0 قرار دارد. ازآنجاییکه پشته ما به سمت پایین رشد میکند، این عدد از پشته فراخوان برای main پایینتر است. پشته فراخوان داخلی در آدرس 0x7fffffffd110 قرار دارد
(به جدول 16-1 مراجعه کنید).
جدول 16-1: استفاده از پشته
آدرس | رویه | محتوا | توضیحات |
0x7fffffffd110 | main | <overhead> | انتهای پشته |
0x7fffffffd0f0 | outer | <overhead> i, j | |
0x7fffffffd0e0 | inner | <overhead> i, k | بالای پشته |
یک جمله کلیدی برای درک بهتر پشته: آخر وارد، اول خارج (last in, first out). هنگامی که کار با inner تمام شد، پشته فراخوان آن حذف میشود و سپس پشته فراخوان outer حذف میشود.
پنل Variables(که در شکل 16-1، بالا سمت راست نشاندادهشده است) متغیرهای i و k را نمایش میدهد. دیباگر متغیرها را در پشته فراخوان برای inner نمایش میدهد که با واقعیت، پشته فراخوان برای inner در پنل دیباگر (بالا سمت چپ) برجسته شده است نشان داده میشود. روی پشته فراخوان outer در پنل دیباگر کلیک کنید سپس پنل Variables را تغییر دهید و متغیرهای مربوط به outer را همانطور که در شکل 16-3 نشاندادهشده است، نمایش دهید.
با دیباگکردن برنامه و عبور از آخرین دستور inner ادامه دهیم. هنگامی که از inner خارج میشویم، پشته فراخوان آن تابع ناپدید میشود، زیرا دیگر inner را اجرا نمیکنیم و نیازی به فضایی برای ذخیره متغیرهای آن نداریم.
شکل 16-4 پشته را بعد از خروج از پشته فراخوان inner نشان میدهد.
حالا فقط دوپشته فراخوان روی پشته وجود دارد.
نویسنده شو !
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.