در این مقاله با چهار اشتباه رایج که برنامه نویسان در زمان کدنویسی با C مرتکب میشوند و پنج راهکار برای جلوگیری از آن آشنا خواهید شد.
اشتباه رایج اول: آزاد نکردن حافظه malloc (یا آزاد کردن آن بیش از یک مرتبه)
یکی از بزرگترین اشتباهات رایج انجام گرفته در C مربوط به مدیریت حافظه است. حافظه اختصاص داده شده (انجام گرفته با استفاده از تابع malloc) در C به طور خودکار آزاد نمیشود. این وظیفه برنامه نویس است تا وقتی دیگر نیازی به استفاده از این حافظه نیست آن را تخلیه کند. خطا در آزاد سازی درخواستهای مکرر برای حافظه در نهایت به نشت حافظه (memory leak) منجر خواهد شد. سعی در استفاده از ناحیهای از حافظه که قبلا آزاد شده است، باعث از کار افتادن برنامه شما میشود، و از آن بدتر، باعث آسیب پذیر شدن برنامه در مقابل حملاتی که از این مکانیزم استفاده میکند خواهد شد.
توجه داشته باشید که نشت حافظه تنها باید نشان دهنده شرایطی باشد که قرار بوده حافظه خالی شود، اما اینگونه نیست. اگر یک برنامه حافظه اختصاص داده شده را به دلیل اینکه برای انجام کار واقعا به آن نیاز بوده نگه دارد، هر چند استفاده از این حافظه ممکن است ناکارامد باشد، اما به معنای نشتی نیست.
اشتباه رایج دوم: خواندن خارج از محدوده یک آرایه
این یکی دیگر از متداولترین اشتباهات خطرناک در زبان برنامه نویسی C است. خواندن فراتر از انتهای یک آرایه میتواند دادههای زاید را بازگرداند. نوشتن فراتر از محدوده یک آرایه نیز ممکن است وضعیت برنامه را خراب کند، یا به طور کامل باعث از کار افتادن آن شود، و یا بدتر از آن به یک عامل حمله برای بدافزار تبدیل شود.
پس چرا بار بررسی محدودههای آرایه به عهده برنامه نویس است؟ در مشخصات رسمی C آمده است، خواندن یا نوشتن یک آرایه فراتر از مرزهای آن «رفتار تعریف نشده» است، یعنی مشخصات در مورد آنچه قرار است اتفاق بیفتد حرفی برای گفتن ندارد.
خواندن و نوشتن خارج از محدوده معمولا توسط کامپایلر به دام نمیافتد، مگر اینکه به طور خاص گزینههای کامپایلر را برای محافظت در برابر آن فعال کرده باشید.
اشتباه رایج سوم: بررسی نکردن نتایج malloc
Malloc و calloc توابعی از کتابخانه C هستند که حافظه اختصاص یافته از طرف سیستم را به دست میآورند. اگر آنها قادر به تخصیص حافظه نباشند، یک خطا ایجاد میکنند. در زمانهای گذشته که کامپیوترها حافظه کمی در اختیار داشتند، این احتمال وجود داشت که فراخوانی malloc موفقیت آمیز نباشد.
با وجودی که کامپیوترهای امروزی چندین گیگابایت رم در اختیار دارند، اما هنوز هم این احتمال وجود دارد که malloc با خطا مواجه شود، به خصوص تحت فشار حافظه بالا یا هنگام اختصاص پشتههای بزرگ حافظه به طور همزمان. این موضوع بیشتر برای برنامه نویسان C رخ میدهد که بلوک بزرگی از حافظه را ابتدا از سیستم عامل اختصاص میدهند بعد آن را برای مصارف خود تقسیم میکنند. اگر اولین اختصاص به دلیل بزرگ بودن بیش از حد با خطا مواجه شود، شما میتوانید از این امتناع باخبر شده و میزان اختصاص را کاهش دهید. اما اگر این خطای اختصاص حافظه را متوجه نشوید، کل برنامه دچار اختلال خواهد شد.
اشتباه رایج چهارم: استفاده از void* برای اشاره گرهای عمومی به حافظه
استفاده از void* برای اشاره به حافظه یک عادت قدیمی و اشتباه است. اشاره گرهای به حافظه همیشه باید char*, unsigned char* یا uintptr_t* باشد. مجموعههای کامپایلر مدرن C باید uintptr_t را به عنوان بخشی از stdint.h فراهم کند.
هنگامی که برچسب گذاری به یکی از این روشها انجام میشود، واضح است که اشاره گر به موقعیت مکانی حافظه ارجاع داده میشود. با uintptr_t* و نظاير آن، به اندازه عنصر و نحوه استفاده از آن نیز اشاره میشود.
جلوگیری از اشتباهات رایج C
در زمان کار با حافظه، آرایهها و اشاره گرها در C چگونه میتوان از این اشتباهات رایج جلوگیری کرد؟ این پنج نکته را به خاطر داشته باشید.
برنامههای C را ساختار دهید تا مالکیت حافظه کاملا واضح باشد
اگر تازه برنامه نویسی یک اپلیکیشن C را آغاز کردهاید، باید به نحوه تخصیص و انتشار حافظه به عنوان یکی از اصول اولیه برنامه فکر کنید. اگر مشخص نباشد که تخصیص حافظه در کجا یا تحت چه شرایطی آزاد میشود، شما با مشکل مواجه خواهید شد. تلاش بیشتری به خرج دهید و تا حد ممکن وضعیت مالکیت حافظه را به طور واضح مشخص کنید.
از گزینههای کامپایلر C که از مشکلات حافظه محافظت میکنند استفاده کنید
با استفاده از گزینههای دقیق کامپایلر میتوان از خیلی از مشکلاتی که در نیمه اول این مقاله به آن اشاره شد جلوگیری کرد. برای نمونه، ویرایشهای Recent از gcc ابزارهايی مثل AddressSanitizer (“ASAN”) را فراهم میکند تا اشتباهات رایج مربوط به مدیریت حافظه بررسی شود.
از طرفی باید توجه داشته باشید که این ابزار همه چیز را تحت پوشش قرار نميدهد. بعضی از این ابزار مثل ASAN، مسائل مربوط به گردآوری و runtime را تحمیل میکنند، بنابراین باید از استفاده از آنها در نسخههای نهایی اجتناب کنید.
برای تجزیه و تحلیل نشت حافظه در کدهای C از Cppcheck و Valgrind استفاده کنید
وقتی صحبت از تجزیه و تحلیل رفتار برنامه در runtime در میان باشد، ابزارهايی هستند که نقص کامپایلرها در این زمینه را پوشش میدهند.
Cppcheck تجزیه و تحلیل ایستا را بر روی کد منبع C انجام میدهد تا به دنبال اشتباهات رایج در مدیریت حافظه و رفتارهای تعریف نشده باشد.
Valgrind ابزارهايی را برای شناسایی حافظه و خطاهای احتمالی در برنامههای در حال اجرای C فراهم میکند. این بسیار قدرتمندتر از استفاده از تجزیه و تحلیل زمان کامپایل است، زیرا شما میتوانید اطلاعات مربوط به رفتار برنامه را در زمان اجرای زنده آن بدست آورید.
این نوع ابزارها حلال تمام مشکلات نیستند، اما میتوان از آنها به عنوان بخشی از یک استراتژی دفاعی در برابر سوء مدیریت حافظه در C استفاده کرد.
مدیریت حافظه در C را با یک جمع کننده زواید خودکارسازی کنید
از آنجا که خطاهای حافظه بارزترین مشکلات رخ داده در C محسوب میشود، یک راهکار ساده برای رفع آن را به شما معرفی میکنیم: مدیریت حافظه در C را به صورت دستی انجام ندهید. از یک جمع کننده زواید (garbage collector) استفاده کنید.
بله این کار در C قابل انجام است. شما میتوانید از چیزی شبیه به Boehm-Demers-Weiser garbage collector برای اضافه کردن مدیریت حافظه خودکار به برنامههای C استفاده کنید. در برخی برنامهها استفاده از Boehm collector حتی میتواند باعث افزایش سرعت کارها شود.
نقطه ضعف اصلی Boehm garbage collector این است که نمیتوان زمانی که به طور پیش فرض از malloc استفاده شده حافظه را اسکن و آزاد سازی کند.
وقتی با زبان دیگری میتوانید کار کنید از C استفاده نکنید
بعضی از مردم صرفا به این دلیل که زبان برنامه نویسی C را مثمر ثمر میدانند و از نوشتن با آن لذت میبرند آن را استفاده میکنند. در حالت کلی بهتر است C را فقط در موارد ضروری و تحت شرایط خاص استفاده کنید.
اگر پروژهای دارید که عملکرد آن عمدتا به دلیل دسترسی I / O یا دسترسی به دیسک محدود میشود، نوشتن آن به زبان C به احتمال زیاد باعث تسریع در انجام آن نمیشود و احتمالا فقط آن را مستعد خطا و مشکل میکند. در حالی که همین برنامه را میتوان به خوبی با Go یا پایتون نوشت. فراموش نکنید که پایتون میتواند به راحتی از کتابخانهها و کدهای سفارشی C استفاده کند.