پرچمداران
-
در مقالات
- همه بخش ها
- فایل
- دیدگاه فایل
- نقد و بررسی فایل
- مقالات
- مقاله دیدگاه
- مقاله نقد و بررسی
- صفحات استاتیک
- صفحه دیدگاه
- صفحه نقد و بررسی
- کتابخانهها
- کتابخانه دیدگاه
- کتابخانه نقد و بررسی
- رویداد
- دیدگاه های رویداد
- بازبینی رویدادها
- تصاویر
- دیدگاه های تصویر
- نقد های تصویر
- آلبوم ها
- نظر های آلبوم
- نقد های آلبوم
- پست ها
- نوشتههای وبلاگ
- دیدگاه های وبلاگ
- بروزرسانی وضعیت
- پاسخ های دیدگاه ها
-
تاریخ سفارشی
-
همه زمان ها
4 خرداد 1397 - 13 اردیبهشت 1403
-
سال
12 اردیبهشت 1402 - 13 اردیبهشت 1403
-
ماه
14 فروردین 1403 - 13 اردیبهشت 1403
-
هفته
6 اردیبهشت 1403 - 13 اردیبهشت 1403
-
امروز
13 اردیبهشت 1403
-
تاریخ سفارشی
سه شنبه, 6 اسفند 1398
-
همه زمان ها
مطالب محبوب
در حال نمایش مطالب دارای بیشترین امتیاز از زمان سه شنبه, 6 اسفند 1398 در مقالات
-
4 امتیازاصطلاحاتی که بهتر است در مورد C++ مدرن بدانید! داشتم به این فکر میکردم که برخی از مبتدیان برنامهنویسی به خصوص کسانی که به سراغ زبانهایی مثل سی++ میروند معمولاً مستقیم وارد کد نویسی میشوند و به این گمان که آغاز برنامهنویسی یعنی نوشتن یک کد با خروجی «سلام، دنیا»! دریغ از آن که بعضی از موارد مانند «معرفی کامپایلر و انواع آن» و حتی «ساختار برنامههای نوشته شده تحت سیپلاسپلاس» و یا حتی «مدیریت حافظه» را در نظر بگیرند! من معمولاً در مقالات و آموزشهای خودم به این اشاره میکنم که قبل از هر چیز باید با ساختار برنامههای نوشته شدهٔ یک زبان آشنا شد و سپس به بررسی موارد دیگر مانند نحو زبان و یا دیگر ویژگیهای آن. بنابراین، یکی از خطرناکترین عواملی که موجب خونریزی داخلی یک نرمافزار در برنامههای نوشته شده توسط برنامهنویس درC++ میشود عدم مدیریت حافظهٔ اختصاص یافته است که باید بعد از اختصاص یافتن حافظه در زمان معین آن را آزادسازی کند. در صورتی که این کار صورت نگیرد عمل Memory Leak (نَشتِ حافظه) رخ داده است. بسیاری از علاقهمندان بر این باورند که چون سی++ دارای GC یا همان Garbage Collector (زبالهروب) نیست که البته صحیح است! سی++ دارای GC نیست و این امر محدودیت یا نکته ضعف آن هم نیست! سی++ همه چیز را آزادانه در اختیار برنامهنویس قرار میدهد تا خود در زمان مناسب روش مدیریت حافظه را انتخاب کند. در علوم رایانه بازیافت حافظه یا زبالهروبی نوعی مدیریت حافظهٔ خودکار است که عمل مدیریت حافظههای اختصاص یافته شده را به دست میگیرد و اکثر زبانهای برنامهنویسی مانند #C، جاوا و دیگر موارد مشابه به آن مجهز به این ویژگی هستند که البته وجود چنین ابزارهایی میتواند توهمی را ایجاد کند مبنی بر آن که دیگر نیازی به مدیریت منابع نیست، اما در بعضی موارد مدیریت منابع هنوز یک الزام است چرا که منابع آزاد شده هنوز هم دلیل بر نشتِ حافظه هستند. این نشت حافظه زمانی اتفاق میافتد که اشیاء هنوز قابل دسترس از طرف اشیاءای که زنده هستند اما هرگز مورد استفادهٔ دوباره قرار نمیگیرند اتفاق بیافتد. در بسیاری از زبانهای برنامهنویسی این ویژگی وجود دارد که طبیعتاً مدیریت توسط GC راه حل بسیار خوب و بی نقصی نیست. اما با توجه به عدم وجود GC در سی++ اکثراً با روشهای دستی برای مدیریت حافظه میپردازند که رایجترین روش آن استفاده از عمل new و delete در اختصاص دادن و آزادسازی حافظه است. بسیاری از ما با سی++ در دانشگاه و یا دروس مرتبط با مفاهیم اولیه برنامهنویسی آشنا شده ایم، اما معمولاً مفاهیم مربوطه برای نسلهای قبلی و منسوخ شدهٔ این زبان است. بهتر است در نظر داشته باشید که برنامهنویسی مدرن یعنی پیروی از اصول و قوانین جدیدی که در تکامل یافتن یک زبان به کار گرفته میشود. RAII : Resource Acquisition is initialization بنابراین، باید در نظر گرفت مدیریت حافظه از استاندارد ۱۱ به بعد این زبان به روشهای بسیار مدرنتری هوشمند سازی شده است. یکی از بهترین تکنیکهای موجود در هستهٔ زبان اصطلاح Raiiاست. الگوی RAII مخفف «Resource Acquisition is initialization» که به عنوان یک اصطلاح در برنامهنویسی مطرح میشود به صورت یک تکنیک (کنترل تخصیص منابع و آزادسازی آنها) یکی از ویژگیهای اصلی در سیپلاسپلاس است. با قرار دادن چنین کدی دیگر نیاز به فراخوانی آن کد توسط برنامهنویس در مخرب (ویرانگر) نیست و کامپایلر خود این کار را انجام میدهد. به طور کلی این الگو هر شیء را مجبور میسازد تا در زمان مواجه با رفتارهای ناهنجار خود را پاکسازی کند. به طور کلی هنگامی که شما یک شیء را مقداردهی اولیه میکنید، قبل از انجام آن باید منابع مورد نیاز آن را تأمین کنید (در سازنده). هنگامی که یک شیء از محدوده خارج میشود، هر منبعی را که مورد استفاده قرار داده است باید آزاد کند (در مخرب - ویرانگر). نکات کلیدی هرگز نباید یک شیء به حالت نیمه آماده یا نیمه از بین رفته وجود داشته باشد! وقتی که یک شیء ساخته میشود، آن شیء باید در حالت آماده باش برای استفاده باشد. وقتی یک شیء از محدوده خارج میشود، باید منابع اختصاص یافتهٔ خود را در حافظه آزاد کند (کاربر مجبور به انجام کار دیگری نیست). آیا RAII عنوان بدی برای مفهوم این تکنیک است! از نظر خالق سیپلاسپلاس نام بهتر میتواند به صورت زیر باشد: مدیریت منابع مبتنی بر حوزه (محدوده یا دامنه) : Scope Based Resource Management چیزی که تکنیک RAII را نقض میکند چیست؟ اشارهگرهای خام و تخصیص حافظه فراخوانی با کلمهٔ کلیدی new برای دست آوردن یا اختصاص دادن منبع (حافظه). فراخوانی با کلمهٔ کلیدی delete برای آزادسازی منبع (حافظه). اما این مورد به صورت خودکار بعد از خروج از محدوده توسط اشارهگرها صورت نمیگیرد. void rawPtrFn() { // acquire memory resourceNode* n = newNode; // manually release memory delete n; } بنابراین در صورتی که برنامهنویس استفاده از کلمهٔ کلیدی delete را برای آزادسازی حافظه فراموش کند (نشتِ حافظه) رخ میدهد. این عمل کافی است تا تکنیک RAII را نقض کنیم. void UseRawPointer() { // Using a raw pointer -- not recommended. Song* pSong = new Song(L"Nothing on You", L"Kambiz Asadzadeh"); // Use pSong... // Don't forget to delete! delete pSong; } بنابراین، راه حل RAII برای این امر در چیست؟ کلاسی داشته باشید که : حافظه را هنگام مقداردهی اولیه تخصیص دهد. حافظه را هنگام فراخوانی مخرب (ویرانگر) آزاد کند. دسترسی به اشارهگرهای زیرین را امکانپذیر کند. اشارهگرهای هوشمند (Smart Pointers) یک اشارهگر هوشمند یک شیء به سبکِ RAII است که تضمین میکند یک اشارهگر در هر زمانی که مناسب باشد حافظهٔ اختصاص یافته شده را آزاد میکند. به عنوان یک قاعده، برنامههای نوشته شده در سیپلاسپلاس مدرن (پیشرفته) هرگز نباید از اشارهگرهای خام (Raw) برای مدیریت حافظهٔ پویا (مشترک) استفاده کنند. برنابراین، در برنامههای مدرن سی++ به ندرت باید از کلمهٔ کلیدی delete جهت آزادسازی حافظه استفاده کرد. در واقع انجام این روش موجب جلوگیری از نشت حافظه است. این ویژگی اساساً مدیریت حافظهٔ خودکار را ارائه میدهد. زمانی که یک اشارهگر هوشمند دیگر استفاده نمیشود (زمانی که از محدودهٔ خود خارج میشود) حافظهٔ مورد نظر خود را به طور خودکار آزاد میکند.توجه داشته باشید که اشارهگرهای سنتی با عنوان اشارهگرهای خام (Raw Pointer) شناخته میشوند. اشارهگرهای هوشمند را میتواند یک شکل کلی از GC در نظر گرفت؛ نوعی مدیریت خودکار وقتی که دیگر توسط برنامه مورد استفاده قرار نمیگیرند حافظهٔ اختصاص یافتهٔ آن شیء به طور خودکار حذف میشود. در استاندارد ۱۱ سیپلاسپلاس سه نوع اشارهگر هوشمند معرفی شده است که همهٔ آنها در فایل سرآیند <memory> از کتابخانهٔ استاندارد STL معرفی شدهاند. کلاس std::unique_ptr یک اشارهگر هوشمند که دارای یک منبع تخصیص حافظهٔ پویا است. این شیء دارای یک اشارهگر به حافظهٔ پشته است، بنابراین نمیتوان آن را کپی کرد. تنها میتوان آن را جابجا (move) و مبادله کرد. خارج از این بیشتر مانند یک اشارهگر عادی رفتار میکند. { std::unique_ptr<Person> person(new Person("Kambiz")); if (person != nullptr) person->SetLastName("Asadzadeh"); if (person) DoSomethingWith(*person); } اگر دقت کنید، اپراتورهای -> و * اطمینان میدهد که unique_ptr میتواند اکثر اوقات شبیه به یک اشارهگر خام (Raw Pointer) استفاده شود. کاربردهای معمول از unique_ptr که باعث میشود از آن را به یک ابزار واقعی و ضروری تبدیل کند به صورت زیر است: آنها را میتوان با خیال راحت در داخل یک ظرف (Container) ذخیره کرد. هنگامی که به عنوان متغیرهای عضو کلاس دیگر استفاده میشوند، نیاز به حذف صریح در مخرب را از بین میبرند. در واقع نیازی نیست در مخرب کلاس خود شیءای را که حافظهای را به خود اختصاص داده است به صورت دستی آزاد کنید. علاوه بر این، موجب جلوگیری تولید خطاهای احتمالی از طرف کپی عضوها برای اشیاءای که باید حافظهٔ پویا داشته باشد در کامپایلر نیز میشود. آنها امنترین و توصیه شدهترین روشهایی برای انتقال مالکیت انحصاری، یا بازگشت به یک unique_ptr از یک تابع که شیء ای را در پشته ساخته است و یا با انتقال یکی از آنها به عنوان آرگومانی که در تابع میتواند به عنوان مالکیت بیشتر پذیرفته شود. در هر دو مورد، std::move به طور کلی باید مورد استفاده قرار بگیرد، و این انتقال مالکیت را به صورت صریح بیان میکند. انتقال مالکیت از قبل تعیین شده از طرف توابع امضاء شده معلوم میشود. از نشت حافطه جلوگیری میکند. همچنین، یک شیء unique_ptr میتواند حافظهٔ اختصاص داده شده را با استفاده از new[] مدیریت کند: { std::unique_ptr<int[]> array(new int[123]); DoSomethingWith(array.get()); } به طور معمول توصیه میشود که برای ساخت یک unique_ptr از std::make_unique() استفاده شود. کلاس std::shared_ptr شامل یک اشارهگری است که دارای یک منبع تخصیص حافظهٔ پویا با تفاوت اینکه میتواند چندین شیء را به صورت اشتراکی از یک منبع مشترک ردیابی کند. در واقع هنگامی که موجودیتهای متعددی همان شیء اختصاص یافته شده به حافظه را به اشتراک میگذارند، البته این وضعیت همیشه مشهود نبوده و یا ممکن است آن را به یک مالک واحدی اتخصاص دهید. این اشاره گر های هوشمند، شمارندهای از یک رشتهٔ ایمن(thread-safe) برای منبع حافظهٔ مشترک حفظ میکند و در زمانی که تعداد مرجع آن به صفر رسید، حذف میشود. در واقع این زمانی رخ میدهد که آخرین شیء مشترک از آن حذف شود. تابع use_count() نیز تعداد مراجع را بر میگرداند. همچنین مشابه unique_ptr این شیء یعنی shared_ptr میتواند آرایههای پویا را مدیریت کند که این ویژگی از استاندارد ۱۷ به بعد ممکن شده است. چندین شیء از shared_ptr ممکن است دارای همان شیء باشند. اگر یکی از موارد زیر اتفاق بیفتد، شیء از بین رفته و حافظهٔ آن آزاد میشود: آخرین بازمانده از شیء shared_ptr از بین رفته باشد. آخرین بازماندهٔ شیٔ shared_ptr که دارای یک اشارهگر از طریق اپراتور = و یا reset() است تعیین میشود. آنچه که shared_ptr را از unique_ptr متمایز میکند آن است که آنها میتوانند کپی شوند: { auto age = std::make_shared<int>(30); auto aliasAge = age; age.reset(); } کلاسstd::weak_ptr مانند std::shared_ptr است اما با تفاوت آن که شمارندهٔ آن افزایش نمییابد و اختیار شیء را به دست نمیگیرد. درواقع، بعضی اوقات نیاز است هنگام ساخت مخازنی از اشیاءای که به اشتراک گذاشته شدهاند بدانید آن شیء وجود دارد یا خیر. کاربرد آن نیز همراه با shared_ptr معتبر است و اطلاعاتی در بارهٔ اشیائی که توسط shared_ptr به دست گرفته است ارائه میکند. چند مثال در بارهٔ اشارهگرهای هوشمند: { std::unique_ptr<int> p(new int); // شیء p قابل استفاده در داخل حوزه است. } // در این بخش که خارج از دامنهٔ اشارهگر است حافظهٔ اختصاص یافته آزاد میشود. همانطور که مشخص است یک شیء که تحت اشارهگر هوشمند مورد استفاده قرار میگیرد تا زمانی که خارج از حوزهٔ خود قرار نگیرد قابل استفاده خواهد بود. نمونه کد پایین مثالی از نحوهٔ نمونه سازی تحت اشارهگرهای هوشمند است. void UseSmartPointer() { // Declare a smart pointer on stack and pass it the raw pointer. unique_ptr<Song> song2(new Song(L"Nothing on You", L"Kambiz Asadzadeh")); // Use song2... wstring s = song2->duration_; //... } // song2 is deleted automatically here.
-
3 امتیازسلام و درود خدمت دوستان عزیز، همانطور که میدانید مهمترین و شاید بزرگترین سوال در حوزهٔ برنامهنویسی این است که من باید کدام زبان برنامهنویسی را انتخاب کنم؟! واقعیت امر این است که این سوال همیشه از سمت علاقهمندان مطرح شده است اما هیچگاه یک پاسخ اساسی در مورد آن ارائه نشده است. البته اساتید و برنامهنویسان حرفهای به خوبی میدانند که زبانهای برنامهنویسی به عنوان ابزارهای کمک کار ما کاربرد دارند و به هیچ عنوان نمیتوان یک زبان را به عنوان اولین و آخرین انتخاب در نظر گرفت، اما شناخت در مورد آنها کمک بسیاری در انتخاب ابزارهای مناسب خواهد کرد. در این پُست من قصد دارم در رابطه با انتخاب یک زبان برنامهنویسی بر اساس نیاز و علایق صحبت کنم تا شما عزیزان بتوانید به یک نتیجهٔ مطلوب برسید. بنابراین، قبل از هر چیز این بسیار مهم است که بدانیم یک زبان برنامهنویسی چیست! و چرا باید از آن استفاده کنیم؟! اجازه دهید نگاهی به دلیل استفاده از زبان برنامهنویسی داشته باشیم، چرا از زبان برنامهنویسی استفاده میکنیم؟ به برقراری ارتباط با یکدیگر فکر کنید، انسان برای برقراری ارتباط با همنوعان نیاز به ابزاری به نام زبان دارد که عناصر اساسی آن حروف است. برای مثال حروف خ-ا-ن-ه با ترکیب شدن به خانه تبدیل شده و شما میتوانید آن را درک کنید و این کدی است که شما توسط آن با جهان بیرون خود ارتباط برقرار میکنید. ممکن است کدهای شما توسط یک زبان دیگر مانند زبان انگلیسی ساخته شود، برای مثال h-o-m-e حرفی است که که نتیجهٔ آن Home خواهد بود. در کشور ما معمولاً زبان مادری هریک از ما فارسی، ترکی (آذربایجانی)، عربی، کردی، لُری و دیگر موارد هستند که به صورت بومی آن را فرا میگیریم و با تسلط بسیاری از آن استفاده کرده و منظور همنوعان خود را درک میکنیم. در بحث کامپیوتر، استفاده از زبان انسانی تا حدی کاربرد دارد که فقط خود انسان آن را درک خواهد کرد نه ماشین! چرا که زبان بومی و اصلی کامپیوتر (ماشین) ۰ و ۱ است نه کاراکتر و حرف! ماشینها برای برقراری ارتباط و درک منظور انسان از واحد ۰ و ۱ استفاده میکنند که مجموعهای از آنها به عنوان دستورالعملهای مشخصی برای کامپیوتر قابل درک است. مغز کامپیوتر یعنی واحد پردازشگر مرکزی (CPU) به عنوان پردازندهٔ مرکزی تمامی دادههای شما را در قالب ۰ و ۱ شناسایی میکند و آنها را درک خواهد کرد. بنابراین زبان بومی ماشین بر خلاف انتظار برای انسان بسیار دشوار است و اگر به فکر این باشید که بخواهید از طریق آن با کامپیوتر ارتباط برقرار کنید شما یک دیوانهٔ تمام عیار بشمار خواهید آمد! اجازه دهید منظورم را سادهتر کنم، کامپیوتر منظور شما را از home درک نخواهد کرد! اما اگر به آن بگویید 01101000 01101111 01101101 01100101 مسلماً خواهد فهمید که منظور شما از آن یعنی home است! حالا اگر منظور شما سلام دنیا باشد باید به کامپیوتر بگویید 01101000 01100101 01101100 01101100 01101111 00100000 01110111 01101111 01110010 01101100 01100100 و همینطور برای برقراری ارتباط بیشتر باید هزاران، میلیونها و میلیاردها ۰ و ۱ را با اصول زبان ماشین در کنار هم قرار دهید تا شاید بتوانید یک دستورالعمل ساده برای انجام یک کار را به آن انتقال دهید! احمقانست نه؟! وقت و زمان برای ما انسانها بسیار با ارزش است و مسلماً به هدر دادن آن به این روش هرچند دانش بسیار بالایی از مهندسی کامپیوتر میطلبد اما حتی اگر شما یک دانشمند هم باشید بکار گیری این روش دیوانگی محض است! ما که کامپیوتر نیستیم! ما انسانیم! ساختار کامپیوتر بسیار شبیه به ساختار انسان است، همانطور که خالق انسان، خداوند یکتا او را آفرید به آن نیز هوش و قدرتِ تفکر داد تا بتواند بر اساس آن رشد کرده و در مسیر پیشرفت قدم بردارد، انسان نیز از دانستههای خود برای ساختههایش استفاده میکند. کامپیوترها به عنوان ابزارهای ساخت دست بشر دارای ساختار بسیار ساده ولی در عین حال بسیار پیچیده هستند که انسان برای برنامهریزی آن نیاز به ابزارهایی دارد (ابزارهایی برای ایجاد دستورات قابل درک برای ماشین). ابزارهای برنامهریزی برای کامپیوتر همانطور که اشاره شد، کامپیوترها هیچ درکی از کدهایی که انسان مینویسد ندارند! بنابراین ما نیاز به ابزاری داریم تا منظور خود را برای درک کامپیوتر ارائه دهیم. حال آن ابزار میتواند یک مفسر (Interpreter) باشد یا یک کامپایلر (Compiler) ! هر دوی این ابزارها وظیفهٔ دریافت زبان سطح بالا (نزدیک به زبان انسان) و تبدیل (ترجمهٔ آن) به زبان ماشین است. با تفاوت اینکه مفسرها کدهای نوشته شده را خط به خط تفسیر کرده و آنها را برای پردازنده اجرا میکند، در حالی که کامپایلر تمامی کدها را به شیء و هر شیء را به کد باینری یکجا ترجمه کرده و هرجا که نیاز بود آنها توسط پردازنده اجرا میشوند. به بیان سادهتر فرض کنید قرار است به زبان روسی صحبت کنید، شما دو روش خواهید داشت: صحبت کردن به زبان روسی به صورت مستقیم استخدام یک مترجم، صحبت با مترجم و ترج گفتههای شما توسط مترجم به طرف مقابل برخی از مزایا و معایب کامپایلر و مفسر نسبت به یکدیگر نکته ۱: مفسرها کدهای نوشته شده را به صورت خط به خط تفسیر و ترجمه میکند اما کامپایلرها آنها را یکجا ترجمه میکند که دارای یک خروجی مانند یک فایل اجرایی است. نکته ۲: برنامهٔ تولید شده تحت کامپایلر توسط سخت افزار (ماشین واقعی) اجرا میشود در صورتی که برنامهٔ تولید شده با مفسر توسط نرمافزار (ماشین مجازی) اجرا میشود. نکته ۳: کامپایلرها عملیات بهینهسازی یا همان (Optimization) را در آخرین مرحله از کامپایل (ترجمه) انجام میدهند، در صورتی که مفسرها عملیات بهینه سازی را در زمان تبدیل انجام میدهند. نکته ۴: سرعت اجرای کدهای کامپایل شده بسیار بیشتر از کدهای تفسیر شده است. برای مثال اگر حلقهای را در نظر داشته باشید که قرار است ۱۰ بار اجرا شود، آن حلقه در حالت مفسر ۱۰ بار تفسیر شده و ۱۰ بار توسط پردازنده اجرا خواهد شد! در حالی که در حال کامپایل شده حلقه یک بار ترجمه میشود و نتیجهٔ آن یک بار توسط پردازنده در زمان نیاز اجرا میشود! این بسیار مهم است و در پردازشهای بسیار بزرگ سرعت برنامههای کامپایل شده به شدت بالاتر از تفسیر شدهها است. نکته ۵: کدهای تولید شده توسط مفسر سطح بالاتری نسبت به کدهای تولید شده توسط کامپایلر دارند در واقع آنها تقریباً قابل خواندن هستند اما کدهای تولید شده توسط کامپایلر غیر قابل خواندن است. نکته ۶: امنیت برنامههای کامپایل شده و همچنین دسترسی به منابع کد آنها از امنیت بیشتری نسبت به برنامههای تحت مفسر دارند. نکته ۷: برنامههای تفسیر شده وابستگی خاصی به سیستمعامل ندارند و در هر جایی که برنامهٔ تفسیر کننده موجود باشد اجرا خواهند شد، در صورتی که برنامههای کامپایل شده برای هر نوع سیستمعامل متفاوت باید مجدداً کامپایل شود که البته برای اجرای آن نیازی به نصب بودن کامپایلر بر روی سیستمعامل نیست. نکته ۸: کامپایلر کد برنامه را به صورت کامل به کُد ماشین ترجمه میکند، بنابراین زمان اجرای آن بسیار کم تر است و انتخاب بسیار خوبی برای دوستداران سرعت است. نکته ۹: استفاده از زبانهای مفسری برای توسعهدهندگان مبتدی آسانتر از نوع کامپایلری میباشد بنابراین یادگیری و استفاده از این نوع زبانها نسبتاً سریعتر و راحتتر از نوع کامپایلری است. فرایند توسعهٔ نرمافزار در این فرایند کامپایلر برنامه را میسازد سپس همه دستورات زبان را از نظر صحت تجزیه و تحلیل میکند و اگر دستوری غلط باشد، اخطار میدهد. در صورتی که خطایی وجود نداشته باشد همهٔ کدها را به کد ماشین تبدیل میکند که در نهایت فایلهای مختلفی را به برنامه اجرایی اضافه میکند و در نهایت برنامه را اجرا میکند. در حالی که مفسر برنامه را میسازد اما، فرایند افزودن فایل اجرایی به برنامه یا تولید کدهای ماشین وجود ندارد بلکه دستورات کد منبع خط به خط در حین زمان اجرا یا به اصطلاح Run-time اجرا میکند. کدام زبان در چه حوزهای کاربرد دارد؟ با توجه به تعاریف بالا نوبت آن رسیده است تا زبان برنامهنویسی مورد نظر خود را بر اساس نیاز و قابلیتهایی که آن در اختیار توسعهدهنده قرار میدهد انتخاب کرد. حوزههای کاربردی زبانهای برنامهنویسی متناسب با کاربرد و رسالت آنها مشخص میشود، به طور کلی زبانهای برنامهنویسی را بهتر است به دو دستهٔ اصلی و فرعی جدا کنیم. در دستهٔ اصلی زبانهایی که پایه و اساس کتابخانهها، نرمافزارهای عظیم، انجینها و همچنین خود زبانهای برنامهنویسی میباشند را زبان مادر و اصلی و تمامی زبانهایی که به عنوانی جهتِ مکمل سازی و یا محصول نوع سوم برای اهداف تجاری ساخته شدهاند را فرعی میگوییم. زبانهای اصلی و مادر: C و ++C زبانهای اصلی و فرعی: Python, Java, Delphi, C#, Swift, Objective-C, Php, Rust, JavaScript زبانهای مکمل رابط کاربری: JavaScript, CSS, Xaml, Xhtml, Html, QML زبانهای مکمل بسترهای بلاکچین: C++, Rust و Solidity درنظر داشته باشید کتابخانهها و برنامههای اساسی و پایه که بخش اعظمی از آنها توسط زبانهای سی++ و سی نوشته میشوند در صورت نیاز برای زبانهای دیگر نیز قابل استفاده هستند. به عنوان مثال سیستمعاملها، نرمافزارهای عظیم، انجینهای بازیسازی، کتابخانههای پرکاربرد و مهم همهٔ آنها توسط زبانهای اصلی توسعه یافتهاند اما در صورت نیاز میتواند از کتابخانههای نوشته شده توسط زبانهای اصلی در زبانهای فرعی نیز استفاده کرد. شاید اینطور به نظر برسد که اگر با زبانهای اصلی هر کاری میتوان انجام داد، پس چرا زبانهای دیگر را مورد استفاده قرار میدهیم؟! جواب سوال این است که زبانهای اصلی و مهم نیاز به دانش بسیار از لحاظ معماری سیستمعامل، کامپایلر و دیگر شاخههای علوم کامپیوتر هستند و نحوِ کُدنویسی در آنها نسبت به زبانهای دیگر مانند جاوا، پایتون، سیشارپ و غیره دشوارتر است. بنابراین ممکن است انتخاب اول برنامهنویسان مبتدی نباشند اما کاربرد آنها جنریک (عمومی) است. اشاره به کاربرد زبانهای محبوب در حوزههای مختلف: توسعهٔ زیرساختها: C, C++, Rust, Go توسعهٔ وبسایت: C++, Java, Php, JavaScript, C#, Ruby, Python توسعهٔ نرمافزارهای موبایل: , C++, Java, C#, Objective-C, Swift, JavaScript, Kotlin توسعهٔ نرمافزارهای دسکتاپ: C/C++, Java, Delphi, VB.Net, C#, JavaScript, Objective-C توسعهٔ نرمافزارهای اِمبِد: C/C++, Python توسعهی بازیهای کامپیوتری: ++C و #C توسعهٔ هوش مصنوعی: C++, Python, R, Prolog, Java, Haskell, AIML توسعهٔ رابطکاربری: JavaScript, QML, XAML نکتهٔ قابل توجهی از کاربرد زبانهای اصلی در این است که خود آنها وابستهٔ زبان برنامهنویسی و محدود بر یک حوزه نیستند و به اصطلاح زبان مادر بشمار میآیند که و در تمامی حوزهها کاربرد دارند. کاربرد در صنایع و حوزههای مختلف بر اساس ویژگیهایی که یک زبان برنامهنویسی ارائه میدهد بسیار مهم است. برای مثال Python, R, Prolog و غیره در حوزهٔ هوش مصنوعی و بیگ دیتا بسیار سادهتر به کمک توسعه دهندگان میآید. در توسعهٔ وبسایت زبانهای برنامهنویسی Node.JS, Php, C#, Asp.Net محبوبیت بیشتری دارند. اما میتوان با توجه به این پست وبسایتهای بسیار سریع و بهینهای را توسعه داد که بی شک نیاز به دانش بسیار بالایی دارد و بهتر است در اهداف خاص از آن استفاده شود. در حوزهٔ موبایل در پلتفرم iOS دو زبان Swift و Objective-C به عنوان زبان اصلی پلتفرمهای iOS, tvOS و watchOS به شمار میآیند. در حوزهٔ اندروید (Android) زبانهای Java و Kotlin به عنوان انتخابهای اول توسعهدهندگان مطرح میشند در صورتی که بسیاری از کتابخانههای اندروید تحت C و ++C توسعه یافته و حتی میتواند با استفاده از ++C اپلیکیشن و بازیهای بسیار سریعتری را تولید کرد. در حوزههای صنایع بازیسازی زبانهایی مانند #C نیز کاربرد دارند، اما ترجیح اول و اصلی در این حوزه بکارگیری ++C است چرا که هیچ زبانی به جز آن نهایت سرعت را ارائه نخواهد داد. آمارها و محبوبیتها سالانه طبق گزارش بسیاری از مراجع نمودارها و آمارهایی در رابطه با ایندکس شدن زبانهای برنامهنویسی ارائه میشود که نمونهای از آنها TIOBE است. متاسفانه باید گفت مقایسهٔ چنین مراجع بر اساس ویژگیهایی که در بالا توضیح دادیم صورت نمیگیرد و صرفاً بر اساس محبوبیت بین کاربران گزارش داده میشوند. برتری زبان نسب به زبان دیگر بر اساس چنین آمارهایی اشتباه بوده و توسط آن نمیتوان یک زبان را به عنوان انتخاب اول در نظر گرفت. همچنین اگر دقت کنید مقایسهٔ زبانهای برنامهنویسی اسکریپتی با کامپایلری و همچنین زبانهایی مانند SQL در بین آنها وجود دارد که از لحاظ منطقی درست نیست چرا که کاربرد زبانها با یکدیگر متفاوت بوده و ملاک آماری این مراجع فقط استقبال کاربران است. تعاریف و هِدایتهای اشتباه به سمت یک زبان برنامهنویسی متاسفانه مشاهده میشود که در بسیاری از گروهها و سایتهای برنامهنویسی چندین زبان برنامهنویسی را به عنوان بهترین انتخاب معرفی میکنند و دلیل انتخاب آنها را مشاهدهٔ رتبههای آن بر اساس ایندکسهای برخی از مراجع و یا طرفداری بعضی از شرکتها و تعصبات بی هدف است! باید در نظر داشته باشید قدرت و ویژگیهای یک زبان ربطی به محبوبیت آن ندارد. اگر احساس میکنید شرکتها تقاضای متخصص زبان برنامهنویسی خاصی را دارد که تکرار میشود به آن معنی نیست که زبانهای برنامهنویسی دیگر در حال منسوخ شدن یا کنار گذاشتن هستند. ارزش زبان ملاک برتری آن است نه محبوبیتش! به عنوان مثال اگر JavaScript رتبهٔ اول از نظر محبوبیت را دارد به آن معنا نیست که Php در حال منسوخ شدن است! هر زبانی کاربرد خودش را نسبت به اهداف و ویژگیهای خود دارد. آیا زبان ماشین منسوخ شده است؟! خیر! این چه سوالیه!!؟ چنین افکار بیهوده را از ذهن خود بیرون بریزید! تمامی زبانهای کامپایلری به جز نوعهای مفسری در نهایت کدهایشان توسط کامپایلر به زبان ماشین یعنی assembly تبدیل میکنند. مثال زیر کد ++C است: int main() { } خروجی آن به زبان ماشین (Assembly) در کامپایلر GCC به صورت زیر خواهد بود: main: push rbp mov rbp, rsp mov eax, 0 pop rbp ret انتخاب چند-سکویی پیشنهاد میشود یا خیر؟ لازم بذکر است که بدانید، ابزارهای چند-سکوی بسیاری وجود دارند که به شما اجازه میدهند بدون داشتن دانش آنچنانی در رابطه با زبانهای برنامهنویسی متعددِ مخصوص سکوهای هدف محصول خود را توسعه دهید. برخی از آنها عبارتند از Xamarin و یا React Native که متاسفانه به دلیل ساختار نامطلوب تبدیل کدهای نوشته شده نتیجهٔ نهایی آنها آنچنان خوب مانند محصولات واقعی زبان بومی نیستند چرا که کدهای آن مستقم به زبان ماشین ترجمه نمیشوند. اما این بسیار مهم است که بدانید ابزارهای بومی چند-سکویی واقعی انتخاب بهتری برای این امر بشمار میآیند که معروفترین آنها Qt است که به صورت بومی بدون اُفت کیفیت محصول نهایی به شما اجازهٔ توسعه محصول در سکوهای مختلف را میدهد که مسلماً دانش بسیار بالایی از سی++ را میطلبد. در نتیجه اگر به دنبال محصول با کیفیت در حوزهٔ چند-سکویی هستید باید دانش بالایی از ++C داشته باشید. نکته: در نظر داشته باشید زبانهای کامپایلری خود به دو دسته نیز تقسیم میشوند که برخی از آنها به صورت مستقیم کدهای نوشته شده را به کد ماشین ترجمه میکنند و برخی از آنها کد نوشته شده را به یک زبان میانی تبدیل و سپس آن را توسط برنامهٔ مجازی برای ماشین برنامهریزی مینمایند. بهتر است توجه داشته آنها مزایا و معایبی نیز خواهند داشت. زبانهای کامپایلری در دو دستهی بومی (Native) و مجازی (Virtual) کامپایل از نوع بومی روشی است که کدهای نوشته شده را به صورت مستقیم به کُد ماشین ترجمه میکند. کامپایل از نوع مجازی روشی است که کدهای نوشته شده را ابتدا به کُدمیانی (کدمشترک یا همان بایت کُد - Byte Code) در جاوا و زبان میانی (CIL) در Net. تبدیل میکند که خودِ آن شبیه به کُد ماشین است. در این فرایند کد مربوطه توسط کامپایلر مخصوص یعنی JIT (کامپایلری از نوع Just-In-Time) در زمان اجرا توسط سیستمعامل، به دستورالعملهای قابل فهم برای پردازنده تفسیر و اجرا میشود (که این فرایند شبیه به فرایند عملکرد اجرایی مفسرها است). زبانهای بومی (زبانهایی که کد آنها به کد ماشین به صورت مستقیم توسط کامپایلر قبل از اجرای آنها توسط سیستمعامل، ترجمه میشوند که به اصطلاح ahead-of-time (جلوتر از زمان) یا همان AOT نام دارد) مانند: C, C++, Rust, Haskell, Clean, Swift, Go, Fortran, D زبانهای مجازی (زبانهایی که کد آنها توسط یک رابط میانی به زبان مشترک ترجمه میشود) : Java و #C مزایا و معایب زبانهای کامپایلری از نوع کلاس بومی (Native) از سرعت بسیار بالایی برخوردار هستند (دلیل آن ترجمهٔ مستقیم کدها به کد ماشین است) در مقابل بزرگترین مزیتی که زبانهای نوع کلاس مجازی (Byte Code) دارند به خاطر وجود یک برنامهٔ واسط جهت شبیهسازی کدهای ترجمه شده به کد قابل فهم برای پردازنده، اجرا شدن آنها در هر پلتفرم بدون کامپایل مجدد امکان پذیر است که البته این ویژگی خود نیازمند نصب بودن JVM بر روی پلتفرم مربوطه میباشد. در نوع بومی برای اجرا در هر پلتفرم لازم است سورس کدها را برای پلتفرم مقصد کامپایل کنید که نیازی به وجود ماشین مجازی مانند JVM یا برنامهٔ خاصی ندارد. سریعترین زبان برنامهنویسی کدام است؟ شاید پاسخ به این سوال به گونهای تعصبی به نظر برسد، اما واقعیت این است که باید حقیقت را پذیرفت! با توجه به دلایلی که به نوع زبانهای کامپایلری آورده شده است مشخص است که سریعترین نوع زبانهای برنامهنویسی باید در دستهٔ شاخهٔ کامپایلری و کلاس Native قرار گرفته باشند چرا که در این مبحث زبانها کامپایلری (مجازی) و مفسری حرفی برای گفتن ندارند. جهت تثبیت آن مستنداتی از بنچمارکها در این بخش آورده شدهاست. حقیقت این است ++C در بدترین حالت ممکن بدون بهینهسازی کدها و فلگهای خاص حداقل ۲ تا ۴ برابر سریعتر از زبانهای کامپایلری دیگر است! تلخترین حقیقت نیز این خواهد بود که ++C حداقل ۱۰۰ تا ۲۰۰ برابر سریعتر از زبانهای مفسری است! با توجه به تجربیات شخصی در صورتی که نوع کامپایلر Clang باشد سرعت کدها به چند برابر از این نیز خواهد رسید! همچنین باید در نظر بگیرید اگر کدهای شما خارج از اصول استاندارد زبان باشد ممکن است نتایج آن به تساوی و حتی پایینترین حالت ممکن برسد. سخن آخر، برای انتخاب زبان برنامهنویسی و به دست آوردن مهارت در آن و در نهایت تبدیل دانش به یک محصول نرمافزاری، بهتر است بر اساس نوع (کامپایلری یا مفسری بودن)، اهمیت سرعت، ویژگیهای آن و کاربردش در حوزههای مختلف تصمیم بگیرید نه بر اساس تعصب و علاقه. دقت کنید که زبانهای برنامهنویسی ابزارهای برنامهنویسی بوده و هرچقدر جعبه ابزار شما کامل باشد توانایی و مهارت شما در توسعهٔ حوزههای مختلف بیشتر خواهد بود. در صورتی که میخواهید در رابطه با انواع روشهای کامپایل و تفاوتهای کامپایل Native، Cross Compile و JIT آشنا شوید، پیشنها میشود مقاله زیر را مطالعه فرمایید.
-
2 امتیازدر این مقاله من قصد دارم در رابطه با تفاوتهای اختصاص دادن حافظه در اِستَک و هیپ توضیحاتی دهم که بسیاری از علاقهمندان راجع به آنها سوال کردهاند. با توجه به اینکه، از اوایل سیستمهای کامپیوتری این تمایز در این وجود داشته است که برنامه های اصلی در حافظه فقط خواندنی مانند ROM ، PROM و یا EEPROM نگه داری میشوند. به عنوان دیگر از زمانی که سیستم ها پیچیدهتر شدند برنامهها از حافظههای دیگری مانند RAM به جای اجرا در حافظه ROM استفاده کردند. این ایده به خاطر این بود که تعدادی از قسمت های حافظه مربوط به برنامه نباید تغییر یابند و در این حالت باید حفظ شوند. در این میان دو بخش .text و .rodata بخشیهایی از برنامه هستند که میتواند به بخش های دیگر برای وظایف خاص تقسیم شوند که در ادامه به آنها اشاره شده است. بخش کد، به عنوان یک بخش متنی (.text) و یا به طور ساده به عنوان متن شناخته میشود. جایی است که بخشی از یک فایل شیء یا بخش مربوطه از فضای آدرس مجازی برنامه که حاوی دستورالعمل های اجرایی است و به طور کلی فقط خواندنی بوده و اندازه ثابتی دارد میباشد. بخش .bss که به عنوانی بخشی ویژه (محل نگه داری اطلاعات تخصیص داده نشده (مقدار دهی نشده)) محلی که متغیرهای سراسری و ثابت با مقدار صفر شروع میشوند. بخش داده (.data) حاوی هر گونه متغیر سراسری و یا استاتیک که دارای یک مقدار از پیش تعریف شده هستند و میتوانند اصلاح شوند. تصویر زیر طرح معمولی از یک حافظه برنامه ساده کامپیوتری را با متن، داده های مختلف، و بخشهای استک و هیپ و bss را نشان میدهد. .section.data < initialized data here> .section .bss < uninitialized data here> .section .text .globl _start _start: <instruction code goes here> برای مثال در کد C به صورت زیر خواهد بود: int val = 3; char string[] = "Hello World"; مقادیر برای این نوع متغیرها در ابتدا در حافظه فقط خواندنی ذخیره میشوند. (معمولا در داخل .text) و در زمان اجرای برنامه که به صورت روتین خواهد بود در بخش .data کپی میشوند. بخش BSS یا همان .BSS در برنامهنویسی کامپیوتر، نام .bss یا bss توسط بسیاری از کامپایلرها و لینکرها برای بخشی از دیتا سِگمنت (Data Segment) استفاده میشود که حاوی متغیر های استاتیک اختصاصی که تنها از بیت هایی با ارزش صفر شروع شده است میباشد. این بخش به عنوان BSS Section و یا BSS Segment شناخته میشود. به طور معمول فقط طول بخش bss نه data در فایل آبجکت ذخیره میشود. برای نمونه، یک متغیر به عنوان استاتیک تعریف شده است static int i; این در بخش BSS خواهد بود. حافظه هیپ (Heap) ناحیهٔ هیپ (Heap) به طور رایج در ابتدای بخشهای .bss و .data قرار گرفته است و به اندازههای آدرس بزرگتر قابل رشد است. ناحیهٔ هیپ توسط توابع malloc, calloc, realloc و free مدیریت میشود که ممکن است توسط سیستمهای brk و sbrk جهت تنظیم اندازه مورد استفاده قرار گیرد. ناحیه هیپ توسط تمامی نخها، کتابخانههای مشترک و ماژولهای بارگذاری شده در یک فرآیند به اشتراک گذاشته میشود. به طور کلی حافطه Heap بخشی از حافظه کامپیوتر شما است که به صورت خودکار برای شما مدیریت نمیشود، و به صورت محکم و مطمئن توسط پردازنده مرکزی مدیریت نمیشود. آن بیشتر به عنوان یک ناحیه شناور بسیار بزرگی از حافظه است. برای اختصاص دادن حافظه در ناحیه هیپ شما باید از توابع malloc(), calloc() که توابعی از C هستند استفاده کنید. یکبار که شما حافظه ای را در ناحیه هیپ اختصاص دهید، جهت آزاد سازی آن باید خود مسئول باشید و با استفاده از تابع free() این کار را به صورت دستی جهت آزاد سازی حافظه اختصاص یافته شده انجام دهید. اگر شما در این کار موفق نباشید، برنامه شما در وضعیت نَشت حافظه (Memory Leak) قرار خواهد گرفت. این بدین معنی است که حافظه اختصاص یافته شده در هیپ هنوز خارح از دسترس قرار گرفته و مورد استفاده قرار نخواهد گرفت. این وضعیت همانند گرفتگی رَگ در بدن انسان است و حافظه نشت شده جهت عملیات در دسترس نخواهد بود. خوشبختانه ابزارهایی برای کمک کردن به شما در این زمینه موجود هستند که یکی از آنها Valgrind نام دارد و شما میتوانید در زمان اشکال زدائی از آن جهت تشخیص نواحی نشت دهنده حافظه استفاده کنید. بر خلاف حافظه اِستک (Stack) حافظه هیپ محدودیتی در اندازه متغیرها ندارد (جدا از محدودیت آشکار فیزیکی در کامپیوتر شما). حافظه هیپ در خواندن کمی کُند تر از نوشتن نسبت به حافظه اِستک است، زیرا جهت دسترسی به آنها در حافظه هیپ باید از اشاره گر استفاده شود. بر خلاف حافظه اِستک، متغیرهایی که در حافظه هیپ ساخته میشوند توسط هر تابعی در هر بخشی از برنامه شما در دسترس بوده و اساسا متغیرهای تعریف شده در هیپ در دامنه سراسری قرار دارند. حافظه اِستک (Stack) ناحیهٔ اِستک (Stack) شامل برنامه اِستک، با ساختار LIFO کوتاه شده عبارت Last In First Out (آخرین ورودی از همه زودتر خارج میشود) به طور رایج در بالاترین بخش از حافظه قرار میگیرد. یک (اشاره گر پشته) در بالاترین قسمت اِستک قرار میگیرد. زمانی که تابعی فراخوانی میشود این تابع به همراه تمامی متغیرهای محلی خودش در داخل حافظه اِستک قرار میگیرد و با فراخوانی یک تابع جدید تابع جاری بر روی تابع قبلی قرار میگیرد و کار به همین صورت درباره دیگر توابع ادامه پیدا میکند. مزیت استفاده از حافظه اِستک در ذخیره متغیرها است، چرا که حافظه به صورت خودکار برای شما مدیریت میشود. شما نیازی برای اختصاص دادن حافظه به صورت دستی ندارید، یا نیازی به آزاد سازی حافظه ندارید. به طور کلی دلیل آن نیز این است که حافظه اِستک به اندازه کافی توسط پردازنده مرکزی بهینه و سازماندهی میشود. بنابراین خواندن و نوشتن در حافظه اِستک بسیار سریع است. کلید درک حافظه اِستک در این است که زمانی که تابع خارج میشود، تمامی متغیرهای موجود در آن همراه با آن خارج و به پایان زندگی خود میرسند. بنابراین متغیرهای موجود در حافظه اِستک به طور طبیعی به صورت محلی هستند. این مرتبط با مفهوم دامنه متغیرها است که قبلا از آن یاد شده است، یا همان متغیرهای محلی در مقابل متغیرهای سراسری. یک اشکال رایج در برنامه نویسی C تلاش برای دسترسی به یک متغیر که در حافظه اِستک برای یک تابع درونی ساخته شده است میباشد. یعنی از یک مکان در برنامه شما به خارج از تابع (یعنی زمانی که آن تابع خارج شده باشد) رجوع میکند. یکی دیگر از ویژگیهای حافظه اِستک که بهتر است به یاد داشته باشید این است که، محدودیت اندازه (نسبت به نوع سیستم عامل متفاوت) است. این مورد در حافظه هیپ صدق نمیکند. خلاصه ای از حافظه اِستک (Stack) حافظه اِستک متناسب با ورود و خروج توابع و متغیرهای درونی آنها افزایش و کاهش مییابد نیازی برای مدیریت دستی حافظه برای شما وجود ندارد، حافظه به طور خودکار برای متغیرها اختصاص و در زمان نیاز به صورت خودکر آزاد میشود در اِستک اندازه محدود است متغیرهای اِستک تنها در زمان اجرای تابع ساخته میشوند مزایا و معایب حافظه اِستک و هیپ حافظه اِستک (Stack) دسترسی بسیار سریع به متغیرها نیازی برای باز پس گیری حافظه اختصاص یافته شده ندارید فضا در زمان مورد نیاز به اندازه کافی توسط پردازنده مرکزی مدیریت میشود، حافظه ای نشت نخواهد کرد متغیرها فقط محلی هستند محدودیت در حافظه اِستک بسته به نوع سیستم عامل متفاوت است متغیرها نمیتوانند تغییر اندازه دهند حافظه هیپ (Heap) متغیرها به صورت سراسری قابل دسترس هستند محدودیتی در اندازه حافظه وجود ندارد تضمینی برای حافظه مصرفی وجود ندارد، ممکن است حافظه در زمانهای خاص از برنامه نشت کرده و حافظه اختصاص یافته شده برای استفاده در عملیات دیگر آزاد نخواهد شد شما باید حافظه را مدیریت کنید، شما باید مسئولیت آزاد سازی حافظه های اختصاص یافته شده به متغیرها را بر عهده بگیرید اندازه متغیرها میتواند توسط تابع realloc() تغییر یابد در اینجا یک برنامه کوتاه وجود دارد که در آن متغیرها در یک حافظه اِستک ایجاد شده اند. #include <stdio.h> double multiplyByTwo (double input) { double twice = input * 2.0; return twice; } int main (int argc, char *argv[]) { int age = 30; double salary = 12345.67; double myList[3] = {1.2, 2.3, 3.4}; printf("double your salary is %.3f\n", multiplyByTwo(salary)); return 0; } ما متغیرهایی را اعلان کردهایم که یک int، یک double و یک آرایه که سه نوع double دارد هستند. این متغیرها داخل اِستک وارد و به زودی توسط تابع main در زمان اجرا حافظه مورد نیاز خود را دریافت خواهند کرد. زمانی که تابع main خارج میشود (برنامه متوقف میشود) این متغیرها همگی از داخل حافظه اِستک خارج خواهند شد. به طور مشابه، در تابع multiplByTwo() متغیر twice که از نوع double است، داخل اِستک وارد شده و در زمان اجرای تابع multiplyByTwo() حافظه به آن اختصاص مییابد. زمانی که تابع فوق خارج شود یعنی به نقطه پایان اجرایی خود برسد، حافظه اختصاص یافته شده به متغیرهای داخلی آن نیز آزاد خواهند شد. به طور خلاصه توجه داشته باشید که تمامی متغیرهای محلی در این نوع تعریف تنها در طول زمان اجرایی زمانی تابع زنده هستند. به عنوان یک یادداشت جانبی، روشی برای نگه داری متغیرها در حافظه اِستک وجود دارد، حتی در زمانی که تابع خارج میشود. آن روش توسط کلمه کلیدی static ممکن خواهد شد که در زمان اعلان متغیر استفاده میشود. متغیری که توسط کلمه کلیدی static تعریف میشود، بنابراین چیزی مانند متغیر از نوع سراسری خواهد بود، اما تنها در داخل تابعی که داخل آن ایجاد شده است قابل مشاهده خواهد بود. این یک ساختار عجیب و غریب است، که احتمالا به جز شرایط بسیار خاص نیازی به آن نباشد. نسخهٔ دیگری از برنامه فوق در قالب حافظه هیپ به صورت زیر است: #include <stdio.h> #include <stdlib.h> double *multiplyByTwo (double *input) { double *twice = malloc(sizeof(double)); *twice = *input * 2.0; return twice; } int main (int argc, char *argv[]) { int *age = malloc(sizeof(int)); *age = 30; double *salary = malloc(sizeof(double)); *salary = 12345.67; double *myList = malloc(3 * sizeof(double)); myList[0] = 1.2; myList[1] = 2.3; myList[2] = 3.4; double *twiceSalary = multiplyByTwo(salary); printf("double your salary is %.3f\n", *twiceSalary); free(age); free(salary); free(myList); free(twiceSalary); return 0; } همانطور که میبینید، استفاده از malloc() برای تخصیص حافظه در حافظه Heap و استفاده از free() جهت آزاد سازی حافظه تخصیص یافته میباشد. این مواجه شدن چیز بسیار بزرگی محسوب نمیشود اما کمی مبهم است. چیز دیگری که باید به آن توجه داشته باشید علامت ستاره (*) است که در همه جای کدها دیده میشود. اینها چه چیزهایی هستند؟ پاسخ این سوال این است : اینها اشاره گر هستند! توابع malloc() و calloc() و free() با اشارهگرهایی مواجه میشوند که مقادیرشان واقعی نیست. اشاره گرها نوع داده ای خاصی در C هستند که آدرس حافظه مربوطه را بر میگردانند. در خط ۵ متغیر twice یک متغیر از نوع double نیست، اما اشاره به یک double دارد. آن آدرس حافظه ای است که نوع double در آن بلوک از حافظه ذخیره شده است. در ++C توسط کلمه کلیدی new که خود آن نیز یک اپراتور محسوب میشود میتوان حافظه ای را در Heap اختصاص داد. به عنوان مثال: int* myInt = new int(256); آدرسهای موجود در حافظه توسط اپراتور new به اشارهگر مربوطه پاس داده میشود. به مثال زیر توجه کنید، متغیر تعریف شده در حافظه اِستک قرار گرفته است: int variable = 256; سوالی که ممکن است افراد کنجکاو از خود بپرسند این است که چه زمانی از Stack و چه زمانی از Heap باید استفاده کنیم؟! خب پاسخ این سوال اینگونه خواهد بود، زمانی که شما نیاز به یک بلوک بسیار بزرگی از حافظه دارید، که در آن یک ساختار بزرگ یا یک ارایه بزرگی را ذخیره کنید و نیاز داشته باشید که متغیرهای شما به مدت طولانی در سرتاسر برنامه شما در دسترس باشند در این صورت از حافظه Heap استفاده کنید. در صورتی که شما نیاز به متغیرهای کوچکی دارید که تنها نیاز است در زمان اجرای تابع در دسترس باشند و قابلیت خواندن و نوشتن سریعتری داشته باشند از نوع حافظه Stack استفاده کنید. فقط فراموش نکنید که حافظه Heap تحت توابع molloc(), realloc(), calloc() و free() مدیریت میشوند. هرچند اشارهگر های هوشمند نیز در ++C وجود دارند اما در بسیاری از مواقع که نیاز است بسیار جزئی و حساس بر روی کدهای خود کار کنید از مدیریت حافظه به صورت دستی استفاده کنید.
-
1 امتیازبا سلام، یا توجه به مقالهٔ ذکر شده زیر در ارتباط با انتخاب زبان برنامهنویسی و تفاوت عمدهٔ زبانهای کامپایلری و مفسری لازم است تعاریفی در رابطه با جزئیات زبانهای کامپایلری که خود تفاوتهایی را شامل میشوند بپردازیم. در صورتی که مقالهٔ زیر را مطالعه نکردهاید پیشنهاد میکنیم قبل از خواندن این مقاله آن را مرور کنید. در این مقاله شما تفاوت عمدهٔ آنها را خواهید آموخت که شامل توضیحات کامپایلر و روشهای کامپایل میباشد. کامپایلر چیست؟ کامپایلر به ابزار (برنامه یا مجموعهای از برنامهها) گفته میشود، که متنِ نوشته شدهٔ برنامهنویسان (در قالب کُد) را که از سطح بالاتر (زبان مبدأ) برخوردار است و درک آن برای انسان مُیسر میباشد، دریافت کرده و آن را به زبان سطح پایینتر (زبان مقصد) مانند اسمبلی یا کُد ماشین ترجمه میکند. زبانهای کامپایلری در دو دستهی بومی (Native) و مجازی (Virtual) کامپایل از نوع بومی روشی است که کدهای نوشته شده را به صورت مستقیم به کُد ماشین ترجمه میکند. کامپایل از نوع مجازی روشی است که کدهای نوشته شده را ابتدا به کُدمیانی (کدمشترک یا همان بایت کُد - Byte Code) در جاوا و زبان میانی (CIL) در Net. تبدیل میکند که خودِ آن شبیه به کُد ماشین است. در این فرایند کد مربوطه توسط کامپایلر مخصوص یعنی JIT (کامپایلری از نوع Just-In-Time) در زمان اجرا توسط سیستمعامل، به دستورالعملهای قابل فهم برای پردازنده تفسیر و اجرا میشود (که این فرایند شبیه به فرایند عملکرد اجرایی مفسرها است). زبانهای بومی (زبانهایی که کد آنها به کد ماشین به صورت مستقیم توسط کامپایلر قبل از اجرای آنها توسط سیستمعامل، ترجمه میشوند که به اصطلاح ahead-of-time (جلوتر از زمان) یا همان AOT نام دارد) مانند: C, C++, Rust, Haskell, Clean, Swift, Go, Fortran, D زبانهای مجازی (زبانهایی که کد آنها توسط یک رابط میانی به زبان مشترک ترجمه میشود) : Java و خانوادهٔ داتنت مانند C#, Visual Basic.Net و C++/CLR نکته قابل توجه در مورد C++/CLR آن است که این نوع استاندارد در مورد سیپلاسپلاس بر پایهٔ چهارچوب داتنت است. در این نسخه از زبان شما با محدودیتهای بسیاری مواجه بوده و به ویژگیها و کیفیت نهایی برنامههای تولید شدهٔ واقعی در قالب Native محروم خواهید بود. روش کامپایل و و انواع آنها کامپایلرها به صورت بومی (Native) و کراس (Cross) تقسیم بندی میشوند. به طور کلی آن دسته از کامپایلرها که کدهای باینری را تولید میکنند از نوع محلی یا همان Native نام دارند؛ در واقع به هر کامپایلی که بر روی سیستمهای معماری x86 نوع x86، بر روی سیستمهای x86-64 نوع x86-64 و بر روی سیستمهای PowerPC نوع powerpc و بر روی arm نوع arm را تولید کند کامپایل بومی میگویند. چرا که تنها برای یک پلتفرمِ هدف کدهای ماشین رو تولید خواهد کرد (در صورت نیاز برای اجرا بر روی پلتفرمهای دیگر باید آن را بر روی پلتفرم متناسب با آن پیکربندی کنید) در واقع یک وابستگی به سختافزار وجود خواهد داشت که کدهای شما بر اساس آن تولید میشود. اما در رابطه با کامپایلرها از نوع Cross یا به اصطلاح عبوری وابستگی خاصی به سختافزار ندارند، در این روش کافی است سختافزار، پلتفرم (معماری و سیستمعامل) مورد نظر را یک بار برای آن معرفی کرده و اقدام به کامپایل کنید. کامپایل به صورت کراس کدها را به برنامهٔ قابل اجرا برای بیشتر از یک پلتفرم فراهم میکند. برای مثال در صورتی که بر روی پلتفرم ویندوز هستید میتوانید برنامهٔ نوشته شدهٔ خود را برای پلتفرم اندروید یا آیاواس که برای arm هستند ارائه دهید. اولین کامپایلری که این ویژگی را پشتیبانی میکند GCC است. این امکان وجود دارد که شما کدهای نوشته شدهٔ خود را بر روی پلتفرم میزبان برای پلتفرمهای هدف (مقصد) کامپایل کنید. البته جدیداً کامپالر کلَنگ (Clang) به عنوان یکی از بهترین انتخاب بین برنامهنویسان ++C جهت کراسکامپایل مطرح میشود. کامپایلرهای پیشنهادی: GCC Clang MSVC مزایا و معایب زبانهای کامپایلری از نوع کلاس بومی (Native) از سرعت بسیار بالایی برخوردار هستند (دلیل آن ترجمهٔ مستقیم کدها به کد ماشین است) در مقابل بزرگترین مزیتی که زبانهای نوع کلاس مجازی (Byte Code) دارند به خاطر وجود یک برنامهٔ واسط جهت شبیهسازی کدهای ترجمه شده به کد قابل فهم برای پردازنده، اجرا شدن آنها در هر پلتفرم بدون کامپایل مجدد امکان پذیر است که البته این ویژگی خود نیازمند نصب بودن JVM بر روی پلتفرم مربوطه میباشد. در نوع بومی برای اجرا در هر پلتفرم لازم است سورس کدها را برای پلتفرم مقصد کامپایل کنید که نیازی به وجود ماشین مجازی مانند JVM یا برنامهٔ خاصی ندارد. کدهای میانی تحت کامپایلِ درجا JIT : Just In Time همانطور اشاره شد زبانهای برنامهنویسی Java و خانوادهٔ Net. به ترتیب توسط Java Byte Code بر روی ماشین مجازی جاوا JVM و CIL : Common Intermediate Language بر روی زیر ساخت CLI : Common Language Infrastructure از هم جدا میشوند. در نظر داشته باشید CIL نام تغییر یافتهٔ MSIL میباشد. معنای ماشین مجازی CLR و JVM جیویام یا همان JVM : Java virtual machine نوعی ماشین مجازی (واسطی) است که وظیفه اجرای کد جاوا را برعهده دارد. زمانی که در مورد برنامههای نوشته شده توسط جاوا صحبت میکنیم، حتما باید JVM بر روی سیستم شما نصب باشد تا قابلیت اجرا شدن برنامههای تحت جاوا را داشته باشد. سیاِلآر یا همان CLR : Common Language Runtime نوعی ماشین مجازی (واسطی) است که کدهای مربوط به CIL را برای سیستم تفسیر و اجرا میکند. البته تفاوتهایی در خروجی این کد با کدهای جاوا وجود دارد که در آن زبان IL به عنوان یک زبان شبیه به زبان ماشین مانند اسمبلی (assembly) میباشد. در CLR کدهای تولید شدهٔ بایتکد، کمتر از دستورالعملها و ابردادههای JVM است. تفاوت اصلی CLR و JVM تفاوت اصلی JVM و CLR در این است که JVM جهت اجرای کدهای جاوا استفاده میشود و CLR مدیریت برنامههای اجرایی داتنت را مدیریت میکند. به طور کلی، JVM امکان اجرای کدهای کامپایل شدهی جاوا را فراهم میکند که در بسیاری از سیستمعاملها و سختافزارها موجود است. از سوی دیگر، CLR یک بستر (محیطی) را برای اجرای برنامههای نوشته شده در چهارچوب داتنت همراه با امکان مدیریت حافظه، مدیریت خطاها، امنیت و غیره را فراهم میکند. نسل جدید JIT برای داتنت (نام کد RyuJIT) به لُطف Net. و نسخهٔ Net Core. نام RyuJIT کُد شناسه از کامپایلر Net. است که وظیفهٔ آن ترجمهٔ کدهای #C به بایتکُد IL است که RyuJIT کدهای بایتکُد از نوع IL را به کُد ماشین ترجمه میکند. همانطور که مشخص است، جهان به سمت محاسبات ۶۴ بیتی حرکت میکند، اما باید در نظر داشته باشید سرعت برنامههای ۶۴ بیتی همیشه بیشتر از ۳۲ بیتیها نمیباشد! برای مثال نمونهای از آن را میتوان به کامپایلر JIT برای داتنت مثال زد؛ تغییرات و بهبودهایی که در نسل بعدی کامپایلر JIT بر روی Net Core. صورت گرفته است نسخهٔ ۶۴ بیتی آن است که اجازه میدهد برنامهها دو برابر سریعتر از نسخهٔ قبلی خود در داتنِت اجرا شود. این امر باعث میشود که نظرات شما را در مورد این نسخه از کامپایلر داتنت تغییر دهد. همانطور که به نظر میرسد، معماری ۳۲ بیتی x86 کامپیوترها که از زمانهای ایجاد تا به کنون در نوع خود بسیار عالی بودهاند، اما مشکل بزرگی که دارند متاسفانه پشتیبانی تا حداکثر ۴ گیگابایت حافظهٔ اصلی (Ram) است. با توجه به رُشد روز افزون معماری ۶۴ بیتی x64 نیاز به حافظههای بیش از ۴ گیگابایت جدی شد و امروزه ما میبینیم که اکثر سختافزارها و حتی دستگاههای موبایل نیز مجهز به حافظههای بیش از ۴ گیگابایت هستند. برای بهرهمندی از قابلیتهای معماری جدید Net Core. کامپایلر خود را با بهینهسازیهای چشمگیری ارائه داده است که میتواند تا دو برابر سریعتر از نسل قبلی خود عمل کند. در نظر داشته باشید که، معماری RyuJIT تقریبا نه سال پیش طراحی شده است و کارهای اجرایی از هفت سال پیش آغاز شده است. RyuJIT به عنوان یک کامپایلر تکامل یافته از JIT32 موجود (که از x86 و ARM32 پشتیبانی میکند) اجرا شد و به تدریج جایگزین بسیاری از بخشهای "بَکاِند" آن کامپایلر با یک تخصیص دهندهٔ رجیستر جدید و تولید کنندهٔ کد همراه با برخی از ویژگیهای جدید و بهبود یافته در "فرانتاِند" برای بهینه سازی در اجرا معرفی شده است. در طول این انتقال به کدهای نسل جدید معماری، سعی بر این بوده است که کدهای نسل قبل را در کنار نسل جدید نگه داشته شود. انجام این کارها مزایایی داشته است مانند حفظ هزینهها و باز نویسیهای بسیار! اما مسلماً هزینههایی هم دربر داشته است که کمترین آنها سردرگم بودن توسعهدهندگان در بارهٔ آیندهٔ Jit بوده است. در حال حاضر عملکرد RyuJIT برای کدهای قدیمی بسیار خوب بوده است و سرانجام وقت آن رسیده است که کدهای نسل قبل در JIT در آیندهای نزدیک تمرکز شود. نسل جدید از JIT با تمرکز بر پشتیبانی از معماری ۶۴ بیتی با نام RyuJIT سریعتر از JIT64 است. زبانهایی که از JIT/CLR پشتیبانی میکنند زبانهای اصلی این کامپایلر C#, VB.Net و C++ Managed یا همان C++/CLR میباشند. نکته: سی++ در این نسخه تغییراتی از جانب مایکروسافت داشته است و از نسخهٔ استاندارد آن کمی متفاوت است. برای مثال مدیریت حافظه به صورت خودکار و همچنین تغییرات جزئی از قبیل سینتکس را دارا میباشد. ساختار برنامههای زبان کامپایلری از نوع بومی (Native) در زبانهای مادر C و ++C در صورتی که تمایل دارید در رابطه با جزئیات ساختار برنامههای سریعترین زبانهای برنامهنویسی یعنی C و ++C مطلع شوید توصیه میشود مقالهٔ مرتبط با آن را که در زیر آمده است مطلعه کنید.