خوش آموز درخت تو گر بار دانش بگیرد، به زیر آوری چرخ نیلوفری را


آموزش زبان ++C : اعداد ممیز شناور (اعداد اعشاری)

آموزش زبان ++C : اعداد ممیز شناور (اعداد اعشاری)
نویسنده : امیر انصاری
متغیرهای از نوع Integer برای حساب کردن اعداد صحیح عالی هستند، اما بعضی وقتها، ما نیاز داریم تا اعداد خیلی بزرگ را ذخیره کنیم، یا اعداد دارای بخش کسری را ذخیره نماییم. یک متغیر از نوع floating point می تواند یک عدد حقیقی (real number) مانند 4320.0 یا 3.33- و یا 0.01226 را در خودش ذخیره کند. بخش floating که به معنای شناور می باشد، به این مفهوم است که در حقیقت بخش اعشاری این نوع متغیرها شناور هستند و می توانند به تعداد مختلف رقم اعشار داشته باشند. مثلاً دارای 1 رقم اعشار، 2 رقم اعشار، 3 رقم اعشار و ... باشند.

سیستم یکپارچۀ سازمانی راهکار



سه نوع مختلف از نوع داده floating point وجود دارد :

  • float
  • double
  • long double

دقیقاً مشابه نوع داده Integer در نوع داده floating point ، زبان ++C اندازه این انواع داده ها را نیز مشخص نکرده است. در معماری های مدرن، نوع داده floating point معمولاً از استاندارد IEEE که در این فرمت 754 باینری می باشد، پشتیبانی می گردد. در این فرمت، یک نوع داده float دارای 4 بایت، نوع داده double دارای 8 بایت و نوع داده long double نیز معمولاً دارای 8 بایت و در برخی موارد دارای 12 بایت یا 16 بایت نیز می باشد.

نوع داده floating point همواره دارای علامت (signed) می باشد (می تواند اعداد مثبت و منفی را در خودش نگهدارد).

آموزش زبان ++C : اعداد ممیز شناور (اعداد اعشاری)
در مثال زیر چند متغیر از نوع floating point معرفی شده اند :

float fValue;
double dValue;
long double dValue2;

وقتی که برای نوع داده floating point لیترال استفاده می کنیم، یک قرارداد وجود دارد که همیشه حداقل یک رقم اعشار را در آن بگنجانیم. این رقم اعشار کمک می کند تا بین مقادیر Integer و مقادیر floating point تمایز وجود داشته باشد.

int x(5); // 5 means integer
double y(5.0); // 5.0 is a floating point literal (no suffix means double type by default)
float z(5.0f); // 5.0 is a floating point literal, f suffix means float type

توجه داشته باشید که به صورت پیش فرض لیترال های از نوع floating point به نوع داده double تفسیر می شوند. قرار دادن یک پسوند f منجر می شود تا آن لیترال به جای double به نوع داده float تفسیر شود.

نماد علمی (Scientific notation)


اینکه متغیرهای از نوع داده floating point چگونه اطلاعات را در خودشان ذخیره می کنند، فراتر از محدوده این دوره آموزشی می باشد، اما بسیار شبیه چگونگی نوشتن اعداد با نماد علمی (Scientific notation) می باشد. نماد علمی (Scientific notation) یک روش معمول خلاصه نویسی در مورد اعداد طولانی می باشد. در نگاه اول نماد علمی (Scientific notation) عجیب و غریب به نظر می رسد، اما اگر درک درستی از نماد علمی (Scientific notation) داشته باشید، به شما کمک می کند تا درک کنید که اعداد اعشاری شناور (floating point) چگونه کار می کنند، و مهمتر از آن محدودیت آنها چه می باشد.

اعداد در نماد علمی (Scientific notation) شکل زیر را دارند :
آموزش زبان ++C : اعداد ممیز شناور (اعداد اعشاری)
برای مثال در نماد علمی زیر :

