زبان برنامه نویسی سی/نوع داده پوچ

پوچ

Void

نوع داده پوچ در زبان C و خانواده آن دو کاربرد و معنا دارد که یکی داده‌ای است که اندازه‌ای ندارد که درمورد اشاره‌گرهای پوچ به کار می‌رود و دیگری به معنی توخالی می‌باشد که درمورد تابع‌های پوچ به کار می‌رود . مورد اول یعنی اشاره‌گرهای پوچ که به آن‌ها اشاره‌گرهای همگانی نیز می‌گوئیم ، اشاره‌گرها‌یی هستند که اندازه‌ای ندارند و به عبارت واضح‌تر می‌توانند هر اندازه‌ای را به خود بگیرند و به همین دلیل است که به آن‌ها همگانی می‌گوئیم . یعنی شما به هر داده‌ای که با اشاره‌گر پوچ اشاره کنید ، اندازه اشاره‌گر شما به اندازه داده‌ای که به آن اشاره نموده‌اید در خواهد آمد . شما با اشاره‌گرهای پوچ می‌توانید به هر داده‌ای اشاره کنید . فقط باید دقت کنید که استاندارد C98/99 اشاره دادن اشاره‌گر پوچ را به تابع‌های اشاره‌گر تعریف نکرده بود و از همین روی کامپایلرها به غیر از محیط‌های پازیکس از آن پشتیبانی نمی‌کردند ولی این عمل در استاندارد C11 گنجانده شده است و شما اگر قصد استفاده از چنین کدی را دارید ، ابندا اطمینان کسب کنید که کامپایلر شما با استاندارد سال ۲۰۱۱ سازمان ایزو برای زبان سی ، سازگار است یا خیر . ضمن اینکه سیستم عامل‌هایی که از محیط پنجره‌ای X و رابط برنامه‌نویسی POSIX ( پازیکس ) استفاده می‌کردند ( که عمدتاً شامل سیستم عامل‌های شبه‌یونیکس می‌شد و می‌شود ) به علت نحوه تعریف محیط پنجره‌ای خود از آن استفاده کرده و می‌کنند برای همین در محیط این سیستم عامل‌ها برنامه‌نویسان از این ترفند نیز استفاده می‌کردند و محدودیتی نداشتند که عمدتاً شامل سیستم عامل‌های لینوکس Linux ، مک‌اواس macOS ، سولاریس Solaris و سیستم عامل‌های سری بی‌اس‌دی BSD می‌شود . ابتدا مثالی از نحوه کاربرد اشاره‌گر پوچ می‌زنیم تا به کاربرد بعدی آن ( تابع ) برسیم :

char c = 'p';
long g = 15248732251;

void *v = &c;
v = &g;

در مثال بالا اشاره‌گر پوچ v ابتدا به کاراکتر c اشاره می‌کند که یک بایت حجم دارد ، اما در اشاره بعدی به داده بلند g اشاره می‌کند و حجم آن به ۴ بایت تغییر می‌کند

دقت کنید : طبق استاندارد C ما مجاز به غیرمستقیم کردن اشاره‌گر پوچ نیستیم . اما با ترفندی که از روش نقش ایفا کردن استفاده می‌کند می‌توانیم به داده‌هایی که اشاره‌گر پوچ به آن‌ها اشاره می‌کند دسترسی پیدا کنیم ( موضوع نقش ایفا کردن داده‌ها در آخرین موضوع از این فصل بررسی خواهد شد ) پس هر جایی از برنامه که کدی بنویسیم که جهت دسترسی به مقدار و موجودی داده اشاره شده توسط اشاره‌گر پوچ ، بخواهیم اشاره‌گر پوچ را غیر مستقیم کنیم باید به آن نقش بدهیم . اما نکته‌ای که در مورد نقش دادن به اشاره‌گر پوچ برای غیر مستقیم کردن آن وجود دارد اینست که داده‌ای که قرار است نقش ایفا کند نیز باید به عنوان اشاره‌گر تعیین گردد . بنابراین از دو علامت استریسک ( * ) باید استفاده کنیم . مثال :

int i = 5;
void *v = &i;
printf("%d", *(int *)v);

