آموزش برنامه نویسی c, برنامه نویسی, توصیه شده

مبانی کار با آرایه‌ها و اشاره‌گرها در زبان C: از اصول اولیه تا مفاهیم پیشرفته – قسمت چهاردهم آموزش برنامه نویسی C

آموزش امبدد C قسمت 14

در قسمت قبل آموزش برنامه نویسی C به دستورات کنترلی زبان C برای امبدد سیستم ها پرداختیم. در این قسمت به آموزش مبانی کار با آرایه‌ها و اشاره‌گرها در زبان C: از اصول اولیه تا مفاهیم پیشرفته می پردازیم.

تا‌کنون، از اعدادصحیح بسیار ساده‌ای برای نمایش داده استفاده کرده‌ایم. اما همه مقادیر در دنیا را نمی‌توان با یک عدد‌صحیح واحد توصیف کرد. در این فصل، برخی از اصول اولیه سازماندهی داده‌ها را یاد خواهید گرفت.
ابتدای کار، با آرایه‌ها آشنا می‌شویم. آرایه‌ها ساختارهای داده‌ای هستند که می‌توانند چندین عنصر را در خود جای دهند. شما با استفاده از یک شاخص عددی (ایندکس) خاص، قادر به دسترسی و انتخاب هر یک از این عناصر خواهید بود. علاوه بر آرایه‌های ساده، کمی فراتر می‌رویم و نحوه‌ی پیاده‌سازی آرایه‌ها توسط کامپایلر و همچنین چگونگی پیاده‌سازی رشته‌های متنی (مانند “Hello World!\n”) با استفاده از آرایه‌ها را بررسی می‌کنیم. در همین راستا، کار با نوعی از داده‌ به‌نام char در زبان C را نیز خواهید آموخت.
در ادامه، با مفهوم اشاره‌گرهای حافظه آشنا می‌شویم که آدرس یک موقعیت خاص در حافظه را نگه‌داری می‌کنند. سپس خواهیم دید که آرایه‌ها و اشاره‌گرها چگونه علی‌رغم شباهت‌هایی که دارند، درعین‌حال تفاوت‌هایی نیز با یکدیگر دارند.
همچنین یاد می‌گیریم که چگونه از کلیدواژه‌یconst برای ایجاد متغیرهایی استفاده کنیم که قابل‌تغییر نباشند؛ این متغیرها با نام ثابت (constant) شناخته می‌شوند. ثابت‌ها به سازماندهی داده‌های شما کمک می‌کنند و از تغییرات ناخواسته در آن‌ها جلوگیری می‌نمایند.

آرایه‌ها

همان‌طور که پیش‌ازاین دیدیم، می‌توان متغیرهای ساده‌ای مانند زیر تعریف کرد:

این متغیر فقط می‌تواند یک مقدار را در خود ذخیره کند. اما ما می‌توانیم متغیرهایی نیز تعریف کنیم که مجموعه‌ای از مقادیر را در خود نگه دارند. برای این کار از array declaration استفاده می‌کنیم. تعداد عناصر آرایه باید درون براکت مشخص شود:

این کد یک آرایه از 5 عددصحیح را با شماره خانه‌های 0 تا 4 که ایندکس نامیده می‌شوند، تعریف می‌کند. ایندکس‌ها از 0 شروع می‌شوند، نه 1. برای دسترسی به عناصر آرایه از براکت‌های مربعی و ایندکس مربوطه استفاده می‌کنیم. به عنوان مثال، کد زیر مقدار 99 را به چهارمین عنصر آرایه (با ایندکس 3) اختصاص می‌دهد:

در زبان C هیچ محدودیتی برای استفاده از ایندکس‌های نامعتبر وجود ندارد. بااین‌حال، استفاده از چنین ایندکس‌هایی نتایج نامشخصی به همراه خواهد داشت (به‌عبارت‌دیگر، احتمالاً اتفاق بدی رخ خواهد داد). به‌عنوان‌مثال، آخرین عنصر آرایه anArray ایندکس 4 دارد، بنابراین کد زیر مجاز است:

اما کد زیر غیرمجاز است:

این کد سعی می‌کند به عنصری که در آرایه وجود ندارد، دسترسی پیدا کند.
اجازه بدهید با یک مثال ببینیم آرایه‌ها در عمل چگونه کار می‌کنند. به کد 6-1 نگاهی بیندازید که برنامه‌ای برای جمع زدن عناصر یک آرایه و خروجی گرفتن مجموع آن‌ها است.