آموزش زبان ++C : اعداد ممیز شناور (اعداد اعشاری)
قسمت 1.2 ضریب علمی (significand) می باشد. و عدد 4 توان (exponent) می باشد. این عدد به عدد 12,000 ارزیابی می گردد.

بنا به قرارداد، اعداد در نماد علمی (Scientific notation) با یک رقم اعشار نوشته می شوند و بقیه عدد بعد از آن می آید.

جرم زمین را در نظر بگیرید. اگر بخواهیم جرم زمین را در مبنای دهدهی (decimal) بنویسیم میشود 5973600000000000000000000 کیلوگرم. این یک عدد بسیار بزرگ است، حتی خیلی بزرگتر از آن است که در یک متغیر Integer با اندازه 8 بایت نیز بتواند ذخیره گردد. همچنین خواندن آن نیز خیلی سخت است (آیا 19 صفر دارد یا 20 صفر؟). اگر جرم زمین را بخواهیم با نماد علمی (Scientific notation) بنویسیم، خواهیم داشت :

آموزش زبان ++C : اعداد ممیز شناور (اعداد اعشاری)
همانطور که می بینید، خواندن نماد علمی آن بسیار ساده تر است. همچنین نماد علمی (Scientific notation) دارای مزیت افزوده ای است که طی آن می توانیم ساده تر دو عدد خیلی بزرگ یا دو عدد خیلی کوچک را با هم مقایسه کنیم. این کار با مقایسه توان (exponent) صورت می پذیرد.

از آنجایی که تایپ کردن توان (exponent) در زبان ++C کار سختی می باشد، ما از حرف 'e' و یا 'E' استفاده می کنیم. حرف e به معنای 10 به توان عدد بعد از آن می باشد. به عنوان مثال نماد علمی زیر :

آموزش زبان ++C : اعداد ممیز شناور (اعداد اعشاری)
در زبان ++C به صورت 1.2e4 نوشته می شود. به عنوان مثالی دیگر، نماد علمی زیر :

آموزش زبان ++C : اعداد ممیز شناور (اعداد اعشاری)
در زبان ++C به صورت 5.9736e24 نوشته می شود.

در مورد اعدادی که از 1 کوچکتر هستند، توان (exponent) آنها می تواند منفی باشد. به عنوان مثال عدد 5e-2 معادل نماد علمی زیر می باشد :

آموزش زبان ++C : اعداد ممیز شناور (اعداد اعشاری)
در واقع ما با استفاده از نماد علمی (Scientific notation) می توانیم مقادیری را به متغیرهای از نوع floating point منتسب کنیم.

double d1(5000.0);
double d2(5e3); // another way to assign 5000

double d3(0.05);
double d4(5e-2); // another way to assign 0.05

روش تبدیل اعداد به نماد علمی (Scientific notation)


از روش زیر برای تبدیل اعداد به نماد علمی استفاده کنید :

  • توان (exponent) شما از صفر آغاز می شود.
  • ممیز را به سمت چپ ببرید به نحوی که تنها یک رقم غیر از صفر در سمت چپ و قبل از ممیز قرار بگیرد.
    • هر بار که ممیز را یک رقم به سمت چپ منتقل می کنید منجر می شود تا توان شما 1 واحد افزایش پیدا کند.
    • هر بار که ممیز را یک رقم به سمت راست منتقل می کنید منجر می شود تا توان شما 1 واحد کاهش پیدا کند.
  • صفرهای قبل از عدد را حذف کنید.
  • صفرهای بعد از عدد را نیز حذف کنید. صفرهای بعد از عدد را تنها در صورتی حذف کنید که عدد اصلی شما دارای ممیز نباشد.

برای درک بهتر به مثالهای زیر توجه کنید :

عدد اصلی : 42030
از آخرین رقم در سمت راست شروع کرده و ممیز را 4 رقم به سمت چپ می بریم : 4.2030e4
هیچ صفری قبل از عدد برای حذف کردن نداریم : 4.2030e4
یک صفر در آخرین رقم سمت راست داریم که حذفش می کنیم : 4.203e4

