روش صحیح و ایمن برای پیمایش معکوس آرایهها در زبان C — Iterating Backwards in C
خلاصه
این مقاله به بررسی اشتباهات رایج برنامهنویسان در پیمایش معکوس آرایهها با متغیرهای unsigned در زبان C میپردازد. نویسنده نشان میدهد که چگونه استفاده از عملگر post-decrement روی انواع داده بدون علامت، کدی تمیزتر، ایمنتر و عاری از خطاهای سرریز فراهم میکند.
پیمایش معکوس یک بافر یا آرایه در زبان C، بهویژه برای برنامهنویسانی که به حلقههای رو به جلو عادت کردهاند، میتواند چالشبرانگیز باشد. این مسئله با ورود ابزارهای هوش مصنوعی و کدهای تولید شده توسط LLMها حتی پررنگتر شده است، زیرا بسیاری از توسعهدهندگان الگوهای اشتباه را تکرار میکنند.
دامهای رایج در پیمایش معکوس
فرض کنید میخواهیم آخرین نمونه از یک عنصر را در یک آرایه پیدا کنیم و امضای تابع به این صورت است:
_Bool find_last(size_t count, int *haystack, int needle, size_t *where);
بسیاری از برنامهنویسان هنگام تبدیل حلقه صعودی به نزولی، این اشتباه را مرتکب میشوند:
for(size_t i = count - 1; i >= 0; i--){
// find needle
}
این کد به هیچ وجه درست کار نمیکند! از آنجایی که نوع متغیر size_t بدون علامت (unsigned) است، شرط i >= 0 همیشه برقرار خواهد بود (تائوتولوژی) و کامپایلرهای مدرن به آن هشدار میدهند. بدتر از آن، اگر مقدار count برابر با صفر باشد، متغیر i بلافاصله تحت جریان سرریز به حداکثر مقدار ممکن یعنی SIZE_MAX تبدیل میشود.
برای حل این مشکل، برخی از توسعهدهندگان به سراغ استفاده از انواع علامتدار (signed) مانند ssize_t میروند:
for(ssize_t i = count - 1; i >= 0; i--){
// find needle
}
اگرچه این روش در عمل کار میکند، اما مشکلات جدیدی ایجاد میکند:
- متغیر حلقه شما اکنون علامتدار است که میتواند هنگام مقایسه با اندازهها و مقادیر
size_t، به دلیل قوانین تبدیل نوع در C دردسرساز شود. - نوع
ssize_tاستاندارد POSIX است و برای اجرا روی سیستمعامل Windows باید بهصورت دستی تعریف شود. - در سیستمهای ۳۲ بیتی با مسائل ارتقاء نوع (promotion) و حتی برش دادهها (truncation) مواجه میشوید.
- خوانایی کد کاهش یافته و بررسی درستی شرط حلقه دشوار میشود.
راه حل اصولی و تمیز
راه حل صحیح و استاندارد برای پیمایش معکوس در C بدون نیاز به تبدیل به نوع علامتدار بسیار ساده است:
for(size_t i = count; i--; ){
// find needle
}
این ساختار چگونه کار میکند؟ کلید اصلی، استفاده از عملگر post-decrement روی متغیر i در بخش شرط حلقه است. این عملگر ابتدا مقدار فعلی i را برمیگرداند تا در بدنه حلقه استفاده شود (که همیشه بین count - 1 تا 0 خواهد بود) و سپس مقدار آن را یکی کم میکند.
هنگامی که i به صفر میرسد، حلقه بررسی میکند که آیا مقدار قبلی غیرصفر بوده است یا خیر؛ سپس مقدار i صفر شده و در تکرار بعدی با رسیدن به صفر، حلقه پایان مییابد. سرریز در انواع داده بدون علامت در زبان C کاملاً تعریفشده (well-defined) است، بنابراین هیچ رفتار تعریفنشدهای (undefined behavior) رخ نخواهد داد.
تابع نهایی به این شکل بسیار تمیز در خواهد آمد:
_Bool find_last(size_t count, int *haystack, int needle, size_t *where){
for(size_t i = count; i--; ){
if(haystack[i] == needle){
*where = i;
return 1;
}
}
return 0;
}
نگاهی به نظرات هکرنیوز
در بحثهای هکرنیوز، برخی توسعهدهندگان باتجربه اشاره کردند که دهههاست از این الگو استفاده میکنند. البته برخی نیز به شوخی از عملگر معروف گندمنما یا همان --> (که ترکیبی از عملگرهای -- و >_ است) یاد کردند؛ عبارتی مانند while (i --> 0) که اگرچه از نظر بصری شبیه به یک فلش به سمت چپ است، اما در واقع همان روش کاهش متغیر پیش از بررسی شرط است که هرچند برای کدهای شخصی جذاب به نظر میرسد، اما ممکن است برای خوانندگان ناآشنا کمی گیجکننده باشد.
این مطلب بهصورت کاملاً خودکار و با هوش مصنوعی از مقالهٔ اصلی و بحث هکرنیوز تهیه شده و ممکن است خطا داشته باشد. حقوق مطلب اصلی متعلق به نویسندهٔ آن است. قوانین و گزارش مشکل