در قسمت ۳۴ آموزش امبدد C با تابع printf، روشهای قالببندی خروجی، جدول ASCII و تفاوت دو جریان stdout و stderr آشنا شدیم. حالا نوبت آن است که مسیر برعکس را بررسی کنیم: چگونه اطلاعات را وارد برنامه کنیم، دادههای دریافتشده را بررسی کنیم و آنها را در یک فایل متنی ذخیره یا از فایل بخوانیم؟
در این قسمت از آموزش، مباحث بعدی فصل چهاردهم کتاب Bare Metal C را دنبال میکنیم. ابتدا با stdin و تابع fgets ورودی را دریافت میکنیم؛ سپس با sscanf دادههای متنی را به مقدار عددی تبدیل میکنیم. بعد از بررسی دلیل ناامنبودن تابع قدیمی gets، با توابع fopen، fprintf، fgets و fclose یک فایل واقعی میسازیم و آن را میخوانیم.
تا اینجا بیشتر از برنامه خروجی گرفتهایم؛ مثلاً با printf متنی را در ترمینال نشان دادهایم. برای دریافت اطلاعات، یکی از جریانهای استاندارد زبان C به نام stdin را در اختیار داریم. در یک برنامه کنسولی معمولی، stdin غالباً به صفحهکلید متصل است؛ با این حال سیستمعامل میتواند ورودی آن را از یک فایل یا جریان دیگر نیز تأمین کند.
یک روش رایج برای دریافت اطلاعات، خواندن یک خط کامل به شکل رشته و سپس تفسیر محتوای آن است. این روش به ما کمک میکند ورودی نامعتبر را شناسایی کنیم و برای طول داده، محدودیت مشخصی داشته باشیم.