عدد اصلی : 0.0078900
ممیز را سه رقم به سمت راست می بریم تا اولین رقم قبل از ممیز (7) غیر از صفر باشد : 0007.8900e-3
صفرهای قبل از عدد را از بین می بریم : 7.8900e-3
از آنجا که عدد اصلی ما دارای ممیز می باشد، صفرهای سمت راست را حذف نمی کنیم : 7.8900e-3

عدد اصلی : 600.410
ممیز را دو رقم به سمت چپ می بریم : 6.00410e2
صفر قبل از عدد نداریم که بخواهیم حذفش کنیم : 6.00410e2
از آنجا که عدد اصلی ما دارای ممیز می باشد، صفرهای سمت راست را حذف نمی کنیم : 6.00410e2

مهمترین چیزی که باید درک کنید این می باشد که : ارقامی که در قسمت ضریب علمی (significand) می باشند، در واقع قسمت ضریب علمی (significand) قسمت قبل از e می باشد، ارقام ضریب علمی (significand) نامیده می شوند. عدد ارقام ضریب علمی (significand) می توانند دقت یک عدد را تعیین کنند. هر چقدر تعداد ارقام موجود در قسمت ارقام ضریب علمی (significand) بیشتر باشد، عدد شما دقیق تر می باشد.

دقت اعداد و حذف صفرهای سمت راست بعد از ممیز


موردی را در نظر بگیرید که از دو دستیار آزمایشگاه خواسته ایم تا سیبی را وزن کنند. یک از آنها بر می گردد و می گوید که وزن سیب 87 گرم بوده است. دستیار دیگر نیز می گوید که وزن سیب 87.000 گرم بوده است. فرض کنیم که ما می دانیم وزن آن سیب بین 86.50 و 87.49 گرم می باشد. در اینجا ممکن است که ترازو و یا دستیار ارقام را به نزدیکترین عدد گرد (رند) کرده باشند تا در واحد گرم پاسخ ما را بدهند. حالا اگر بخواهیم این وزن کشی را دقیق تر انجام بدهیم ممکن است با محدوده 86.9950 و 87.0049 گرم مواجه شویم، که تغییر پذیری کمتری دارد.

بنابراین در نماد علمی (Scientific notation)، ما ترجیح می دهیم تا صفرهای بعد از اعشار را نگهداری کرده و حذف نکنیم، چرا که آن صفرها می توانند اطلاعات مفیدی را در مورد دقت عدد مربوطه به ما ارائه بدهند.

با این حال در ++C ، ارقام 87 و 87.000 دقیقاً یکسان تلقی می شوند، و کامپایلر برای هر دوی آنها مقدار یکسانی را ذخیره می کند. در نتیجه، از نظر فنی ما دلیل منطقی برای نگهداری صفرهای بعد از ممیز و در سمت راست نداریم، پس چرا این صفرها را حذف نمی کنیم. دلیل علمی عدم حذف این صفرها این می باشد که کدهای نوشته شده توسط ما مستند سازی گردند تا دقت محاسبه را از روی کد بتوانیم بدانیم.

دقت و محدوده


کسر 1/3 را در نظر بگیرید. عدد دهدهی (decimal) معادل این کسر، عدد ...0.333333333333 می باشد که می تواند تا بی نهایت ادامه پیدا کند. یک عدد که می تواند تا بی نهایت ادامه پیدا کند، نیاز به بی نهایت حافظه برای ذخیره سازی اش می باشد، اما ما به طور معمول فقط 4 یا 8 بایت برای ذخیره سازی آن در اختیار داریم. نوع داده floating point تنها می تواند تعداد مشخصی از ارقام اعشار را نگهداری کند و بقیه آن از بین خواهد رفت. دقت یک متغیر از نوع floating point تعیین می کند که چند رقم اعشار را می توان در آن متغیر ذخیره کرد تا داده های کمتری از بین بروند.