کد 6-1: استفاده پایه از آرایه

ما با تعریف یک متغیر به نام NUMBER_ELEMENTS شروع می‌کنیم که تعداد عناصر در آرایه را نگه می‌دارد. کلیدواژه const به کامپایلر C می‌گوید که این متغیر نباید تغییر کند (در ادامه بیشتر در این مورد صحبت خواهیم کرد).
ما از این ثابت در دو جا استفاده می‌کنیم. اولین مورد برای تعریف آرایه است. مورد دوم تعداد عمل‌کردن حلقه روی عناصر آرایه است. درحالی‌که می‌توانستیم به‌جای آن از مقدار 5 در هر دوی این مکان‌ها استفاده کنیم، اما این کار باعث معرفی یک عددجادویی به کد ما می‌شد. یک عدد‌جادویی عددی است که در چندین مکان در برنامه ظاهر می‌شود اما ارتباط آن با کد مشخص نیست. استفاده از عدد جادویی خطرناک است؛ در این مثال، اگر عدد 5 را در تعریف آرایه تغییر دهیم، باید به یاد داشته باشیم که عدد 5 را در حلقه نیز تغییر دهیم. با استفاده از تعریف ثابت، اندازه آرایه را فقط در یک مکان تعریف می‌کنیم. استفاده از یک ثابت برای اندازه آرایه تضمین می‌کند که فقط باید اندازه را در یک مکان تغییر دهید و برنامه به‌طور خودکار در هر جایی که از اندازه استفاده می‌شود، تنظیم می‌شود. این کار در زمان صرفه‌جویی می‌کند و خطر ایجاد باگ را کاهش می‌دهد.

برمی‌گردیم به کد. حالا باید چند عدد داخل آرایه قرار دهیم، پس این کار را با اختصاص‌دادن یک مقدار به هر یک از اندیس‌های آن انجام می‌دهیم. بعد از آن، از یک حلقه for برای دسترسی به تک‌تک عناصر آرایه استفاده می‌کنیم. استیتمنت for یک عبارت رایج برنامه‌نویسی C برای این کار است. این حلقه از صفر شروع می‌شود و تا زمانی که ایندکس کمتر از (<) اندازه آرایه باشد ادامه پیدا می‌کند. ایندکس باید کمتر از 5 باشد، زیرا numbers[5] یک عنصر غیرمجاز است.
آرایه‌ها را می‌توان درست مثل متغیرهای ساده، هنگام تعریف، مقداردهی اولیه کرد. برای این کار کافی است همه عناصر را داخل آکولاد { } لیست کنیم:

در این حالت، تعداد عناصر باید با اندازه آرایه مطابقت داشته باشد، در غیر این صورت یک پیام هشدار دریافت خواهید کرد.
C یک زبان باهوش است. می‌تواند اندازه آرایه را از تعداد عناصر استنتاج کند، بنابراین این تعریف نیز کار می‌کند:

درک عمیق: اشاره‌گرها

پدر من، جمله‌ای معروف به من یاد داد: «مقدارهایی به همراه اشاره‌گرهایشان وجود دارند.» شکل 14-1 را ببینید تا نمودار دقیقی از معنای این جمله را مشاهده کنید. با اینکه به نظر ساده می‌رسد، درک این نمودار بسیار مهم است.

شکل 6-1: مقدارها و اشاره‌گرها به آن‌ها

شکل 14-1: مقدارها و اشاره‌گرها به آن‌ها