در مثال بالا نوع داده v که اشاره‌گر پوچ است که نقش یک صحیح ( int ) را ایفا کرده است . چرا که در متن برنامه خود می‌خواهیم توسط تابع کتابخانه‌ای printf به محتوای متغیز i دسترسی پیدا کنیم . ضمن اینکه باید دقت کنید که در هنگام نقش دادن به داده‌ها ، نقشی را بدهید که مقدار داده از دست نرود و همچنین فضای اضافی اشغال نشود . یعنی اگر در اینجا اشاره‌گر پوچ به یک داده بلند ( long ) اشاره می کرد و به آن نقش صحیح می‌دادیم ، مقدار خروجی دستخوش تغییر می‌شد و بخشی از مقدار آن ممکن بود از دست برود ، از سویی دیگر اگر نوع نقشی را که قرار بود ایفا کند ، نوع داده خیلی بلند ( long long ) تعریف می‌کردیم مقداری از دست نمی‌رفت اما باعث می‌شد تا فضای بیشتری از حافظه اشغال شود . پس مسلماً نوع داده بلند ( long ) برای آن کاملاً مناسب می‌بود ( نه مقدار آن از دست می‌رفت و نه فضای اضافی اشغال می‌کرد )

اما کاربرد اشاره‌گرهای پوچ چیست ؟ کاربرد عمده اشاره‌گرهای پوچ در ایجاد تابع‌های همگانی است . اگر پارامتر یا پارامترهای یک تابع از نوع اشاره‌گر پوچ تعریف شوند ، تابع مذکور می‌تواند هر نوع داده‌ای را به عنوان آرگومان دریافت کند ( یعنی تفاوتی نمی‌کند یک کاراکتر باشد یا صحیح یا بلند یا مثبت یا منفی ) برخلاف تابع‌های معمولی که فقط نوع پارامتر یا پارامترهایی که برایشان تعریف شده را می‌پذیرند ، تابع‌های همگانی که پارامترهای آن‌ها از نوع اشاره‌گر پوچ تعریف می‌شوند می‌توانند هر نوع داده‌ای که به آن‌ها داده شد را بدون در نظر گرفتن نوع آن ، به عنوان آرگومان دریافت کرده و با توجه به کدهای داخل تابع ، آن‌ها را مورد پردازش قرار دهند

استفاده از عملیات منطقی و یا ریاضی بر روی اشاره‌گرهای پوچ در استاندارد C تعریف نشده است . بنابراین پاره‌ای از کامپایلرها از آن پشتیبانی نمی‌کنند و به شما این اجازه را نمی‌دهند . اما برخی از کامپایلرها مثل GCC این اجازه را به شما می‌دهند . اما دقت کنید که در زمان نوشتن کدهای مربوطه ، باید به اشاره‌گر پوچ خود نقش بدهید

استفاده دیگر اشاره‌گرهای پوچ ، ایجاد اشاره‌گرهایی به آدرس خاصی از حافظه موقت می‌باشد که در مبحث پیشین آن را بیان کردیم . اما استفاده از نوع داده پوچ به جای نوع داده دیگری مثل int بهتر است . چرا که هر مقدار کوچک یا بزرگی که بخواهیم می‌توانیم در آن ذخیره کنیم ( بدون از دست رفتن اطلاعات و یا اشغال فضای بیهوده ) . همچنین به کمک اشاره‌گرهای پوچ می‌توان تمام آدرس‌های حافظه موقت را مشاهده نمود و حتی مقدار آنها را تغییر داد ؛ برای دریافت آدرس حافظه موقتِ یک داده‌ی دیگر که توسط اشاره‌گر پوچ مورد اشاره قرار گرفته است نیازی به ایفای نقش نیست ، اما باید آرگومان متناسب با آن را با فرمت کننده p% تعریف کنید . مثال :

#include<stdio.h>

int main(void)
{
	long l = 5684997;
	void* vptr = &l;
	printf("%p\n", vptr);
	return 0;
}

در مثال بالا vptr آدرس متغیر l بر روی حافظه موقت را در مبنای شانزده شانزدهی نشان خواهد داد

نکته : مقدار اشاره‌گرهای پوچ ، همواره در مبنای شانزده و به صورت هگزادسیمال ( شانزده‌شانزدهی ) باز گردانده می‌شوند