وقتی با استفاده از std::cout یک عدد floating point را چاپ می کنید، در std::cout به صورت پیش فرض تا 6 رقم اعشار برای آن عدد در نظر گرفته می شود و از این رو بقیه ارقام اعشاری آن نادیده گرفته خواهد شد.

برنامه زیر این موضوع را به شما نشان می دهد :

#include "iostream"
int main()
{
float f;
f = 9.87654321f; // f suffix means this number should be treated as a float
std::cout << f << std::endl;
f = 987.654321f;
std::cout << f << std::endl;
f = 987654.321f;
std::cout << f << std::endl;
f = 9876543.21f;
std::cout << f << std::endl;
f = 0.0000987654321f;
std::cout << f << std::endl;
return 0;
}

خروجی این برنامه به شکل زیر می باشد :

9.87654
987.654
987654
9.87654e+006
9.87654e-005

توجه کنید که هر کدام از این اعداد تنها 6 رقم اعشار دارند.

همچنین توجه کنید که cout در برخی جاها از نماد علمی (Scientific notation) برای نمایش اعداد استفاده کرده است. بسته به کامپایلر شما، در قسمت توان تعداد ارقام حداقلی خاصی نمایش داده می شود. نترسید، 9.87654e+006 همان 9.87654e6 می باشد، تنها تعدادی 0 اضافی قبل از آن قرار داده شده است. تعداد ارقام حداقلی توان در مشخصات کامپایلر نمایش داده می شود، در کامپایلر ویژوال استودیو حداقل 3 رقم بعد از توان وجود دارد، در برخی کامپایلرها 2 رقم بعد از توان وجود دارد که مبتنی بر استاندارد C99 می باشد.

با این حال، ما می توانیم با استفاده از تابع std::setprecision که در هدر iomanip قرار دارد، این مقدار حداقلی را تغییر بدهیم.

#include "iostream"
#include "iomanip" // for std::setprecision()
int main()
{
std::cout << std::setprecision(16); // show 16 digits
float f = 3.33333333333333333333333333333333333333f;
std::cout << f << std::endl;
double d = 3.3333333333333333333333333333333333333;
std::cout << d << std::endl;
return 0;
}

خروجی این برنامه :

3.333333253860474
3.333333333333334

از آنجا که ما تعداد ارقام اعشار را به 16 تنظیم کردیم، هر کدام از اعداد بالا با 16 رقم اعشار نمایش داده می شوند. اما همانطور که خودتان هم می بینید، تعداد ارقام اعشار دقیقاً 16 رقم نمی باشند!

تعداد ارقام اعشار بک متغیر از نوع floating point هم به اندازه (size) متغیر بستگی دارد (مقادیر floats نسبت به مقادیر double دقت کمتری دارند) و هم به مقدار خاصی که ذخیره می شود نیز بستگی دارد (برخی مقادیر نسبت به مقادیر دیگر دقت بیشتری دارند). مقادیر float بین 6 تا 9 رقم اعشار دارند، در بیشتر مقادیر float حداقل 7 رقم ارزشمند اعشار وجود دارد (برای همین است که برخی از مقادیر اعشاری بعد از پاسخ ما دور انداخته شده اند). مقادیر Double بین 15 و 18 رقم اعشار دارند، و اغلب مقادیر Double حداقل 16 رقم اعشار دارند. مقادیر long double حداقل دارای 15 یا 18 و یا 33 رقم اعشار می باشند که این بستگی به تعداد بایت هایی که آن متغیر اشغال کرده است، دارد.

مسائل مربوط به دقت اعشار، تنها بر روی اعداد کسری تاثیر نمی گذارند، این مسئله بر روی هر عددی که تعداد ارقام ارزشمند زیادی باشد تاثیر خواهد گذارد. بیایید یک عدد بزرگ را در نظر بگیریم :

#include "iostream"
#include "iomanip" // for std::setprecision()