عدد‌صحیح یک مقدار است. در واقع، این یک مقدار است که عددصحیح درونش قرار دارد. یک اشاره‌گر، آدرس یک مقدار است.
اندازه مقدارها متفاوت است. عددصحیح uint64_t یک مقدار نسبتاً بزرگ است، درحالی‌که uint8_t یک مقدار کوچک است. نکته کلیدی در تفاوت اندازه مقدارها است. یک اشاره‌گر اندازه‌ای ثابت دارد. مقداری که به آن اشاره می‌کند می‌تواند بزرگ یا کوچک باشد، اما اندازه اشاره‌گر همیشه یکسان است.
اشاره‌گرها برای دسترسی سریع به ساختارهای داده و اتصال ساختارهای داده به هم مفید هستند.
اشاره‌گرها در برنامه‌نویسی C ابزاری قدرتمند برای دسترسی و دستکاری داده‌ها هستند. مزایای آن‌ها شامل دسترسی سریع، کارآمدی و انعطاف‌پذیری در ساختارهای داده پیچیده است. اما در مقابل، استفاده نادرست از اشاره‌گرها می‌تواند منجر به خطاهای رایج و باگ هایی پیچیده شوند.
استفاده از اشاره‌گرها نیازمند دقت و احتیاط است. قبل از استفاده از آن‌ها در برنامه‌های خود، باید به طور کامل نحوه عملکردشان را درک کنید.
در برنامه‌نویسی امبدد، ما به طور مستقیم با خانه‌های حافظه کار می‌کنیم. تمامی متغیرها و کدهای برنامه ما در بخش‌هایی از حافظه قرار می‌گیرند. استفاده از اشاره‌گرها به ما این امکان را می‌دهد که به جای فراخوانی متغیرها با نام، از آدرس حافظه آن‌ها استفاده کنیم. عملگرهای اشاره‌گر * و & نقش کلیدی در این فرایند دارند. عملگر & آدرس یک متغیر را به ما می‌دهد، در حالی که * به محتوای ذخیره‌شده در آن آدرس دسترسی پیدا می‌کند. این دو عملگر به مثابه دروازه‌هایی هستند که امکان دسترسی مستقیم به حافظه و مقداردهی به متغیرها از طریق مکان آن‌ها در حافظه را فراهم می‌کنند.

برای اعلام یک اشاره‌گر، از علامت ستاره (*) استفاده می‌شود تا نشان دهد که متغیر یک اشاره‌گر است و نه یک مقدار:

عملگر آدرس (&) کنار یک متغیر ,اشاره‌گر آن متغیر را برمیگرداند:

thingPtr به مقداری اشاره می‌کند. عملگر dereference (*) یک اشاره‌گر را به مقدار اصلی باز می‌گرداند:

این دستور، مقداری که thingPtr به آن اشاره می‌کند را به otherThing اختصاص می‌دهد.
برنامه زیر نشان می‌دهد که این عملیات چگونه کار می‌کنند. در این برنامه، یک نوع جدید از کامند‌های printf به نام %p معرفی می‌شود که اشاره‌گرها را چاپ می‌کند:

جزئیات بیشتر در مورد کد

در ابتدای برنامه، سه متغیر و دو اشاره‌گر تعریف می‌کنیم. برای اینکه اشاره‌گرها به‌راحتی قابل‌تشخیص باشند، از پسوند Ptr در انتهای نام آن‌ها استفاده کرده‌ایم. در این مرحله، smallPtr به چیزی اشاره نمی‌کند.

چاپ مقدار و سایز : smallThing

قبل استفاده از اشاره‌گر، مقدار و سایز smallThing را با استفاده از دو فراخوانی printf چاپ می‌کنیم (بخش ۱). خروجی این کار به‌صورت زیر خواهد بود:

بررسی اشاره‌گر

حالا به سراغ اشاره‌گر می‌رویم (بخش ۲). ابتدا مقدار اشاره‌گر را چاپ می‌کنیم که یک آدرس حافظه است. فرض می‌کنیم روی یک تراشه x86 با اشاره‌گرهای ۶۴ بیتی کار می‌کنیم، پس مقدار اشاره‌گر یک عدد ۶۴ بیتی خواهد بود. مقدار واقعی این عدد به نحوه مقدار‌دهی حافظه بستگی دارد که در فصل ۱۱ به طور کامل توضیح داده می‌شود.
با چاپ sizeof(smallPtr) می‌بینیم که اندازه آن واقعاً ۸ بایت یا ۶۴ بیت است و مقداری که smallPtr به آن اشاره می‌کند، عدد ۵ است. در مجموع، این سه فراخوانی printf خروجی زیر را تولید می‌کنند:

largePtr بررسی

کاری مشابه با largePtr انجام می‌دهیم. توجه داشته باشید که با وجود تفاوت در سایز داده‌ای که به آن اشاره می‌شود، اندازه خود اشاره‌گر ثابت باقی می‌ماند. سایز اشاره‌گر به نوع پردازنده بستگی دارد، نه نوع داده‌ای که به آن اشاره می‌کند. روی پردازنده STM32، آدرس‌ها ۳۲ بیتی هستند، بنابراین اشاره‌گر یک مقدار ۳۲ بیتی خواهد بود. در یک تراشه x64 با آدرس‌های ۶۴ بیتی، سایز اشاره‌گر ۴ بایت است.

استفاده از دیباگر برای بررسی اشاره‌گرها

