در قسمت قبل آموزش برنامه نویسی C به دستورات کنترلی زبان C برای امبدد سیستم ها پرداختیم. در این قسمت به آموزش مبانی کار با آرایهها و اشارهگرها در زبان C: از اصول اولیه تا مفاهیم پیشرفته می پردازیم.
تاکنون، از اعدادصحیح بسیار سادهای برای نمایش داده استفاده کردهایم. اما همه مقادیر در دنیا را نمیتوان با یک عددصحیح واحد توصیف کرد. در این فصل، برخی از اصول اولیه سازماندهی دادهها را یاد خواهید گرفت.
ابتدای کار، با آرایهها آشنا میشویم. آرایهها ساختارهای دادهای هستند که میتوانند چندین عنصر را در خود جای دهند. شما با استفاده از یک شاخص عددی (ایندکس) خاص، قادر به دسترسی و انتخاب هر یک از این عناصر خواهید بود. علاوه بر آرایههای ساده، کمی فراتر میرویم و نحوهی پیادهسازی آرایهها توسط کامپایلر و همچنین چگونگی پیادهسازی رشتههای متنی (مانند “Hello World!\n”) با استفاده از آرایهها را بررسی میکنیم. در همین راستا، کار با نوعی از داده بهنام char در زبان C را نیز خواهید آموخت.
در ادامه، با مفهوم اشارهگرهای حافظه آشنا میشویم که آدرس یک موقعیت خاص در حافظه را نگهداری میکنند. سپس خواهیم دید که آرایهها و اشارهگرها چگونه علیرغم شباهتهایی که دارند، درعینحال تفاوتهایی نیز با یکدیگر دارند.
همچنین یاد میگیریم که چگونه از کلیدواژهیconst برای ایجاد متغیرهایی استفاده کنیم که قابلتغییر نباشند؛ این متغیرها با نام ثابت (constant) شناخته میشوند. ثابتها به سازماندهی دادههای شما کمک میکنند و از تغییرات ناخواسته در آنها جلوگیری مینمایند.
آرایهها
همانطور که پیشازاین دیدیم، میتوان متغیرهای سادهای مانند زیر تعریف کرد:
1 | int aNumber; // یک عدد |
این متغیر فقط میتواند یک مقدار را در خود ذخیره کند. اما ما میتوانیم متغیرهایی نیز تعریف کنیم که مجموعهای از مقادیر را در خود نگه دارند. برای این کار از array declaration استفاده میکنیم. تعداد عناصر آرایه باید درون براکت مشخص شود:
1 | int anArray[5]; // یک آرایه از اعداد |
این کد یک آرایه از 5 عددصحیح را با شماره خانههای 0 تا 4 که ایندکس نامیده میشوند، تعریف میکند. ایندکسها از 0 شروع میشوند، نه 1. برای دسترسی به عناصر آرایه از براکتهای مربعی و ایندکس مربوطه استفاده میکنیم. به عنوان مثال، کد زیر مقدار 99 را به چهارمین عنصر آرایه (با ایندکس 3) اختصاص میدهد:
1 | anArray[3] = 99; // ذخیره یک عنصر در آرایه |
در زبان C هیچ محدودیتی برای استفاده از ایندکسهای نامعتبر وجود ندارد. بااینحال، استفاده از چنین ایندکسهایی نتایج نامشخصی به همراه خواهد داشت (بهعبارتدیگر، احتمالاً اتفاق بدی رخ خواهد داد). بهعنوانمثال، آخرین عنصر آرایه anArray ایندکس 4 دارد، بنابراین کد زیر مجاز است:
1 | anArray[4] = 0; // مجاز |
اما کد زیر غیرمجاز است:
1 | anArray[5] = 9; // غیرمجاز، 5 بزرگتر از حد مجاز است. |
این کد سعی میکند به عنصری که در آرایه وجود ندارد، دسترسی پیدا کند.
اجازه بدهید با یک مثال ببینیم آرایهها در عمل چگونه کار میکنند. به کد 6-1 نگاهی بیندازید که برنامهای برای جمع زدن عناصر یک آرایه و خروجی گرفتن مجموع آنها است.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <stdio.h> int main() { const int NUMBER_ELEMENTS = 5; // تعداد عناصر int numbers[NUMBER_ELEMENTS]; // اعداد int sum = 0; // مجموع تا کنون int current; // عنصر جاری که در حال جمعزدن هستیم numbers[0] = 5; numbers[1] = 8; numbers[2] = 9; numbers[3] = -5; numbers[4] = 22; // روی تکتک عناصر حلقه بزن و آنها را جمع بزن for (current = 0; current < NUMBER_ELEMENTS; ++current) { sum += numbers[current]; } printf("Total is %d\n", sum); return (0); } |
کد 6-1: استفاده پایه از آرایه
ما با تعریف یک متغیر به نام NUMBER_ELEMENTS شروع میکنیم که تعداد عناصر در آرایه را نگه میدارد. کلیدواژه const به کامپایلر C میگوید که این متغیر نباید تغییر کند (در ادامه بیشتر در این مورد صحبت خواهیم کرد).
ما از این ثابت در دو جا استفاده میکنیم. اولین مورد برای تعریف آرایه است. مورد دوم تعداد عملکردن حلقه روی عناصر آرایه است. درحالیکه میتوانستیم بهجای آن از مقدار 5 در هر دوی این مکانها استفاده کنیم، اما این کار باعث معرفی یک عددجادویی به کد ما میشد. یک عددجادویی عددی است که در چندین مکان در برنامه ظاهر میشود اما ارتباط آن با کد مشخص نیست. استفاده از عدد جادویی خطرناک است؛ در این مثال، اگر عدد 5 را در تعریف آرایه تغییر دهیم، باید به یاد داشته باشیم که عدد 5 را در حلقه نیز تغییر دهیم. با استفاده از تعریف ثابت، اندازه آرایه را فقط در یک مکان تعریف میکنیم. استفاده از یک ثابت برای اندازه آرایه تضمین میکند که فقط باید اندازه را در یک مکان تغییر دهید و برنامه بهطور خودکار در هر جایی که از اندازه استفاده میشود، تنظیم میشود. این کار در زمان صرفهجویی میکند و خطر ایجاد باگ را کاهش میدهد.
برمیگردیم به کد. حالا باید چند عدد داخل آرایه قرار دهیم، پس این کار را با اختصاصدادن یک مقدار به هر یک از اندیسهای آن انجام میدهیم. بعد از آن، از یک حلقه for برای دسترسی به تکتک عناصر آرایه استفاده میکنیم. استیتمنت for یک عبارت رایج برنامهنویسی C برای این کار است. این حلقه از صفر شروع میشود و تا زمانی که ایندکس کمتر از (<) اندازه آرایه باشد ادامه پیدا میکند. ایندکس باید کمتر از 5 باشد، زیرا numbers[5] یک عنصر غیرمجاز است.
آرایهها را میتوان درست مثل متغیرهای ساده، هنگام تعریف، مقداردهی اولیه کرد. برای این کار کافی است همه عناصر را داخل آکولاد { } لیست کنیم:
1 2 | // چند عدد برای جمعزدن تعریف کنیم int numbers[5] = {5, 8, 9, -5, 22}; |
در این حالت، تعداد عناصر باید با اندازه آرایه مطابقت داشته باشد، در غیر این صورت یک پیام هشدار دریافت خواهید کرد.
C یک زبان باهوش است. میتواند اندازه آرایه را از تعداد عناصر استنتاج کند، بنابراین این تعریف نیز کار میکند:
1 2 | // چند عدد برای جمعزدن تعریف کنیم int numbers[] = {5, 8, 9, -5, 22}; |
درک عمیق: اشارهگرها
پدر من، جملهای معروف به من یاد داد: «مقدارهایی به همراه اشارهگرهایشان وجود دارند.» شکل 14-1 را ببینید تا نمودار دقیقی از معنای این جمله را مشاهده کنید. با اینکه به نظر ساده میرسد، درک این نمودار بسیار مهم است.
عددصحیح یک مقدار است. در واقع، این یک مقدار است که عددصحیح درونش قرار دارد. یک اشارهگر، آدرس یک مقدار است.
اندازه مقدارها متفاوت است. عددصحیح uint64_t یک مقدار نسبتاً بزرگ است، درحالیکه uint8_t یک مقدار کوچک است. نکته کلیدی در تفاوت اندازه مقدارها است. یک اشارهگر اندازهای ثابت دارد. مقداری که به آن اشاره میکند میتواند بزرگ یا کوچک باشد، اما اندازه اشارهگر همیشه یکسان است.
اشارهگرها برای دسترسی سریع به ساختارهای داده و اتصال ساختارهای داده به هم مفید هستند.
اشارهگرها در برنامهنویسی C ابزاری قدرتمند برای دسترسی و دستکاری دادهها هستند. مزایای آنها شامل دسترسی سریع، کارآمدی و انعطافپذیری در ساختارهای داده پیچیده است. اما در مقابل، استفاده نادرست از اشارهگرها میتواند منجر به خطاهای رایج و باگ هایی پیچیده شوند.
استفاده از اشارهگرها نیازمند دقت و احتیاط است. قبل از استفاده از آنها در برنامههای خود، باید به طور کامل نحوه عملکردشان را درک کنید.
در برنامهنویسی امبدد، ما به طور مستقیم با خانههای حافظه کار میکنیم. تمامی متغیرها و کدهای برنامه ما در بخشهایی از حافظه قرار میگیرند. استفاده از اشارهگرها به ما این امکان را میدهد که به جای فراخوانی متغیرها با نام، از آدرس حافظه آنها استفاده کنیم. عملگرهای اشارهگر * و & نقش کلیدی در این فرایند دارند. عملگر & آدرس یک متغیر را به ما میدهد، در حالی که * به محتوای ذخیرهشده در آن آدرس دسترسی پیدا میکند. این دو عملگر به مثابه دروازههایی هستند که امکان دسترسی مستقیم به حافظه و مقداردهی به متغیرها از طریق مکان آنها در حافظه را فراهم میکنند.
برای اعلام یک اشارهگر، از علامت ستاره (*) استفاده میشود تا نشان دهد که متغیر یک اشارهگر است و نه یک مقدار:
1 | uint8_t* thingPtr; // یک اشارهگر به یک عددصحیح |
عملگر آدرس (&) کنار یک متغیر ,اشارهگر آن متغیر را برمیگرداند:
1 2 3 | uint8_t thing = 5; // یک چیز thingPtr = &thing; thingPtr اشاره به مقدار '5' می کند.// |
thingPtr به مقداری اشاره میکند. عملگر dereference (*) یک اشارهگر را به مقدار اصلی باز میگرداند:
1 2 | otherThing = *thingPtr; مقداری را که thingPtr به آن اشاره میکند دریافت میکند//. |
این دستور، مقداری که thingPtr به آن اشاره میکند را به otherThing اختصاص میدهد.
برنامه زیر نشان میدهد که این عملیات چگونه کار میکنند. در این برنامه، یک نوع جدید از کامندهای printf به نام %p معرفی میشود که اشارهگرها را چاپ میکند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /* * Demonstrate pointers. */ #include <stdio.h> #include <stdint.h> int main() { uint8_t smallThing = 5; // مقدار کوچک uint8_t smallThing2 = 6; // مقدار کوچک دیگر uint64_t largeThing = 987654321; // مقدار بزرگ uint8_t* smallPtr; // اشارهگر به مقدر کوچک uint64_t* largePtr; // اشارهگر به مقدار بزرگ 1 printf("smallThing %d\n", smallThing); printf("sizeof(smallThing) %lu\n", sizeof(smallThing)); printf("largeThing %ld\n", largeThing); printf("sizeof(largeThing) %lu\n", sizeof(largeThing)); smallPtr به smallThing2 اشاره میکند .// smallPtr = &smallThing; 2 printf("smallPtr %p\n", smallPtr); printf("sizeof(smallPtr) %lu\n", sizeof(smallPtr)); printf("*smallPtr %d\n", *smallPtr); // smallPtr points to smallThing2. smallPtr = &smallThing2; printf("*smallPtr %d\n", *smallPtr); largePtr = &largeThing; printf("largePtr %p\n", largePtr); printf("sizeof(largePtr) %lu\n", sizeof(largePtr)); printf("*largePtr %ld\n", *largePtr); return (0); } |
جزئیات بیشتر در مورد کد
در ابتدای برنامه، سه متغیر و دو اشارهگر تعریف میکنیم. برای اینکه اشارهگرها بهراحتی قابلتشخیص باشند، از پسوند Ptr در انتهای نام آنها استفاده کردهایم. در این مرحله، smallPtr به چیزی اشاره نمیکند.
چاپ مقدار و سایز : smallThing
قبل استفاده از اشارهگر، مقدار و سایز smallThing را با استفاده از دو فراخوانی printf چاپ میکنیم (بخش ۱). خروجی این کار بهصورت زیر خواهد بود:
1 2 | smallThing 5 sizeof(smallThing) 1 |
بررسی اشارهگر
حالا به سراغ اشارهگر میرویم (بخش ۲). ابتدا مقدار اشارهگر را چاپ میکنیم که یک آدرس حافظه است. فرض میکنیم روی یک تراشه x86 با اشارهگرهای ۶۴ بیتی کار میکنیم، پس مقدار اشارهگر یک عدد ۶۴ بیتی خواهد بود. مقدار واقعی این عدد به نحوه مقداردهی حافظه بستگی دارد که در فصل ۱۱ به طور کامل توضیح داده میشود.
با چاپ sizeof(smallPtr) میبینیم که اندازه آن واقعاً ۸ بایت یا ۶۴ بیت است و مقداری که smallPtr به آن اشاره میکند، عدد ۵ است. در مجموع، این سه فراخوانی printf خروجی زیر را تولید میکنند:
1 2 3 | smallPtr 0x7fffc3935dee sizeof(smallPtr) 8 *smallPtr 5 |
largePtr بررسی
کاری مشابه با largePtr انجام میدهیم. توجه داشته باشید که با وجود تفاوت در سایز دادهای که به آن اشاره میشود، اندازه خود اشارهگر ثابت باقی میماند. سایز اشارهگر به نوع پردازنده بستگی دارد، نه نوع دادهای که به آن اشاره میکند. روی پردازنده STM32، آدرسها ۳۲ بیتی هستند، بنابراین اشارهگر یک مقدار ۳۲ بیتی خواهد بود. در یک تراشه x64 با آدرسهای ۶۴ بیتی، سایز اشارهگر ۴ بایت است.
1 2 3 4 5 | largeThing 987654321 sizeof(largeThing) 8 largePtr 0x7fffc3935df0 sizeof(largePtr) 8 *largePtr 987654321 |
استفاده از دیباگر برای بررسی اشارهگرها
برای اینکه واقعاً ببینید اشارهگرها به چه چیزی اشاره میکنند، این برنامه را در محیطSTM32 وارد کنید و آن را با استفاده از دیباگر اجرا نمایید. یک نقطه توقف (breakpoint) درست بعد از مقداردهی به تمام متغیرها قرار دهید و برنامه را تا رسیدن به آن نقطه اجرا کنید.
با باز کردن پنل متغیرها (Variables panel)، میتوانیم تمام متغیرها و مقادیر آنها را مشاهده کنیم (شکل 14-۲ را ببینید).
معمولاً، مقدار خود اشارهگر چندان جالب نیست. نکتهی مهمتر، چیزی است که اشارهگر به آن اشاره میکند. با کلیککردن روی آیکن مثبت (+)، زیرمجموعهی smallPtr گسترش پیدا میکند و میتوانیم ببینیم که smallPtr به عدد ۶ (همچنین شناخته شده با کاراکتر \006) اشاره میکند. به طور مشابه، مشاهده میکنیم که largePtr به عدد ۹۸۷۶۵۴۳۲۱ اشاره میکند.
آرایه و حساب اشارهگر در زبان C
زبان C آرایهها و اشارهگرها را بسیار شبیه به هم در نظر میگیرد. کد زیر را در ببینید:
1 2 | int array[5] = {1,2,3,4,5}; int* arrayPtr = array; |
ما مقدار array را به arrayPtr اختصاص دادهایم، نه &array. دلیل این است که C به طور خودکار یک آرایه را زمانی که مانند یک اشارهگر استفاده میشود، به یک اشارهگر تبدیل میکند. در واقع، آرایهها و اشارهگرها تقریباً قابلتعویض هستند، به جز اینکه به روشهای مختلفی تعریف میشوند.
حالا بیایید به یک عنصر از آرایه دسترسی پیدا کنیم:
1 | int i = array[1]; |
این ترکیب (syntax) مشابه عبارت زیر است که میگوید مقدار arrayPtr را بگیرید، ۱ به آن اضافه کنید (ضرب در اندازه دادهای که به آن اشاره میشود) و سپس دادهای را که توسط نتیجهی این عبارت اشاره میشود، برگردانید:
1 | int i = *(arrayPtr + 1); |
برنامهی زیر رابطه بین آرایهها و اشارهگرها را با جزئیات بیشتری نشان میدهد:
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 | /* * Demonstrate the relationship between arrays and pointers. */ #include <stdio.h> int main() { int array[] = {1,2,3,4,-1}; // Array int* arrayPtr = array; // Pointer to array // Print array using array. for (int index = 0; array[index] >= 0; ++index) { printf("Address %p Value %d\n", &array[index], array[index]); } printf("--------------\n"); // Same thing with a pointer for (int index = 0; *(arrayPtr +index) >= 0; ++index) { printf("Address %p Value %d\n", arrayPtr + index, *(arrayPtr + index)); } printf("--------------\n"); // Same thing using an incrementing pointer for (int* current = array; *current >= 0; ++current) { printf("Address %p Value %d\n", current, *current); } } |
اولین کاری که این برنامه انجام میدهد، چاپ آدرس و محتویات هر عنصر آرایه به روش معمولی است: با استفاده از یک حلقه for برای دسترسی به هر ایندکس (به ترتیب).
در حلقه بعدی، با استفاده از اشارهگر arithmetic ، چاپ میکنیم. حالا باید دقیقاً بدانیم با چه چیزی سروکار داریم. متغیر array یک آرایه است. عبارت array[index] یک عددصحیح است و عملگر (&) یک عددصحیح را به یک اشارهگر تبدیل میکند، بنابراین &array[index] یک اشارهگر است. در نتیجه، این کد آدرسهای حافظه زیر را برای هر عنصر آرایه چاپ میکند:
1 2 3 4 | آدرس x7fffa22e06100 مقدار 1 آدرس x7fffa22e06140 مقدار 2 آدرس x7fffa22e06180 مقدار 3 آدرس x7fffa22e061c0 مقدار 4 |
مقدار اشارهگر هر بار 4 برابر (اندازه یک عدد صحیح) افزایش مییابد، بنابراین array[0] در آدرس x7fffa22e06100 قرار دارد و array[1] در مکان حافظهی 4 بایت بزرگتر، یعنی x7fffa22e06140 قرار دارد.
این روش از اشارهگرarithmetic استفاده میکند. (در واقع در روش اول هم از حساب اشارهگر استفاده کردیم، اما زبان C آن را از ما پنهان کرد. با این حلقه، میتوانید ببینید که arrayPtr + 1 برابر با x7fffa22e06140 است که دقیقاً با &array[1] یکسان است. باز هم توجه داشته باشید که در حساب اشارهگر، اعداد به طور خودکار با اندازهی نوع دادهای که به آن اشاره میشود، مقیاسبندی میشوند. در این مورد، نوع دادهی مورد اشاره int است، بنابراین عبارت arrayPtr + 1 در واقع arrayPtr + 1 * sizeof(int) است، و از این رو 0x7fffa22e0610 + 1 در اصل
0x7fffa22e0610 + 1 * sizeof(int) است که برابر با x7fffa22e06140 میباشد.
در نهایت، کار مشابهی را برای بار سوم با استفاده از یک اشارهگر افزایشی انجام میدهیم.
استفاده از اشارهگرها برای دسترسی به آرایهها رایج است، زیرا افراد بسیاری فکر میکنند این کار کارآمدتر از ایندکس آرایه است. بههرحال، محاسبهی array[index] شامل محاسبهی آدرس میباشد، اما تکنولوژی کامپایلرها در طول سالها بهبودیافته پس مشکلی در این بابت نیست.(استفاده از روشهایی که هزینه کمتری دارند، در برنامههای بزرگ حیاتی است). کامپایلرهای امروزی در تولید کد کارآمدتر بسیار خوب عمل میکنند، بنابراین استفاده از اشارهگرها برای دسترسی به آرایهها خیلی بهینه تر نیست.
بااینحال، استفاده از منطق آدرسدهی گیجکنندهتر است. مشخص نیست که به چه چیزی اشاره میشود و محدودیتهای آرایه چیست، اما زمانی که برنامه نویسی پیچیده میشود زمانهایی کمک کننده خواهد بود انتخاب اینکه با چه روشی کار کنید کاملا به شما و برنامه شما بستگی دارد.