int main()
{
float f(123456789.0f); // f has 10 significant digits
std::cout << std::setprecision(9); // to show 9 digits in f
std::cout << f << std::endl;
return 0;
}

خروجی این برنامه :

123456792

عدد 123456792 بزرگتر از عدد 123456789 می باشد. عدد 123456789.0 ده رقم ارزشمند دارد، اما مقادیر float معمولاً می توانند تا 7 رقم اعشار را در خود نگهدارند. ما بخشی از دقت عدد را از دست می دهیم!

در نتیجه، یک چیزی که در هنگام استفاده از اعداد floating point باید در موردش احتیاط کنیم اینست که در مورد ارقامی که دقت بیشتری نیاز دارند مراقب باشیم.

استاندارد IEEE 754 را در نظر بگیرید :

آموزش زبان ++C : اعداد ممیز شناور (اعداد اعشاری)
ممکن است در جدول بالا کمی عجیب به نظر برسد که اعداد floating point با اندازه 12 بایتی محدوده یکسانی با اعداد floating point با اندازه 16 بایتی دارند. دلیل این مساله اینست که تعداد ارقامی که به توان اختصاص داده می شود در هر دوی آنها یکسان می باشد، با این حال، ارقام 16 بایتی دقت خیلی بیشتری دارند.

قانون : نوع داده double را بر نوع داده float ترجیح بدهید، مگر اینکه واقعاً حافظه برای شما یک چالش باشد، چرا که کمبود دقت در نوع float اغلب منجر به اشتباه می شود.

خطاهای گرد کردن (Rounding errors)


یکی از دلایلی که اعداد floating point می توانند مشکل ساز باشند، ناشی از اینست که تفاوت های غیر قابل تشخیص بین مقادیر باینری (آنطور که داده ها ذخیره می شوند) و مقادیر دهدهی (آنطور که ما فکر می کنیم) وجود دارد. عدد کسری 1/10 را در نظر بگیرید. در مبنای دهدهی (decimal) این عدد به سادگی به صورت 0.1 نمایش داده می شود، و ما به عدد 0.1 به عنوان یک عدد ساده فکر می کنیم. با این حال در مبنای باینری (binary)، عدد 0.1 توسط دنباله نامتناهی زیر نمایش داده می شود :

0.00011001100110011…

به همین دلیل، هنگامی که عدد 0.1 را به یک مقدار floating point نسبت می دهیم، ما دچار مشکل دقت اعشار می گردیم.

شما می توانید تاثیر این مشکل را در مثال زیر ببینید :

#include "iostream"
#include "iomanip" // for std::setprecision()

int main()
{
double d(0.1);
std::cout << d << std::endl; // use default cout precision of 6
std::cout << std::setprecision(17);
std::cout << d << std::endl;
return 0;
}

خروجی این برنامه :

0.1
0.10000000000000001

در خط اول خروجی، تابع cout مقدار 0.1 را همانطور که ما انتظار داریم چاپ می کند.

در خط بعدی از خروجی برنامه، هنگامی که از تابع cout می خواهیم تا 17 رقم اعشار دقت را بالا ببرد، می بینیم که مقدار موجود در متغیر d کاملاً 0.1 نمی باشد! این مساله به این دلیل است که نوع داده double با توجه به محدودیت حافظه اش مجبور است تا تقریب را در جایی کوتاه کند، که نتیجه اش این می شود که عدد خروجی دقیقاً 0.1 نخواهد بود. این مساله خطای گرد کردن (Rounding error) نامیده می شود.

خطاهای گرد کردن (Rounding errors) می توانند عواقب غیر منتظره ای داشته باشند :

#include "iostream"
#include "iomanip" // for std::setprecision()

int main()
{
std::cout << std::setprecision(17);

double d1(1.0);
std::cout << d1 << std::endl;

double d2(0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1); // should equal 1.0
std::cout << d2 << std::endl;
}

1
0.99999999999999989