برای اینکه واقعاً ببینید اشاره‌گرها به چه چیزی اشاره می‌کنند، این برنامه را در محیطSTM32 وارد کنید و آن را با استفاده از دیباگر اجرا نمایید. یک نقطه توقف (breakpoint) درست بعد از مقداردهی به تمام متغیرها قرار دهید و برنامه را تا رسیدن به آن نقطه اجرا کنید.
با باز کردن پنل متغیرها (Variables panel)، می‌توانیم تمام متغیرها و مقادیر آن‌ها را مشاهده کنیم (شکل 14-۲ را ببینید).

شکل ۶-۲: پنل متغیرها با اشاره‌گرها

شکل 14-۲: پنل متغیرها با اشاره‌گرها

معمولاً، مقدار خود اشاره‌گر چندان جالب نیست. نکته‌ی مهم‌تر، چیزی است که اشاره‌گر به آن اشاره می‌کند. با کلیک‌کردن روی آیکن مثبت (+)، زیرمجموعه‌ی smallPtr گسترش پیدا می‌کند و می‌توانیم ببینیم که smallPtr به عدد ۶ (همچنین شناخته شده با کاراکتر \006) اشاره می‌کند. به طور مشابه، مشاهده می‌کنیم که largePtr به عدد ۹۸۷۶۵۴۳۲۱ اشاره می‌کند.

آرایه و حساب اشاره‌گر در زبان C

زبان C آرایه‌ها و اشاره‌گرها را بسیار شبیه به هم در نظر می‌گیرد. کد زیر را در ببینید:

ما مقدار array را به arrayPtr اختصاص داده‌ایم، نه &array. دلیل این است که C به طور خودکار یک آرایه را زمانی که مانند یک اشاره‌گر استفاده می‌شود، به یک اشاره‌گر تبدیل می‌کند. در واقع، آرایه‌ها و اشاره‌گرها تقریباً قابل‌تعویض هستند، به جز اینکه به روش‌های مختلفی تعریف می‌شوند.
حالا بیایید به یک عنصر از آرایه دسترسی پیدا کنیم:

این ترکیب (syntax) مشابه عبارت زیر است که می‌گوید مقدار arrayPtr را بگیرید، ۱ به آن اضافه کنید (ضرب در اندازه داده‌ای که به آن اشاره می‌شود) و سپس داده‌ای را که توسط نتیجه‌ی این عبارت اشاره می‌شود، برگردانید:

برنامه‌ی زیر رابطه بین آرایه‌ها و اشاره‌گرها را با جزئیات بیشتری نشان می‌دهد:

اولین کاری که این برنامه انجام می‌دهد، چاپ آدرس و محتویات هر عنصر آرایه به روش معمولی است: با استفاده از یک حلقه for برای دسترسی به هر ایندکس (به ترتیب).
در حلقه بعدی، با استفاده از اشاره‌گر arithmetic ، چاپ می‌کنیم. حالا باید دقیقاً بدانیم با چه چیزی سروکار داریم. متغیر array یک آرایه است. عبارت array[index] یک عددصحیح است و عملگر (&) یک عددصحیح را به یک اشاره‌گر تبدیل می‌کند، بنابراین &array[index] یک اشاره‌گر است. در نتیجه، این کد آدرس‌های حافظه زیر را برای هر عنصر آرایه چاپ می‌کند:

مقدار اشاره‌گر هر بار 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] شامل محاسبه‌ی آدرس می‌باشد، اما تکنولوژی کامپایلرها در طول سال‌ها بهبودیافته پس مشکلی در این بابت نیست.(استفاده از روش‌هایی که هزینه کمتری دارند، در برنامه‌های بزرگ حیاتی است). کامپایلرهای امروزی در تولید کد کارآمدتر بسیار خوب عمل می‌کنند، بنابراین استفاده از اشاره‌گرها برای دسترسی به آرایه‌ها خیلی بهینه تر نیست.
بااین‌حال، استفاده از منطق آدرس‌دهی گیج‌کننده‌تر است. مشخص نیست که به چه چیزی اشاره می‌شود و محدودیت‌های آرایه چیست، اما زمانی که برنامه نویسی پیچیده میشود زمانهایی کمک کننده خواهد بود انتخاب اینکه با چه روشی کار کنید کاملا به شما و برنامه شما بستگی دارد.

انتشار مطالب با ذکر نام و آدرس وب سایت سیسوگ، بلامانع است.

شما نیز میتوانید یکی از نویسندگان سیسوگ باشید.   همکاری با سیسوگ

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *