PDA

View Full Version : گفتگو: بحث راجب سوالاتی حرفه ای در ++C,C



PC2st
سه شنبه 12 مرداد 1389, 12:17 عصر
این آزمون را برای این تدارک دیدم، تا افرادی که می‌خواهند، میزان آشنائی خود با زبان ‪C++‎‎‎‬ را تخمین بزنند... این یک مسابقه نیست، پس لطفاً برای سنجش اطلاعات خود، از گوگل استفاده نکنید :چشمک: (هر چند جواب بعضی از سوالها را شاید نتوانید از گوگل بیابید). این آزمون ۶ قسمت است و هر قسمت شامل یک یا چندین سوال است، این سوالها در رابطه با حل الگوریتم نیست، سوالها بطور خالص مربوط به زبان است:

۱) چگونه می‌توان یک آرایهٔ ارجاعی (reference array) داشت؟ کاربرد؟ چرا نمی‌توان ارجاعی به خود آرایه (reference to array) داشت؟
۲) یک template class چگونه در هر translation unit نمونه‌سازی می‌شود؟ این رفتار آنها چه مزیتها و ایرادهایی دارد؟
۳) ++C از چه نوع الگوی طراحی برای مدیریت حافظه استفاده می‌کند؟ چرا ++C زباله‌روب ندارد؟ چگونه یک مدیر حافظهٔ همراه با زباله‌روب پیاده‌سازی کنیم، در حالیکه بتوان از STL هم بطور مستقیم استفاده کرد؟
۴) استفاده از throw چه تاثیری بر روی performance دارد؟ استفاده از try...catch چطور؟
۵) چگونه می‌توان یک typedef از نوع توابع را ایجاد کرد؟ کاربرد؟ چرا نمی‌توان متغیری از این نوع را ایجاد کرد؟
۶) کلاس‌های والد مجازی (virtual base classes)، چگونه در سلسله مراتب ارث‌بری نمونه‌سازی می‌شوند (راهنمائی: منظور از نمونه‌سازی: صدازدن تابع سازنده است) (با چه ترتیبی و توسط چه کلاسی)؟

اگر افراد علاقه‌مند زیاد بود، جواب آنها را متعاقباً اعلام می‌کنم :لبخند::شیطان:. این سوالها را در حیطهٔ استاندارد زبان ++C جواب بدهید.

دوستان عزیز، اگر شما هم سوالهای خفنی (بهمراه جوابها) برای آزمون دارید، ممنون می‌شوم و خوب خواهد شد که در اینجا ذکر کنید.

tdkhakpur
سه شنبه 12 مرداد 1389, 14:05 عصر
همه سوالات یک سطر جواب داره و بستگی به سطح استفاده از زبان سی دارد سوالات شما برای زمانی دارای جواب هست که در سطح پایین برنامه بنویسید مثلا اگر با بورلند سی 2006 یا 2007 کار کنید سوال بند 3 شما معنی ندارد و سایر مطالبی که شما ذکر کردید همنیطور بسته به شرایط میتواند پاسخ آسانی داشته باشد.
حالا اگر موضوع درخواستهای پیچیده زبان سی هست من هم یکی اضافه کنم.
7 - کلاس task برای چه کاری مورد استفاده قرار میگیرد؟ نمونه و تحلیل ازش دارید؟

PC2st
سه شنبه 12 مرداد 1389, 14:53 عصر
جواب این سوالات دقیقاً مطابق با استاندارد زبان ++C است، یعنی چیزی فراتر از رفتار زبان ++C نیست.
اینطور بگویم، جواب این سوالات در همهٔ کامپایلرها در همه سیستم‌عامل‌ها در همه جا یکسان است، چون استاندارد زبان ++C آنها را تعریف و مشخص کرده است. تنها سوال چهارم ممکن است پاسخ‌دهنده را به شک و شبهه بیاندازد (و خوب جواب همهٔ این سوالها باید جوابی باشد که توسط استاندارد ++C مشخص شده).

درمورد بند ۳، استاندارد زبان به آن اشاره کرده، حتی Bjarne Stroustrup به صراحت در وب‌سایت شخصی‌اش دربارهٔ آن نوشته و همهٔ این سوالها چیزی نیست که به پیاده‌سازی و به کامپایلر مربوط باشد، دقیقاً اینها جزئی از زبان ++C هستند.


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

PC2st
پنج شنبه 14 مرداد 1389, 10:22 صبح
این گفتگو می‌توانست محل خوبی برای بحث پیرامون این سوال‌ها باشد، ظاهراً در اینجا علاقه‌مند به آزمون نداریم :چشمک:. اینطور فکر نکنید که دانستن پاسخ این سوالات کاربردی نیست. اصلاً اینطور نیست. دانستن پاسخ این سوالها (و البته سایر سوالهای مشابه و از این دست) به برنامه‌نویس دید بهتری نسبت به پیاده‌سازی راه حل‌ها می‌دهد. بطور خلاصه پاسخ این سوال‌ها و آزمون، این است:

۱) چگونه می‌توان یک آرایهٔ ارجاعی (reference array) داشت؟
آرایهٔ ارجاعی بصورت زیر تعریف می‌شود:


Type (&ref_arr)[LEN] = arr;
Type نوع اعضای مورد ارجاع آن و LEN هم طول آن است. مثال:


int arr[3] = {1, 2, 3};
int (&ref_arr)[3] = arr;
int ref_len = sizeof(ref_arr);
int ref_element = ref_arr[1];
کاربرد؟ در صورتی که طول آرایه در زمان کامپایل مشخص باشد (همانند آرایه‌های معمولی)، استفاده از این نوع مناسب است، می‌توان پارامتر ورودی تابع را از این نوع تعریف کرد و توسط عملگر sizeof به اندازهٔ آن دست یافت. معمولا در صورت استفاده از templateها، کاربردی‌تر هستند. در غیر اینصورت، استفاده از اشاره‌گر برای دسترسی به آرایه و ورودی توابع مناسب است و در اینصورت باید اندازهٔ آرایه را به عنوان یک پارامتر مجزا به تابع بفرسید.
چرا نمی‌توان ارجاعی به خود آرایه (reference to array) داشت؟ چون آرایه‌ها همواره به عنوان یک اشاره‌گر pass می‌شوند. یعنی آرایه‌ای به نام adp که اعضای آن از نوع int باشد، در اینصورت، خود آرایه از نوع *int خواهد بود، مثال:


int adp[3] = {1, 2, 3};
int* adp_ptr = adp;
پس برای ارجای به آرایه، نمی‌توان نوع آن را یعنی *int را به &int تبدیل کرد (کاملاً غیر عملی). پس برای ارجاع به یک آرایه، در حقیقت باید ارجاعی به اشاره‌گر نوع آن داشت یعنی:


const int*& adp_ref = adp;
نوع &*int را از راست به چپ اینطور بخوانید: «ارجاعی به اشاره‌گری از نوع int»

yaseriran
پنج شنبه 14 مرداد 1389, 10:47 صبح
درود!

من به عنوان کسی که حتی بعضی اوقات c++ رو تدریس می کنم، نتوانستم به هیچ یک از سوالات شما پاسخ مناسب بدهم.

نخست از شما سپاسگذاری می کنم بابت پرسش هاتون. و درخواست دارم که پاسخ پرسشهاتون رو هم در این تاپیک قرار دهید.

سپاس...



رندان سلامت می کنند، جان را غلامت می کنند...

