زبان برنامه نویسی سی/تبدیل و جایگزینی دادهها
در این موضوع میپردازیم به تبدیل انواع دادهها به یکدیگر که از دو طریق غیرعلنی ( خودکار ) و علنی انجام می شود و اینکه طی تبدیل انواع دادهها چه رخ میدهد و نتیجه چه خواهد شد و همچنین ایجاد اسامی جایگزین برای انوع دادهها
ایفای نقش داده Type Casting
ویرایشتبدیل داده (type conversion) در دانش برنامهنویسی که در زبان سی با مفهومی به نام ایفای نقش داده (type casting) وجود دارد ، به رفتار کردن کامپایلر با نوعی از داده به صورت نوع دیگری از داده میگویند که به صورت علنی و غیر علنی میباشد که در ادامه به آنها میپردازیم و در انتهای مبحث به رویدادی که برای دادهها طی تبدیل آنها رخ میدهد مطابق با استاندارد که ناقص است و بیشتر دلبهخواه عرضهکننده کامپایلر میباشد میپردازیم ( استاندارد C تمام تبدیلها را تعریف نکرده است ؛ اما بیشتر نویسندههای کامپایلرهای مطرح از روش یکسانی استفاده میکنند )
۱ - غیر علنی : در حالت غیر علنی ( implicit ) شما برنامهای را مینویسید که عملوندهای عملگرها ( عملگری مثل عملگر جمع که عملیات جمع بر روی عملوندها را انجام میدهد و عملوند به دادهای میگویند که عملگر بر روی آنها عمل محاسباتی ، منطقی یا ... را انجام میدهد ) از نوع دادههای متفاوتی هستند یا دادهای را از نوع دادهای دیگر جز پارامتر تابع ، به عنوان آرگومان به آن تابع می فرستید ( پاس میدهید ) . مثلاً یک نوع داده از نوع صحیح را با یک نوع داده از نوع اعشاری ، جمع میبندید و سپس آن را در یک نوع داده کاراکتری ذخیره میکنید یا یک نوع داده صحیح را به عنوان آرگومان به یک تابعی می فرستید که پارامتر متناظر با آن آرگومان ، در تابع به صورت کاراکتر تعریف شده است ( مثلاً اولین پارامتر تابع به صورت پارامتر کاراکتر تعریف شده است ؛ اما شما موقع فراخوانی تابع ، به تابع یک نوع داده صحیح را می فرستید )
در اینجا کامپایلر به صورت خودکار دادهها را به یک دیگر تبدیل میکند (implicit) که تا جای ممکن با یکدیگر سازگار باشند یا مطابق با دستور شما باشد ( اگر امکان پذیر باشد )
مثال :
#include<stdio.h>
int main()
{
int result = 0;
float pi = 3.14159265;
double neper = 2.718281828459045;
result = pi + neper;
printf("%d\n", result);
return result;
}
خروجی این برنامه ، عدد 5 خواهد بود . در انتهای همین مبحث ، پیش از بحث typedef به صورت کامل خواهیم نوشت که کامپایلر چگونه انواع دادهها را به یکدیگر تبدیل میکند . اما به صورت ساده در مثال بالا ، داده اعشاری را که عدد پی ( پای ) را در خود ذخیره کرده بود ، با داده اعشاری با دقت دوبرابر که عدد نپر ( همان اویلر ) را در خود ذخیره کرده بود جمع کردیم و نتیجه را داخل یک نوع داده صحیح گذاشتیم که داده صحیح خروجی تابع است و در خروجی خطدستوری نمایش داده میشود . به زبان ساده ؛ نوع داده صحیح نمیتواند قسمتهای اعشاری در خود ذخیره کند ؛ پس آن را نادیده میگیرد . این یکی از کاربردهای تبدیل دادهها در قسمت انواع صحیح و اعشاری است که قسمت اعشاری را حذف کنیم
۱ - علنی : در نقش دادن علنی ( explicit ) که از این پس با اصطلاح انگلیسی آن یعنی کست کردن از آن یاد میکنیم به صورت صریح ( explicit ) به کامپایلر دستور میدهیم تا با داده ما به عنوان نوع داده دیگری رفتار کند . در این نوع کست کردن که به دستور ما انجام میشود ؛ باید شناسه دادهای که میخواهیم با آن به عنوان نوع دادهای دیگر رفتار شود در مقابل یک جفت پرانتز باز و بسته بنویسیم و در داخل جفت پرانتز خود نوع دادهای که قرار است شناسه ما نقش آن را ایفا کند قرار میدهیم . مثلاً char c را داریم و میخواهیم از آن به عنوان یک عدد استفاده کنیم ؛ پس مینویسیم :
(int)c;
در زبان C بسیاری از دادهها توسط کامپایلر قابل تبدیل به یکدیگر نیستند اما برخی را میتوان به یکدیگر تبدیل کرد ؛ مثل مغیرهای پایه ؛ که ما با روش کست کردن علنی به صورت صریح ( explicit ) به کامپایلر دستور میدهیم تا نوع داده مورد نظر ما را به دادهای که طی کست کردن تعیین نمودهایم تبدیل کند . البته در کامپایلرهای جدید که با استاندارد C11 یا C18 سازگار هستند تمام تبدیلها به صورت خودکار قابل انجام است ؛ اما اگر از کامپایلر قدیمی استفاده میکنید یا کامپایلر شما نمیتواند تبدیلهای مجاز را خودکار انجام دهد و به شما خطایی مشابه با casting expected میدهد ؛ داده خود را کست کنید . عواقب کست کردن و تبدیل داده به عهده ماست . بنابراین باید دقت کنید که وقتی میخواهید تبدیلی را انجام بدهید ؛ پس از تبدیل ، دادهای از دست نرود یا زائدهای به وجود نیاید و هر خطایی که در برنامه رخ بدهد از دید بسیاری از کامپایلرها پنهان خواهد ماند ( به شما اخطاری داده نمیشود ) و این خود شما هستید که پس از اجرای برنامه و دیدن نقص در اجرای برنامه خود باید پیدا کنید که کجا کد خطایی نوشتهاید ؛ این مسئله در بخشهای بسیاری از برنامه امکان پذیر است که یکی از آن بخشها همین مبحث کست کردن میباشد . مثلاً یک اشارهگر کاراکتری را با نوع اشارهگر صحیح عوض میکنید و این باعث میشود تا زمان اشاره کردن به فضای بزرگتری از حافظه اشاره شود و دادههای زائد در نتیجه شما لحاظ شوند . در انتهای همین مبحث میپردازیم به اینکه طی تبدیل دادهها چه رخدادی به وقوع میپیوندد تا بهتر تصمیم بگیرید که در کجای برنامه میتوانید داده خود را کست کنید
نتیجه کست کردن علنی و غیر علنی در تبدیل دادهها به یکدیگر در برخی کامپایلرها متفاوت است ؛ برای کسب اطمینان به راهنمای کامپایلر خود مراجعه کنید . برای همین ، زمانی که میخواهید داده شما به دلخواه شما ( و متفاوت با آنچه که کامپایلر میتواند خودکار تبدیل کند و میکند ) تبدیل شود ؛ میتوانید دادههای خود را کست کنید . برای این کار نوع داده خود را داخل یک جفت پرانتز باز و بسته بنویسید
مثال :
#include<stdio.h>
int main()
{
int a = 255;
printf("%c\n", (char)a);
return 0;
};
}
در مثال بالا از یک متغیر از نوع صحیح به عنوان یک متغیر از نوع کاراکتر استفاده نمودیم تا مقدار ASCII عدد ۲۵۵ را نمایش دهد ( کاراکترهای اضافه شده ASCII ) . کامپایلری مثل GCC که با آخرین استانداردهای زبان C نوشته میشود میتواند در صورت کست نکردن شما هم آن را تبدیل کند ؛ اما اگر نتیجه شما به غیر از کاراکتر � بود یا کامپایلر شما خطایی مشابه با آنچه که قبلتر نیز اشاره کردیم ؛ یعنی casting expected یا خطایی با واژههای types و incompatible به این معنی که نوع دادهها با یکدیگر ناسازگار هستند ؛ در خروجی به شما نشان داد ؛ حتماً آن را علنی ، کست کنید . به مثال زیر دقت کنید :
#include<stdio.h>
int main()
{
double p = 0.1;
printf("%lf, %d\n", (1/p), (int)(1/p));
return 0;
}
در این مثال ما حتماً باید آرگومان دوم تابع printf را کست کنیم ؛ در صورت کامپایل قطعه کد بالا و اجرای آن شما با 10.000000 و 10 رو به رو میشوید اما اگر تابع را به این شکل فراخوانی کنیم :
printf("%lf, %d\n", (1/p), (1/p));
با اعلام هشدار کامپایلر نظیر : warning: format ‘%d’ expects argument of type ‘int’, but argument 3 has type ‘double’ رو به رو خواهید شد ( در کامپایلر جیسیسی و کلنگ ) در کامپایلرهای دیگر نیز با هشداری نظیر همین اعلام هشدار مواجه خواهید شد . که در واقع به شما میگوید آرگومانهای تابع printf منتظر یک مقدار اعشاری با دقت دو برابر و یک صحیح است ( مطابق با کد ) اما در متغیرهای نسبت داده شده ، متغیر دوم از نوع اعشاری است که با کست کردن آن مشکل برطرف میشود ؛ دقت کنید که در صورت دادن هشدار نیز برنامه کامپایل شده اما مقداری که برای آرگومان دوم برمیگرداند چیزی شبیه : 988039304 است که در واقع طی تبدیل ناصحیح متغیر اعشاری در حافظه موقت به مقدار داده از نوع صحیح حاصل شده است
در مثال بعدی میبینید که کست کردن نابهجا نیز عواقبی در پی دارد :
#include <stdio.h>
int main()
{
char c1 = 'h', *c2 = &c1;
int **iptr = &c2;
printf("%d\n", **iptr);
return 0;
}
با کامپایل کردن قطعه کد بالا ، با هشداری نظیر : warning: initialization from incompatible pointer type [-Wincompatible-pointer-types] رو به رو خواهید شد ؛ اما برنامه کامپایل میشود . در صورت اجرای برنامه با مقادیر آشغال و زباله ( garbage value ) مواجه خواهید شد ( مقادیر پرت و پلا که نسبت و ارتباطی با آنچه متغیر اشاره شده یعنی c1 در خود جای داده ندارند و در هر بار فراخوانی با مقدار دیگری رو به رو میشوید ) . در قطعه کد بالا ، یک کاراکتر ( c1 ) مقدار و موجودی h را در خود ذخیر کرده است و یک کیلوبایت حجم دارد ؛ کاراکتر دوم اشارهگر است و به کاراکتر c1 اشاره میکند و باز هم یک بایت حجم دارد و در نهایت iptr یک اشارهگر به اشارهگر از نوع صحیح است که در سیستمهای ۶۴ بیتی ، ۴ بایت است و در سیستمهای ۳۲ بیتی قدیمی ، ۲ بایت ؛ بنابراین با اشاره iptr به نوع ناسازگار هم تبدیل مشکل پیدا میکند و هم اینکه فضایی که به آن اشاره میکند از فضایی که باید اشاره کند بزرگتر است و کامپایلر به همین منظور به شما هشدار میدهد
به مثال زیر دقت کنید :
#include <stdio.h>
int main()
{
int i = 5684;
printf("%d\n", (char)i);
return 0;
}
در متغیر صحیح i مقدار 5684 را ذخیره کردهایم ؛ در فراخوانی تابع printf آرگومان را عددی و از نوع صحیح تعیین کردهایم که البته متغیر i را به آن ارجاع دادهایم ؛ اما به کامپایلر دستور دادهایم تا در برنامه خروجی با متغیر i مثل کاراکتر رفتار کند و یک متغیر کاراکتری نهایتاً میتواند تا عدد ۲۵۵ را در خود ذخیره کند که در صورت کامپایل و اجرای این قطعه کد خواهید که بخش بزرگی از عدد ما از دست میرود و خروجی ما 52 خواهد بود ؛ در ادامه ، چگونگی تبدیل دادهها را خواهیم نوشت تا بهتر بتوانید تصمیم بگیرید که کجا مجاز هستید تا کست کنید و کجا مناسب است تا کست کنید و اگر اختلالی پیش آمد بدانید که به لحاظ تکنیکی در این تبدیل چه رخ داده است ( که به شما کمک میکند تا در صورت وجود مشکل در برنامهتان ، کدهای نوشته شده توسط شما که باعث ایجاد خطا شده را راحتتر پیدا کیند )
اشارهگرها را تنها میتوان به اشارهگر کست کرد ، طبق استاندارد کست کردن یک غیر اشارهگر به اشارهگر غیر مجاز نیست ، اما این کار علاوه بر بیهوده بودن میتواند باعث ایجاد خطا در برنامه شما شود ( بدین شکل که آدرس حافظه را به جای محتوای آن بازگرداند ) ، کست کردن اشارهگر به عنوان غیر اشارهگر نیز غیر مجاز نیست اما باعث ایجاد خطا میشود و داده کست شده آدرس حافظه را به جای مقدار مورد اشاره باز میگرداند . کست کردن به عنوان اشارهگر با قرار دادن دادهای که قرار است کست شود در داخل جفت پرانتز باز و بسته و یک علامت استریسک ( ستاره ) پس از نوشتن نوع داده انجام میشود و اگر بخواهید آن را غیر مستقیم کنید تا مقدار دادهای را که اشارهگر به آن اشاره نموده ، بازگرداند یک استریسک دیگر ، پیش از جفت پرانتز قرار میدهید
مثال :
#include<stdio.h>
int main()
{
long ln = 514228935;
long *lnptr = &ln;
printf("%d\n", *(int*)lnptr);
return 0;
}
در قطعه کد بالا یک متغیر از نوع صحیح بلند با نام ln مقدار 514228935 را در خود ذخیره میکند ، سپس یک متغیر اشارهگر از نوع صحیح بلند با نام lnptr به آن اشاره میکند . در انتها در تابع کتابخانهای printf آرگومان خود را از نوع صحیح معمولی تعیین میکنیم و به آن اشارهگر lnptr را که به عنوان اشارهگر صحیح معمولی کست کردهایم که آن را غیر مستقیم نیز نمودهایم تا مقدار اشاره شده را باز گرداند . در سیستمهای ۶۴ بیتی ، قطعه کد بالا بدون هیچ ایرادی کامپایل شده ، اجرا میشود و خروجی درست را نمایش میدهد ؛ اما از آنجایی که در سیستمهای ۳۲ بیتی قدیمی هر int فقط ۲ بایت است ، مقدار ذخیره شده در ln قابل ذخیره در صحیح معمولی نیست ؛ برای همین با اعداد دیگری مواجه خواهید شد ! همچنین اگر مقدار خود را در همین قطعه کد بالا و در سیستم ۶۴ بیتی خودمان به 5142289356215 تغییر دهیم ، با اعداد و مقادیر دیگری رو به رو خواهیم شد و البته مقدار ذکر شده بزرگتر از ۳۲ بیت و ۴ بایت است ، بنابراین قابل ذخیره در داده بلند نیست . در کست کردن اشارهگرها نیز باید دقت کنید که اگر نوع داده کست شده ، کوچکتر از نوع داده اصلی باشد و مقداری که در آن ذخیره شده در نوع دادهای که میخواهیم کست کنیم جا نشود یا مقادیری از دست میروند یا با آدرسها و مقادیر اشتباهی و زباله رو به رو خواهیم شد . از سویی دیگر ، اگر نوع داده اصلی از نوع داده کست شده ، کوچکتر باشد ( یعنی نوع داده کست شده بزرگتر باشد ) اشارهگر به خانههایی دیگری اشاره خواهد کرد و باز با مقادیر زباله رو به رو خواهیم شد
اگر مبحث نوع داده پوچ را به یاد داشته باشید ، گفتیم که اشارهگر پوچ نمیتواند غیر مستقیم شود مگر آنکه کست شود ( نقش نوع داده دیگری که جز نوع پوچ است را ایفا کند ) . اشارهگرهای پوچ درست مثل اشارهگرهای معمولی کست میشوند و قوانینی که در مورد کست کردن اشارهگرها گفته شد در مورد اشارهگرهای پوچ ( void pointers ) نیز صادق است . سعی کنید همواره اندازه نوع دادهای که اشارهگر پوچ به آن اشارهکرده با اندازه دادهای که قرار است کست شود ، برابر باشد تا نه فضای اضافی اشغال شود و یا خطایی ایجاد شود و نه مقداری از دست برود
مثال :
#include <stdio.h>
int main()
{
long l = 5894476523;
void *vptr = &l;
printf("%li\n", *(long*)vptr);
return 0;
}
در مثال بالا متغیری از نوع داده صحیح بلند با نام l با مقدار 5894476523 تعریف شده است . اشارهگری از نوع داده پوچ با نام vptr به آن اشاره کرده است و در نهایت در تابع printf آرگومانی از نوع صحیح بلند تعیین نموده و اشارهگر پوچ vptr را ضمن کست کردن به نوع داده اشارهگر صحیح بلند که غیر مستقیم نیز شده است به آن نسبت دادهایم
تابعها
هنوز به مبحث تایع نرسیدهایم اما با این حال چند بار در طول همین سه فصل به صورت مختصر نوشتهایم که تابع ، در زبان C دادهای است که دادههای دیگری را میتواند به عنوان پارامتر داخل جفت پرانتزی که در مقابل نام تابع قرار میگیرند تعریف کند تا بعد از فراخوانی و فرستاده شدن دادههایی که به عنوان آرگومان تعریف میشوند و به ترتیب پارامترهای تابع هستند روی آن آرگومانها ، درست هماطور که در تابع برای پارامترهایش تعریف شده ، پردازش انجام دهد که برای این پردازش از ماکروها ، تابعهای کتابخانهای ، دستورها و عملگرها استفاده میکنیم ( ضمن اینکه تابع میتواند هیچ پارامتری نداشته باشد که در نتیجه آرگومانی هم نمیتواند بپذیرد )
با این مقدمه ، میخواهیم به این مطلب نیز اشاره کنیم که تابعها خود و پارامترهایشان میتوانند به عنوان اشارهگر تعریف شوند تا تابع اشارهگر به تابع دیگری اشاره کند و یا پارامتر اشارهگر ، به تابع اشارهگر دیگری اشاره کند و موقع فراخوانی تابعمان ، که پارامتر اشارهگری دارد ، یک اشارهگر را با کمک عملگر آدرس به تابع خود نسبت دهیم . اما این کار از طریق عملی مشابه با کست کردن امکانپذیر است ؛ این کار کست کردن تابع نیست ، اما درست مشابه کست کردن انجام میشود
در تعریف تابع اشارهگر نوع داده تابع را مینویسم ، سپس داخل یک جفت پرانتز باز و بسته یک استریسک میگذاریم و نام تابع را مقابل آن مینویسیم سپس پرانتزهای باز و بسته پارامترهای تابع را مینویسم که در صورت نداشتن پارامتر در داخل پرانتزهای متناظر کلیدواژه void را مینویسیم . مثل :
int (*fn_ptr)(int a)
{
printf("%d\n", a);
return 0;
}
پارامتر اشارهگر همانند دادههای معمولی است ، مگر آنکه پارامتر خود را به عنوان یک تابع اشارهگر تعریف کنید تا یک تابع اشارهگر را به عنوان آرگومان به آن پاس بدهیم مثل :
void vfunc (void(*func)())
{
//some codes
return some value;
}
در الگوی بالا تابع vfunc که از نوع داده پوچ است ، پارامتری با نام func که تابع اشارهگر پوچ است را تعریف میکند که خود پارامتری ندارد و در داخل بلوک تابع vfunc مورد پردازش قرار میدهد و در هنگام فراخوانی تابع vfunc ، باید تابع اشارهگر پوچی را به آن با کمک عملگر آدرس ، پاس بدهیم
همچنین واضح است که میتوان تابع اشارهگری تعریف کرد که پارامترهایی از نوع تابع اشارهگر دارد ؛ مثل :
long (*funcl)(int (*ifn)(), void (*vfn)())
{
//some codes
return some value;
}
در مثال بالا funcl یک تابع از نوع صحیح بلند اشارهگر است که دو پارامتر دارد : یکی تابع صحیح اشارهگری با نام ifn که پارامتری ندارد و دیگری تابع پوچ اشارهگری با نام vfn که آن هم پارامتری ندارد
چگونگی تبدیل دادهها
ویرایشیک نکته را پیش از بررسی چگونگی تبدیل دادهها بگوئیم ؛ آن هم اینکه در صورتی که مشخص نکرده باشید که متغیر شما علامتدار است یا بدون علامت ، معمولاً توسط کامپایلر به صورت پیش فرض علامتدار در نظر گرفته می شود ( که در بحث متغیرها هم ذکر شده است ) اما اگر مقداری بزرگتر از آنچه که در متغیر علامتدار جا میشود ، ذخیره کنید ، بیشتر کامپایلرهای مطرح به صورت خودکار آن را بدون علامت در نظر میگیرند . گرچه طبق استاندارد ، شما موظف به تعیین کردن علامتدار بودن یا بدون علامت بودن تمام متغیرهای خود هستید ( که در مورد بدون علامتها ، در صورتی است که مقداری بزرگتر از آنچه که در علامتدار جا میشود را میخواهید ذخیره کنید )
اولین نحوه تبدیل که استاندارد است این است که اگر چند متغیر را با نوع دادههای مختلف با یکدیگر در تعامل بگیرید و به وسیله عملگرها آنها را مورد پردازش ریاضی یا منطقی قرار دهید ؛ متغیرهای شما همگی به نوع دادهای تبدیل میشوند که در بین متغیرهای شما بزرگترین نوع داده را دارد که طبیعتاً هیچ یک از مقادیر متغیرهای شما از دست نمیرود اما فضای اضافی و زائدی ایجاد میشود . دوم اینکه طبق استاندارد کامپایلر باید تمام تلاش خود را بکند تا به نحوی تبدیل انجام شود که مقدار و موجودی در داده نهایی ( نتیجه ) از دست نرود ( اما زمانی که امکانپذیر نباشد یا دستور خود شما باشد ، مطمئناً امکان از دست رفتن مقادیر وجود دارد ) . استاندارد برای اینکه اگر یک علامتدار و بدون علامت را مثلاً جمع ببندیم و نوع متغیر نتیجه را معین نکنیم ، تعریفی ندارد که باید نتیجه علامتدار باشد یا بدون علامت . اما اکثر کامپایلرها در صورتی که امکان حفظ علامت در نتیجه باشد ، نتیجه را علامتدار میکنند ( اگر نوع داده خروجی و نتیجه را unsigned معین کنیم ، کامپایلر علامت را نادیده میگیرد اما اگر خروجی و نتیجه مثلاً int باشد و ما یک علامتدار و یک بدون علامت را از هم کسر کنیم ، کامپایلر در ایفای نقش غیر علنی ، علامت را در خروجی منظور میکند و int ما را به عنوان signed int در نظر میگیرد )
در تبدیل متغیرهای غیر اعشاری ( مثل int و long ) به متغیرهای اعشاری ( مثل float و double ) ، یک بخش اعشاری درست به همان اندازهای که بزرگترین نوع اعشاری را دارد ( در قسمت اعشار خود ) ، به همه آنها اضافه خواهد شد . در تبدیل انواع دادههای اعشاری به دادههای صحیح ، کامپایلرهای مدرن بخش اعشاری را نادیده میگیرند اما برخی کامپایلرها بر اساس گرد کردن اعداد اعشاری کار میکنند که معمولاً بر اساس گرد کردن در ریاضی است ( مثلاً کوچکتر از ۰٫۵ به عدد کوچکتر گرد میشود و بزرگتر از ۰٫۵ به عدد بزرگتر ) و برخی کامپایلرها نیز در تبدیل دادههای اعشاری به غیر اعشاری ، بین کست کردن ایفای نقش غیرعلنی و علنی تفاوت قائل میشوند ؛ برای دانستن اینکه کامپایلر شما چگونه عمل میکند به راهنمای آن مراجعه کنید . در دادههایی که همگی اعشاری هستند ( تبدیل انواع اعشاری با دقت معمولی یا دوبرابر یا long double ) ، تبدیلها به نحوی انجام میشود که قسمت اعشار از بین نرود ولی اگر مقدار ما ، در داده نتیجه جا نشود ، اول از قسمت اعشاری مقادیر حذف میشوند ( که با گرد کردن نیز همراه است ) . ضمن اینکه در فراخوانی تابع ، در صورت همخوانی نداشتن نوع داده آرگومان با پارامتر ، قوانین پیشین ، اعمال میگردند
در پایان باید بدانید که در تبدیل دادهها ، اگر قرار بر از دست رفتن دادهها باشد ، در سیستمهای بیگاندیَن که دادهها از خانههای ابتدایی به سمت خانههای انتهایی حافظه شروع به پر شدن میکنند ، دادهها از قسمت انتهایی پاک شده و حذف میشوند ؛ اما در سیستمهای لیتلاندیَن دادهها از سمت انتهایی خافظه شروع به پر شدن میکنند از سمت ابتدایی ، دادهها و مقادیر نادیده گرفته شده و یا حذف میشوند
برای درک بهتر مطلب ، در مثال آخرِ کست کردن ، مقدار 5684 ما تبدیل شد به 52 ، اما چرا ؟ در یک کامپیوتر خانگی که معمولاً هم از نوع لیتلاندین است عدد 5684 در حافظه موقت به صورت 0001011000110100 ذخیره میشود که اگر ۸ بیت آن را ( معادل ۱ بایت ) حذف کرده و در یک متغیر کاراکتر ذخیره کنیم ( که البته همان طور که گفته شد از قسمت ابتدایی ، دادهها شروع به حذف شدن میکنند ) عدد 00110100 باقی خواهد ماند که در مبنای دودویی ( باینری ) است و در مبنای دسیمال یا همان دهدهی میشود 52 . همین مسئله در مورد مقادیر اعشاری نیز صادق است اما مقادیر اعشاری در حافظه ، مطابق با استانداد انستیتوی جهانی مهندسین برق و الکترونیک به صورت :
-(2 - 2^-23) × 2^127 to (2 - 2^-23) × 2^127
ذخیره میشوند ( که این مقدار در مورد دقت معمولی است ، نه دو برابر ) که تقریبا معادل ۳٫۴۰۲۸۲۳− در ۱۰ به توان ۳۸ تا ۳٫۴۰۲۸۲۳ در ۱۰ به توان ۳۸ میشود :
-3.4E+38 to +3.4E+38
برای کسب اطلاعات بیشتر به صفحه زیر مراجعه کنید :
https://en.wikipedia.org/wiki/Single-precision_floating-point_format
توضیح : این مبحث مربوط به نوشتن کامپایلر میشود که مبتنی بر علم ریاضیات بوده و در علوم برق و الکترونیک نیز مورد استفاده قرار میگیرد ؛ اما به صورت مختصر در اینجا به نحوه ذخیره شدن و پردازش اعداد اعشاری در زبانهای سطح پائینی مثل C و به صورت سختافزاری ، اشاره میکنیم . عدد اعشاری با دقت معمولی ، ۳۲ بیت است . بیت آخر که ذخیره شده ( بیت اول در خانههای حافظه موقت ) علامتِ عدد را مشخص میکند . عدد ۱- به توان عددی که در بیت اول قرار دارد میرسد که این عدد یا ۱ است که ۱- به توان ۱ میشود ۱- و علامت ، منفی است یا میشود ۰ که در نتیجه ۱- به توان ۰ میشود ، ۱ و علامت ، مثبت است ؛ پس ۱ نمایانگر منفی بودن عدد و ۰ نمایانگر مثبت بودن عدد میباشد . هشت بیت بعدی به صورت ۲ به توان آن عدد منهای ۱۲۷ میباشد و در مبنای دهدهی محاسبه میشود ؛ که از ۰۰۰۰۰۰۰۰ شروع میشود که به صورت قرار دادی نمایانگر عدد ۰ میباشد و اگر در قسمت اعشاری همه اعداد ۰ باشند ، عدد ، ۰ منظور میشود . اما اگر در قسمت اعشاری ( مابقی بیتها ) مقداری ذخیره شده باشد ، ۰۰۰۰۰۰۰۰ به عنوان ۱ منظور میشود که منهای ۱۲۷ میشود ۱۲۶- که در نهایت میشود ۱۲۶-۲ و مقداری که در ۸ بیتِ توان ، ذخیره میشود تا ۲۵۴ ادامه مییابد ( مقدار ۲۵۵ به صورت قرار دادی برای بینهایت ذخیره شده است که با توجه به علامت ، میتواند منفی بینهایت یا مثبت بینهایت باشد ) که ۲۵۴ منهای ۱۲۷ میشود ۱۲۷ و عدد ۲ به توان ۱۲۷ میرسد . مقداری که از ۸ بیت توان با ۲ به توان آن عدد به دست آمد در ۲۳ بیت بعدی که قسمت اعشاری را ذخیره میکنند ضرب میشود . منتها قسمت اعشاری به صورت اعشار دودویی ( باینری ) ذخیره میشوند که در کامپیوتر با یک ۱ ، نیز جمع میشود . در دستگاه دودویی و در کامپیوتر که معمولاً لیتلاندین میباشد بیت بیست و سوم که شماره ۲۲ ـم میشود ؛ ۲ به توان ۱- میرسد و ضربدر عدد بیت بیست و سوم میشود و جمع میشود با ۲ به توان ۲- ضربدر عدد بیت بیست و دوم که میشود شماره ۲۱ و همین طور تا بیت اول که میشود شماره ۰ ادامه پیدا میکند . مثلاً ۰٫۰۱۱۱ که در مبنای دودویی میباشد ، در مبنای دهدهی خودمان میشود : ۰٫۴۳۷۵ ؛ ۲ به توان ۱- ضربدر ۰ میشود ۰ که جمع میشود با ۱ ضربدر ۲ به توان ۲- که میشود ۰٫۲۵ و جمع میشود با ۱ ضربدر ۲ به توان ۳- که میشود ۰٫۱۲۵ و جمع میشود با ۱ ضربدر ۲ به توان ۴- که میشود ۰٫۰۶۲۵ که در نهایت عدد ۰٫۴۳۷۵ به دست میآید . ضمن اینکه در استاندارد ۷۵۴ انستیتوی جهانی مهندسین برق و الکترونیک ، استثناهایی هم در نظر گرفته شده است که با مطالعه لینک بالا و یا پیوند فایل پیدیاف استاندارد IEEE 754 میتوانید از تمام مطالب استاندارد آگاه شوید
در تبدیلهای اعشاری بزرگتر به کوجکتر ( مثلاً double به float ) قسمت غیراعشاری از دست نمیرود و ابتدا از قسمت اعشاری و آن هم از قسمت انتهای آن ، مقادیر نادیده گرفته شده و پاک میشوند که البته با گرد شدن نیز همراه است و طی تبدیل ، اگر قرار بر از دست رفتن مقادیری باشد ، هم رقمهای اعشاری کوچکتر حذف میشوند و هم عدد اعشاری گرد میشود
typedef
ویرایشکلیدواژه typedef برای تعیین یک یا چند شناسه به عنوان جایگزین ، جهت اعلان و تعریف یک نوع داده که خود میتواند از کلیدواژههایی تشکیل شده باشد استفاده میشود . ضمن اینکه شما میتوانید هر چند بار که خواستید برای یک یا چند نوع داده ، جایگزین یا جایگزینهایی تعریف کنید . مثلاً شما میتوانید به جای اعلان و یا تعریف یک نوع داده صحیح ، به جای کلیدواژه int ، یک اسم مثل integer از روی int بسازید و از آن جهت اعلان و تعریف دادههای صحیح استفاده کنید . دقت کنید که با این کار کلیدواژه int منسوخ نمیشود ( یعنی همچنان میتوانید در برنامه از کلیدواژه int برای اعلان و تعریف دادههای عددی صحیح خود استفاده کنید ) . شکل کلی ایجاد جایگزینها برای انوع داده توسط typedef به شکل زیر است :
typedef data-type new-name
از typedef جهت خواناتر شدن کدهای نوشته شده استفاده میشود که این خواناتر شدن در دو محور معنی مییابد : ۱− کوتاهتر نوشته میشود ۲− به کدهای نوشته شده معنای قابل درکی میبخشد یا اینکه برای قابل حمل بودن و راحتتر شدن ویرایش برنامه استفاده میشود ؛ مثلاً فرض کنید برای دادههایی از برنامه خود متناسب با کاربردشان ، نوعی را چندین بار ایجاد کردهاید که کوچکتر یا بزرگتر از مقداری که به آن نسبت داده شده است و در هر صورت نامتناسب با مقداری که میگیرد ( یا قرار است در کامپیوتر و سیستم عامل دیگری بگیرد ) است ، در چنین شرایطی برنامهنویس دیگر که قرار است کد شما را اصلاح کند با مشکلی بزرگ مواجه میشود و محبور است یک به یک تمام دادهها را عوض کند ، اما اگر از typedef استفاده کنید با یک تغییر در نوع داده مشکل حل میشود
مثال :
#include<stdio.h>
int main(void)
{
typedef unsigned long int big_number;
big_number lightspeed = 299792458;
printf("Light\'s Speed is %li m\\s\n", lightspeed);
return 0;
}
در مثال بالا نام جدید big_number را به عنوان شناسهای تعیین نمودهایم که جایگزین کلیدواژههای unsigned long int به عنوان نوع داده صحیح بلند بدون علامت شده است ؛ سپس با نوشتن big_number داده lightspeed را تعریف نمودهایم که از نوع داده صحیح بلند بدون علامت است که مقدار سرعت نور را در خود ذخیره کرده است و شناسه داده را به عنوان آرگومان به تابع printf پاس دادهایم . دقت کنید که ما باید شناسه داده را پاس بدهیم و نه big_number را که نوع داده را تعیین میکند
ما میتوانیم همزمان چند جایگزین را برای تعیین یک نوع داده مشخص کنیم مثل :
#include<stdio.h>
int main()
{
typedef unsigned char little_number, pixel_value;
little_number i = 96;
pixel_value red = 64;
printf("%d,%d\n", i, red);
return 0;
}
در مثال بالا دو جایگزین برای تعریف unsigned char تعیین نموده ایم و با کمک یکی از آنها یعنی little_number کاراکتر بدون علامت i را تعریف کرده و با کمک دیگری یعنی pixel_vlaue متغیر کاراکتری بدون علامت red را تعریف نموده و سپس هر دو را در خروجی رابط خطدستوری چاپ کردهایم
دقت کنید : ۱ − در سیستمهایی که مبتنی بر استاندارد پازیکس POSIX نوشته میشوند که شامل همه سیستم عاملهای مبتنی بر یونیکس یا شبهیونیکسها میشود پسوند t_ در کتابخانهها برای بسیاری از پردازشها ، دادهها و تابعها از پیش تعریف شده است ؛ بنابراین مؤکداً توصیه میکنیم که اگر میخواهید شناسه جایگزینی بسازید ( درست مثل شناسههای دیگر که پیشتر نیز گفتیم ) از اسامیای که t_ را در نام خود دارند ، استفاده نکنید و از آنها پرهیز کنید ، چرا که بسیاری از خطاها در برنامهنویسی در سیستمهای سازگار با پازیکس به همین خاطر رخ میدهند ۲ − typedef و شناسههایی که ایجاد کردهاید مشمول قانون حوزه دید در زبان C میشوند ( بنابراین شناسهای که با typedef داخل یک بلوک تعریف شده ، داخل بلوکی دیگر قابل استفاده نیست و از دید آن بلوک پنهان میماند )
مثال :
#include<stdio.h>
typedef unsigned char uchar;
void foo(void);
int main()
{
uchar ch = 'a';
printf("ch inside main() : %c\n", ch);
foo();
return 0;
}
void foo(void)
{
uchar ch = 'z';
printf("ch inside foo() : %c\n", ch);
}
در مثال بالا شناسه uchar در تابع mian مقدار a را دارد و در تابع foo مقدار z را . ابتدا یک شناسه جایگزین با نام uchar برای یک نوع داده کاراکتری بدون علامت تعریف کردیم که سراسری است . سپس داخل تابع main مقدار و موجودی a را در داخل کاراکتر بدون علامت ch که با کمک شناسه جایگزین uchar تعریف شده است قرار دادیم و سپس در بلوک تابع foo بار دیگر کاراکتر بدون علامت ch را تعریف نمودیم ( باز هم با استفاده از شناسه جایگزین uchar ) که مقدار و موجودی z را داخل آن قرار دادیم . در هر دو تابع مقدار ch را با کمک تابع کتابخانهای printf در خروجی رابط خطدستوری چاپ نمودهایم
ما در زبان C مجاز هستیم تا اشارهگرها را نیز با کمک typedef تعریف و جایگزین نمائیم مثلاً typedef int* iptr شناسه iptr را به عنوان اعلان کننده اشارهگری از نوع صحیح تعریف میکند . بنابراین با الگوی نوشته شده اگر در داخل برنامه خود بنویسیم : iptr *a شناسه a اشارهگر به اشارهگری از نوع صحیح خواهد بود و iptr b[10] شناسه b را به عنوان آرایه ۱۰ عنصری از اشارهگر از نوع صحیح تعریف میکند
از کلیدواژه typedef میتوان برای تعریف ساختمانها و اجتماعها هم استفاده کرد . برای این کار هم میتوانید با استفاده از typedef و نوشتن کلیدواژه struct یا union و با استفاده از نام برچسب ساختمان یا اجتماع ( به ترتیب ) و سپس با نام جایگزینِ شناسه ، نمونه ایجاد کنید و هم میتوانید هنگام تعریف ساختمان یا اجتماع ، پیش از کلیدواژه struct یا union کلیدواژه typedef را بنویسید سپس برچسب ساختمان یا اجتماع را نوشته و سپس بعد از بلوک و پیش از سمیکالن ( نقطه ویرگول ) ِ ساختمان یا اجتماع نام یا نامهای جایگزین را بنویسید و سپس با نام جایگزین ، نمونه یا نمونههایی ایجاد کنید
مثال :
#include<stdio.h>
int main(void)
{
struct mystr
{
int a;
long l;
char c;
};
typedef struct mystr mysamp;
mysamp firststr;
printf("%d, %d, %c\n", firststr.a = 5, firststr.l = 9588479, firststr.c = 'H');
return 0;
}
#include<stdio.h>
int main(void)
{
typedef struct mystr
{
int a;
long l;
char c;
}sample, mysamp;
mysamp firststr;
printf("%d, %d, %c\n", firststr.a = 5, firststr.l = 9588479, firststr.c = 'H');
return 0;
}
در هر دو مثال برای ساختمان mystr با کمک نام جایگزین mysamp نمونه firststr را ایجاد کرده و در تابع printf به اعضای ساختمان مقدار داده و آنها را نمایش دادهایم ولی دو با شکل متفاوت و در مثال دوم دو جایگزین برای ساختمان mystr ایجاد کردهایم که از اولی یعنی sample استفادهای نشده است اما مجازیم تا از آن نیز برای ایجاد نمونههایی از ساختمان mystr استفاده کنیم
تا اینجا فقط کاربرد معنا بخشیدن typedef را نشان دادیم ؛ اما فرض کنید میخواهید نمونههای اشارهگری از یک ساختمان بسازید ، با کمک typedef کار شما راحتتر و کد نوشته شده شما خواناتر خواهد شد
مثلاً اگر یک ساختمان با نام node داشته باشید و بخواهید از آن نمونههای اشارهگر زیادی بسازید یک بار مینویسید typedef node* nodeptr و سپس با کمک نام جایگزین nodeptr چندین نمونه اشارهگر با نامهای معمولی startptr و endptr و curptr و prevptr و errptr و refptr با نوشتن در مقابل nodeptr ایجاد کنید . از این ترفند در مورد ساخت لیستهای پیوندی نیز استفاده میشود
در مورد تابعهای اشارهگر نیز typedef به وفور به کار میرود ، مخصوصاً که بخواهیم تابعهای اشارهگری تعریف کنیم که پارامترهای آنها تابعهای اشارهگر دیگری باشند . مثال :
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
typedef int (*function)(int a, int b);
int call_function(function p, int a, int b) {
return p(a, b);
}
int main(void) {
int res;
res = call_function(&add, 10, 5);
printf("add: %d\n", res);
res = call_function(&sub, 10, 5);
printf("sub: %d\n", res);
return 0;
}
نکته : در مثال بالا میتوان به جای اینکه تابع function را با typedef اشارهگر تعریف کرد ، آن را یک تابع غیراشارهگر تعریف نموده و سپس در تابع call_function پارامتر p را اشارهگر تعریف نمود تا تابع function را به عنوان آرگومان بپذیرد
دقت کنید : در جایگزینی تابع به واسطه typedef نامی که برای تابع خود انتخاب میکنید ، همان نام جایگزین برای تابع است
توضیح : در مثال بالا یک تابع با نام add تعریف نمودیم که دو پارامتر a و b را میپذیرد و آنها را با هم جمع میکند و حاصلجمع را به عنوان خروجی عرضه میکند . تابع sub دو پارامتر a و b را میگیرد و b را از a کم میکند و نتیجه را عرضه میکند . تابعی از نوع صحیح اشارهگر با نام function که با همین نام جایگزین نیز شده است ( به خاطر وجود کلیدواژه typedef ) با دو پارامتر a و b اعلان شده و سپس تابع call_function با پارامتر اول که یک function است که خود یک صحیح اشارهگر میباشد تعریف شده و همچنین دو پارامتر دیگر یعنی a و b را میپذیرد و به عنوان خروجی تابعی را از نوع صحیح اشارهگر که روی دو پارامتر a و b پردازش کرده را به عنوان خروجی عرضه میکند . در نهایت در تابع main یک متغیر صحیح با نام res به عنوان محصول اعلان شده که بار اول call_function را فرا میخواند و مقدار آن را ذخیره میکند که به عنوان آرگومان ، تابع add و اعداد 10 و 5 را به ترتیب به آن پاس دادهایم و سپس عدد حاصل را در خروجی رابط خطدستوری پرینت میکند . در بار دوم متغیر res برابر با خروجی فراخوانی تابع call_function با آرگومانهای تابع sub و اعداد 10 و 5 ( به ترتیب ) است و این مقدار در خروجی رابط خطدستوری با کمک تابع printf چاپ میشود
میتوان تابعهای اشارهگری که تابعهای اشارهگری نیز دارند را هم با کمک typedef خواناتر نمود . مثلاً در کرنل FreeBSD نوشته شده بود :
void (*signal(int sig, void (*func)(int)))(int);
که به جای آن میتوان نوشت :
typedef void (*SignalHandler)(int signum);
SignalHandler signal(int signum, SignalHandler handler);
که بسیار خواناتر است . SignalHandler نام تابع و نام جایگزین تابع پوچ اشارهگر است که یک پارامتر از نوع صحیح دارد و به کمک آن تابع signal را تعریف نمودهایم تا تابعی از نوع پوچ اشارهگر باشد که یک پارامتر صحیح دارد و یک پارامتر دیگر با نام handler که تابعی از نوع پوچ اشارهگر است که یک پارامتر صحیح نیز دارد
به مثال زیر دقت کنید :
struct dim3 {
int x;
int y;
int z;
};
struct dim3 * (*fnptr)( struct dim3 *, int );
که میتوان آن را به صورت زیر نیز نوشت :
typedef struct {
int x;
int y;
int z;
} DIM3;
typedef DIM3 * DIM3FN( DIM3 *, int );
DIM3FN * fnptr;
که بسیار خلاصهتر و خواناتر است . هر دو قطعه کد یک کار را انجام میدهند اما کد دوم خلاصهتر و خواناتر است . در توضیح قطعه کد دوم : ابتدا با کمک typedef یک ساختمان را که سه عضو صحیح دارد با نام جایگزین DIM3 ایجاد کردیم . در خط پایانی fnptr از نوع DIM3FN است که در خط قبلتر توسط typedef به عنوان تابع با نام DIM3FN و با نام جایگزین همسان ( یعنی DIM3FN ) تعریف شده بود که از نوع ساختمان اشارهگر است و یک پارامتر DIM3 که یک ساختمان اشارهگر است و یک پارامتر دیگر که صحیح است تعریف نموده و در هنگام فراخوانی میپذیرد ( چون هنوز به بحث تابع نرسیدهایم بیش از این مثالهای پیچیده نمیزنیم و توضیحات ما در سطحی که مثال میزنیم برای آشنایی با تابع و توضیح در مورد بحثی که در آن هستیم میباشند )
نکته : با کمک اشارهگرها و ساختمانها و البته با استفاده از typedef نوعی داده میتوان ایجاد کرد که به آن داده نامرئی میگویند و دسترسی به اعضای آن امکانپذیر نیست مگر با استفاده از تابعهای دسترس . پس از به اتمام رسیدن بحثهای زبان C به بیان ترفندهایی که در زبان C به کار میروند میپردازیم
تفاوت typedef و دستور مستقیم define
typedef ، تنها شناسهای را که برای آن معین نمودهایم با نوع داده تعریف شده جایگزین میکند در حالی که دستور مستقیم define هر چیزی را حتی مقدار و موجودیای را که با define تعریف کرده باشیم جایگزین میکند . define میتواند حتی کلیدواژهها و یا تابعهای کتابخانهای را هم جایگزین کند ( که البته کاری ریسکپذیر است و باید از آن اجتناب شود ) typedef مشمول قانون حوزه دید میشود اما دستور مستقیم define# حوزهای ندارد و هر جا که تعریفی برای آن مقرر شده باشد پس از دستور ، هر تعریف شده را ، با مقدارش جایگزین میکند که البته یکی از تفاوتهای عمده typedef و define این است که define نمیتواند نوع دادهای را تعریف کند و فقط هر جا که کاراکتر یا رشته تعریف شده برای خود ، در سر راهش قرار بگیرد با مقداری که برای آن تعریف شده جایگزین میکند مثلاً typedef با کد typedef float * fp هر fp را به اشارهگر اعشاری تبدیل میکند و اگر بنویسیم : fp a, b, c معادل خواهد بود با : float *a, *b, *c در حالی که اگر از دستور مستقیم define استفاده کنیم و بنویسیم :
#define fp float *
و سپس در یک خط کد fp a, b, c را بنویسیم ، معادل خواهد بود با : float *a, b, c که فقط متغیر اولی ، یعنی a را به عنوان متغیر اعشاری اشارهگر تعریف میکند و نه b و c را و این دو متغیر اعشاری غیر اشارهگر اعلان میشوند