اگر چه ممکن است که انتظار داشته باشیم مقادیر d1 و d2 با هم برابر باشند، اما می بینیم که اینطور نمی شود. اگر در داخل یک شرط برنامه، متغیرهای d1 و d2 را با هم مقایسه کنیم، برنامه آنطور که انتظار داریم عمل نخواهد کرد. در این مورد در آموزشهای آینده و در مبحث Relational operators بیشتر بحث خواهیم کرد.

آخرین نکته در مورد خطاهای گرد کردن (Rounding errors) : عملگرهای ریاضی (مانند جمع و ضرب) تمایل دارند تا خطاهای گرد کردن را افزایش بدهند. بنابراین، اگر چه مقدار 0.1 در هنگامی که 17 رقم ارزشمند داشته باشیم دچار خطای گرد کردن می شود، هنگامی که 0.1 را ده بار با هم جمع کنیم، خطای گرد کردن به رقم ارزشمند 16 ام می رسد.

NaN و Inf


دو دسته بندی خاص از اعداد floating point وجود دارد. اولی Inf است که به معنای بی نهایت (infinity) می باشد. Inf می تواند مثبت یا منفی باشد. دومین دسته بندی NaN است که به معنای "نه یک عدد" (Not a Number) می باشد. خود NaN ها سه نوع مختلف دارند که البته در مورد انواع آن بحث نخواهیم کرد.

برنامه زیر این موارد را به شما نشان می دهد :

#include "iostream"

int main()
{
double zero = 0.0;
double posinf = 5.0 / zero; // positive infinity
std::cout << posinf << std::endl;

double neginf = -5.0 / zero; // negative infinity
std::cout << neginf << std::endl;

double nan = zero / zero; // not a number (mathematically invalid)
std::cout << nan << std::endl;

return 0;
}

خروجی این برنامه در کامپایلر ویژوال استودیو 2012 به شکل زیر می باشد :

1.#INF
-1.#INF
-1.#IND

INF مختصر شده infinity (بی نهایت) می باشد، و IND مختصر شده indeterminate (نامشخص) می باشد. توجه داشته باشید که نتایج چاپ شده در مورد Inf و NaN با توجه به پلتفرم مورد استفاده ممکن است متفاوت باشند، بنابراین آنچه شما در کامپیوترتان می بینید ممکن است متفاوت از این خروجی باشد.

نتیجه گیری


برای خلاصه کردن این بخش، دو چیز را در مورد اعداد floating point باید بخاطر داشته باشید :

  1. اعداد floating point برای ذخیره سازی اعداد خیلی بزرگ و یا اعداد خیلی کوچک، شامل آنهایی که بخش کسری دارند، عالی می باشد.
  2. اعداد floating point اغلب دارای خطاهای کوچکی در مورد گرد کردن می باشند، حتی اگر عدد شما دارای ارقام اعشار کمتری نسبت به دقت لحاظ شده در برنامه باشد. خیلی از وقتها، به این خطاهای گرد کردن بی توجهی می شود، چرا که این خطاها خیلی کوچک هستند و همینطور خیلی وقتها آنها در خروجی بریده شده و مشخص نمی گردند. در نتیجه، مقایسه اعداد از نوع floating point با یکدیگر ممکن است نتایج مورد نظر شما را در پی نداشته باشد. همینطور انجام عملیات ریاضی بر روی این اعداد ممکن است خطاهای گرد کردن (Rounding errors) را رشد بدهند.


آموزش قبلی : آموزش زبان ++C : متغیرهای integer با عرض ثابت و مبحث متغیرهای بدون علامت

آموزش بعدی : آموزش زبان ++C : مقادیر بولی و مقدمه ای بر بیانیه if



نمایش دیدگاه ها (3 دیدگاه)

دیدگاه خود را ثبت کنید:

انتخاب تصویر ویرایش حذف
توجه! حداکثر حجم مجاز برای تصویر 500 کیلوبایت می باشد.