در این بخش میخواهیم سه نوع دادهی سفارشی موجود در زبان C یعنی struct، union و enum را با یکدیگر ترکیب کنیم و یک شکل هندسی را روی صفحه نمایش رسم نماییم. این شکل میتواند مربع، مستطیل، دایره یا مثلث باشد که هر یک به روش خاصی توصیف میشوند.
برای توصیف یک مربع فقط به یک ضلع نیاز داریم:
1 2 3 4 5 | struct square { unsigned int side; // اندازهی ضلع مربع }; |
برای توصیف یک مستطیل به عرض و ارتفاع نیاز داریم:
1 2 3 4 5 6 7 | struct rectangle { unsigned int width; // عرض مستطیل unsigned int height; // ارتفاع مستطیل }; |
با داشتن شعاع میتوانیم یک دایره رسم کنیم:
1 2 3 4 5 | struct circle { unsigned int radius; // شعاع دایره }; |
در نهایت، برای رسم یک مثلث، قاعده و ارتفاع آن را توصیف میکنیم:
1 2 3 4 5 6 7 | struct triangle { unsigned int base; // قاعده مثلث unsigned int height; // ارتفاع مثلث }; |
برای تعریف نوع دادهی کلی برای اشکال مختلف، به ساختاری نیاز داریم که باید بتواند هر کدام از این موارد را نگهداری کند که نشان میدهد به یک union نیاز داریم. اما برای رسم یک شکل، نهتنها به توصیف آن، بلکه به نوع شکل (shape) نیز نیاز داریم. نوع دادهی enum برای لیست محدودی از مقادیر ساده طراحی شده است:
1 2 3 4 5 6 7 8 9 10 11 | enum shapeType { SHAPE_SQUARE, SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_TRIANGLE }; |
حالا میتوانیم ساختار دادهی خود را تعریف کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | struct shape { enum shapeType type; // نوع شکل union { struct square theSquare; struct rectangle theRectangle; struct circle theCircle; struct triangle theTriangle; } dimensions; }; |
اولین فیلد، type، حاوی نوع شکلی (shape) است که در ساختار قرار دارد. فیلد دوم ابعاد شکل را در بر میگیرد که از union استفاده شده است، زیرا اشکال مختلف دارای ابعاد متفاوتی هستند.
✅ نکته
کد مربوط به رسم شکلها بهصورت زیر است:
1 2 3 4 5 6 7 8 9 10 11 | void drawShape(const struct shape* const theShape) { switch (theShape->type) { case SHAPE_SQUARE: drawSquare(theShape->dimensions.theSquare.side); break; case SHAPE_RECTANGLE: drawRectangle(theShape->dimensions.theRectangle.width, theShape->dimensions.theRectangle.height); // ... اشکال دیگر } } |
این الگوی طراحی در برنامهنویسی C رایج است: یک union که میتواند انواع مختلفی از دادهها را نگه دارد و یک enum که نوع دادهی موردنظر را مشخص میکند.
در این بخش با توجه به مطالب که تا اینجا آموختیم، مشخصات یک سختافزار را تبدیل به ساختار (structure) در زبان C میکنیم.
رابط سیستم کامپیوتری کوچک (Small Computer System Interface) یا به اختصار SCSI برای ارائه روشی استاندارد جهت انتقال داده بین دستگاهها طراحی شده است. این رابط در سال ۱۹۸۶ راهاندازی شد و از آن زمان تاکنون به طور قابل توجهی توسعه یافته و بهبود پیدا کرده است. SCSI با ارسال یک ساختار به نام “بلوک کامند” به دستگاه و دریافت داده و پیامهای وضعیت در مقابل آن کار میکند.
هنگامی که استاندارد SCSI برای اولینبار نوشته شد، کامند READ (6) را تعریف کرد که آدرس بلوک را به 16 بیت محدود میکرد. این محدودیت به معنای امکان استفاده از دیسکی تا حداکثر 16 مگابایت بود که برای آن زمان حجم بزرگی محسوب میشد.
باتوجهبه نیاز دیسکهای بزرگتر، متخصصان SCSI مجبور به ایجاد کامندهای جدیدی برای پشتیبانی از این نوع دیسکها شدند. به ترتیب، دستورات READ (10)، READ (12)، READ (16) و READ (32) ارائه شد.
کامند READ (32) از یک آدرس بلوک 64 بیتی استفاده میکند که امکان استفاده از دیسکهای بسیار بزرگتر را فراهم میکند.
شکل زیر بلوک کامند مربوط به دستور READ (10) را نشان میدهد. برای خواندن دادهها از دیسک، به یک ساختار C نیاز داریم تا این اطلاعات را در خود جای دهد و آن را به دستگاه ارسال کند.
بلوک کامند READ (10)
در نگاه اول، به نظر ترجمه سادهای میآید:
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 | struct read10 { uint8_t opCode; // کد عملیات برای خواندن uint8_t flags; // بیتهای پرچم uint32_t lba; // آدرس بلوک منطقی uint8_t group; // گروه کامند uint16_t transferLength; // طول دادهای که باید خوانده شود uint8_t control; بیتهای کنترل (فقط NACA تعریف شده است) // }; #include <assert.h> int main() { assert(sizeof(struct read10) == 10); // ... } |
بهخاطر اینکه ما محتاط و دقیق هستیم، اولین کاری که در برنامه انجام دادیم قراردادن یک دستور assert بود تا اطمینان پیدا کنیم تعریف ما از ساختار read10 با سختافزار مطابقت دارد. دستور assert درصورتیکه شرط تعریف شده برقرار نباشد، برنامه را متوقف میکند. در این مثال، انتظار داشتیم بلوک کنترل read10 دقیقاً ۱۰ بایت حجم داشته باشد، اما خروجی دستور assert نشان داد که اینطور نیست و مشکلی وجود دارد. ما هم با مشکل مواجه شدیم؛ چون دستور assert با شکست مواجه شد.
اما چرا این اتفاق افتاد؟ با بررسی دقیقتر ساختار، متوجه میشویم که فیلد lba (از نوع uint32_t) روی یک مرز ۲ بایتی تراز شده است. کامپایلر ترجیح میدهد این فیلد را روی یک مرز ۴ بایتی قرار دهد، بنابراین برای اینکه ترازبندی را برقرار کند، ۲ بایت پرکننده (padding) به ساختار اضافه کرده است. برای اینکه ساختار ما دقیقاً ۱۰ بایت حجم داشته باشد و با سختافزار مطابقت پیدا کند، نیاز به فشرده کردن ساختار (packing) داریم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | struct read10 { uint8_t opCode; // کد عملیات برای خواندن uint8_t flags; // بیتهای پرچم uint32_t lba; // آدرس بلوک منطقی uint8_t group; // گروه کامند uint16_t transferLength; // طول دادهای که باید خوانده شود uint8_t control; // بیتهای کنترل (فقط NACA تعریف شده است) } __attribute__((packed)); |
برای اینکه ساختار ما دقیقاً ۱۰ بایت داشته باشد و با سختافزار مطابقت کند، از ویژگی packed استفاده میکنیم. این ویژگی به کامپایلر GCC دستور میدهد که هیچ بایت پرکنندهای به ساختار اضافه نکند. در نتیجه، ساختار ما از نظر کارایی ممکن است ایدهآل نباشد، اما بهاینترتیب با سختافزار مطابقت پیدا میکند. نکته مهم این است که با استفاده از ویژگی packed دستور assert ما با موفقیت اجرا میشود و نشان میدهد که حجم ساختار صحیح است.
کلیدواژه typedef در زبان برنامهنویسی C به ما امکان میدهد که برای انواع داده موجود، نامهای جدید تعریف کنیم. برای مثال، دستور زیر یک نوع داده جدید به نام dimension تعریف میکند:
1 | typedef unsigned int dimension; // برای استفاده در نقشهها |
این نوع داده معادل unsigned int است و میتوان از آن مانند هر نوع داده دیگری استفاده کرد:
1 | dimension width; |
عرض شیء بر حسب) furlong واحد اندازهگیری قدیمی( //
نحوه نوشتن typedef شبیه به تعریف یک متغیر است. این دستور شامل کلیدواژه typedef، نام اولیه و همچنین نام جدید تعریفشده میباشد:
1 2 3 | typedef [نوع داده] [نام جدید نوع داده] typedef initialtype newtypename; // تعریف یک نوع داده |
یک مثال از typedef را میتوان در فایل stdint.h پیدا کرد که در بسیاری از برنامههای ما گنجانده شده است:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // این تعاریف نوع داده وابسته به سیستم هستند typedef signed char int8_t; typedef unsigned char uint8_t; typedef signed short int int16_t; typedef unsigned short int uint16_t; typedef signed int int32_t; typedef unsigned int uint32_t; |
در اوایل پیدایش زبان C، بسته به پردازنده، نوع داده int میتوانست ۱۶ یا ۳۲ بیتی باشد. در اوایل برنامهنویسی، اگر کاربران میخواستند از یک عدد صحیح ۱۶ بیتی (که استاندارد قدیمی C از آن پشتیبانی نمیکرد) استفاده کنند، باید چیزی شبیه به این را در کد خود قرار میدادند:
1 2 3 4 5 6 7 8 9 | #ifdef ON_16_BIT_CPU typedef signed int int16_t; #else // ON_32_BIT_CPU typedef signed short int int16_t; #endif |
پس از سالها نیاز به تعریف انواع داده دقیق خودمان، کمیته استانداردهای C فایل هدر stdint.h را ایجاد کرد و آن را به بخشی از زبان تبدیل نمود.
زبان C به ما امکان استفاده از اشارهگرهای توابع را میدهد که در زمان استفاده از فراخوانها (callback) کاربردی هستند. برای مثال، ممکن است بخواهیم به یک سیستم گرافیکی بگوییم که در زمان فشردن یک دکمه، تابع خاصی را فراخوانی کند. کد مربوط به این کار ممکن است شبیه به این باشد:
1 | registerButtonPressHandler(functionToHandleButtonPress); |
پارامتر functionToHandleButtonPress یک اشارهگر به تابعی است که یک عدد صحیح برمیگرداند و تنها یک آرگومان دریافت میکند که یک اشارهگر ثابت به یک رویداد است. این جمله کمی پیچیده است و زمانی که آن را به زبان C ترجمه کنیم، حتی پیچیدهتر هم میشود:
1 | int (*ButtonCallback)(const struct event* const theEvent); |
مجموعه اول از پرانتزها الزامی هستند، زیرا بدون آنها ما در حال تعریف تابعی هستیم که یک اشارهگر به عدد صحیح برمیگرداند:
1 2 3 | تعریف تابعی که int* برمیگرداند// int* getPointer(...) |
بهجای اینکه این قوانین پیچیده برای ترکیب زبان را بهخاطر بسپاریم، بیایید با استفاده از typedef این ترکیب را ساده کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 | // تعریف نوع دادهی تابع برای callback تابع typedef int ButtonCallbackType(const struct event* const theEvent); اشارهگر به تابع// callback typedef ButtonCallbackType* ButtonCallbackPointer; این کار تعریف تابع registerButtonPressHandler را از این شکل: void registerButtonPressHandler(int (*callbackPointer) (const struct event* const theEvent)); |
به این شکل تغییر میدهد:
1 | void registerButtonPressHandler(ButtonCallbackPointer callbackPointer); |
typedef روشی برای سازماندهی انواع داده برای سادهسازی کد و همچنین واضحتر کردن آن فراهم میکند.
typedef و struct
همانطور که قبلاً دیدیم، میتوانیم از struct برای تعریف یک نوع داده ساختاریافته استفاده کنیم.
1 2 3 4 5 6 7 | struct rectangle { uint32_t width; // عرض مستطیل uint32_t height; // ارتفاع مستطیل }; |
برای استفاده از این ساختار، باید از کلیدواژه struct استفاده کنیم:
1 | struct rectangle bigRectangle; // یک مستطیل بزرگ |
دستور typedef به ما این امکان را میدهد تا در استفاده از کلیدواژه struct اجتناب کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 | typedef struct { uint32_t width; uint32_t height; } rectangle; rectangle bigRectangle; |
در این مثال، typedef به کامپایلر C میگوید که میخواهیم یک نوع داده جدید به نام rectangle تعریف کنیم.
برخی از برنامهنویسان بر این باورند که استفاده از typedef برای تعریف نوع داده structure باعث سادهتر و تمیزتر شدن کد میشود. برخی دیگر استفاده از struct را ترجیح میدهند؛ زیرا باعث میشود که به طور واضح مشخص شود یک متغیر از نوع structure است. این ترکیب اختیاری است، بنابراین هرکدام که برای شما بهتر کار میکند را انتخاب کنید..
نویسنده شو !
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.