در زبان C، بررسی مرزهای آرایه بهصورت خودکار انجام نمیشود. به این معنا که اگر بخواهید به عناصری خارج از محدوده مجاز یک آرایه دسترسی پیدا کنید، زبان C بهطور پیشفرض هیچ خطایی تولید نمیکند. به عنوان مثال، اگر یک آرایه ۵ عضوی با نام int a[5] تعریف کنید، تنها عناصر مجاز آن شامل a[0]، a[1]، a[2]، a[3] و a[4] خواهند بود. با این حال، دسترسی به مقادیر خارج از این محدوده، مانند a[5]، a[6] یا حتی a[932343]، توسط زبان C محدود نمیشود.
مشکل اینجاست که دسترسی به مقادیر خارج از محدوده، ممکن است به آدرسهای حافظهای منجر شود که متعلق به متغیرها یا دادههای دیگر هستند. در واقع، دسترسی به آرایه در زبان C شبیه به کار با اشارهگرها است؛ به این صورت که آدرس ابتدایی آرایه در نظر گرفته شده و با توجه به مقدار ایندکس به جلو حرکت میکند، بدون اینکه بررسی شود آیا از طول آرایه تجاوز شده است یا خیر. به عبارت دیگر، C فرض میکند تمام خانههای حافظهای که با استفاده از ایندکس مشخص میشوند، معتبر هستند و بنابراین بدون هشدار دادهها را در آن آدرسها مینویسد یا میخواند.
این مسئله میتواند باعث ایجاد باگهای پیچیده و دشوار برای عیبیابی شود. گاهی اوقات یافتن علت این باگها نیازمند صرف زمان زیادی است. بنابراین، همیشه خودتان را ملزم کنید که مرزهای آرایه را بررسی کنید.
کد 6-2 نشان میدهد وقتی که از محدوده آرایه خارج میشویم (که سرریز آرایه نامیده میشود) چه اتفاقی میافتد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | .bad.c /* * Demonstrate what happens * when you overflow an array. */ #include <stdio.h> int main() { int numbers1[5] = {11,12,13,14,15}; // Some numbers int numbers2[5] = {21,22,23,24,25}; // Variable to be overwritten 1 printf("numbers2[0] %d\n", numbers2[0]); 2 numbers1[8] = 99; // <------------ Illegal // Illegal -- loops past the end for (int i = 0; i < 9; ++i) printf("numbers1[%d] %p\n", i, &numbers1[i]); printf("numbers2[%d] %p\n", 0, &numbers2[0]); 3 printf("numbers2[0] %d\n", numbers2[0]); return (0); } |
کد 6-2: سرریز آرایه
نکته کلیدی که باید به آن توجه کنیم numbers2[0] است که هنگام مقداردهی اولیه آن را روی 21 تنظیم میکنیم. وقتی برای اولینبار آن را چاپ میکنیم (در خط 1)، مقدار آن در واقع 21 است. بااینحال، زمانی که بعداً آن را چاپ میکنیم (در خط 3)، مقدار آن 99 است. چه اتفاقی افتاد؟
بیایید به خروجی این برنامه نگاه کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | numbers2[0] 21 numbers1[0] 0x7ffc5e94ff00 numbers1[1] 0x7ffc5e94ff04 numbers1[2] 0x7ffc5e94ff08 numbers1[3] 0x7ffc5e94ff0c numbers1[4] 0x7ffc5e94ff10 numbers1[5] 0x7ffc5e94ff14 numbers1[6] 0x7ffc5e94ff18 numbers1[7] 0x7ffc5e94ff1c numbers1[8] 0x7ffc5e94ff20 numbers2[0] 0x7ffc5e94ff20 numbers2[0] 99 |
از این خروجی، میبینیم که حافظه از آدرس 0x7ffc5e94ff00 تا 0x7ffc5e94ff13 به آرایه numbers1 اختصاصدادهشده است. متغیر numbers2، حافظه از آدرس 0x7ffc5e94ff20 تا 0x7ffc5e94ff33 را دریافت میکند. این چیدمان حافظه در جدول 6-1 به صورت بصری نمایش داده شده است.
جدول 6-1: چیدمان حافظه
متغیر | آدرس | محتوا |
numbers1 | 0x7ffc5e94ff00 | 11 |
| 0x7ffc5e94ff04 | 12 |
| 0x7ffc5e94ff08 | 13 |
| 0x7ffc5e94ff0c | 14 |
| 0x7ffc5e94ff10 | 15 |
numbers2 | 0x7ffc5e94ff20 | 21 |
| 0x7ffc5e94ff24 | 22 |
| 0x7ffc5e94ff28 | 23 |
| 0x7ffc5e94ff2c | 24 |
| 0x7ffc5e94ff30 | 25 |
دستورالعمل در خط 2 کد 6-2 از یک ایندکس غیرمجاز استفاده میکند، زیرا numbers1 تنها دارای پنج عنصر است. بنابراین، کدام حافظه را رونویسی می کند؟ از خروجی برنامه میبینیم که آدرس این مقدار 0x7ffc5e94ff20 است. این به طور عجیبی، آدرس numbers2[0] نیز هست. برنامه زمانی که محتوای numbers2[0] را برای بار دوم چاپ میکند، خرابی حافظه بلافاصله آشکار میشود.
این برنامه یک نمونه ساده از مشکلاتی است که میتواند در صورت سرریزشدن آرایه رخ دهد. در دنیای واقعی، شناسایی چنین مشکلاتی بسیار سختتر است. معمولاً این خطاها بهصورت رفتار عجیب برنامه ظاهر میشوند که مدتها بعد از خطای ایندکس رخ میدهد، بنابراین دیباگ آنها پیچیده است.
از این نوع اشتباه اجتناب کنید. رایجترین اشتباهی که برنامهنویسان مبتدی C مرتکب میشوند، فراموشکردن شروع ایندکس آرایههای C از 0 و پایان به اندازه1- است. به عنوان مثال، ممکن است کد زیر را بنویسید:
1 2 3 4 5 6 7 | int array[5]; // اشتباه for (int i = 1; i <= 5; ++i) array[i] = 0; |
در دنیای برنامهنویسی امبدد، به دلیل نبود امکاناتی مثل Valgrind و ضدعفونیکننده آدرس GCC که در سیستمهای لینوکس وجود دارند، برای جلوگیری از سرریز آرایه باید خیلی مراقب باشیم. این ابزارها هنگام اجرای برنامه، سرریز آرایه را بررسی میکنند.
کاراکترها و رشتهها
تابهحال در مورد کار با اعداد صحبت کردیم، اما ممکن است گاهی بخواهید انواع دیگری از دادهها مانند متن را در برنامههای خود قرار دهید. برای این کار، ما به یک نوع متغیر جدید به نام char روی میآوریم که یک کاراکتر واحد را در داخل علامت (‘) نگه میدارد. بهعنوانمثال، کد زیر یک متغیر کاراکتری به نام stop ایجاد میکند تا کاراکتر ‘S’ را نگه دارد:
1 | char stop = 'S'; // کاراکتر برای نشاندادن توقف |
یکرشته، آرایهای از کاراکترها است که با یک کاراکتر (\0) در انتهای رشته خاتمه مییابد. کاراکتر \0 همچنین به عنوان کاراکتر تهی (NUL (با یک L)) شناخته میشود. دلیل این نامگذاری این است که در ارتباطات سریال اولیه، هیچ معنایی نداشت.
برای تمرین استفاده از رشتهها، بیایید نگاهی به برنامه زیر بیندازیم که رشته “Hello World” را چاپ میکند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /* Hello World با استفاده از متغیر رشته * */ #include <stdio.h> // کاراکترهایی که باید چاپ شوند const char hello[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0'}; int main() { puts(hello); // نوشتن رشته و خط جدید return (0); } |
ابتدا یک رشته به نام hello با مقدار “Hello World” تعریف میکنیم. این مقداردهی به طور صریح هر عنصر از رشته را تعریف میکند. شما بهسختی چنین مقداردهیهایی را در دنیای واقعی مشاهده میکنید زیرا C یک میانبر دارد که کار را بسیار آسانتر میکند (بهزودی آن را خواهیم دید). این مثال برای یادگیری خوب است؛ اما برای استفاده مناسب نیست.
در ادامه، رشته را با استفاده از تابع استاندارد C به نام puts چاپ میکنیم. تابع puts یکرشته منفرد را چاپ میکند که تابع سادهای است، درحالیکه printf میتواند قالببندی را انجام دهد که یک تابع بزرگ و پیچیده است. تابع puts همچنین یک خط جدید اضافه میکند، بنابراین ما یک خط جدید در رشته اصلی خود قرار نمیدهیم.
C یک میانبر برای مقداردهی اولیه رشتهها دارد که به ما امکان میدهد همان دستور را بهصورت زیر بنویسیم:
1 | const char hello[] = "Hello World"; // کاراکترهایی که باید چاپ شوند |
هر دو عبارت آرایهای از 12 کاراکتر ایجاد کرده و آن را مقداردهی اولیه میکنند. (“Hello World” شامل 11 کاراکتر است و کاراکتر دوازدهم کاراکتر NUL (‘\0’) است که وقتی از میانبر استفاده میکنید، به طور خودکار اضافه میشود.
ازآنجاییکه آرایهها و اشارهگرها بسیار شبیه به هم هستند، میتوانید رشته را بهصورت یک اشارهگر نیز تعریف کنید:
1 | const char* const hello = "Hello World"; // کاراکترهایی که باید چاپ شوند |
دقت کنید که اکنون دو کلمه کلیدی const داریم. اینجا کمی پیچیده میشود. اولین const بر اشارهگر تأثیر میگذارد این نشان میدهد که خود اشارهگر نیز ثابت است و نمیتوان آدرس دیگری به آن اختصاص داد؛ دومی بر دادههایی که به آنها اشاره میشود تأثیر میگذارد و نشان میدهد که دادهای که اشارهگر به آن اشاره میکند نمیتواند تغییر کند. برنامه زیر نحوه عملکرد این موارد را نشان میدهد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | /** * @brief Program to demonstrate the use of const * with pointers */ char theData[5] = "1234"; // Some data to play with char* allChange; // Pointer and value can change const char* dataConst = "abc" // Char const, pointer not char* const ptrConst = theData; // Char var, ptr not const char* const allConst = "abc"; // Nobody change nothing int main() { char otherData[5] = "abcd"; // Some other data allChange = otherData; // Change pointer *allChange = 'x'; // Change data dataConst = otherData; // Change pointer // *dataConst = 'x'; // Illegal to change data // ptrConst = otherData; // Illegal to change pointer *ptrConst = 'x'; // Change data // allConst = otherData; // Illegal to change pointer // *allConst = 'x'; // Illegal to change data return (0); } |
این برنامه هر روش ممکنی را که میتوان از const برای تعریف یک اشارهگر کاراکتر استفاده کرد، نشان میدهد. ما سعی خواهیم کرد اشارهگر و دادههای مورد اشاره را تغییر دهیم. بسته به اینکه اصلاحکنندههای const را کجا قرار میدهیم، برخی از این عبارات با شکست مواجه میشوند و برخی دیگر کار میکنند.
خلاصه
در ابتدای این دوره با متغیرهایی که میتوانستند مقادیر واحدی را نگه دارند، کار کردیم. آرایهها به ما اجازه میدهند با مجموعهای از دادهها کار کنیم. این به ما قدرت بیشتری در سازماندهی اطلاعات میدهد.
رشتهها نوع خاصی از آرایه هستند. آنها کاراکترها را نگه میدارند و یک نشانگر پایان رشته برای مشخصکردن انتهای خود دارند.
اشارهگرها و آرایهها ازاینجهت شبیه هستند که هر دو میتوانند برای دسترسی به بخشی از حافظه استفاده شوند. اندازه آرایهها محدود است (اگرچه ممکن است سرریز کنند)، درحالیکه اندازه اشارهگرها محدود نیست. زبان C استفاده از اشارهگرها را محدود نمیکند و این قدرت زیادی به زبان میدهد. این قدرت میتواند بدرستی ، مانند کار با ورودی/خروجی نگاشت حافظه (memory-mapped I/O)، یا به اشتباه، مانند تخریب تصادفی حافظه، استفاده شود.
همانطور که میبینیم، C به برنامهنویسان این قدرت را میدهد که از دستگاههای خود به طور کامل استفاده کنند. اما این قدرت بهایی دارد. C مانع از انجام کارهای غیرمعمول توسط شما نمیشود. C ابزارهایی مانند آرایهها و اشارهگرها را برای سازماندهی دادههای شما در اختیار شما قرار میدهد. این به عهده شماست که از آنها به طور عاقلانه استفاده کنید.
مسائل برنامهنویسی
برنامهای بنویسید که
- کمترین و بیشترین عدد موجود در یک آرایه از اعداد صحیح را پیدا کند.
- آرایهای از اعداد صحیح را اسکن کند و اعدادی را که بهصورت پشتسرهم تکرار شدهاند، پیدا کند.
- آرایهای از اعداد صحیح را اسکن کند و اعدادی را که در هر جای آرایه تکرار شدهاند، پیدا کند.
- فقط اعداد فرد موجود در یک آرایه را چاپ کند.
- روی یک رشتهی متنی کار کند و حرف اول هر کلمه را به حرف بزرگ تبدیل کند. برای این کار باید از توابع استاندارد کتابخانهی C به نامهای isalpha و toupper استفاده کنید.