کاربرد دیگر نوع داده پوچ در مبحث تابع‌ها می‌باشد . از آنجایی که هنوز به مبحث تابع نرسیده‌ایم ( اما با این حال تا بدینجا چند بار به آن اشاره نموده‌ایم ) نمی‌توانیم مفصلاً راجع به تابع بحث کنیم . اما تابع‌ها در C داده‌هایی هستند از نوع متغیرها که پارامترهایی برای آن‌ها می‌توان تعریف نمود تا با توجه به بلوک تابع ، پس از ارجاع داده‌هایی به تابع ، روی داده‌های فرستاده شده پردازش‌های تعریف شده توسط ما را انجام داده و مقداری را به عنوان خروجی ارائه دهد . برخی تابع‌ها پارامتر ندارند و تنها مجموعه‌ای از عملیات را داخل خود انجام می‌دهند ( طبق تعریف ما ) و برای همین برای سهولت در اجرای چند عمل به صورت متوالی از تابع خود استفاده می‌کنیم . ضمن اینکه تعداد زیادی تابع ، در فایل‌های سرآیند استاندارد C موجود هستند که ارتباط برنامه‌نویس با سخت افزار ، سیستم عامل و فایل‌ها را فراهم کرده و بسیاری از کارهای دیگر را تسهیل می‌کنند ( این تابع‌ها تابع‌های کتابخانه‌ای نام دارند و در داخل فایل‌های سرآیند قرار دارند که با ضمیمه کردن فایل سرآیند مربوطه - با دستور مستقیم include - می‌توانیم از آن‌ها استفاده کنیم )

تابعی که با نوع داده void تعریف می‌شود نمی‌تواند خروجی‌ای داشته باشد . یعنی نوع داده تابع ما پوچ است . از همین روی استفاده از دستور return در تابع پوچ به غیر از حالت استاندارد : ;return غیر مجاز است . یعنی اگر بنویسیم ;return a * b کامپایلر از ما خطا خواهد گرفت . از آنجایی که نوشتن استاندارد کد همیشه توصیه می‌شود در پایان تابع پوچ خود بنویسید : ;return ؛ اما تابع پوچ به چه دردی می‌خورد ؟ تابعی که با یک نوع داده پایه مثل int تعریف شده باشد پس از کامپایل و اجرای برنامه مقداری از فضای حافظه را اشغال خواهد نمود و تعریف شده است تا هر زمان که لازم شد فراخوانی شود . اما تابع پوچ زمانی که نوبتش فرا می‌رسد و در برنامه فراخوانده می‌شود اجرا شده و سپس پایان یافته و از بین می‌رود ( یعنی فضایی که برای آن تخصیص یافته بود پاک می‌شود ) و روال اجرای برنامه باز می‌گردد به ادامه برنامه‌ای که ما نوشته ایم . برای مثال تابع main جزء اصلی برنامه ما حساب می‌شود و طبق استاندارد هر برنامه‌ای که به زبان C نوشته می‌شود باید یک تابع داشته باشد با نام main که کنترل تمام برنامه را بر عهده دارد و ما از طریق تابع main است که تابع‌های دیگر را فراخوانی می‌کنیم . حال اگر شما بخواهید برنامه شما کاری را انجام دهد و سپس بسته شده و کاملاً حافظه اشغال شده را باز گرداند می‌توانید از نوع داده پوچ برای تعریف آن استفاده کنید . در غیر این صورت ( یعنی اگر برنامه شما باید بافی بماند و طبق دستور خاصی خارج شود ) هرگز آن را پوچ تعریف نکنید

همان طور که گفته شدتابع‌های پوچ مقداری را باز نمی‌گردانند . این جزء خاصیت تابع void است . و اگر در قسمتی ، تابع پوچی تعریف کنید که فراخوانی شود ( در تابع mian یا در تابعی که در تابع mian فراخوانی شده ) پس از پایان آن اجرای برنامه را به مابفی کدها می‌سپارد . بنابراین مثلاً اگر بخواهید تابع ماشین حسابی را بنویسید هرگز نباید از نوع داده void برای تعریف تابع خود استفاده کنید . چرا که قرار است ماشین حساب شما عددی را به عنوان خروجی صادر کند ، که اگر پوچ باشد امکان پذیر نیست