شکل ۳۵-۱ ـ مسیر داده از صفحهکلید یا فایل ورودی به stdin، تابع fgets، بافر کاراکتری و سپس sscanf
تابع fgets از کتابخانه stdio.h یک رشته را از جریان ورودی میخواند و داخل آرایهای از کاراکترها قرار میدهد. شکل کلی آن به صورت زیر است:
| 1 | fgets(buffer, size, stream); |
در این دستور، buffer حافظه مقصد، size ظرفیت بافر بر حسب بایت و stream جریان ورودی است. تابع حداکثر size - 1 نویسه میخواند تا یک بایت برای نویسه پایان رشته یعنی \0 باقی بماند. اگر کاراکتر پایان خط (\n) در این محدوده خوانده شود، معمولاً در رشته ذخیره میشود. در صورت پایان ورودی پیش از دریافت هر کاراکتر یا وقوع خطا، مقدار NULL برگردانده میشود.
در مثال زیر با استفاده از fgets نام کاربر را میخوانیم. سپس با کمک strcspn کاراکتر پایان خط را، در صورت وجود، حذف میکنیم تا خروجی مرتبتری داشته باشیم.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <stdio.h> #include <string.h> int main(void) { char name[64]; printf("Your name: "); fflush(stdout); if (fgets(name, sizeof(name), stdin) == NULL) { fprintf(stderr, "Could not read the name.\n"); return 1; } name[strcspn(name, "\n")] = '\0'; printf("Hello, %s!\n", name); return 0; } |
نمونه اجرا:
| 1 2 | Your name: Sara Hello, Sara! |
دستور fflush(stdout) باعث میشود متن درخواست نام، حتی در محیطهایی که خروجی بافر میشود، قبل از انتظار برای ورودی نمایش داده شود. استفاده از sizeof(name) هم ظرفیت واقعی آرایه را به تابع اعلام میکند.
نکته: اگر یک خط ورودی از ظرفیت بافر بزرگتر باشد، fgets فقط بخشی از آن را میخواند و باقی داده در جریان ورودی میماند. در برنامههای واقعی باید این وضعیت نیز مدیریت شود؛ از جمله با بررسی وجود \n و تخلیه ادامه خط در صورت لزوم.
گاهی ورودی صرفاً یک نام یا متن نیست؛ مثلاً میخواهیم کاربر دو عدد وارد کند یا از یک سطر متنی چند مقدار استخراج کنیم. تابع sscanf یک رشته موجود در حافظه را براساس الگوی قالببندی تحلیل میکند. تفاوت آن با scanf این است که sscanf بهجای خواندن مستقیم از stdin، داده را از یک رشته دریافت میکند.
برای مثال، الگوی "%d %d" به معنی خواندن دو عدد صحیح از یک رشته است. علامت & در مقابل متغیرها ضروری است، زیرا تابع باید مقدار تبدیلشده را در آدرس آن متغیرها ذخیره کند. مقدار بازگشتی sscanf تعداد تبدیلهای موفق است.
| 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 | #include <stdio.h> int main(void) { char line[128]; int first = 0; int second = 0; char extra; printf("Enter two integers: "); fflush(stdout); if (fgets(line, sizeof(line), stdin) == NULL) { fprintf(stderr, "Input error or end of input.\n"); return 1; } if (sscanf(line, "%d %d %c", &first, &second, &extra) != 2) { fprintf(stderr, "Please enter exactly two integers.\n"); return 1; } printf("Sum = %lld\n", (long long)first + second); return 0; } |
نمونه ورودی و خروجی صحیح:
| 1 2 | Enter two integers: 17 25 Sum = 42 |
نمونه ورودی نامعتبر:
| 1 2 | Enter two integers: 17 25 bad Please enter exactly two integers. |
در این مثال وجود %c سوم صرفاً برای تشخیص متن اضافه بعد از دو عدد است. وقتی فقط دو عدد و فاصلههای مجاز وارد شوند، تعداد تبدیلهای موفق برابر ۲ خواهد بود. اما اگر کاراکتر اضافی وارد شود، تابع میتواند تبدیل سومی انجام دهد و برنامه آن را رد میکند.
دقت فنی: این روش برای آموزش و ورودیهای معمولی مناسب است؛ ولی برای اعتبارسنجی سختگیرانه اعداد، بهخصوص مقادیر خارج از محدوده int، بهتر است از strtol و بررسی خطا و محدوده استفاده کنیم. همچنین برای برنامه تولیدی باید طول کامل خط ورودی را کنترل کرد.
در کدهای قدیمی زبان C ممکن است با تابع gets روبهرو شوید. مشکل اصلی این تابع آن است که طول بافر مقصد را دریافت نمیکند. بنابراین اگر تعداد کاراکترهای ورودی بیشتر از فضای اختصاصیافته باشد، احتمال نوشتن داده بیرون از محدوده حافظه یا Buffer Overflow وجود دارد. چنین خطایی ممکن است برنامه را از کار بیندازد و حتی به یک آسیبپذیری امنیتی تبدیل شود.
به همین دلیل، تابع gets از استاندارد C11 حذف شده است و نباید در پروژههای جدید از آن استفاده کرد. بهجای آن از fgets همراه با بررسی ظرفیت بافر و مدیریت ورودی طولانی استفاده کنید.
شکل ۳۵-۲ ـ مقایسه تابع حذفشده gets با تابع fgets و محدودسازی طول داده برای جلوگیری از سرریز بافر
تا این بخش، اطلاعات را از ورودی استاندارد دریافت میکردیم. اما در بسیاری از برنامهها نیاز داریم دادهها پس از پایان اجرای برنامه نیز باقی بمانند؛ برای مثال گزارش اندازهگیری سنسور، فایل تنظیمات یا گزارش خطاها.
در زبان C برای مدیریت فایلهای استاندارد از نوع FILE * استفاده میکنیم. این نوع، یک اشارهگر به ساختار مدیریتی جریان فایل در کتابخانه C است. برای بازکردن فایل از تابع fopen استفاده میشود:
| 1 | FILE *file = fopen("sensor_log.txt", "w"); |
آرگومان اول نام یا مسیر فایل است و آرگومان دوم حالت دسترسی را تعیین میکند. اگر بازکردن فایل موفق نباشد، تابع مقدار NULL را برمیگرداند؛ بنابراین همیشه باید نتیجه آن را بررسی کنیم.
| حالت | کاربرد | رفتار مهم |
|---|---|---|
r | خواندن فایل | فایل باید از قبل وجود داشته باشد. |
w | نوشتن فایل | فایل جدید میسازد یا محتوای فایل موجود را پاک میکند. |
a | افزودن داده در انتهای فایل | محتوای قبلی را نگه میدارد. |
r+ | خواندن و نوشتن | فایل باید موجود باشد و بهصورت خودکار پاک نمیشود. |
w+ | خواندن و نوشتن | فایل را ایجاد میکند یا محتوای قبلی را حذف میکند. |
a+ | خواندن و افزودن داده | نوشتن در انتهای فایل انجام میشود. |
حرف b نیز در حالتهایی مثل rb و wb برای حالت باینری بهکار میرود که موضوع قسمت بعدی آموزش است.
نکته مهم: استفاده از "w" روی فایلی که از قبل وجود دارد، محتوای قبلی آن را حذف میکند. اگر قصد دارید فقط اطلاعات جدید را به انتهای گزارش اضافه کنید، حالت "a" مناسبتر است.
شکل ۳۵-۳ ـ مقایسه عملکرد حالتهای r، w، a، r+، w+ و a+ در تابع fopen
فرض کنید میخواهیم نتایج اندازهگیری دما را در یک فایل متنی نگه داریم. در این مثال، فایلی با نام sensor_log.txt ایجاد میکنیم و سه سطر اطلاعات در آن قرار میدهیم. برای نوشتن متن در فایل از fprintf استفاده میکنیم؛ عملکردی شبیه printf دارد، اما مقصد خروجی را صریحاً مشخص میکنیم.
| 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 | #include <stdio.h> int main(void) { FILE *file = fopen("sensor_log.txt", "w"); if (file == NULL) { perror("Opening sensor_log.txt"); return 1; } if (fprintf(file, "Time,Temperature\n") < 0 || fprintf(file, "10:00,24\n") < 0 || fprintf(file, "10:05,25\n") < 0) { perror("Writing sensor_log.txt"); fclose(file); return 1; } if (fclose(file) == EOF) { perror("Closing sensor_log.txt"); return 1; } puts("Text file created."); return 0; } |
خروجی ترمینال:
| 1 | Text file created. |
پس از اجرای موفق، فایل sensor_log.txt معمولاً در پوشه کاری فعلی برنامه ساخته میشود. محتوای آن به این شکل خواهد بود:
| 1 2 3 | Time,Temperature 10:00,24 10:05,25 |
در این کد چند نکته مهم وجود دارد: ابتدا نتیجه fopen بررسی میشود؛ سپس خروجیهای fprintf کنترل میشوند؛ و در پایان از fclose برای بستن فایل استفاده میکنیم. بررسی نتیجه بستن فایل مهم است، چون ممکن است خطای نوشتن بافرشده در زمان بستهشدن آشکار شود.
حالا همان فایل ساختهشده در بخش قبل را میخوانیم. اینبار fopen را با حالت "r" فراخوانی میکنیم و در یک حلقه، هر بار قسمتی از فایل را تا پایان خط یا پرشدن بافر دریافت میکنیم. تابع fputs نیز متن خواندهشده را بدون افزودن خودکار کاراکتر پایان خط در خروجی نمایش میدهد.
| 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 | #include <stdio.h> int main(void) { FILE *file = fopen("sensor_log.txt", "r"); char line[128]; if (file == NULL) { perror("Opening sensor_log.txt"); return 1; } while (fgets(line, sizeof(line), file) != NULL) { fputs(line, stdout); } if (ferror(file)) { perror("Reading sensor_log.txt"); fclose(file); return 1; } if (fclose(file) == EOF) { perror("Closing sensor_log.txt"); return 1; } return 0; } |
خروجی:
| 1 2 3 | Time,Temperature 10:00,24 10:05,25 |
حلقه تا زمانی ادامه مییابد که fgets بتواند دادهای بخواند. پس از پایان حلقه، با ferror بررسی میکنیم که توقف خواندن به دلیل خطای ورودی بوده است یا خیر. توجه کنید که اگر طول یک سطر از ظرفیت آرایه بزرگتر باشد، ممکن است آن سطر در چند بار فراخوانی fgets خوانده شود.
شکل ۳۵-۴ ـ جریان ذخیره داده با fopen و fprintf و بازیابی اطلاعات با fopen و fgets و بستن فایل با fclose
مثالهای این قسمت را میتوانید روی یک کامپیوتر دارای سیستمعامل با کامپایلر GCC اجرا کنید. در پروژههای Embedded Linux نیز همین توابع استاندارد C برای خواندن تنظیمات، ثبت لاگ و پردازش فایلها کاربرد گستردهای دارند.
اما در یک میکروکنترلر Bare-metal مانند STM32، معمولاً فایلسیستم و دیسک بهصورت پیشفرض وجود ندارند. برای استفاده از فایلها ممکن است به حافظه SD، فایلسیستمی مانند FatFs و درایور ذخیرهسازی نیاز داشته باشیم. در چنین شرایطی صرفاً نوشتن fopen کافی نیست و باید کتابخانه C یا لایه فایلسیستم را به سختافزار متصل کنیم.
بهعنوان نمونه، یک دیتالاگر مبتنی بر STM32 میتواند داده سنسور دما را دریافت و روی کارت حافظه ذخیره کند؛ ولی برای این کار علاوه بر منطق برنامه، به راهاندازی رابط سختافزاری و فایلسیستم نیز نیاز خواهیم داشت. پس تفاوت زبان C و کتابخانه استاندارد با امکانات واقعی سختافزار و سیستمعامل را همیشه در نظر بگیرید.
fopen را با NULL مقایسه کنید و در صورت خطا پیام مناسبی نمایش دهید.w فایل موجود را خالی میکند؛ برای اضافهکردن داده از a استفاده کنید.fgets ظرفیت بافر را رعایت میکند، اما ممکن است بقیه خط در ورودی باقی بماند.sscanf را بررسی کنید؛ دریافت رشته به معنی معتبر بودن محتوای آن نیست.fclose را اجرا و نتیجه آن را بررسی کنید."C:\\logs\\sensor_log.txt". برای مسیرهای قابلحمل تا حد امکان از مسیر نسبی استفاده کنید.جمع بندی
در قسمت ۳۵ آموزش امبدد C یاد گرفتیم که خواندن ورودی بهتر است در دو مرحله انجام شود: ابتدا دریافت یک رشته با fgets و سپس بررسی و تبدیل داده با sscanf. همچنین دیدیم چرا gets تابع امنی نیست و چگونه میتوان با fopen یک فایل متنی را باز کرد، با fprintf در آن نوشت، با fgets از آن خواند و در پایان با fclose آن را بست.
در قسمت ۳۶ به ادامه فصل چهاردهم میپردازیم و سراغ فایلهای باینری، توابع fread و fwrite، کپیکردن فایل و مفاهیم Buffering و Flushing میرویم.
تمرین
a بازنویسی کنید تا با هر بار اجرای آن، سطر جدیدی به فایل اضافه شود و سطرهای قبلی پاک نشوند.منبع آموزشی: کتاب Bare Metal C: Embedded Programming for the Real World نوشته Stephen Oualline، فصل ۱۴ (بخشهای Reading Data، The Evil gets Function و Opening Files). مثالهای این مقاله با نگارش آموزشی مستقل بازنویسی شدهاند.
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.