کامپایلر پایه 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 با مقدار تعریفشده جایگزین شده است.
پیشپردازنده همه چیز را به معنای واقعی کلمه در نظر میگیرد، که میتواند شما را به دردسر بیندازد، همانطور که در اینجا نشان داده شده است:
|
1 2 3 4 |
square.c#include <stdio.h>#define SIDE 10 + 2 // اندازهی ضلع + حاشیهint main() { printf("مساحت %d\n", SIDE * SIDE); return (0); } <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
این مثال مساحت یک مربع را محاسبه میکند. برای تعریف ضلع مربع، حاشیه کوچکی در نظر گرفته شده است. برای بدست آوردن مساحت، ضلعها را در هم ضرب میکنیم و نتیجه را چاپ میکنیم.
با این حال، این برنامه دارای یک ایراد است: مقدار متغیر SIZE برابر با 12 نیست، بلکه 10 + 2 است. پیشپردازنده یک ویرایشگر متن ساده است و درک کاملی از نحو زبان برنامهنویسی C یا عملیات حسابی ندارد.
پس از پیشپردازش برنامه، میتوانیم محل اشتباه خود را به وضوح ببینیم.
|
1 2 3 4 |
square.i# 5 "square.c"int main() { printf("مساحت %d\n", 10 + 2 * 10 + 2); return (0);} <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
همانطور که قبلا ذکر شد، پیشپردازنده 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 4 5 |
#define CUBE(x) ((x) * (x) * (x))int x = 5; int y = CUBE(x++); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
بعد از اجرای این کد، مقدار x به جای 6، 8 خواهد بود. بدتر از آن، مقدار y میتواند هر چیزی باشد، زیرا قوانین ترتیب اجرای C در هنگام ترکیب عملیات ضرب (*) و افزایش (++) مبهم است.
اگر میخواهید چنین کدی بنویسید، توابع درونخطی (inline functions) را در نظر بگیرید که فراخوانی تابع را با بدنهی تابع جایگزین میکنند:
|
1 2 3 4 |
static inline int CUBE_INLINE(const int x) { return (x * x * x);} <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
این حتی زمانی که از عبارت زیر استفاده کنید هم کار میکند:
|
1 |
y = CUBE_INLINE(x++); |
اما باز هم، نباید به این شکل کد بنویسید. در عوض، به این شکل بنویسید:
|
1 2 3 4 |
x++;y = CUBE_INLINE(x); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
هر زمان که ممکن است، به جای ماکروهای پارامترهدار از توابع درونخطی (inline functions) استفاده کنید. از آنجایی که توابع درونخطی جزئی از زبان C هستند، کامپایلر میتواند اطمینان حاصل کند که از آنها به درستی استفاده میشود (برخلاف پیشپردازنده که فقط متن را به صورت کورکورانه جایگزین میکند).
تا الان از ماکروها برای تعریف ثابتها و عبارات ساده استفاده کردهایم. ما میتوانیم از دستور #define برای تعریف کد استفاده کنیم. در اینجا یک مثال آورده شده است:
|
1 2 3 4 5 6 7 8 9 10 |
#define FOR_EACH_VALUE for (unsigned int i = 0; i < VALUE_SIZE; ++i) ... int sum = 0;FOR_EACH_VALUE sum += value[i]; <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
این کد چند مشکل دارد. اول اینکه، مشخص نیست متغیر i از کجا آمده است. همچنین، ما نحوه افزایش مقدار آن را پنهان کردهایم، به همین دلیل است که به ندرت از چنین ماکروهایی استفاده میشود.
یک ماکروی رایج، ماکرویی است که عملکرد یک تابع کوتاه را تقلید میکند. بیایید یک ماکرو به نام DIE تعریف کنیم که یک پیام را چاپ میکند و سپس برنامه را میبندد:
|
1 2 3 4 |
// تعریف اشتباه#define DIE(why) \ printf("Die: %s\n", why); \ exit(99); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
ما از بکاسلش (\) برای گسترش ماکرو روی چندین خط استفاده میکنیم. ما میتوانیم از این ماکرو به شرح زیر استفاده کنیم:
|
1 2 3 4 |
void functionYetToBeImplemented(void) { DIE("Function has not been written yet");} <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
در این مورد، این عمل بیشتر به دلیل شانس تا طراحی کار میکند. مشکل اینجاست که DIE شبیه یک تابع به نظر میرسد، بنابراین میتوانیم آن را به عنوان یک تابع در نظر بگیریم.
حالا بیایید آن را درون یک شرط if قرار دهیم:
|
1 2 3 4 |
// کد مشکلدارif (index < 0) DIE("Illegal index"); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
برای اینکه بفهمیم چرا این دارای مشکل است ، بیایید به خروجی گسترشیافته این کد نگاه کنیم:
|
1 2 3 4 |
if (index < 0) printf("Die %s\n", "Illegal index"); exit(99); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
این کد به درستی تورفتگی ندارد:
|
1 2 3 4 |
if (index < 0) printf("Die %s\n", "Illegal index");exit(99); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
به عبارت دیگر، برنامه همیشه exit میشود، حتی اگر مقدار index معتبر باشد.
ببینیم آیا میتوانیم با قرار دادن آکولاد ({}) در اطراف دستورات مشکل را حل کنیم:
|
1 2 3 4 5 |
// تعریف به شکل نه چندان بد#define DIE(why) { \ printf("Die: %s\n", why); \ exit(99); \} <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
حالا این کد در زیر به درستی کار میکند:
|
1 2 3 4 |
// کد مشکلدارif (index < 0) DIE("Illegal index"); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
اما، در این مورد کار نمیکند:
|
1 2 3 4 5 |
if (index < 0) DIE("Illegal index");else printf("Did not die\n"); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
این کد یک پیام خطا ایجاد میکند: else بدون if قبلی.
اما درست همانجا یک if داریم. بیایید به خروجی گسترشیافته نگاه کنیم:
|
1 2 3 4 |
if (index < 0) { printf("Die: %s\n", why); \ exit(99); \} ; // <=== به دو کاراکتر اینجا دقت کنیدelse print("Did not die\n"); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
مشکل اینجا این است که C قبل از else یک دستور به پایان رسیدن با سمیکالن (;) یا مجموعهای از دستورات محصور شده در آکولاد ({}) را میخواهد. C نمیداند با مجموعهای از دستورات محصور شده در آکولاد که با سمیکالن به پایان میرسد، چه کاری انجام دهد.
راه حل این مشکل استفاده از یک دستور مبهم C به نام do/while است. به شکل زیر است:
|
1 2 3 4 5 |
do { // دستورات} while (شرط); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
دستورات درون بلوک بعد از do همیشه یک بار اجرا میشوند، و سپس تا زمانی که شرط درست باشد، دوباره اجرا میشوند. اگرچه این دستور بخشی از استاندارد زبان C است، اما من آن را تنها دو بار در موارد واقعی دیدهام، و یکی از آن دفعات بهعنوان شوخی بود.
با این حال، این دستور برای ماکروهای کد استفاده میشود:
|
1 2 3 4 5 |
#define DIE(why) do { \ printf("Die: %s\n", why); \ exit(99); \} while (0) <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
این کار میکند چون میتوانیم بعد از آن یک سمیکالن قرار دهیم:
|
1 2 3 4 5 |
if (index < 0) DIE("Illegal index"); // به سمیکالن در انتهای دستور دقت کنیدelse printf("Did not die\n"); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
این کد به کد زیر گسترش مییابد:
|
1 2 3 4 5 |
if (index < 0) do { printf("Die: %s\n", "Illegal index"); exit(99); } while (0);else printf("Did not die\n"); <span style="color: #ff0000;">######## مهندس اناری گرامی لطفا این برنامه را بررسی بفرمائید . به هم ریخته است ####</span> |
از نظر ترکیبی، do/while یک دستور واحد است و بدون مشکل میتوانیم بعد از آن یک سمیکالن اضافه کنیم. کد داخل آکولاد (printf و exit) به طور ایمن درون حلقه do/while محصور شدهاند. کد خارج از آکولاد یک دستور است، و این همان چیزی است که ما میخواهیم. حالا کامپایلر، ماکروی کد را میپذیرد.
آموزش پیشرفته مدیریت حافظه Flash و Linker Script...
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.