PC2st
پنج شنبه 14 مرداد 1389, 10:55 صبح
ممنون دوست عزیز :لبخندساده:
پاسخ به سوال ۲ را ابتدا مختصر و سپس با توضیح می‌دهم:
۲) یک template class چگونه در هر translation unit نمونه‌سازی می‌شود؟
مختصر: هر translation unit دارای template classهای نمونه‌سازی خودش است و این نمونه‌های ایجاد شدهٔ کلاس‌ها در یک translation unit در translation unit دیگری قابل استفاده نیستند. به همین خاطر در استاندارد جدید ++C معروف به C++‎0x مفهوم extern templateها افزوده شد.
توضیح: translation unit به همان واحدهایی گفته می‌شود که شما توسط کامپایلر، در نهایت یک فایل با پسوند o ایجاد می‌کنید. در هر یک از این واحدها، زمانیکه شما از یک template استفاده می‌کنید، به ازای هر نوعی که برای template مشخص کرده‌اید، یک کلاس جدید ایجاد می‌شود. بطور مثال:


std::vector<int> vec;
vec.push_back(1);
کامپایلر، زمانیکه کدهای بالا را می‌بینید، template کلاس vector را برای نوع int نمونه‌سازی می‌کند (یعنی کلاسی توسط کامپایلر ایجاد می‌شود که از نوع <std::vector<int باشد، چیزی شبیه به metaclassها در پایتون اما در زمان کامپایل صورت می‌گیرد) کامپایلر زمانیکه به خط اول از کدهای بالا می‌رسد، همهٔ اعضای template کلاس vector را در کلاس جدید قرار نمی‌دهد، بلکه آنهایی را که برنامه‌نویس استفاده کرده است را در کلاس جدید قرار می‌دهد. بنابراین در خط دوم از کدهای بالا، کامپایلر تابع push_back را در کلاس جدید قرار می‌دهد. بنابراین، کلاس <std::vector<int که در اینجا مورد استفاده قرار گرفته، در این translation unit فقط حاوی یک تابع عضو است و سایر توابع و اعضایی که در STL و توسط vector معرفی شده‌اند، در اینجا همانند این است که اصلا تعریف نشده‌اند. به همین خاطر است که شما می‌بینید که کلاس‌های STL اعضای زیادی دارند. بطور مثال کلاس std::string حاوی دو تابع length و size است که دقیقا کار هم را انجام می‌دهند، اما تا زمانیکه برنامه‌نویس از آنها استفاده نکند، در زمان run-time همانند این است که این دو تابع هیچوقت وجود نداشته است.
همانطور که در جواب مختصر گفتم و جواب مورد انتظار آزمون این است که: «این کلاس‌های ایجاد شده در هر translation unit مجزا و مختص به خودش است و در سایر translation unit ها استفاده نمی‌شود، بلکه سایر translation unit ها هم خودشان بدون توجه به دیگر translation unit ها کلاس مورد نیاز خودشان را از روی template ایجاد می‌کنند.»
این رفتار آنها چه مزیتها و ایرادهایی دارد؟
این رفتار به افزایش performance کمک می‌کند و در مواردی می‌تواند به کاهش آن منجر شود. مثلا فرض کنیم دو translation unit به نام‌های A و B داریم. اگر در A کلاس vector فقط با دارای ۳ تابع نمونه‌سازی شده باشد و ۹۰ درصد کارهای برنامه هم در A و توسط آن کلاس صورت گرفته باشد. حال اگر در B کلاس vector دارای ۱۲ تابع باشد، چون هر translation unit کلاس خاص خودش را از روی template ایجاد کرده، پس کلاس بزرگی که در B تعریف شده، تاثیری بر روی کلاس کوچکی که بسیار زیاد در A استفاده می‌شود، ندارد. این مورد باعث افزایش performance است. اما اگر translation unitهای زیادی داشته باشیم و در همهٔ آنها کلاس vector بسیار مشابه هم نمونه‌سازی شده باشند، در این حالت کاهش کارآیی را داریم. بنابراین در C++‎0x مفهوم extern template اضافه شده تا نمونه‌سازی از روی templateها را به سایر translation unit ها واگذار کند.

