مقدمه
با توجه به استفاده از زبان برنامه نویسی C به صورت گسترده در انواع میکروکنترلرها به عنوان منعطف ترین زبان برنامه نویسی و لزوم درک این زبان پرکاربرد ، شاهد عدم استفاده از تمام قابلیت های این زبان در پروژه های مربوطه هستیم. در جزوه حاضر سعی شده است تا این قابلیت های برنامه نویسی پیشرفته تشریح و در اختیار همگان قرار گیرد و نکاتی که در برنامه نویسی پیشرفته باید رعایت شود تا سیستم روان ، بدون هنگ و بهینه باشد.
انواع دستورات پیشپردازش
یکی از امکانات زبان C فرمانهای پیش ترجمه یا پیش پردازش است. بهطوریکه از عنوان آن مشخص است این فرمانها قبل از شروع ترجمهی برنامه و در یک مرحلهی مقدماتی بررسیشده، نتیجه آن روی متن برنامه اعمال میگردد. استفاده از این فرمانها ازیکطرف باعث سهولت برنامهنویسی شده، از طرف دیگر باعث بالا رفتن قابلیت اصلاح و جابهجایی برنامه میگردد. حال فرض کنید که یک ثابت در چندین جای برنامهی شما ظاهر شود، استفاده از یک نام نمادین برای ثابت ایدهی خوبی به نظر میرسد. سپس میتوان بهجای قرار دادن ثابت در طول برنامه از نام نمادین استفاده کرد.
درصورتیکه لازم باشد تا مقدار ثابت را تغییر دهیم، بدون استفاده از نام نمادین، باید همه جای برنامه را با دقت برای یافتن و جایگزین کردن ثابت نگاه کنیم، آیا میتوانیم این کار را در C انجام دهیم؟!
C قابلیت ویژهای به نام پیش پردازشگر دارد که امکان تعریف و مرتبط ساختن نامهای نمادین را با ثابتها فراهم میآورد. پیش پردازشگر C پیش از کامپایل شدن اجرا میشود.
پیش پردازنده نوعی مترجم است که دستورات توسعهیافتهای از یکزبان را به دستورات قابلفهم برای کامپایلر همان زبان تبدیل میکند. قبل از اینکه برنامه کامپایل شود، پیش پردازنده اجرا میشود و دستورات را که با نماد # شروعشدهاند را ترجمه میکند. سپس کامپایلر برنامه را کامپایل میکند
. دستورات پیش پردازنده در زبان سی شامل موارد زیر هستند:
توضیحات | دستور پیش پردازشی |
اضافه کردن فایل کتابخانه | #include |
تعریف یک ماکرو | #define |
حذف یک ماکرو | #undef |
اگر یک ماکرو تعریف شده است آنگاه | #ifdef |
اگر یک ماکرو تعریف نشده است آنگاه | #ifndef |
اگر شرط برقرار بود آنگاه | #if |
انتهای بلوک if | #endif |
در غیر این صورت | #else |
ایجاد یک خطا در روند کامپایل برنامه | #error |
تغییر رفتا کامپایلر | #pragma |
نکتهای که در مورد پیش پردازشگر باید در نظر داشت این است که پیش پردازشگر C مبتنی برخط است. هر دستور ماکرو با یک کاراکتر خط جدید به پایان میرسد، نه با سمیکالن.
پیش پردازنده define
دستور defineرایجترین دستور پیش پردازشی است که به آن ماکرو می گویند. این دستور هر مورد از رشته کاراکتری خاصی (که نام ماکرو هست) را با مقدار مشخصشدهای (که بدنه ماکرو هست) جایگزین می کند. نام ماکرو همانند نام یک متغیر در C است که باید با رشته تعریفکننده ماکرو حداقل یک فاصله داشته باشد و بهتر است جهت مشخص بودن در برنامه با حروف بزرگ نمایش داده شود. بهعنوانمثال دستور #define MAX 20 موجب میشود تا مقدار ماکروی MAX در سرتاسر برنامه برابر با ۲۰ فرض شود؛ یعنی در هر جای برنامه از ماکروی MAX استفاده شود مثل این است که از عدد ۲۰ استفادهشده است.
کاربرد دیگر دستور defineدر تعریف ماکروهایی است که دارای پارامتر باشند. این مورد بهصورت زیر استفاده میشود:
1 |
#define <تعریف ماکرو (اسامی پارامترها) <نام ماکرو |
تعریف ماکرو مشخص میکند که چه عملی باید توسط ماکرو انجام گیرد. اسامی پارامترها متغیرهایی هستند که در حین اجرای ماکرو به آن منتقل میشوند که اگر تعداد آنها بیشتر از یکی باشد، با کاما از هم جدا می شوند. از دستور undefجهت حذف ماکرویی که پیشتر تعریفشده است استفاده میکنیم. روش استفاده از این دستور بهصورت زیر است:
1 |
#undef <نام ماکرو> |
در اینجا نام ماکرو شناسهای است که پیشتر توسط دستور defineتعریفشده است.
پیش پردازنده include
ضمیمه کردن فایلها توسط دستور پیش پردازنده #include انجام میگیرد. این دستور بهصورت های زیر مورد استفاده قرار میگیرد:
1 2 |
#include "نام فایل" #include <نام فایل> |
فایلهای سرآیند به دودسته تقسیم میشوند:
- هدر فایلهایی که همراه کامپایلر C وجود دارند و پسوند همهی آنها .h است.
- هدر فایلهایی که توسط برنامهنویس نوشته میشوند.
روش اول دستور #include برای ضمیمه کردن فایلهایی استفاده میشود که توسط برنامهنویس نوشتهشدهاند و روش دوم برای ضمیمه فایلهایی استفاده میشوند که همراه کامپایلر وجود دارند.
1 2 |
#include <stdio.h> #include "myheader.h" |
فایلهای سرآیند از اهمیت ویژهای برخوردارند، زیرا :
- بسیاری از توابع مثل getcharو putcharدر فایلهای سرآمد مربوط به سیستم، بهصورت ماکرو تعریفشدهاند.
- با فایلهای سرآیندی که برنامهنویس مینویسد، علاوه بر تعریف ماکروها، میتوان از بسیاری از تعاریف تکراری جلوگیری کرد.
دستورات پیشپردازش شرطی
در حالت معمولی، دستور if برای تصمیمگیری در نقاط مختلف به کار میرود. شرطهایی که در دستور if ذکر میشوند در حین اجرای برنامه ارزشیابی میشوند؛ یعنی اگر شرط ذکرشده در دستور if درست باشد یا نادرست، این دستور و کلیه دستورات دیگر که در بلاک if قرار دارند ترجمه میشوند ولی در دستورات پیش پردازنده شرطی، شرطی که در آن ذکر میشود در حین ترجمه ارزشیابی میشود. دستورات پیش پردازنده شرطی عبارتاند از : #if,#else,#endif, #ifdef, #ifndef
دستور if بهصورت زیر به کار میرود :
1 2 3 4 5 |
#if عبارت شرطی مجموعه ی دستورات۱ #else مجموعه دستورات۲ #endif |
در این دستور در صورتی که عبارت شرطی بعد از if برقرار باشد ، مجموعه دستورات ۱ و در غیر این صورت مجموعه دستورات ۲ کامپایل می شود.
برخلاف دستور if در C، حکمهای تحت کنترل دستور #ifdef در آکولادها محصور نمیگردند. بهجای آکولادها برای پایان بخشیدن به بلوک #ifdef باید از دستور #endif استفاده شود.
1 2 3 |
#ifdef DEBUG /* Your debugging statements here */ #endif |
دستور #ifndef عکس دستور #ifdef عمل میکند. اگر ماکرویی که نام آن در جلوی #ifndef قرار دارد در یک دستور #define تعریفنشده باشد، مجموعه حکمهای ذکرشده کامپایل میگردند، در غیر این صورت کامپایل نخواهند شد. قالب کلی استفاده شبیه #ifdef است:
1 2 3 4 5 6 |
#ifdef <نام ماکرو> مجموعه ی دستورات . . . #endif |
دستور #ifndef هم برای پایان به دستور#endif نیاز دارد .
1 2 3 |
#ifndef MESSAGE #define MESSAGE "You wish!" #endif |
دستور #error موجب جلوگیری از ادامه ترجمهی برنامهی توسط کامپایلر شده، بهصورت زیر به کار میرود:
1 |
#error پیام خطا |
پیام خطا، جملهای است که کامپایلر پس از رسیدن به این دستور، آن را بهصورت زیر در صفحهنمایش ظاهر میکند:
1 |
error: شماره خط نام فایل |
پیش پردازنده pragma
این دستور به کامپایلر این امکان را می دهد که ماکروهای مورد نظر را بدون مداخله با کامپایلرهای دیگر تولید کند. به صورت کلی از این ماکرو به صورت زیر استفاده می شود :
1 |
#pragma name |
که در آن name میتواند یکی از حالت های زیر باشد :
- #pragma warn- : غیرفعال کردن اخطارهای صادر شده از کامپایلر
- #pragma warn+ : فعال کردن اخطارهای صادر شده از کامپایلر
- #pragma opt- : بهینه کننده کد توسط کامپایلر را غیر فعال می کند
- #pragma opt+ : بهینه کننده کد توسط کامپایلر را فعال می کند
- #pragma optsize- : بهینه کننده برنامه نسبت به سرعت
- #pragma optsize+ : بهینه کننده برنامه نسبت به حجم
نکته : معادل دستور optsize را میتوانید از طریق نرم افزار کدویژن در آدرس زیر تنظیم نمایید :
Project/Configuration/C Compiler/Codevision/Optimized for
- #pragma savereg+ : این دستور هنگامی که وقفه ای رخ دهد ، میتواند رجیسترهای R0,R1,R22,R23,R24,R25,R26,R27,R30,R31 و SREG را ذخیره نماید.
- #pragma savereg- : این دستور مخالف دستور قبلی است و رجیسترها را پاک می کند.
- #pragma regalloc+ : این دستور متغیرهای سراسری را به رجیسترها اختصاص می دهد.(معادل کلمه کلیدی register )
- #pragma regalloc- : این دستور برخلاف دستور قبلی متغیر سراسری را در حافظه SRAM تعریف می کند.
نکته : معادل دستور regalloc را میتوانید از طریق نرم افزار کدویژن در آدرس زیر تنظیم کنید :
Project/Configuration/C Compiler/Code Generation/Automatic Register Allocation
- #pragma promoteacher+ : این دستور متغیرهای char را به int تبدیل می کند.
- #pragma promoteacher- : این دستور متغیر های int را به char تبدیل می کند.
نکته : معادل دستور promoteacher را میتوانید از طریق نرم افزار کدویژن در آدرس زیر تنظیم کنید :
Project/Configuration/C Compiler/Code Generation/Promote char to int
- #pragma unchar+ : این دستور نوع داده char را به unsigned char تبدیل می کند.
- #pragma unchar- : این دستور نوع داده char را بدون تغییر و به همان صورت signed char تعریف می کند.
نکته : معادل دستور unchar را میتوانید از طریق نرم افزار کدویژن در آدرس زیر تنظیم کنید :
Project/Configuration/C Compiler/Code Generation/Char is unsigned
- #pragma library این دستور یک فایل کتابخانه ای با پسوند .lib را به برنامه پیوند میزند.
نحوه ساخت فایل های کتابخانه
تقسیم برنامه های بزرگ به واحد های کوچکتر یا اصطلاحا ماژولار کردن برنامه ، از جهات مختلفی ، بسیار سودمند است. به خوانایی برنامه کمک زیادی می کند. برنامه می تواند توسط چندین نفر نوشته شود ، تغییرات در برنامه به سهولت انجام می شود و از هر ماژول در پروژه های دیگر میتوان بهره گرفت.
هر فایل کتابخانه شامل دو فایل است :
- فایل هدر ( header ) : این فایل که با پسوند .h است حاوی الگوی توابع و پیش پردازنده ها ( ماکرو ) است.
- فایل سورس ( source ) : این فایل که با پسوند .c است حاوی بدنه توابعی است که الگوی آن در هدر فایل تعریف شده است.
این دو فایل در کنار هم درون پوشه پروژه اصلی قرار می گیرند. هدر فایل هایی که تنها شامل عملگرهای پیش پردازنده هستند ، نیازی به فایل سورس ندارند. در هر کامپایلر یا با هر ویرایشگر متنی ( مثل Notepad ) میتوان این دو فایل را ایجاد کرد و با پسوند مربوطه ذخیره کرد.
نحوه استفاده از کتابخانه در برنامه
برای استفاده از کتابخانه ای که خود ساخته ایم ابتدا باید هدر فایل آن را بوسیله ” به برنامه اضافه کنیم. به صورت زیر :
1 |
#include "headerfile.h" |
بعد از اضافه کردن هدر فایل میتوانیم از ثوابت و توابعی که درون کتابخانه تعریف کرده ایم در برنامه اصلی استفاده کنیم.
الگوی ساخت فایل هدر
برای ساخت هدر فایل با پسوند .h باید الگویی را رعایت نمود. برای مثال می خواهیم یک کتابخانه برای اتصال keypad به میکرو ایجاد کنیم. برای این کار ابتدا یک فایل با پسوند .h ساخته و سپس درون آن به صورت الگوی زیر می نویسیم :
1 2 3 4 5 6 7 8 |
#ifndef _KEYPAD_H #define _KEYPAD_H #include <headerfiles.h> #define … … void Functions(void); … #endif |
همانطور که مشاهده می کنید الگوی نوشتن هدر فایل به این صورت است که در ابتدا با استفاده از ماکروی ifndef/endif و سپس نوشتن نام هدر فایل با حروف بزرگ و دقیقا به همان الگوی مثال زده شده ( یک “_” در ابتدا و یک “_” به جای نقطه ) و استفاده از ماکروی #define یک ماکرو جدید با نام _KEYPAD_H تعریف کردیم. این گونه نوشتن را محافظت از برنامه یا Header Guard گویند. هدرگارد باعث میش شود تا ماکروها و توابع تعریف شده فقط و فقط یکبار تعریف شده باشند ( جلوگیری از تعریف آنها با نام یکسان) .
سپس اگر هدر فایل به کتابخانه های دیگری نیاز دارد ، آنها را با #include اضافه می کنیم. بعد از آن تعریف ثوابت را با استفاده از #define انجام می دهیم. اگر تغیر نوع در متغیر ها وجود دارد آنها را بعد از ثوابت typedef می کنیم. در پایان الگوی تعریف توابعی که میخواهیم آنها را در فایل سورس تشریح کنیم را باید در این قسمت بیاوریم.
الگوی ساخت فایل سورس
فایل سورس نیز دارای الگویی است که باید رعایت شود. برای ساخت فایل سورس برای keypad.h به صورت زیر عمل می کنیم :
1 2 3 4 5 |
#include "keypad.h" void Functions(void){ بدنه تابع } … |
همانطور که مشاهده می کنید ، در ابتدای هر فایل سورس ، فایل هدر include می شود و در خطوط بعدی تنها بدنه توابع طبق قوانین مربوط به توابع آورده می شود.
نوع داده sfrb و sfrw درکدویژن
در کامپایلر کدویژن این دو نوع داده ای برای دستیابی به رجیسترهای I/O موجود در حافظه SRAM میکرو اضافه شده است. در حقیقت با تعریف این دو نوع داده در هدر فایل میکروکنترلر ( برای مثال هدر فایل mega32.h ) قابلیت دسترسی راحت بیتی به رجیسترهای I/O برای کاربر فراهم شده است. بنابراین اگر فایل .h هر میکرویی را باز نمایید ، درون آن این نوع داده ای را مشاهده می کنید. نحوه تعریف آن به صورت زیر است :
1 2 |
sfrb آدرس رجیستر=نام رجیستر ; sfrw آدرس رجیستر=نام رجیستر ; |
در سمت راست تساوی آدرس رجیستر مورد نظر در حافظه SRAM و در سمت چپ تساوی نام دلخواهی را وارد می کنیم. مثال :
1 2 3 |
sfrb PORTA=0x1b; sfrb DDRA=0x18; ... |
همه این تعاریف مربوط به پورت ها و رجیسترهای میکروکنترلرهای AVR در هدر فایل هر یک موجود است که با include کردن آن به برنامه این رجیسترها اضافه می شود و نیازی به تعریف مجدد در برنامه نیست. بنابراین تنها آنچه که در برنامه اصلی از آن استفاده می شود ، استفاده از عملگر نقطه ( dot ) برای دسترسی بیتی به رجیسترهاست که به صورت زیر است :
1 |
نام رجیستر . n مقدار = ; |
که در آن n برای رجیسترهای تعریف شده با sfrb بین ۰ تا ۸ و برای sfrw بین ۰ تا ۱۵ است.
نکته ۱ : آدرس رجیسترهای حافظه SRAM برای هر میکروکنترلری در انتهای دیتاشیت آن آورده شده است.
نکته ۲ : تفاوت بین sfrb,sfrw در این است که از sfrb برای دسترسی بیتی به رجیسترهای ۸ بیتی و از sfrw برای دسترسی بیتی به رجیسترهای ۱۶ بیتی استفاده می شود.
نکته ۳ : در معماری میکروکنترلرهای AVR تنها به رجیسترهایی که در آدرس ۰ تا ۱FH قرار دارند میتوان دسترسی بیتی داشت. بنابراین این محدودیت برای دستور sfrb و sfrw نیز برقرار خواهد بود.
اشاره گرها در C
زمانی که یک متغیر تعریف می شود ، بخشی از حافظه را اشغال می کند. بسته به نوع متغیر این بخش میتواند یک یا چند بیت یا بایت باشد. اشاره گر خود نیز یک متغیر است که به جای ذخیره کردن داده آدرس محل قرارگیری متغیرهای دیگر را در خود ذخیره می کند. یعنی یک اشاره گر به محل ذخیره یک متغیر در حافظه اشاره می کند. هر اشاره گر می تواند آدرس متغیر هم نوع خود را در خود نگه دارد. برای مثال برای نگه داری آدرس یک متغیر int نیاز به تعریف یک اشاره گر از نوع نیاز به تعریف یک اشاره گر از نوع int می باشد. قالب تعریف یک اشاره گر را در زیر مشاهده می کنید :
1 2 3 |
نام اشاره گر * نوع اشاره گر int *x; char *y; |
همانطور که مشاهده می شود تنها تفاوت یک اشاره گر با متغیر در * قبل از نام اشاره گر است. برای نگه داری دائمی یک اشاره گر ، محل ذخیره یک اشاره گر میتواند توسط یکی از کلمات flash,eeprom تعیین شود. در صورت تعیین نکردن محل حافظه ، اشاره گر به صورت پیش فرض در حافظه SRAM ذخیره خواهد شد.
مقدار دهی به اشاره گر
برای اینکه آدرس محل یک متغیر را در یک اشاره گر ذخیره کنیم ، از عملگر & استفاده می کنیم. مثال :
1 2 3 |
int *x,y; y=142; x=&y; |
در این مثال در ابتدا یک اشاره گر و یک متغیر هر دو از نوع int تعریف شده است. به متغیر y مقداری نسبت داده شده است. برای اینکه آدرس محل ذخیره متغیر y در حافظه را داشته باشیم ، از عملگر & استفاده می کنیم.
دسترسی به محتوای یک اشاره گر
برای اینکه به محتوای یک اشاره گر که به محلی از حافظه اشاره می کند را داشته باشیم ، از عملگر * استفاده می کنیم. مثال :
1 2 3 4 |
int *x,y,z; y=142; x=&y; z=*x; |
یک متغیر z به برنامه اضافه کردیم و در آن محتوای x را نسبت دادیم. بنابراین z=142 می شود.
عملیات روی اشاره گرها
عملیات جمع و تفریق را می توان روی متغیرهای اشاره گر انجام داد اما ضرب و تقسیم را روی یک اشاره گر نمی توان استفاده کرد. نکته مهمی که باید به آن توجه کرد این است که چون اشاره گر آدرسی در حافظه است وقتی عملیاتی که روی آن انجام می گیرد رفتار متفاوتی دارد. برای مثال عمل جمع اشاره گر را به تعداد بایت های نوع داده آن حرکت می دهد.
مثال : چون a اشاره گری به یک عدد int است و نوع int 2 بایت دارد با عمل افزایش ۲ واحد به a اضافه می شود. یعنی به ۲ بایت بعدی حافظه اشاره می کند.
1 2 3 4 |
int a=10; int *p; p=&a; p=p+2; |
ارتباط اشارهگر با آرایه و رشته
در زبان برنامهنویسی C، بین آرایهها با رشتهها و اشارهگرها، ارتباط نزدیکی وجود دارد. اشارهگرها حاوی آدرس هستند و اسم هر آرایه یا رشته نیز یک آدرس است. اسم آرایه، آدرس اولین عنصر آرایه را مشخص میکند؛ بهعبارتدیگر، اسم هر آرایه، آدرس اولین محلی را که عناصر آرایه ازآنجا به بعد در حافظه ذخیره میشوند، نگهداری میکند؛ بنابراین اسم هر آرایه، یک اشارهگر است.
به مثال زیر توجه کنید:
1 2 |
int table [5]; int *p; |
همانطور که در مثال صفحه قبل مشاهده میشود، یک آرایه با ۵ عنصر به نام table و اشارهگر p هر دو از نوع int معرفیشدهاند. اگر اولین عنصر آرایه table در محل ۱۰۰۰ حافظه واقعشده باشد، نام آرایه به محل ۱۰۰۰ آرایه اشاره خواهد کرد.
1 |
p=table; |
چون هر دو متغیر table و p از جنس اشاره گر هستند ، یدون هیچ عملگری آنها را میتوان برابر هم قرار داد. در نتیجه میتوان گفت موارد زیر با یکدیگر معادل هستند :
*(p+1) معادل table[1]
p[2] معادل *(table+1)
*p معادل *table
استفاده از اشاره گرها در توابع
توابعی که قبلا با آن آشنا شدیم، تنها یک مقدار را به خروجی باز می گرداندند. با استفاده از اشاره گرها در ورودی توابع ، میتوان توابعی ساخت که بیش از یک خروجی داشته باشند. در این روش که به آن فراخوانی تابع با ارجاع گفته می شود، به جای متغیرهای ورودی در هنگام تعریف تابع ، اشاره گر قرار می گیرد و در هنگام فراخوانی تابع ، به جای متغیرهای ورودی آدرس آنها قرار می گیرد. بنابراین اگر تابعی می خواهیم که چند پارامتر خروجی دارد، می توانیم پارامترهای خروجی را در لیست پارامترهای ورودی تابع و به صورت اشاره گر تعریف کنیم تا تابع آنها را پر کرده و تحویل ما دهد. به مثال زیر توجه کنید :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void fx(int *a,int *b,int *c); void main(void){ int x=10,y=20,z; fx(&x,&y,&z); } void fx(int *a,int *b,int *c){ int i,j,k; i=*a; j=*b; i=i/2; j=j/2; *a=I; *b=j; *c=i+j; } |
در این مثال محتوای ورودی به متغیر های I,j ریخته می شود و سپس بر روی آن عملیات مورد نظر صورت می گیرد. در این تابع هر دو متغیر I,j تقسیم بر دو شده است. سپس مقدار جدید i به جای محتوای آدرس a قرار می گیرد. همچنین مقدار جدید متغیر j به جای محتوای آدرس اشاره گر b و جمع I,j نیز در محتوای آدرسی قرار می گیرد که اشاره گر c به آن اشاره می کند. در نتیجه در پایان برنامه x=5 y=10 و z=15 می شود. ( تابعی با ۳ ورودی و ۳ خروجی )
ساختارها در C
همانطور که تا اینجا آموختیم، آرایهها میتوانند برای جمعآوری گروههایی از متغیرهایی با نوع مشابه مورداستفاده قرار گیرند؛ بنابراین نمیتوان بهعنوانمثال آرایهای تعریف کرد که شامل پنجخانه از نوع صحیح و پنجخانه از نوع اعشاری باشد. از طرفی هم در کاربردهای مختلف برنامهنویسی نیاز به تعریف کردن عناصر مختلف در کنار هم و منسوب کردن یک نام به همهی آنها داریم تا بتوانیم مجموعهی آنها را بهصورت یکجا مورد پردازشهایی مانند بازنویسی آنها بهصورت یکجا، ارسال کل آنها به یک تابع و یا آمادهسازی و برگرداندن همهی آنها بهعنوان نتیجه یک تابع قرار دهیم.
فرض کنید دادههای مربوط به یک دانشجو مثل شماره دانشجویی، نام خانوادگی، نام، جنسیت، تعداد واحد گذرانده شده و معدل کل را که دارای نوعهای متفاوتی هستند را بخواهیم تحت یک نام تعریف کنیم.
یا بهعنوانمثال دیگر، اگر بخواهیم اطلاعات مربوط به کارکنان شرکتی را که شامل نام کارمند (از نوع کاراکتری)، شماره کارمندی (از نوع عدد صحیح)، حقوق (از نوع عدد صحیح) و … است، تحت یک نام ذخیره کنیم، در این صورت متغیر معمولی و آرایه پاسخگوی نیاز ما نیستند. اکنون میخواهیم بدانیم که چگونه قطعات دادههایی را که نوع یکسان ندارند، (مانند مثال فوق) جمعآوری کنیم.
پاسخ این است که میتوانیم متغیرهایی با نوعهای مختلف را با نوع دادهای به نام ساختار گروهبندی کنیم. بنابراین میتوان گفت که ساختار در زبان C، نامی برای مجموعهای از متغیرهاست که این متغیرها میتوانند هم نوع نباشند، یعنی میتواند ساختاری از انواع مختلف دادهها ازجمله float, int, char, unsigned char و … را درقالب یک نام در خود داشته باشد و کاربر در هر زمان به آنها دسترسی داشته باشد.
قالب تعریف ساختار را در زیر مشاهده میکنیم:
1 2 3 4 5 6 |
struct [structure tag]{ member definition member definition ... member definition }[one or more structure variables]; |
نام ساختار یا (structure tag) از قانون نامگذاری برای متغیرها تبعیت میکند. عضوهای ساختار یا (member)، متغیرهایی هستند که قسمتی از ساختار میباشند و همانند یک متغیر معمولی یا آرایه، باید اسم و نوع هرکدام مشخص باشد. لیست نامها یا (structure variables) هم متغیرهایی هستند که قرار است ساختمان این ساختار را داشته باشند. برای استفاده از عناصر ساختار معرفیشده باید متغیرهایی از نوع ساختار پسازآن معرفی شود. دو روش برای این کار وجود دارد.
در زیرقالب روش اول را مشاهده میکنید:
1 2 3 |
نام ساختار struct{ عناصر ساختار }نام متغیرها; |
به مثال زیر توجه کنید:
1 2 3 4 5 6 7 8 |
struct student_record{ long student_number; /*شماره دانشجویی*/ char first_name[21]; /*نام دانشجو*/ char last_name[31]; /*نام خانوادگی*/ char gender_code; /*کد جنسیت*/ float average; /*معدل کل*/ int passed_units ; /*واحدهای گذرانده شده*/ }student1; /*تعریف متغیری از نوع ساختمان این ساختار */ |
در این تعریف student_record نام الگوی تعریفشده برای این ساختار است که میتواند نوشته نشود و student1 نام متغیری است با ساختمان این ساختار که دارای شش عضو است. درصورتیکه بعد از خاتمهی تعریف ساختار (بعد از { ) نامی نوشته نشود، فقط یک الگو تعریفشده است و چون متغیری با ساختمان این ساختار تعریفنشده، حافظهای اشغال نخواهد شد. در این حالت میتوان در ادامهی برنامه از کلمهی struct و نام ساختار (در اینجا student_record) برای تعریف ساختارهای موردنیاز استفاده کرد. در زیرقالب روش دوم را میبینید که پس از معرفی ساختار صورت میگیرد:
1 2 |
struct <نام متغیرها> <نام ساختار>; struct s_type p1,p2; |
در بالا متغیرهایی بانام p1,p2 از نوع ساختار s_type معرفیشدهاند. هرکدام از اینها حاوی کل ساختار معرفیشده، میباشند. درروش دوم در زمان معرفی ساختار، متغیرهایی از نوع ساختار در انتهای آن معرفی میشوند.
نکته ۱ : در کامپایلر کدویژن با استفاده از کلمات کلیدی eeprom و flash این قابلیت وجود دارد که ساختارها را در ناحیه SRAM ، FLASH یا EEPROM تعریف نمود. در صورتی که نام محل ذخیره سازی ذکر نشود ، کامپایلر به طور پیش فرض حافظه SRAM را انتخاب میکند.
نکته ۲ : ساختارهایی که در حافظه flash تعریف می شوند ، از نوع ساختار ثابت می باشند و نمیتوان در عناصر آن نوشت و فقط میتوان آنها را خواند.
دسترسی به عناصر ساختار
قالب دسترسی به عناصر ساختار با استفاده از نقطه ( dot ) و به صورت زیر است:
نام عناصر موردنظر در ساختار. نام متغیر از نوع ساختار
ابتدا نام ساختار حاوی آن عضو و سپس نام عضو بیانشده و این نامها با عملگر عضو ساختار که علامت آن نقطه است به یکدیگر مرتبط میشوند. این عملگر که دارای بالاترین تقدم عملیات بوده ترتیب اجرایش از چپ به راست است. توجه شود که بهصورت قراردادی در دو طرف عملگر نقطه فاصله خالی گذاشته نمیشود. اگر عناصر از نوع آرایه باشند، ذکر اندیس آرایه جهت دستیابی به آن عنصر ضروری است.
مثال: ساختاری را تعریف کنید که اطلاعات یک دانشجو را در خود ذخیره نماید.
1 2 3 4 5 6 |
struct student{ long int id; char name[20]; float average; int age; }s; |
مثال: مقداردهی اولیه به عناصر متغیر ساختاری s
1 2 3 4 |
s.id = 860133710; s.name = "ali"; s.average = 17.64; s.age = 18; |
تخصیص ساختارها
در مقداردهی اولیه میتوان مجموعهای از مقادیر را به یک ساختار نسبت داد، ولی انجام چنین کاری در متن برنامه بهصورت دستور اجرایی امکانپذیر نیست. تنها عمل تخصیص که در مورد رکوردها در زبان C تعریفشده است، تخصیص یک ساختار به ساختار دیگری با ساختمان دقیقا یکسان (هر دو ساختار از طریق یک struct تعریفشده باشند) هست که در این صورت محتویات هر فیلد از ساختار مبدأ به فیلد متناظر از ساختار مقصد منتقل میشود. ساختار مبدأ میتواند متغیری در برنامه یا خروجی یک تابع باشد.
اشارهگرها و ساختارها
در زبان C تعریف اشارهگر از نوع ساختار، همانند تعریف سایر انواع اشارهگرها امکانپذیر است. همانطور که در فراخوانی تابع میتوانید اشارهگری را ارسال کنید که به آرایه ارجاع میدهد، میتوانید اشارهگری که به ساختار اشاره میکند را نیز ارسال کنید.
بههرحال برخلاف ارسال ساختار به تابع که نسخهی کاملی از ساختار را به تابع میفرستد، ارسال اشارهگر به ساختار فقط آدرس ساختار را به تابع میفرستد. سپس تابع میتواند جهت دستیابی مستقیم به اعضای ساختار از آدرس استفاده میکند و از سرریزی تکرار ساختار پرهیز نماید، بنابراین این روش جهت ارسال اشارهگر به ساختار کارآمدتر است، نسبت به اینکه خود ساختار را به تابع ارسال کنید.
اشارهگر ساختار به دو منظور استفاده میشود:
- امکان فراخوانی به روش ارجاع را در توابعی که دارای آرگومان از نوع ساختار هستند، فراهم میکند.
- برای ایجاد فهرستهای پیوندی و سایر ساختار دادههایی که با تخصیص حافظه پویا سروکار دارند، به کار میرود.
وقتیکه ساختارها از طریق فراخوانی به روش ارجاع به توابع منتقل میشوند، سرعت انجام عملیات بر روی آنها بیشتر میگردد. لذا در حین فراخوانی توابع، بهتر است بهجای ساختار، آدرس آن را منتقل نمود. عملگر & برای مشخص کردن آدرس ساختار مورداستفاده قرار میگیرد.
تعریف اشارهگرهای ساختار مانند تعریف متغیرهای ساختار است، با این تفاوت که قبل از اسم متغیر، علامت * قرار میگیرد.
مثال زیر را در نظر بگیرید، در این دستورات person یک متغیر ساختار و p یک اشارهگر ساختار تعریفشده است.
1 2 3 4 |
struct bal{ float balance; char name[80]; }person, *p; |
اکنون دستور زیر را در نظر بگیرید
1 |
p = &person; |
با این دستور، آدرس متغیر ساختار person در اشارهگر p قرار میگیرد. برای دسترسی به محتویات عناصر ساختار از طریق اشارهگر، باید اشارهگر را در داخل پرانتز محصور کنید. بهعنوانمثال دستور زیر موجب دسترسی به عنصر balance از ساختار person میشود. علت قرار دادن متغیر اشارهگر در پرانتز، این است که تقدم عملگر نقطه از * بالاتر است.
1 |
(*p).balance; |
بهطورکلی برای دسترسی به عناصر ساختاری که یک اشارهگر به آن اشاره میکند به دو روش میتوان عمل کرد:
- ذکر نام اشارهگر در داخل پرانتز و سپس نام عناصر موردنظر که با نقطه از هم جدا میشوند. (مثل دسترسی به عنصر balance از ساختار person توسط اشارهگر p)
- استفاده از عملگر -> که روش مناسبتری است. اگر بخواهیم با استفاده از عملگر-> به عنصر balance از ساختار person دسترسی داشته باشیم باید به طریق زیر عمل کنیم (علامت -> متشکل از علامت منها و علامت بزرگتر است).
1 |
p -> balance; |
در اینجا بیان این نکته ضروری است که آرایهها، اشارهگرها و ساختارها دارای ارتباط نزدیکی باهم هستند. در ضمن عمگرهای مربوط به آنها شامل زیر نویس یا []، عضور رکورد یا نقطه و دستیابی غیرمستقیم به عضو رکورد یا -> همگی دارای بالاترین تقدم هستند. عملگرهای دستیابی غیرمستقیم یا * و استخراج آدرس یا & هم دارای تقدم دوم میباشند. حال اگر این عبارتها همراه همدیگر در عبارتی ظاهر شوند باید کمی با دقت کد مورد نظر نوشته شود. به مثال زیر دقت کنید:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct point{ float x; float y; }; struct line{ struct point strat; struct point end; char *name; } a = {1, 1, 10, 20, "ab"}; struct line *pa, *pm, m[] = {{2, 3, 4, 5, "cd"},{4,6,8,1, "mn"},{8,5,4,2, "xy"}}; pa = &a; pm = &m[1]; |
با توجه به تعاریف بیان شده و اینکه ترتیب اجرای دو عملگر عضو ساختار و دستیابی غیرمستقیم از چپ به راست است، عبارتهای زیر معادل هستند:
1 2 3 4 5 |
a.start.x pa->start.x (a.start).x (pa->start).x (*pa).start.x |
دو عبارت زیر نیز یک معنی میدهند:
1 2 |
m[1].end.y pm->end.y |
با این توضیح که در اولی دستیابی از طریق اندیس خانهی یکم آرایهی m که یک ساختار است به عضو end و سپس به عضو y انجام شده است، ولی در دومی دستیابی به عضو end از طریق آدرس خانهی یکم آرایهی m که در متغیر pm قرار دارد انجام شده است.
آرایهای از ساختارها
در C، نام آرایه را میتوانید در مقابل نام ساختار قرار دهید تا آرایهای از ساختارها را اعلام کنید. یکی از بیشترین موارد کاربرد ساختارها، استفاده از آنها بهعنوان عناصری از آرایه است. برای تعریف آرایهای از ساختارها، ابتدا نوع ساختار را تعریف کرده سپس همانند متغیرهای معمولی، آرایهای از آن نوع را تعریف میکنیم. برای نمونه، با فرض داشتن ساختاری به نام x حکم زیر راداریم:
1 |
struct x array_of_structure[8]; |
آرایهای به نام array _of_structure از Struct x اعلام کرده است. این آرایه ۸ عنصر دارد، هر عنصر آن نمونه یکتایی از Struct x است. مثال زیر را در نظر بگیرید:
1 2 3 4 5 6 |
struct student{ char name[21]; int stno; float ave; }; struct student st[100]; |
در این دستورات، آرایه ۱۰۰ عنصری st طوری تعریف میشود که هر یک از عناصر آن، از نوع ساختار student است.
یونیونها در C
ساختمان یونیون کاملا مشابه با ساختار است با این تفاوت که به جای کلمه کلیدی struct از کلمه کلیدی union استفاده می شود. اما در یونیون محل مشخصی از حافظه ، بین دو یا چند متغیر به صورت اشتراکی مورد استفاده قرار می گیرد به طوری که متغیرها همزمان نمیتوانند از این محل در حافظه استفاده کند ، بلکه هر متغیر در زمان های متفاوتی می تواند این محل را مورد استفاده قرار دهد. بنابراین فضایی که یک یونیون در حافظه اشغال می کند مانند فضایی که یک ساختمان اشغال می کند نیست. بلکه یونیون بیشترین طول متغیر درون خود را ( به لحاظ طول بیت ) به عنوان طول خود در نظر می گیرد و این فضا را بین بقیه متغیرهای درون یونیون به اشتراک می گذارد. یونیون نیز همانند ساختمان دو روش تعریف دارد. نحوه تعریف یونیون به صورت زیر است :
1 2 3 4 |
union نام یونیون { عناصر یونیون }; |
مثال :
1 2 3 4 5 6 |
union u_type { char I; int x; float y; } |
در این یونیون تعریف شده چون بزرگترین نوع float است که ۴ بایت حافظه را اشغال می کند ، بنابراین کل یونیون ۳۲ بیت از حافظه را اشغال میکند.
روش اول معرفی یونیون : بعد از تعریف
1 |
union نام متغیرها نام یونیون ; |
مثال :
1 |
union u_type i1,i2; |
روش دوم معرفی یونیون : در حین تعریف
مثال :
1 2 3 4 5 6 |
union u_type { char I; int x; float y; }i1,i2; |
دسترسی به عناصر یونیون : با استفاده از نقطه صورت می گیرد. مثال :
1 2 3 4 |
i1.i=33; i2.x=2048; i1.y=6.28; i2.i=254; |
نکته : هنگامی که از نوع های داده ای کوچکتر از ۳۲ بیت استفاده شود بقیه بیت ها بدون استفاده و ۰ هستند.
نوع داده شمارشی در C
این نوع داده قابلیت نامگذاری یک مجموعه را می دهد. برای مثال میتوان روزهای هفته را درون یک داده شمارشی قرار داد به طوری که روز اول هفته عدد ۰ و روز آخر هفته عدد ۶ تخصیص می یابد. قالب معرفی نوع داده شمارشی را در زیر مشاهده می کنید :
1 2 3 4 5 6 7 |
enum نام نوع شمارشی { عنصر اول; عنصر دوم; ... عنصر آخر; }; |
مثال :
1 2 3 4 5 6 7 |
enum color { red; green; blue; yellow; }; |
روش اول معرفی نوع شمارشی : بعد از تعریف
1 2 |
enum نام متغیرها نام نوع شمارشی ; enum color c1,c2; |
روش دوم معرفی نوع شمارشی : در حین تعریف
مثال :
1 2 3 4 5 6 7 |
enum color { red; green; blue; yellow; }c1,c2; |
نحوه مقدار دهی به نوع شمارشی : مثال :
1 2 |
c1=red; c2=blue; |
تغییر نام انواع داده ها با دستور typedef
توسط این دستور در زبان c میتوان نام داده هایی همچون int,char,float,… را به هر نام دلخواه دیگری تغییر داد. این دستور دو مزیت دارد :
- موجب می شود تا برای نوع داده هایی که نام طولانی دارند ، نام کوتاه تری انتخاب کرد.
- اگر برنامه به کامپایلر دیگری منتقل شود و کامپایلر جدید و قبلی از نظر نوع و طول داده ها مطابقت نداشته باشد، برای حل مشکل کافیست فقط در دستور typedef تغییراتی ایجاد شود.
قالب استفاده از این دستور را در زیر مشاهده می کنید :
1 |
typedef نام جدید نام موجود ; |
در قالب بالا نام موجود یکی از نوع های معتبر در زبان سی است و نام جدید نامی دلخواه برای آن می باشد. مثال :
1 |
typedef char str; |
بعد از دستور بالا میتوان به جای char از str برای تعریف داده های جدید استفاده کرد. مثال :
1 |
str x,y; |
لزوم برنامه نویسی به سبک ماژولار
در طراحی و توسعه ی یک پروژه لازم است به موارد زیر توجه شود:
تولید کننده ای که بخواهد برای هر سیستم جدید از ابتدا شروع به طراحی برنامه کند نمی تواند برای مدت طولانی توانایی رقابت در بازار را داشته باشد. زبان برنامه نویسی که استفاده می شود باید توانایی ایجاد کتابخانه های انعطلاف پذیر را داشته باشد، تا برنامه نویس بتواند از کتابخانه هایی که آزمایش (test)، اشکال زدایی (debug) و تایید (verify) شده اند در پروژه های آینده استفاده کند. همچنین لازم است امکان انطباق کتابخانه با میکروکنترلرهای جدید وجود داشته باشد.
کارکنان یک مجموعه تغییر می کنند و حتی اگر ثابت باشند، حافظه ی انسان بازه ی زمانی محدودی را به خاطر می آورد. هر سیستمی نیاز به ارتقا و به روز رسانی دارد و اگر برنامه ی آن به شیوه ی درستی نوشته و مستند سازی نشده باشد، درک و تغییر آن مشکل می شود. بنابراین برنامه ی خوب، برنامه ای است که امکان درک و تغییر آن در هر زمان وجود داشته باشد (نه فقط به وسیله ی طراح اولیه، بلکه به وسیله افراد دیگر).
علاوه بر موارد فوق لازم است به این نکته توجه شود که در اجرای پروژه های بزرگ و پیچیده اگر از سبک برنامه نویسی مناسبی استفاده نشود، نگهداری و اشکال زدایی برنامه بسیار مشکل می شود، هزینه ها افزایش می یابد و امکان موفقیت طرح به حداقل می رسد. همچنین گاهی لازم است پروژه های بزرگ به اجزای کوچکتری خرد شوند و هر بخش را فرد و یا تیم مستقلی پیاده سازی کند. با استفاده از سبک برنامه نویسی ماژولار، می توانیم کتابخانه هایی ایجاد کنیم که به سادگی قابل تغییر و قابل استفاده در پروژه های دیگر هستند.
مفهوم ماژول نرم افزاری
ماژول به معنای مولفه و یا جزئی از برنامه است که خود شامل تعدادی تابع مرتبط با یکدیگر است. معمولا مجموع توابعی که داخل یک ماژول قرار می گیرند عملکرد خاصی را پیاده سازی می کنند.
برخی از ماژول ها ارتباط جانبی داخلی یا خارجی ندارند و معمولا فرآیندی را بر روی داده انجام می دهند. در این نوع ماژول ها، بخشی از برنامه به عنوان مشتری، سرویسی را از ماژول درخواست می کند و ماژول آن درخواست را پاسخ می دهد.
برخی از ماژول ها نیز ارتباط برنامه با یک سخت افزار داخلی (مانند پورت های سریال، مبدل آنالوگ به دیجیتال و …) یا خارجی (مانند نمایشگر LCD، صفحه کلید، سنسور و …) را برقرار می کنند. ماژول شامل دو بخش پیاده سازی و واسط (Interface) است. در بخش پیاده سازی، بدنه ی توابع قرار می گیرند و در بخش دوم نحوه ی استفاده از توابع، در اختیار کاربر (مشتری سرویس) قرار می گیرد.
یکی از دلایل اساسی استفاده از ماژول این است که بتوان بخش های لازم از یک موضوع را مشخص و بخش های غیر ضروری را پنهان کرد. بنابراین بخش واسط، شامل اطلاعاتی است که برای استفاده از آن موضوع مورد نیاز است و بخش پیاده سازی، شامل چگونگی انجام آن موضوع است. به پنهان سازی جزئیات غیر ضروری، Abstraction گفته می شود. از این مفهوم در طراحی بسیاری از وسایل استفاده می شود. به عنوان مثال زمانی که یک خودرو را می رانیم لزوما نیاز نیست که از نحوه ی عملکرد موتور و سایر اجزا آن آگاهی داشته باشیم، بلکه کافی است که واسط استفاده موتور(شامل پدال های کلاچ، ترمز و گاز) در اختیارمان باشد و نحوه ی به کارگیری این واسط را بدانیم. در برنامه نویسی نیز، Abstraction مفهوم بسیار ارزشمندی است که در زبان های شی گرا (مانند java و C++ ) به شکل عالی و در زبان های ماژولار به شکل ابتدایی از آن پشتیبانی شده است.
متاسفانه زبان C از شی گرایی و برنامه نویسی ماژولار پشتیبانی نمی کند، اما با استفاده از فایل های سورس (source) و هدر (header) می توان به تا حد قابل قبولی به شیوه ی ماژولار برنامه نویسی کرد.
برنامه نویسی به روش ماشین حالت
یکی از روش های برنامه نویسی پیشرفته ، استفاده از ساختار ماشین حالت یا State Machine در برنامه نویسی می باشد. این سبک از برنامه نویسی دارای کمترین هنگ و خطا و بیشترین انعطاف پذیری است. در این روش از حلقه switch برای تشریح حالت های مختلف و از نوع شمارشی enum برای تدوین انواع حالت ها استفاده خواهیم کرد. بنابراین ساختار کلی این روش به صورت زیر است :
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 |
… enum { s0,s1,s2,….,sn}state; void main { … while(1){ کاری که هر بار باید انجام شود و حالت های مختلف بر اساس آن شکل می گیرد switch(state) { case s0: کارهایی که در حالت اول باید صورت گیرد if( شرط رفتن به حالت بعد ) state=حالت بعدی case s1: کارهایی که در حالت دوم باید صورت گیرد if( شرط رفتن به حالت بعد ) state=حالت بعدی . . . case sn: کارهایی که در حالت آخر باید صورت گیرد if( شرط رفتن به حالت بعد ) state=حالت بعدی default: کارهایی که در صورت برقرار نبودن هیچ یک از حالت های فوق انجام می شود } } } |
با کلیک بر روی تصویر زیر به بخش بعدی آموزش بروید
در صورتی که این مطلب مورد پسندتان بود لایک و اشتراک گذاری فراموش نشود.
دیدگاه (8)
سلام دستتون درد نکنه
آخرش من متوجه نشدم منظورتون از ماکرو چیه؟
شما به هر دستور پیش پردازشی مثل#define یا مثلا #ifdefیه ماکرو میگین؟
بله دقیقا
من فایل سورسمو اینجوری نوشتم
delay.h”#include”
void delay_s(unsigned int x){
x=x*CCLK;
while(x–)
}
وفایل هدر رو هم به این صورت
#ifndef_DELAY_H
#define_DELAY_H
define CCLK=12000000#
void delay_s(unsigned int x);
end if#
ایرادش کجاس؟
جواب نمیده
غلط زیاد داره لطفا اینگونه سوالات را ایمیل بزنید یا در تلگرام مطرح کنید
سلام
جوانهایی مثل شما هستند که ما به نژاد اریایی خود افتخار میکنیم برایت ارزوی سلامتی دارم
سلام دوست عزیز ممنونم از لطف و حسن نظرتون
خیلی جامع و کامل بود.
خیلی خیلی ممنونم که وقت میزارید
👍