کامپایل شرطی به ما امکان تغییر محتوای کد در زمان کامپایل را میدهد. استفاده کلاسیک از این ویژگی، داشتن نسخههای دیباگ و تولید برنامه است. دستورالعمل ifdef / #endif#کد بین دو دستورالعمل را کامپایل میکند اگر یک نماد تعریف شده باشد. در ادامه مثالی آمده است:
|
1 2 3 4 5 |
int main() { #ifdef DEBUG printf("Debug version\n"); #endif // DEBUG |
کمپایل مشروط به ما امکان میدهد تا محتویات کد خود را در زمان کامپایل تغییر دهیم. استفاده کلاسیک از این ویژگی، داشتن نسخه دیباگ و نسخه نهایی (production) از یک برنامه است.
عبارت دستوری ifdef / #endif#کد بین این دو دستور را کامپایل میکند، در صورتی که یک نماد (سمبل) تعریف شده باشد. در اینجا یک مثال آورده شده است:
|
1 2 3 4 5 6 7 8 9 |
int main() { #ifdef DEBUG printf("Debug version\n"); #endif // DEBUG } |
به طور دقیق، کامنت DEBUG//مورد نیاز نیست، اما حتما آن را لحاظ کنید زیرا جفت کردن عبارات ifdef/#endif#به اندازه کافی دشوار است.
اگر برنامه شما به شکل زیر باشد:
|
1 2 3 4 5 6 7 8 9 10 11 |
#define DEBUG // Debug version int main() { #ifdef DEBUG printf("Debug version\n"); #endif // DEBUG } |
در این صورت خروجی پیشپردازنده به صورت زیر خواهد بود:
|
1 2 3 4 5 |
int main() { printf("Debug version\n"); } |
از طرف دیگر، اگر برنامه شما به شکل زیر باشد:
|
1 2 3 4 5 6 7 8 9 10 11 |
//#define DEBUG // Release version int main() { #ifdef DEBUG printf("Debug version\n"); #endif // DEBUG } |
در این حالت، هیچ خروجی برای printf تولید نمیشود، زیرا نماد DEBUG تعریف نشده است.
پس خروجی پیشپردازنده به صورت زیر خواهد بود:
|
1 2 3 4 5 |
int main() { // Nothing } |
از آنجایی که DEBUG تعریف نشده است، هیچ کدی تولید نمیشود.
تمام دستورات ifdef#باعث میشوند برنامه ظاهر زشتی پیدا کند و این برای ما کمی ناخوشایند است. کد زیر را در نظر بگیرید:
|
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 31 32 33 |
int main() { #ifdef DEBUG printf("Debug version\n"); #endif // DEBUG #ifdef DEBUG printf("Starting main loop\n"); #endif // DEBUG while (1) { #ifdef DEBUG printf("Before process file \n"); #endif // DEBUG processFile(); #ifdef DEBUG printf("After process file \n"); #endif // DEBUG } } |
میتوانیم با کد بسیار کمتر به همان نتیجه برسیم:
|
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 |
#ifdef DEBUG #define debug(msg) printf(msg) #else // DEBUG #define debug(msg) /* nothing */ #endif // DEBUG int main() { debug("Debug version\n"); debug("Starting main loop\n"); while (1) { debug("Before process file \n"); processFile(); debug("After process file \n"); } } |
توجه کنید که ما از دستورelse#در پیشپردازنده برای معکوس کردن مفهوم if#استفاده کردهایم. اگر DEBUG تعریف شده باشد، فراخوانیهای debug با فراخوانیهای printf جایگزین میشوند؛ در غیر این صورت، آنها با فضای خالی جایگزین میشوند. در این مورد، به ترفند do/while نیاز نداریم زیرا ماکرو کد حاوی یک فراخوانی تابع (بدون سمیکالن) است.
دستور دیگر، ifndef# است که در صورتی که یک نماد تعریف نشده باشد، درست است و در غیر این صورت به همان روشی که دستور ifdef#استفاده میشود.
ما میتوانیم نمادها را به سه روش تعریف کنیم:
ما قبلاً نمادهای تعریفشده داخل برنامه را توضیح دادیم، پس بیایید به دو گزینه دیگر نگاه کنیم.
برای تعریف یک نماد روی خط فرمان، از گزینه D- استفاده کنید:
|
1 |
$ gcc -Wall -Wextra -DDEBUG -o prog prog.c |
آرگومان DDEBUG- نماد DEBUG را تعریف میکند تا پیشپردازنده بتواند از آن استفاده کند. به عبارتی دیگر، این دستور معادل نوشتن define DEBUG 1 # در ابتدای کد است، قبل از اینکه برنامه اصلی شروع به کامپایل شدن کند. ما از این نماد در کد قبلی برای مدیریت این موضوع که آیا عبارات debug در خروجی نهایی گنجانده شوند یا نه، استفاده کردیم.
علاوه بر نمادهایی که ما به صورت دستی به کامند کامپایل اضافه میکنیم، نرمافزار STM32 Workbench یک فایل Makefile برای کامپایل برنامهای ایجاد میکند که تعدادی نماد را در خط فرمان تعریف میکند. مهمترین نماد با گزینه DSTM32F030x8- تعریف میشود. فایل CMSIS/device/stm32f0xx.h از نماد STM32F030x8 برای دربرگرفتن فایلهای خاص برد استفاده میکند:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
##if defined(STM32F030x6) #include "stm32f030x6.h" #elif defined(STM32F030x8) #include "stm32f030x8.h" #elif defined(STM32F031x6) #include "stm32f031x6.h" #elif defined(STM32F038xx) |
فریمور STM32 از تعدادی برد پشتیبانی میکند که فقط یکی از آنها NUCLEO-F030R8 است. هر تراشه مجموعهای متفاوت از دستگاههای ورودی/خروجی دارد که در مکانهای مختلفی قرار گرفتهاند. شما لازم نیست نگران محل قرارگیری آنها باشید، زیرا فریمور با استفاده از کد قبلی مکان مناسب را پیدا میکند. این کد میگوید، “اگر من یک STM32F030x6 هستم، فایل هدر مربوط به آن برد را دربربگیر؛ اگر یک STM32F030x8 هستم، فایل هدر مربوط به آن برد را دربربگیر” و به همین ترتیب.
دستورهای استفاده شده if# و elif# هستند. دستور if# بررسی میکند که آیا عبارتی در ادامه آمده درست است یا نه (در این مورد، اینکه آیا STM32F030x6 تعریف شده است). اگر درست باشد، کد پس از آن کامپایل میشود. elif# ترکیبی از else# و if# است که میگوید اگر عبارت درست نباشد، یک عبارت دیگر را بررسی کن. دستور دیگر، defined، در صورتی که نماد تعریف شده باشد، مقدار درست برمیگرداند.
در نهایت، خود پیشپردازنده تعدادی نماد را تعریف میکند، مانند __VERSION__ (برای مشخص کردن نسخه کامپایلر) و __linux (در سیستمهای لینوکس). برای اینکه ببینید چه نمادهایی در سیستم شما از پیش تعریف شدهاند، از دستور زیر استفاده کنید:
|
1 |
$ gcc -dM -E - < /dev/null |
نماد __cplusplus تنها در صورتی تعریف میشود که شما یک برنامه C++ را کامپایل کنید. اغلب اوقات، کدهایی شبیه به این را در فایلها مشاهده خواهید کرد:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#ifdef __cplusplus extern "C" { #endif // کد اصلی #ifdef __cplusplus } #endif |
این کد بخشی از مکالمه و هماهنگیای است که C++ برای استفاده از برنامههای C به آن نیاز دارد. فعلاً میتوانید آن را نادیده بگیرید.
دستور include #به پیشپردازنده میگوید کل یک فایل را وارد کند، گویی بخشی از فایل اصلی است. دو فرم برای این دستور وجود دارد:
|
1 2 3 |
#include <file.h> // فایلهای هدر سیستم #include "file.h" // فایلهای هدر کاربری |
فرم اول فایلهای هدر سیستم (فایلهایی که با کامپایلر یا کتابخانههای سیستمی که استفاده میکنید، همراه هستند) را وارد میکند. فرم دوم فایلهایی را که خودتان ایجاد کردهاید، وارد میکند.
یکی از مشکلات فایلهای هدر این است که ممکن است دو بار وارد شوند. اگر این اتفاق بیفتد، با تعداد زیادی نماد تکراری تعریفشده و مشکلات دیگری روبرو خواهید شد. راهحل این مشکل افزودن یک نگهبان (sentinel) با استفاده از الگوی طراحی زیر است:
|
1 2 3 4 5 6 7 |
#ifndef __FILE_NAME_H__ #define __FILE_NAME_H__ // بدنه فایل #endif /* __FILE_NAME_H__ */ |
در بار اول که به این کد برخورد میکنیم، نماد __FILE_NAME_H__ (نگهبان) تعریف نشده است. بنابراین، کل فایل هدر در برنامه گنجانده میشود. این همان چیزی است که میخواهیم، چون فقط یک بار به این فایل نیاز داریم.
در دفعات بعدی که پیشپردازنده به این کد میرسد، نماد __FILE_NAME_H__ قبلاً تعریف شده است. دستور ifndef #باعث میشود که کد بعد از آن تا رسیدن به endif #در انتهای فایل، دیگر شامل نشود. بنابراین، اگرچه فایل هدر دو بار در لیست include #قرار گرفته باشد، محتوای آن فقط یک بار در خروجی نهایی ظاهر میشود.
علاوه بر دستورات اصلی، چند دستور پیشپردازنده کماهمیتتر نیز وجود دارد که مفید هستند، مانند warning، #error #و pragma#.
دستور warning #در صورت مشاهده شدن، یک هشدار کامپایلر را نمایش میدهد:
|
1 2 3 4 5 6 7 |
#ifndef PROCESSOR #define PROCESSOR DEFAULT_PROCESSOR #warning "No processor -- taking default" #endif // PROCESSOR |
در این مثال، اگر نماد PROCESSOR تعریف نشده باشد، پیشپردازنده یک هشدار با متن “No processor — taking default” نمایش میدهد و سپس مقدار پیشفرض DEFAULT_PROCESSOR را برای این نماد در نظر میگیرد.
دستور مرتبط، error#، یک خطا صادر میکند و کامپایل شدن برنامه شما را متوقف میکند:
|
1 2 3 4 5 |
#ifndef RELEASE_VERSION #error "No release version defined. It must be defined." #endif // RELEASE_VERSION |
اگر نماد RELEASE_VERSION تعریف نشده باشد، این دستور خطایی با متن “No release version defined. It must be defined” صادر میکند و کامپایلر دیگر به کامپایل کد ادامه نمیدهد.
دستور #pragma برای تعریف کنترلهای وابسته به کامپایلر استفاده میشود. در اینجا یک مثال آورده شده است:
|
1 2 3 4 5 6 7 |
// I wish they would fix this include file. #pragma GCC diagnostic ignored "-Wmissing-prototypes" #include "buggy.h" #pragma GCC diagnostic warning "-Wmissing-prototypes" |
این کد برای کامپایلر GCC نوشته شده است. دستور pragma# هشدارهای مربوط به نبودن prototype (اعلامیه اولیه توابع) را خاموش میکند، سپس فایل هدر باگدار buggy.h را در بر میگیرد و در نهایت، دوباره هشدارها را روشن میکند.
پیشپردازنده یک پردازشگر ماکرو ساده است و در نتیجه، همانطور که قبلاً توضیح داده شد، برای اینکه به مشکل برنخوریم، مجبور بودیم مجموعهای از قواعد سبک را اتخاذ کنیم. قدرت پیشپردازنده همچنین به ما اجازه میدهد تا برای آسانتر کردن کارمان، چند ترفند جالب انجام دهیم. یکی از آنها ترفند enum است که در فصل ۸ در مورد آن بحث کردیم. در این بخش، به کامنت کردن کد نگاهی خواهیم انداخت.
گاهی اوقات، برای تست برنامه نیاز داریم بخشی از کد را غیرفعال کنیم. یک راه برای انجام این کار، کامنت کردن کد است. برای مثال، فرض کنید فرآیند حسابرسی باگ دارد؛ میتوانیم آن را غیرفعال کنیم تا گروه حسابرسی کار خود را به درستی انجام دهند.
در اینجا کد اصلی آورده شده است:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
int processFile(void) { readFile(); connectToAuditServer(); if (!audit()) { printf("ERROR: Audit failed\n"); return; } crunchData(); writeReport(); } |
و در اینجا کدی وجود دارد که حسابرسی از آن حذف شده است:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
int processFile(void) { readFile(); // connectToAuditServer(); // غیرفعال شده // if (!audit()) { // غیرفعال شده // printf("ERROR: Audit failed\n"); // غیرفعال شده // return; // غیرفعال شده // } // غیرفعال شده crunchData(); writeReport(); } |
هر خطی که میخواستیم حذف شود، اکنون با نشانگر کامنت (//) شروع میشود.
با این حال، کامنت کردن تک تک خطوط کار پر زحمتی است. در عوض، میتوانیم از کامپایل مشروط برای حذف کد استفاده کنیم. تنها کاری که باید انجام دهیم احاطه کردن کد با عبارات ifdef UNDEF #و endif // UNDEF #است، به این صورت:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
int processFile(void) { readFile(); #ifdef UNDEF connectToAuditServer(); if (!audit()) { printf("ERROR: Audit failed\n"); return; } #endif // UNDEF crunchData(); writeReport(); } |
کد داخل بلاک ifdef/#endif #فقط در صورتی کامپایل میشود که نماد UNDEF تعریف شده باشد. تعریف کردن این نماد کار عاقلانهای نیست چون معنای خاصی ندارد. استفاده از if 0 / #endif #نیز به همین نتیجه میرسد، با این مزیت که به عقل برنامهنویسهای دیگر وابسته نیست.
پیشپردازنده C یک ویرایشگر متن خودکار ساده اما قدرتمند است. استفاده صحیح از آن میتواند برنامهنویسی را به شدت آسانتر کند. این ابزار به شما امکان میدهد ماکروهای عددی ساده و همچنین ماکروهای کد کوچک تعریف کنید. (البته تعریف ماکروهای کد بزرگ هم ممکن است، اما بهتر است این کار را انجام ندهید.)
یکی از مهمترین ویژگیهای آن، دستور include #است که به اشتراکگذاری رابطها بین ماژولها کمک میکند. همچنین، قابلیتهای ifdef #به شما اجازه میدهند تا با استفاده از کامپایل مشروط، یک برنامه بنویسید که شخصیتهای مختلفی داشته باشد.
با این حال، باید به خاطر داشته باشید که پیشپردازنده، ترکیب (syntax) زبان C را درک نمیکند. در نتیجه، برای استفاده مؤثر از این سیستم، باید چندین قانون سبک و الگوی برنامهنویسی را به خاطر بسپارید.
با وجود تمام محدودیتها و پیچیدگیهایش، پیشپردازنده میتواند ابزاری قدرتمند برای ایجاد برنامههای C باشد.
یک ماکرو برای جابهجایی دو عدد صحیح بنویسید.
آشنایی با پیشپردازنده C و ماکروها: از #define...
مدیریت حافظه Heap در C | تفاوت برنامهنویسی...
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.