PC2st
پنج شنبه 14 مرداد 1389, 11:51 صبح
۳) ++C از چه نوع الگوی طراحی برای مدیریت حافظه استفاده می‌کند؟
از الگوی طراحی RAII (مخفف Resource Acquisition Is Initialization) که توسط بیئرن استروستوپ ابداع و برای زبان ++C بکار گرفته شد.
توضیح ساده: این الگوی طراحی مشخص می‌کند تا زمانیکه تابع سازندهٔ یک شیئ با موفقیت به پایان نرسد، آن شیئ در حافظه جایی ندارد و حافظهٔ اخذ شده برای آن شیئ، از آن گرفته می‌شود. به همین خاطر است که در صورت بروز خطا در تابع سازندهٔ یک کلاس، همهٔ حافظهٔ اخذ شده برای آن شیئ، از آن گرفته می‌شود (و همچنین برای کلاس‌های والدش).
RAII یعنی «اختصاص‌دادن حافظه همان مقداردهی است»، پس تا وقتی که توسط تابع سازنده، شیئ مقداردهی نشده، شما می‌توانید فرض کنید که اصلا حافظه‌ای اختصاص نیافته است.
چرا ++C زباله‌روب ندارد؟
این الگوی بکار رفته برای مدیریت حافظهٔ ++C هیچ نوع زباله‌ای برجای نمی‌گذارد که لازم به پاک سازی باشد. در صورتی که از اشاره‌گرها بطور مستقیم استفاده کنید، RAII به شما کمک نمی‌کند، بنابراین به همین خاطر است که همواره به ++C نویسان توصیه شده که از کلاس‌ها استفاده کنند. توابع مخرب هر کلاس نیز در پایان بلاک، صدا زده می‌شود تا کلاس در همانجا نابود شود.
توضیح اضافه: اما در زبان‌هایی مثال #C و جاوا باید زباله‌روب وجود داشته باشد، چون نحوه مدیریت حافظه در آن زبان با ++C فرق دارد، آنها زمانیکه یک شیئ باید نابود شود، آن را نابود نمی‌کنند (بر خلاف ++C) بلکه آنها را برای پاک شدن توسط زباله‌روب علامت می‌زنند. زباله‌روب ممکن است در این زبان‌ها، هر زمانی صدا زده شود (بسیار دیر یا بسیار زود یا ...).
چگونه یک مدیر حافظهٔ همراه با زباله‌روب پیاده‌سازی کنیم، در حالیکه بتوان از STL هم بطور مستقیم استفاده کرد؟
در STL این نیاز تدارک دیده شده و همواره آخرین پارامتر templateها مربوط به آن است، مثال:


std::basic_string <_C, _T, _A>

پارامتر A_ برای این منظور همواره در STL containerها وجود دارد و بطور پیش‌فرض برابر با نوع <allocator<_C است و به همین دلیل برنامه‌نویس اکثراً کاری به آن ندارند (و نیاز ندارند).
در رابطه با جواب آزمون، باید کلاسی که تمام شرایط کلاس allocator را دارد را پیاده‌سازی کرده (تمام اعضای کلاس allocator را در کلاس خود پیاده‌سازی کنید) و به عنوان پارامتر در کلاس‌های STL استفاده کنیم. معمولا برای پیاده‌سازی مدیر حافظه از قابلیت placement new استفاده می‌شود تا همهٔ حافظهٔ مورد نیاز بصورت یکجا گرفته شده و سپس تک تک اشیاء در آن حافظه نمونه‌سازی شوند. این کار سریعتر است تا اینکه برای هر شیئ یک حافظه جداگانه تخصیص داده شود.

PC2st
پنج شنبه 14 مرداد 1389, 12:17 عصر
۴) استفاده از throw چه تاثیری بر روی performance دارد؟ استفاده از try...catch چطور؟
استاندارد زبان ++C در این رابطه چیزی نگفته است. دلیل اینکه استاندارد زبان این مورد را مسکوت قرار داده این است که این مورد به پیاده‌سازی زبان مربوط است و نه به طراحی آن.

