کامپایلر پایه C ویژگیهای قدرتمندی دارد، ولی از عهده بعضی از کارهای ساده برنمی آید. برای دور زدن این محدودیتها، پیشپردازندهای به زبان اضافه شد. پیشپردازنده در درجه اول یک پردازشگر ماکرو (کلان) است، برنامهای که متن را با متن دیگری جایگزین میکند، اما همچنین میتواند بر اساس شرایط خاص متن را اضافه یا حذف کند و اقدامات دیگری انجام دهد. هدف این است که یک برنامه (پیشپردازنده) کار ویرایش متن کوچک و سادهای را انجام داده و سپس خروجی آن را به کامپایلر اصلی بدهد. از آنجایی که این دو مرحله (و چند مرحلهی دیگر) پشت فرمان gcc پنهان هستند، به سختی به آنها فکر میکنید، اما آنها وجود دارند.
بعنوان مثال، به کد زیر نگاه کنیم:
|
1 2 3 4 5 6 7 |
#define SIZE 20 // اندازهی آرایه int array[SIZE]; // آرایه --snip-- for (unsigned int i = 0; i < SIZE; ++i) { |
زمانی که SIZE برای نشان دادن ۲۰ تعریف شده است، پیشپردازنده اساسا یک جستجوی سراسری انجام میدهد و SIZE را با ۲۰ جایگزین میکند.
کتابخانهی HAL که با میکروکنترلر STM استفاده میکنیم، به دو روش از پیشپردازنده بهرهی گسترده میبرد. اول، هدرها برای هر بیت قابل دریافت و تنظیم در پردازنده، یک دستور #define دارند، که تعداد آنها هم کم نیست. دوم، شرکت STMicroelectronics تنها یک تراشه تولید نمیکند؛ بلکه طیف وسیعی از آنها را میسازد. بجای داشتن ۲۰ فایل هدر متفاوت با اطلاعات مربوط به ۲۰ تراشه، از فرآیندی به نام کامپایل شرطی استفاده میکند تا فقط بخشهای مورد نیاز از فایل هدر را کامپایل کند.
بیایید با ماکروهای ساده شروع کنیم. یک ماکرو اساساً الگویی (در این مورد، SIZE) است که با چیز دیگری (در این مورد، ۲۰) جایگزین میشود. دستورالعمل پیشپردازندهی #define برای تعریف الگو و جایگزین استفاده میشود:
|
1 2 3 4 5 6 7 8 |
size.c #define SIZE 20 The size is SIZE |
این یک برنامهی C نیست. پیشپردازنده روی هر چیزی، از جمله متن سادهی انگلیسی، کار میکند. بیایید آن را با استفاده از پرچم -E از طریق پیشپردازنده اجرا کنیم، که به gcc میگوید برنامه را فقط از طریق پیشپردازنده اجرا کند و متوقف شود:
|
1 |
$ gcc -E size.c |
نتایج پیشپردازنده در اینجا آمده است:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 1 "size.c" # 1 "<built-in>" # 1 "<command-line>" # 31 "<command-line>" # 1 "/usr/include/stdc-predef.h" 1 3 4 # 32 "<command-line>" 2 1 # 1 "size.c" 2 The size is 20 |
خطوطی که با علامت هشتگ (#) شروع میشوند، نشانگرهای خط نام دارند. آنها از یک علامت هشتگ، شمارهی خط و نام فایل (و برخی موارد دیگر) تشکیل شدهاند. از آنجایی که پیشپردازنده ممکن است خطوطی را اضافه یا حذف کند، بدون آنها برای کامپایلر غیرممکن است که بداند در کجای فایل ورودی اصلی قرار دارد.
بسیاری از اتفاقات قبل از پردازش اولین خط رخ میدهند، اما در نهایت به دومین بار (1) میرسیم و خروجی (2) نشان میدهد که SIZE با مقدار تعریفشده جایگزین شده است.
پیشپردازنده همه چیز را به معنای واقعی کلمه در نظر میگیرد، که میتواند شما را به دردسر بیندازد، همانطور که در اینجا نشان داده شده است:
square.c
|
1 2 3 4 5 6 7 |
#include <stdio.h> 1 #define SIDE 10 + 2 // Size + margin int main() { 2 printf("Area %d\n", SIDE * SIDE); return (0); } |
این مثال مساحت یک مربع را محاسبه میکند. برای تعریف ضلع مربع، حاشیه کوچکی در نظر گرفته شده است. برای بدست آوردن مساحت، ضلعها را در هم ضرب میکنیم و نتیجه را چاپ میکنیم.
با این حال، این برنامه دارای یک ایراد است: مقدار متغیر SIZE برابر با 12 نیست، بلکه 10 + 2 است. پیشپردازنده یک ویرایشگر متن ساده است و درک کاملی از نحو زبان برنامهنویسی C یا عملیات حسابی ندارد.
پس از پیشپردازش برنامه، میتوانیم محل اشتباه خود را به وضوح ببینیم.
square.i
|
1 2 3 4 5 6 |
# 5 "square.c" int main() { printf("Area %d\n", 10 + 2 * 10 + 2); return (0); } |
همانطور که قبلا ذکر شد، پیشپردازنده C را درک نمیکند. هنگامی که از دستور زیر استفاده میکنیم، SIZE را به جای ۱۲ به صورت لفظی ۱۰ + ۲ تعریف میکند:
|
1 |
#define SIDE 10 + 2 // اندازهی ضلع + حاشیه |
و همانطور که می بینید، ۱۲ × ۱۲ عددی متفاوت از ۱۰ + ۲ × ۱۰ + ۲ است.
هنگام استفاده از #define برای تعریف ثابتهایی پیچیدهتر از یک عدد ساده، کل عبارت را در پرانتز قرار میدهیم، همانطور که در اینجا نشان داده شده است:
|
1 |
#define SIDE (10 + 2) // اندازهی ضلع + حاشیه |
پیروی از این قاعده نگارشی از بروز نتایج نادرست به دلیل ترتیب غیرمنتظره عملیات پس از جایگزینی جلوگیری میکند. برای جلوگیری کامل از مشکل ارزیابی نادرست ماکرو، زمانی که هدف از #define تنظیم یا محاسبه یک مقدار در یک مکان و سپس استفاده از آن در کل برنامه است، از const استفاده کنید که در هر کجا که امکان دارد نسبت به #define ارجحیت دارد. در اینجا نمونهای از این موضوع آورده شده است:
|
1 |
const unsigned int SIDE = 10 + 2; // این کار میکند. |
دلیل اصلی قاعده فوق این است که اصلاح کننده const بخشی از زبان برنامه نویسی C است و کامپایلر عبارت انتساب یافته به یک متغیر const را ارزیابی میکند، به همین دلیل SIDE در واقع برابر با 12 است.
هنگامی که C برای اولین بار طراحی شد، هیچ تعدیل کننده const نداشت، بنابراین همه مجبور بودند از دستور #define استفاده کنند، به همین دلیل #define به طور گسترده استفاده میشود، حتی اگر const مدرن تر برای مدتی در دسترس بوده.
ماکروهای دارای پارامتر به ما امکان میدهند آرگومانهایی را به ماکروها بدهیم. به عنوان مثال:
|
1 2 3 4 5 |
#define DOUBLE(x) (2 * (x)) --snip-- printf("Twice %d is %d\n", 32, DOUBLE(32); |
در این مثال، نیازی به قرار دادن پرانتز دور آرگومان در هنگام گسترش نیست. میتوانیم ماکرو را به این صورت بنویسیم:
|
1 |
#define DOUBLE_BAD(x) (2 * x) |
اما چرا این روش بد است؟ بیایید ببینیم چه اتفاقی میافتد وقتی از این ماکرو با یک عبارت استفاده میکنیم:
|
1 |
value = DOUBLE_BAD(1 + 2); |
قاعدهی استایل در ماکروهای پارامترهدار قرار دادن آرگومانها داخل پرانتز است. بدون پرانتز، عبارت DOUBLE(1+2) به شکل زیر گسترش پیدا میکند:
|
1 |
DOUBLE(1+2) = (2 * 1 + 2) = 4 // اشتباه |
با پرانتز، نتیجهی این میشود:
|
1 |
DOUBLE(1+2) = (2 * (1 + 2)) = 6 |
ما یک قانون داشتیم که میگوید به جز در خطوط جداگانه، از ++ یا — استفاده نکنیم. بیایید ببینیم چه اتفاقی میافتد وقتی این قانون را با استفاده از یک ماکروی پارامترهدار زیر پا میگذاریم:
|
1 2 3 |
#define CUBE(x) ((x) * (x) * (x)) int x = 5; int y = CUBE(x++); |
بعد از اجرای این کد، مقدار x به جای 6، 8 خواهد بود. بدتر از آن، مقدار y میتواند هر چیزی باشد، زیرا قوانین ترتیب اجرای C در هنگام ترکیب عملیات ضرب (*) و افزایش (++) مبهم است.
اگر میخواهید چنین کدی بنویسید، توابع درونخطی (inline functions) را در نظر بگیرید که فراخوانی تابع را با بدنهی تابع جایگزین میکنند:
|
1 2 3 |
static inline int CUBE_INLINE(const int x) { return (x * x * x); } |
این حتی زمانی که از عبارت زیر استفاده کنید هم کار میکند:
|
1 |
y = CUBE_INLINE(x++); |
اما باز هم، نباید به این شکل کد بنویسید. در عوض، به این شکل بنویسید:
|
1 2 |
x++; y = CUBE_INLINE(x); |
هر زمان که ممکن است، به جای ماکروهای پارامترهدار از توابع درونخطی (inline functions) استفاده کنید. از آنجایی که توابع درونخطی جزئی از زبان C هستند، کامپایلر میتواند اطمینان حاصل کند که از آنها به درستی استفاده میشود (برخلاف پیشپردازنده که فقط متن را به صورت کورکورانه جایگزین میکند).
تا الان از ماکروها برای تعریف ثابتها و عبارات ساده استفاده کردهایم. ما میتوانیم از دستور #define برای تعریف کد استفاده کنیم. در اینجا یک مثال آورده شده است:
|
1 2 3 4 5 6 |
#define FOR_EACH_VALUE for (unsigned int i = 0; i < VALUE_SIZE; ++i) --snip-- int sum = 0; FOR_EACH_VALUE sum += value[i] |
این کد چند مشکل دارد. اول اینکه، مشخص نیست متغیر i از کجا آمده است. همچنین، ما نحوه افزایش مقدار آن را پنهان کردهایم، به همین دلیل است که به ندرت از چنین ماکروهایی استفاده میشود.
یک ماکروی رایج، ماکرویی است که عملکرد یک تابع کوتاه را تقلید میکند. بیایید یک ماکرو به نام DIE تعریف کنیم که یک پیام را چاپ میکند و سپس برنامه را میبندد:
|
1 2 3 4 |
// Defined badly #define DIE(why) \ printf("Die: %s\n", why); \ exit(99); |
ما از بکاسلش (\) برای گسترش ماکرو روی چندین خط استفاده میکنیم. ما میتوانیم از این ماکرو به شرح زیر استفاده کنیم:
|
1 2 3 |
void functionYetToBeImplemented(void) { DIE("Function has not been written yet"); } |
در این مورد، این عمل بیشتر به دلیل شانس تا طراحی کار میکند. مشکل اینجاست که DIE شبیه یک تابع به نظر میرسد، بنابراین میتوانیم آن را به عنوان یک تابع در نظر بگیریم.
حالا بیایید آن را درون یک شرط if قرار دهیم:
|
1 2 3 |
// Problem code if (index < 0) DIE("Illegal index"); |
برای اینکه بفهمیم چرا این دارای مشکل است ، بیایید به خروجی گسترشیافته این کد نگاه کنیم:
|
1 2 3 |
if (index < 0) printf("Die %s\n", "Illegal index"); exit(99); |
این کد به درستی تورفتگی ندارد:
|
1 2 3 |
if (index < 0) printf("Die %s\n", "Illegal index"); exit(99); |
به عبارت دیگر، برنامه همیشه exit میشود، حتی اگر مقدار index معتبر باشد.
ببینیم آیا میتوانیم با قرار دادن آکولاد ({}) در اطراف دستورات مشکل را حل کنیم:
|
1 2 3 4 5 |
// Defined not as badly #define DIE(why) { \ printf("Die: %s\n", why); \ exit(99); \ } |
حالا این کد در زیر به درستی کار میکند:
|
1 2 3 |
// Problem code if (index < 0) DIE("Illegal index"); |
اما، در این مورد کار نمیکند:
|
1 2 3 4 |
if (index < 0) DIE("Illegal index"); else printf("Did not die\n"); |
این کد یک پیام خطا ایجاد میکند: else بدون if قبلی.
اما درست همانجا یک if داریم. بیایید به خروجی گسترشیافته نگاه کنیم:
|
1 2 3 4 5 6 7 |
if (index < 0) { printf("Die: %s\n", why); \ exit(99); \ }; // <=== Notice two characters here. else print("Did not die\n"); |
مشکل اینجا این است که C قبل از else یک دستور به پایان رسیدن با سمیکالن (;) یا مجموعهای از دستورات محصور شده در آکولاد ({}) را میخواهد. C نمیداند با مجموعهای از دستورات محصور شده در آکولاد که با سمیکالن به پایان میرسد، چه کاری انجام دهد.
راه حل این مشکل استفاده از یک دستور مبهم C به نام do/while است. به شکل زیر است:
|
1 2 3 4 |
do { // Statements } while (condition); |
دستورات درون بلوک بعد از do همیشه یک بار اجرا میشوند، و سپس تا زمانی که شرط درست باشد، دوباره اجرا میشوند. اگرچه این دستور بخشی از استاندارد زبان C است، اما من آن را تنها دو بار در موارد واقعی دیدهام، و یکی از آن دفعات بهعنوان شوخی بود.
با این حال، این دستور برای ماکروهای کد استفاده میشود:
|
1 2 3 4 5 |
#define DIE(why) do { \ printf("Die: %s\n", why); \ exit(99); \ } while (0) |
این کار میکند چون میتوانیم بعد از آن یک سمیکالن قرار دهیم:
|
1 2 3 4 5 |
if (index < 0) DIE("Illegal index"); // Note semicolon at the end of the statement. else printf("Did not die\n"); |
این کد به کد زیر گسترش مییابد:
|
1 2 3 4 5 6 7 |
if (index < 0) do { printf("Die: %s\n", "Illegal index"); exit(99); } while (0); else printf("Did not die\n"); |
از نظر ترکیبی، do/while یک دستور واحد است و بدون مشکل میتوانیم بعد از آن یک سمیکالن اضافه کنیم. کد داخل آکولاد (printf و exit) به طور ایمن درون حلقه do/while محصور شدهاند. کد خارج از آکولاد یک دستور است، و این همان چیزی است که ما میخواهیم. حالا کامپایلر، ماکروی کد را میپذیرد.
متاسفانه روان و قابل فهم نیست. با اینکه من در کد نویسی از پیش پردازنده استفاده میکنم. مقدار توضیحات گنگ هستن.
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.