در قسمت های قبلی دوره، با چالشهای خروجی ساده روی کنسول روبرو شدیم. اما با وجود سیستمعامل در این بخش، مدیریت ورودی و خروجی بسیار سادهتر شده است.
دلیل این امر، پنهان شدن پیچیدگیهای این فرآیند توسط سیستمعامل است. به این صورت که شما فقط کافیست عبارت Hello World\n را بنویسید و سیستمعامل بهطور خودکار اطلاعات را به محل مورد نظر ارسال میکند.
در این فصل، به بررسی سیستم I/O زبان C میپردازیم. این سیستم شامل تابع printf برای خروجی و همچنین توابعی برای خواندن و نوشتن فایلهای روی دیسک به روشی کارآمد و انعطافپذیر میشود.
ما قبلاً چند بار از تابع printf برای خروجی ساده استفاده کردهایم. فرمت کلی این تابع به صورت زیر است:
|
1 |
printf(format-string, argumen+t, ...) |
رشته فرمت(format-string) به printf میگوید که چه چیزی را چاپ کند. هر کاراکتری به جز % مستقیماً چاپ میشود. کاراکتر % نشاندهندهی یک مشخصه فیلد (field specification) است. این مشخصه به printf میگوید که به لیست آرگومانها (argument list) مراجعه کند و آرگومان بعدی را بر اساس مشخصه فیلد چاپ کند.برای مثال:
|
1 |
printf("Number: ->%d<-\n", 1234); // خروجی: ->1234<- |
مشخصه فیلد %d میتواند با یک عدد اصلاح شود:
|
1 2 3 4 5 |
printf("Number: ->%3d<-\n", 12); // خروجی: -> .12<- (استفاده از . برای فضا) printf("Number: ->%-3d<-\n", 12); // خروجی: ->12. <- (استفاده از . برای فضا) printf("Number: ->%3d<-\n", 1234); // خروجی: ->1234<-(حداقل ۳ کاراکتر) |
در این مثالها، %3d به printf میگوید که از حداقل سه کاراکتر برای چاپ عدد استفاده کند. فیلد %-3d به printf میگوید که عدد را با حداقل سه کاراکتر چاپ کند و آن را چپچین (left-justify) کند.
تاکنون فقط با کاراکتر تبدیل d آشنا شدیم که برای تبدیل یک آرگومان عدد صحیح به متن در جهت چاپ استفاده میشود. جدول 34-1 کاراکترهای تبدیل اصلی را نشان میدهد.
جدول 34-1: کاراکترهای تبدیل اصلی در C
|
کاراکتر تبدیل |
نوع آرگومان |
توضیحات |
|
%d |
عدد صحیح |
انواع char و short int هنگام ارسال به عنوان پارامتر به int ارتقا پیدا میکنند، بنابراین این فرمت برای این سه نوع نیز کار میکند. |
|
%c |
کاراکتر |
به دلیل ارتقا، این کاراکتر تبدیل در واقع یک آرگومان عدد صحیح را میگیرد و آن را به عنوان یک کاراکتر چاپ میکند. |
|
%o |
عدد صحیح |
به صورت اُکتالی چاپ میکند. |
|
%x |
عدد صحیح |
به صورت هگزادسیمالی چاپ میکند. |
|
%f |
دابل |
برای هر دو نوع float و double کار میکند زیرا همه پارامترهای float هنگام ارسال به عنوان آرگومان به double ارتقا پیدا میکنند. |
|
%l |
عدد صحیح بلند |
نوع long int به تبدیل خاص خود نیاز دارد زیرا نوع int به طور خودکار به long int ارتقا پیدا نمیکند. |
برای اینکه به صورت کاربردی نحوهی کار با رشتههای فرمتبندی را نشان دهیم، یک برنامهی کوتاه مینویسیم که جدولی از کاراکترهای قابل چاپ به همراه مقادیر هگزادسیمال و اکتالی آنها ایجاد کند. این برنامه (کد 34-1) به ما این فرصت را میدهد که دادههای مشابه را به چهار روش مختلف نمایش دهیم و فرمتهای مختلفی را درون یک دستور printf امتحان کنیم.
کد منبع (ascii.c):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * Print ASCII character table (only printable characters). */ #include <stdio.h> int main() { for (char curChar = ' '; curChar <= '~'; ++curChar) { printf("Char: %c Decimal %3d Hex 0x%02x Octal 0%03o\n", curChar, curChar, curChar, curChar); } return (0); } |
کد 34-1: برنامهای برای ایجاد جدول ASCII
ابتدا، رشته فرمت %c کاراکتر را به صورت یک کاراکتر چاپ میکند. سپس، کاراکتر را به عنوان یک عدد سه رقمی دهدهی (%3d ) چاپ میکنیم. برای دقت، نوع آرگومان یک کاراکتر است و به یک عدد صحیح ارتقا پیدا میکند. به دلیل عدد ۳ در مشخصه آرگومان، این عدد شامل سه کاراکتر خواهد بود. پس از آن، با استفاده از فرمت %02x به صورت هگزادسیمالی چاپ میکنیم. صفر (0) به printf میگوید که در صورت نیاز، خروجی را با صفرها پر کند تا به عرض دلخواه (که در اینجا ۲ است) برسد. در نهایت، با رشته %03o به صورت اکتالی چاپ میکنیم.
لیست 34-2 خروجی این برنامه را نشان میدهد.
|
1 2 3 4 5 6 7 8 9 10 |
Char: Decimal 32 Hex 0x20 Octal 0040 Char: ! Decimal 33 Hex 0x21 Octal 0041 Char: " Decimal 34 Hex 0x22 Octal 0042 Char: # Decimal 35 Hex 0x23 Octal 0043 Char: $ Decimal 36 Hex 0x24 Octal 0044 Char: % Decimal 37 Hex 0x25 Octal 0045 Char: & Decimal 38 Hex 0x26 Octal 0046 Char: ' Decimal 39 Hex 0x27 Octal 0047 Char: ( Decimal 40 Hex 0x28 Octal 0050 --snip-- |
لیست 34-2: خروجی کد 34-1 (ascii.c)
تابع printf ستون اصلی سیستم I/O زبان C است. این تابع به ما کمک میکند انواع مختلفی از دادهها را روی کنسول چاپ کنیم. اما همانطور که در بخشهای بعدی خواهیم دید، این تنها جایی نیست که میتوانیم بنویسیم.
هنگامی که یک برنامه شروع میشود، سیستمعامل سه فایل از پیش تعریفشده را باز میکند:
به طور پیش فرض، این فایلها به کنسول متصل هستند، اما مترجم خط فرمان شما میتواند آنها را به فایلهای روی دیسک یا موارد دیگر وصل کند.
تابع fprintf دادهها را به یک فایل خاص ارسال میکند. برای مثال:
|
1 2 |
fprintf(stdout, "Everything is OK\n"); fprintf(stderr, "ERROR: Something bad happened\n"); |
تابع printf صرفاً یک تابع میانبر است که جایگزین fprintf(stdout, …) میشود.
تابعهایی که برای خواندن دادهها طراحی شدهاند ساده به نظر میرسند، اما متأسفانه اینطور نیست. تابع scanf همتای printf است که وظیفه خواندن داده را برعهده دارد. برای مثال:
|
1 2 |
// Reads two numbers (do not use this code) scanf("%d %d", &aInteger, &anotherInteger); |
در ابتدا، به آمپرسندهای (&) جلوی آرگومانها توجه کنید. scanf نیاز به تغییر دادن آرگومانها دارد، به همین دلیل از & استفاده میکنیم ؛ بنابراین، آرگومانها باید بر اساس آدرس (address) پاس داده شوند.
رشته فرمت فرستادهشده در scanf بسیار شبیه به رشته فرمت برای printf است، اما یک مشکل بزرگ با scanf همراه است ( مگر اینکه یک متخصص حرفهای باشید): هرگز نمیدانید scanf چگونه با فاصله (whitespace) برخورد میکند. بنابراین، ما از آن استفاده نمیکنیم.
به جای آن، از تابع fgets برای گرفتن یک خط واحد از ورودی و سپس از sscanf برای تجزیه رشته حاصل استفاده میکنیم:
|
1 2 |
fgets(line, sizeof(line), stdin); // <span style="color: #00ff00;">Read</span> a line sscanf(line, "%d %d", &aInteger, &anotherInteger); |
فرم کلی fgets به صورت زیر است:
|
1 |
char* result = fgets(buffer, size, file); |
که در آن:
result اشاره به رشتهای است که به تازگی خوانده شده (buffer) یا در صورت رسیدن به انتهای فایل (EOF) مقدار NULL خواهد داشت.
buffer یک آرایه کاراکتری است که خط در آن قرار میگیرد.
file یک نشانگر فایل (file handle) است که نشان میدهد کدام فایل را باید خواند (در حال حاضر stdin تنها فایلی است که میشناسیم).
در buffer همیشه خاتمهدهنده صفر (0\) وجود خواهد داشت، بنابراین حداکثر size-1 کاراکتر در buffer قرار خواهد گرفت. (کل یک خط خوانده میشود، حتی اگر buffer به اندازه کافی برای نگهداشتن آن بزرگ نباشد.)
تابع sscanf بسیار شبیه به تابع scanf است، با این تفاوت که اولین آرگومان آن اکنون یک رشته است. سایر آرگومانها مشابه هستند. تابع sscanf تعداد آیتمهایی را که تبدیل کرده است برمیگرداند.
کد قبلی فرض میکند که همه چیز به درستی کار میکند. بیایید آن را دوباره بنویسیم و این بار خطاها را بررسی کنیم:
|
1 2 3 4 5 6 7 8 9 10 |
if (fgets(line, sizeof(line), stdin) == NULL) { fprintf(stderr, "ERROR: Expected two integers, got EOF\ n"); return (ERROR); } if (sscanf(line, "%d %d", &aInteger, &anotherInteger) != 2) { fprintf(stderr, "ERROR: Expected two integers.\n"); return (ERROR) } |
اگر اولین فراخوانی به fgets مقدار NULL برگرداند، مشکلی پیش آمده است. سپس یک پیام خطا به فایل از پیش تعریفشده برای خطاها (stderr) چاپ میکنیم و یک کد خطا به فراخواننده (caller) برمیگردانیم. در مرحله بعد، یک sscanf انجام میدهیم که باید دو عدد صحیح پیدا کند. اگر این کار را انجام ندهد، دوباره یک پیام خطا چاپ میکنیم و یک کد خطا برمیگردانیم.
تابع fgets یک تابع اختصاری مشابه برای خواندن داده از stdin دارد. این تابع gets نامیده میشود و فرم کلی آن به صورت زیر است:
|
1 |
result = gets(buffer); |
تابع gets یک خط از داده را میخواند و آن را در buffer قرار میدهد، بدون اینکه بررسی کند که آیا buffer ظرفیت نگهداشتن آن را دارد یا خیر.
استفاده از تابع gets خطرناک است. از آن استفاده نکنید. این تابع در اوایل دوران محاسبات، زمانی که کامپیوترها کمیاب بودند و فرض میشد هرکسی که از آنها استفاده میکند باهوش و صادق است، ساخته شد. از آن زمان به بعد، شرایط تغییر کرده است و صداقت افراد بسیار کمتر شده است. هکرها به سرعت کشف کردند چگونه برنامههایی که از gets استفاده میکنند را با ارسال دادهای بیش از ظرفیت buffer خراب کنند. این حمله، حمله سرریز پشته
(stack smashing attack) نامیده میشود. برای مدتی، gets رایجترین حفره امنیتی شناختهشده بود.
کامپایلر فعلی GCC استفاده از gets را دشوار میکند. اول، stdio.h آن را تعریف نمیکند مگر اینکه یک ماکرو کامپایل شرطی (conditional compilation macro) را به درستی تعریف کنید. هنگامی که برنامه را کامپایل میکنید، کامپایلر به شما هشدار میدهد و سپس هنگام لینک شدن برنامه، لینکر نیز به شما هشدار میدهد.
لیست 34-3 نشان میدهد که هنگام کامپایل کردن یک برنامه با استفاده از gets چه اتفاقی میافتد:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ gcc -Wall -Wextra -o gets gets.c Agets.c: In function 'main': gets.c:17:5: warning: 'gets' is deprecated [-Wdeprecateddeclarations] gets(line); ^~~~ In file included from gets.c:11:0: /usr/include/stdio.h:577:14: note: declared here extern char *gets (char *__s) __wur __attribute_deprecated__; ^~~~ /tmp/cc5H1KMF.o: In function `main': gets.c:(.text+0x1f): warning: the `gets' function is dangerous and should not be used. |
لیست 34-3: تلاش برای استفاده از gets
از حجم خروجی میتوانید ببینید که کامپایلر GCC چقدر تلاش میکند تا شما را از استفاده از gets منصرف کند.
حالا به چند موردی که نباید از آنها استفاده کنیم نگاهی انداختیم، بیایید به چیزهایی که باید استفاده کنیم نگاهی بیندازیم.
فایلهای از پیش تعریفشده stdin, stdout, و stderr نشانگرهای فایل (file handles) هستند. تابع fopen به شما امکان ایجاد نشانگرهای فایل را میدهد. کد 34-4 یک مثال ساده را نشان میدهد.
file.c
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <stdio.h> int main() { 1 FILE* outFile = 2 fopen("hello.txt", "w"); if (outFile == NULL) { fprintf(stderr, "ERROR: Unable to open 'hello.txt'\n"); exit(8); } if (fprintf(outFile, "Hello World!\n") <= 0) { fprintf(stderr, "ERROR: Unable to write to 'hello.txt'\n"); exit(8); } if (fclose(outFile) != 0) { fprintf(outfile, “ERROR: Unable to close 'hello.txt'\n"); exit(8); } return (0); } |
کد 34-4: نسخه فایلی “Hello World”
اول، اعلان FILE* (1) یک نشانگر فایل جدید را تعریف میکند. تمام عملیات روی فایل به یک نشانگر فایل نیاز دارند. سپس فراخوانی fopen (2) را داریم که فرم کلی آن به صورت زیر است:
|
1 |
result = fopen(filename, mode); |
mode (حالت) میتواند یکی از موارد زیر باشد:
r فقط خواندنی
w فقط نوشتنی
r+ خواندنی و نوشتنی
a الحاق (نوشتن و شروع از انتهای فایل)
b همراه با سایر حالتها برای فایلهای باینری (در بخش بعدی توضیح داده میشود) استفاده میشود.
از قرارداد نامگذاری مایکروسافت که از بکاسلش (\) به عنوان جداکننده استفاده میکند، مراقب باشید. اگر میخواهید فایلی را در بالاترین سطح دایرکتوری به نام root با نام فایل file.txt در لینوکس یا macOS بنویسید، از دستور زیر استفاده میکنید:
|
1 |
FILE* fopen("/root/file.txt", "w"); |
در سیستمهای مایکروسافت، روش زیر کار نمیکند:
|
1 |
// اشتباهFILE* fopen("\root\file.txt", "w"); |
زیرا بکاسلش (\) کاراکتر فرار (escape character) است و چون \r کاراکتر <return> و \f کاراکتر
<form-feed> هستند، در واقع سعی میکنید فایل زیر را بنویسید:
|
1 |
<return>oot<form-feed>ile.txt |
آنچه میخواهید این است:
|
1 |
// درستFILE* fopen("\\root\\file.txt", "w"); |
با فرار از کاراکتر فرار، یک بکاسلش واقعی در مسیر به دست میآورید.
حالا که فایلها را باز کردیم، میتوانیم به خواندن و نوشتن در آنها بپردازیم. متن را میتوان با fprintf نوشت و با fgets خواند. در بخش بعدی، به نوع دیگری از فایلها یعنی فایلهای باینری نگاهی خواهیم انداخت.
تا الان خودمان را به فایلهای متنی محدود کردهایم، اما سیستم I/O زبان C میتواند از طریق توابع fread و fwrite فایلهای باینری را مدیریت کند.
فرم کلی تابع fread به صورت زیر است:
|
1 |
result = fread(buffer, elementSize, size, inFile); |
در اینجا:
buffer یک اشاره گر به بافر دادهای است که دادهها در آن قرار خواهند گرفت.
elementSize همیشه ۱ است (برای توضیح به باکس زیر مراجعه کنید).
size اندازه بافر است که معمولاً برابر با sizeof(buffer) میباشد و inFile فایلی است که باید خوانده شود.
این تابع تعداد آیتمهای خواندهشده را برمیگرداند که به دلیل اینکه elementSize برابر با ۱ است، تعداد بایتهای خواندهشده را نشان میدهد. مقدار ۰ برای پایان فایل و یک عدد منفی برای خطای I/O برگردانده میشود.
ELEMENTSIZE
قصد اولیه این بود که elementSize اندازه یک عنصر در آرایه بافر باشد و size تعداد عناصری باشد که باید خوانده شود. نتیجه، تعداد عناصر خواندهشده بود. با این حال، با گذشت زمان، تقریباً همه برنامهها میخواستند تعداد معینی از بایتها را بخوانند، نه تعداد معینی از عناصر. بنابراین، در دنیای واقعی، elementSize برابر با ۱ است و size تعداد بایتهای قابل خواندن است.
من در طول ۴۰ سال برنامهنویسی، فقط دو بار کدی را دیدهام که در آن elementSize برابر با ۱ نبوده است. این اتفاق بسیار نادر است.
تابع fwrite ساختاری مشابه دارد:
|
1 |
result = fwrite(buffer, elementSize, size, inFile); |
همه چیز یکسان است، به جز اینکه داده نوشته میشود به جای اینکه خوانده شود.
ما از فراخوانیهای fread و fwrite برای کپی کردن یک فایل استفاده میکنیم. از آنجایی که هنوز نحوهی پاس دادن آرگومانها روی خط فرمان را یاد نگرفتهایم (به فصل ۱۵ مراجعه کنید)، نام فایلهای کد سخت (hardcoded) شده به infile.bin و outfile.bin تنظیم شدهاند. کد 34-5 کد این کار را نشان میدهد.
copy.c
|
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 34 35 36 37 38 39 40 41 42 43 44 |
/** * Copy infile.bin to outfile.bin. */ #include <stdio.h> #include <stdlib.h> #include <stdbool.h> int main() { // The input file 1 FILE* inFile = fopen("infile.bin", "rb"); if (inFile == NULL) { fprintf(stderr, "ERROR: Could not open infile.bin\n"); exit(8); } // The output file FILE* outFile = fopen("outfile.bin", "wb"); if (outFile == NULL) { fprintf(stderr, "ERROR: Could not create outfile.bin\n"); exit(8); } char buffer[512]; // A data buffer while (true) { // Read data, collect size 2 ssize_t readSize = fread(buffer, 1, sizeof(buffer), inFile); if (readSize < 0) { fprintf(stderr, "ERROR: Read error seen\n"); exit(8); } 3 if (readSize == 0) { break; } 4 if (fwrite(buffer, 1, readSize, outFile) != (size_t)readSize) { fprintf(stderr, "ERROR: Write error seen\n"); exit(8); } } fclose(inFile); fclose(outFile); return (0); } |
کد 34-5: کپی کردن یک فایل
اول، به فراخوانی fopen (1) توجه کنید. ما فایل را با مد(mode) rb باز میکنیم، که به سیستم میگوید ما قصد خواندن فایل (r) را داریم و آن فایل باینری است(b).
حالا به فراخوانی fread (2) نگاهی بیندازیم. مقدار برگشتی این تابع ssize_t است که یک نوع استاندارد است و به اندازهای بزرگ است که بتواند اندازه بزرگترین شیء (ساختار، آرایه، Union) که ممکن است وجود داشته باشد را نگه دارد. همچنین میتواند مقدار ۱- را برای نشان دادن خطا نگه دارد.
اگر تمام دادهها را از فایل خوانده باشیم، fread مقدار ۰ را برمیگرداند. وقتی این اتفاق میافتد، کار ما تمام شده است، بنابراین از حلقه اصلی خارج میشویم (خروج از حلقه در بخش (3) مشخص شده است).
حالا به فراخوانی fwrite (4) میرسیم که مقدار size_t را برمیگرداند. این یک نوع بدون علامت است که اندازه بزرگترین شیء قابل قرارگیری در یک برنامه را نگه میدارد، اما چون بدون علامت است، نمیتواند مقدار خطا را نگه دارد. چه اتفاقی میافتد وقتی fwrite هنگام نوشتن چیزی دچار خطا میشود؟ تا جایی که میتواند مینویسد و تعداد بایتهای نوشتهشده را برمیگرداند، بنابراین هرگز کد خطا را برنمیگرداند. (فقط یک نوشتن کوتاه)
توجه داشته باشید که fread یک مقدار از نوع ssize_t برمیگرداند در حالی که fwrite مقدار size_t را برمیگرداند. دلایل خوبی برای این تفاوت وجود دارد، اما به این معنی است که اگر بخواهیم بررسی کنیم تعداد بایتهایی که سعی کردهایم بنویسیم با تعداد بایتهایی که واقعاً از fwrite خواستهایم یکی باشد، یک هشدار کامپایلر دریافت میکنیم:
|
1 2 3 4 |
35 if (fwrite(buffer, 1, readSize, outFile) != readSize) { Warning: signed vs. unsigned compare |
این هشدار به خاطر مقایسهی یک مقدار با علامت (readSize) با یک مقدار بدون علامت (fwrite که size_t برمیگرداند) است.
برای رفع این هشدار، نیاز به قرار دادن یک کست (cast) داریم تا به کامپایلر C بگوییم: بله، میدانم که در حال ترکیب کردن انواع با علامت و بدون علامت هستیم، اما به دلیل نحوهی تعریف توابع fread و fwrite مجبور به این کار هستم:
|
1 2 |
if (fwrite(buffer, 1, readSize, outFile) != (size_t)readSize) { |
با قرار دادن (size_t) جلوی readSize، به کامپایلر اعلام میکنیم که به طور موقت، readSize را به نوع size_t تبدیل کنیم تا مقایسهی درستی انجام شود.
همچنین توجه داشته باشید که در آخرین خواندن، احتمالاً ۵۱۲ بایت کامل دریافت نمیکنیم. به همین دلیل است که از readSize در دستور fwrite به جای sizeof(buffer) استفاده میکنیم. زیرا readSize نشاندهندهی تعداد واقعی بایتهای خواندهشده در آخرین تکرار حلقه است.
سیستم I/O زبان C از I/O بافرینگ استفاده میکند، یعنی هنگامی که شما از printf یا fwrite استفاده میکنید، دادهها ممکن است بلافاصله به دستگاه خروجی ارسال نشوند. در عوض، آنها تا زمانی که سیستم به اندازهی کافی داده برای کارایی بهتر داشته باشد، در حافظه ذخیره خواهند شد.
دادههایی که به کنسول میروند بافرگیری خطی (line buffered) دارند، یعنی اگر بخشی از یک خط را چاپ کنید، ممکن است تا زمانی که بقیهی خط ارسال نشود، خارج نشود. بیایید ببینیم چگونه این موضوع میتواند ما را در برنامهی کد 34-6 دچار مشکل کند.
کد 34-6: تقسیم بر صفر
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * Demonstrate how buffering can fool * us with a divide-by-zero bug. */ #include <stdio.h> int main() { int zero = 0; // The constant zero, to trick the // compiler into letting us divide by 0 int result; // Something to put a result in printf("Before divide "); result = 5 / zero; printf("Divide done\n"); printf("Result is %d\n", result); return (0); } |
هنگام اجرای این برنامه، انتظار دارید خروجی زیر را ببینید:
|
1 |
Before divide Floating point exception (core dumped) |
اما چیزی که واقعاً میبینید این است:
|
1 |
Floating point exception (core dumped) |
اولین فکر شما ممکن است این باشد که printf اجرا نشده است، اما اجرا شده است. دادهها وارد یک بافر شده و در زمان خروج برنامه (abort) در بافر باقی ماندند و این باعث ایجاد تصور اشتباه مبنی بر عدم کارکرد printf شد.
تقسیم عدد صحیح بر صفر منجر به خطای ممیز شناور (floating-point exception) و خروج برنامه
(core dumped) میشود. تقسیم ممیز شناور بر صفر باعث میشود که مقدار NaN به نتیجه اختصاص داده شود و برنامه خارج نخواهد شد. در مورد این رفتار، سابقهی زیادی وجود دارد. اصطلاح core dumped باقی ماندهی زمانی است که حافظهی رایانه واقعاً از هستههای فریت (ferrite cores) تشکیل شده بود. در مورد خطای ممیز شناور برای تقسیمهای صحیح، زمانی که شروع به برنامهنویسی کردم یک مورد عجیب قدیمی بود و هنوز نمیدانم چرا کارها به این شکل انجام میشدند.
برای رفع این مشکل، باید به سیستم I/O بگوییم «دادههای بافر شده را اکنون بنویس»، که این کار با تابع fflush انجام میشود:
|
1 |
printf("Before divide ");fflush(stdout); |
تخلیهی دادهها تضمین میکند که میتوانیم آنها را ببینیم. از طرف دیگر، نمیخواهیم بعد از هر نوشتن، تخلیه انجام دهیم، زیرا این کار باعث بیاثر شدن هدف بافرگیری که کارآمدتر کردن I/O است، میشود.
در نهایت، پس از اینکه کارمان با یک فایل تمام شد، باید به C اطلاع دهیم که دیگر به آن نیاز نداریم. این کار را با استفاده از تابع fclose انجام میدهیم:
|
1 |
int result = fclose(file); |
در اینجا file نشاندهندهی فایل (از نوع FILE*) است که میخواهیم ببندیم و result نیز مقدار برگشتی تابع است که در صورت موفقیت ۰ و در صورت بروز خطا غیرصفر خواهد بود.
در دنیای سیستمهای امبدد (embedded systems)، I/O کار دشواری است زیرا باید کدی برای کار مستقیم با دستگاه بنویسید و برای هر نوع دستگاه متفاوتی به کد جداگانهای نیاز دارید.
سیستم I/O زبان C برای پنهان کردن تمام این جزئیات از شما طراحی شده است. همچنین ویژگیهای مفیدی مانند فرمتبندی، بافرگیری و استقلال از دستگاه را ارائه میدهد. سیستم I/O بافرینگ برای اکثر برنامههای عمومی به خوبی کار میکند.
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.