در واقع هر آن چیزی که به طراحی زبان مربوط باشد، توسط کمیتهٔ استاندارد ++C مشخص شده است.

توضیح اضافه: کامپایلر مختار هست با هر روشی که دوست دارد آن را پیاده‌سازی کند. معمولا کامپایلرهای جدید (حداقل در کامپایلر GCC اینطور است)، روشی را استفاده می‌کنند که try...catch تاثیری بر روی performance نداشته باشد و اگر در جائی خواندید که try...catch باعث کندی اجرا می‌شود، هیچگاه آن را به عنوان یک اصل، باور نکنید (بهتر است آزمایش کنید که کامپایلر شما چگونه آن را پیاده‌سازی کرده است). در مقابل، وقتی که خواسته شده که try...catch کاهش کارآیی نداشته باشد، این بار مسئولیت بر روی دوش throw می‌افتد.

استفاده از throw در کدها، به خودی خود، موجود کاهش کارآیی و کاهش سرعت نمی‌شود. زمانی این کاهش سرعت تاثیرگذار است که دستور throw در زمان اجرا صورت گیرد، مثال:


if (false)
{
throw 1;
}
این کد کاهش کارآیی را در زمان اجرا به دنبال نخواهد داشت، زیرا هیچگاه دستور throw در run-time اجرا نمی‌شود (چون شرط if همواره منفی است). پس در صورت وجود دستور throw در برنامه و کامپال آن، شما کاهش performance ندارید، بلکه این کاهش performance در زمانی است که دستور throw در run-time اجرا (و عملی) شود. زمانیکه دستور throw اجرا می‌شود (در run-time)، باید در یک جدول برای وجود try...catch در توابع outer (توابع بیرونی که باعث اجرای throw شدند) جستجو می‌کند و همین مورد باعث می‌شود که اجرای عملیات throw کند باشد.

توصیه می‌شود که از throw برای بررسی خطا در حلقه استفاده نکنید (throw شدن بصورت تکراری بسیار بر روی سرعت برنامه تاثیر منفی دارد). در واقع استفاده از throw در کدها، همانطور که (در مواردی) می‌تواند باعث افزایش کارآیی شود، همانطور هم می‌تواند کاهش کارآیی را در پی داشته باشد (اگر از آن در جای اشتباهی استفاده شود، این کاهش کارآیی گریبان‌گیر برنامه‌نویس است). در صورت استفاده از throw افزایش کارآیی برای حالتی است که یک خطا به ندرت (و غیر منتظره) ممکن است رخ دهد، بهتر است در این حالت از throw برای صدور و از try...catch برای بررسی آن استفاده شود به جای آنکه ارقام بازگشتی و از if...else برای بررسی خطا استفاده کنید.

PC2st
پنج شنبه 14 مرداد 1389, 12:33 عصر
۵) چگونه می‌توان یک typedef از نوع توابع را ایجاد کرد؟
بصورت زیر:


typedef ReturnType F(ParameterType1, ParameterType2);

در اینحالت برای ایجاد یک ارجاع به تابع (reference to function) از آن بصورت زیر استفاده می‌شود:


ReturnType MyFunction(ParameterType1 arg1, ParameterType2 arg2);
...
F& func_ref = MyFunction;
...
ReturnType return_value = func_ref(param1, param2);

و همچنین برای ایجاد یک اشاره‌گر به تابع (pointer to function) بصورت زیر:


ReturnType MyFunction(ParameterType1 arg1, ParameterType2 arg2);
...
F* func_ref = &MyFunction;
...
ReturnType return_value = (*func_ref)(param1, param2);

البته حتماً لازم نیست که func_ref را dereference کنیم.