طبق استاندارد C ، تابعی که پارامتر ندارد و ما نمی‌خواهیم پارامتری برای آن در نظر بگیریم و تعریف کنیم باید در پرانتزهای تابع مورد نظر کلیدواژه void را ذکر کنیم . یعنی مثلاً می‌نویسیم :
int hello(void)
سپس ما مجاز نخواهیم بود تا داده‌ای را به عنوان آرگومان به تابع ارجاع دهیم تا مورد پردازش قرار بگیرد ؛ چرا که تابع بدون پارامتر تعریف شده و نمی‌تواند آرگومانی را پذیرا باشد . گرچه اکثر کامپایلرها در صورت ننوشتن کلیدواژه void و جا انداختن آن منظور برنامه‌نویس را ، تابع بدون پارامتر در نظر می‌گیرند ( یعنی از شما خطا نخواهند گرفت که پارامتری تعریف نکرده‌اید ) اما بهتر است شما همواره به صورت استاندارد کد خود را بنویسید یعنی پرانتز باز و بسته خود را () خالی نگذارید ( همان طور که هم‌اینک نوشتیم ) و حتماً کلیدواژه void را ذکر کنید مثل :


#include<stdio.h>
int hello(void)
{
	printf("hello world!\n");
	return 0;
}

int main(void)
{
	hello();
	
	return 0;
}

می‌پذیریم که در صورت آشنایی تازه شما با زبان C بخش‌هایی از کد بالا نامفهوم است . اما اگر کتاب را تا انتها دنبال کنید به خوبی تمام مفاهیم را درک خواهید کرد اما به صورت خلاصه توضیح می‌دهیم که یک تابع از نوع داده صحیح تعریف کردیم ( hello ) که پارامتری نمی پذیرد و در بلوک آن ( که قسمت ضروری تابع است ) از تابع کتابخانه‌ای printf استفاده نمودیم که در فایل سرآیند stdio.h قرار دارد و وظیفه آن چاپ فرمت شده متن است print formatted و به عنوان آرگومان ، می‌توان هر متنی را به آن فرستاد و همچنین از کاراکتر‌های خروج و البته فرمت کننده می‌توان در آن استفاده نمود که ما در اینجا از n\ استفاده کردیم . بعدها خواهید دید که اگر بخواهید در متن خود بخش‌هایی را که معلوم نیست چه چیز خواهند بود در تابع printf بگنجانید ، می‌توانید متغیر‌هایی را تعریف کنید و به تابع بفرستید که در این راه باید از کاراکترهای فرمت کننده کمک بگیرید ( اگر به یاد داشته باشید کاراکتر نوشته شده توسط ما - n\ - در قطعه کد بالا خط جدیدی را در برنامه ایجاد می‌کند ) ضمن اینکه برنامه بالا فقط در محیط خط‌دستوری قابل اجراست و زبان C به خودی خود رابط کاربری گرافیکی ندارد ( که البته ما در این کتاب به رابط‌های موجود برای زبان C اشاره کرده و منابع آن را ضمیمه می‌کنیم ) در پایان ، دستور return می‌گوید که تابع چه چیزی باید باز گرداند که ما نوشته ایم 0 ! یعنی مقدار 0 و خالی را باز می‌گرداند که در ادامه کدهای بعدی اجرا شوند اما در تابع main که جزء زبان سی بوده و اجرا و کنترل تمام برنامه توسط تابع main انجام می‌شود و حتی تابع‌های دیگر را باید از طریق تابع main فراخوانی کنیم ، پس از فراخوانی تابع hello در تابع main با دستور ;return 0 تابع به اتمام می‌رسد و فضایی که برای اجرای برنامه در حافظه موقت اشغال شده بود ، آزاد می‌گردد ( دقت کنید که تابع اصلی برنامه یعنی main نمی‌تواند خروجی دیگری را بازگرداند و با نوشتن ;return 0 تابع با موفقیت به پایان می‌رسد و اگر با موفقیت به پایان نرسد می‌نویسیم ;return -1 یا ;return 1 که طبق استاندارد تعریفی برای آن در نظر گرفته نشده است و هر کامپایلر به اختیار خود 0 را طبق استاندارد پایان موفق تلقی می‌کند و هر مقدار غیر 0 را خطا و ناتمام )