کاربرد؟
برای templateها کاربرد دارد، مثل کلاس std::function در استاندارد C++0x و بصورت زیر:


std::function<RetrunType (ParameterType1, ParameterType2)> f;

چرا نمی‌توان متغیری از این نوع را ایجاد کرد؟
چونکه توابع را نمی‌توان کپی کرد، چونکه توابع یک شیئ نیستند که نمونه‌سازی شوند. پس اگر نوع F به همان صورت قبل تعریف شده باشد:


F func = MyFunction;

این غلط است، زیرا MyFunction شیئ نیست که بتوان از آن نمونه‌سازی کرد یا کپی گرفت، فقط می‌توان به MyFunction ارجاع یا اشاره داشت.

PC2st
پنج شنبه 14 مرداد 1389, 13:16 عصر
۶) کلاس‌های والد مجازی (virtual base classes)، چگونه در سلسله مراتب ارث‌بری نمونه‌سازی می‌شوند (راهنمائی: منظور از نمونه‌سازی: صدازدن تابع سازنده است) (با چه ترتیبی و توسط چه کلاسی)؟
به طور خلاصه: ابتدا همهٔ کلاس‌های والد مجازی با توجه به سطح دسترسی کلاس فرزند، مستقیماً نمونه‌سازی می‌شوند، سپس سایر کلاس‌های والد و در ادامهٔ آن خود کلاس فرزند باید نمونه‌سازی شود.

توضیح تکمیلی: مثال:


class X { /*...*/ };
class Y { /*...*/ };
class A : public X { /*...*/ };
class B : public Y { /*...*/ };
class G : public A, public B { /*...*/ };
در صورت ایجاد متغیری از G، به ترتیب کلاسهای X و A و Y و B و G نمونه‌سازی می‌شوند (و متد سازندهٔ آنها نیز به همین ترتیب صدا زده می‌شود). اما این مثال را برای استفاده از کلاس والد مجازی، تغییر داده می‌شود:


class X { /*...*/ };
class Y { /*...*/ };
class A : public X { /*...*/ };
class B : public virtual Y { /*...*/ };
class G : public A, public B { /*...*/ };
در اینحالت، کلاس B دارای یک والد مجازی به از نوع کلاس Y است.
بنابراین، در صورت ایجاد ایجاد متغیری از G، به ترتیب کلاسهای Y و X و A و B و G نمونه‌سازی می‌شوند (و متد سازندهٔ آنها نیز به همین ترتیب صدا زده می‌شود). همانطور که می‌بینید Y در ابتدای همه نمونه‌سازی شده است و این مهم است که Y بصورت مستقیم توسط آخرین کلاس فرزند یعنی کلاس G نمونه سازی شده است. بنابراین اگر آخرین کلاس فرزند یعنی G نتواند به تابع سازندهٔ کلاس Y دسترسی داشته باشد، در اینصورت تابع سازندهٔ این کلاس را نمی‌تواند صدا بزند و در ادامه، کلاس G نمونه‌سازی نخواهد شد. این مثال شبیه به مثالی است که خالق این زبان در سایتش آورده است:


class A;
class X
{
friend class A;
X() { }
};
class A : public virtual X { /*...*/ };
class G : public A { /*...*/ };
سعی کنید تا یک نمونه از کلاس G را ایجاد کنید. چون X یک والد مجازی است، پس باید توسط G نمونه‌سازی شود (بطور مستقیم) و مسلماً چون کلاس G به تابع سازندهٔ کلاس X دسترسی ندارد، پس نمونه‌ای از G ساخته نمی‌شود.
اینبار کلمه virtual را برای ارث‌بری A حذف کنید. در اینجالت کلاس X توسط A نمونه‌سازی می‌شود و به تابع سازندهٔ مخفی آن نیست (بعنوان دوست) دسترسی دارد. پس اینبار می‌توانید از کلاس G نمونه‌سازی کنید.

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