رفتن به مطلب
مرجع رسمی سی‌پلاس‌پلاس ایران
  • مدیریت منابع در ++C و آشنایی با اصطلاحات مدرن


    کامبیز اسدزاده

    اصطلاحاتی که بهتر است در مورد C++  مدرن بدانید!

    داشتم به این فکر می‌کردم که برخی از مبتدیان برنامه‌نویسی به خصوص کسانی که به سراغ زبان‌هایی مثل سی++ می‌روند معمولاً مستقیم وارد کد نویسی می‌شوند و به این گمان که آغاز برنامه‌نویسی یعنی نوشتن یک کد با خروجی «سلام، دنیا»! دریغ از آن‌ که بعضی از موارد مانند «معرفی کامپایلر و انواع آن» و حتی «ساختار برنامه‌های نوشته شده تحت سی‌پلاس‌پلاس» و یا حتی «مدیریت حافظه» را در نظر بگیرند! من معمولاً در مقالات و آموزش‌های خودم به این اشاره می‌کنم که قبل از هر چیز باید با ساختار برنامه‌های نوشته شدهٔ یک زبان آشنا شد و سپس به بررسی موارد دیگر مانند نحو زبان و یا دیگر ویژگی‌های آن.

    dangers-of-garbage-collection-blog-header@2x.png

    بنابراین، یکی از خطرناک‌ترین عواملی که موجب خونریزی داخلی یک نرم‌افزار در برنامه‌های نوشته شده توسط برنامه‌نویس درC++  می‌شود عدم مدیریت حافظهٔ اختصاص یافته است که باید بعد از اختصاص یافتن حافظه در زمان معین آن را آزادسازی کند. در صورتی که این کار صورت نگیرد عمل Memory Leak (نَشتِ حافظه) رخ داده است.

    بسیاری از علاقه‌مندان بر این باورند که چون سی++ دارای GC یا همان Garbage Collector (زباله‌روب) نیست که البته صحیح است! سی++ دارای GC نیست و این امر محدودیت یا نکته ضعف آن هم نیست! سی++ همه چیز را آزادانه در اختیار برنامه‌نویس قرار می‌دهد تا خود در زمان مناسب روش مدیریت حافظه را انتخاب کند. در علوم رایانه بازیافت حافظه یا زباله‌روبی نوعی مدیریت حافظهٔ خودکار است که عمل مدیریت حافظه‌های اختصاص یافته شده را به دست می‌گیرد و اکثر زبان‌های برنامه‌نویسی مانند #C، جاوا  و دیگر موارد مشابه به آن مجهز به این ویژگی هستند که البته وجود چنین ابزار‌هایی می‌تواند توهمی را ایجاد کند مبنی بر آن که دیگر نیازی به مدیریت منابع نیست، اما در بعضی موارد مدیریت منابع هنوز یک الزام است چرا که منابع آزاد شده هنوز هم دلیل بر نشتِ حافظه هستند.

    این نشت حافظه زمانی اتفاق می‌افتد که اشیاء هنوز قابل دسترس از طرف اشیاءای که زنده هستند اما هرگز مورد استفادهٔ دوباره قرار نمی‌گیرند اتفاق بی‌افتد.

    در بسیاری از زبان‌های برنامه‌نویسی این ویژگی وجود دارد که طبیعتاً مدیریت توسط GC راه حل بسیار خوب و بی نقصی نیست. اما با توجه به عدم وجود GC در سی++ اکثراً با روش‌های دستی برای مدیریت حافظه می‌پردازند که رایج‌ترین روش آن استفاده از عمل new و delete در اختصاص دادن و آزاد‌سازی حافظه است.

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

     

    ram.png

    RAII : Resource Acquisition is initialization

     

    بنابراین، باید در نظر گرفت مدیریت حافظه از استاندارد ۱۱ به بعد این زبان به روش‌های بسیار مدرن‌تری هوشمند سازی شده است. یکی از بهترین تکنیک‌های موجود در هستهٔ زبان اصطلاح Raiiاست.

    الگوی RAII مخفف «Resource Acquisition is initialization» که به عنوان یک اصطلاح در برنامه‌نویسی مطرح می‌شود به صورت یک تکنیک (کنترل تخصیص منابع و آزاد‌سازی آن‌ها) یکی از ویژگی‌های اصلی در سی‌پلاس‌پلاس است. با قرار دادن چنین کدی دیگر نیاز به فراخوانی آن کد توسط برنامه‌نویس در مخرب (ویرانگر)  نیست و کامپایلر خود این کار را انجام می‌دهد. به طور کلی این الگو هر شیء را مجبور می‌سازد تا در زمان مواجه با رفتار‌های ناهنجار خود را پاکسازی کند.

    •  به طور کلی هنگامی که شما یک شیء را مقدار‌دهی اولیه می‌کنید، قبل از انجام آن باید منابع مورد نیاز آن را تأمین کنید (در سازنده).
    • هنگامی که یک شیء از محدوده‌ خارج می‌شود، هر منبعی را که مورد استفاده قرار داده است باید آزاد کند (در مخرب - ویرانگر).

    نکات کلیدی

    1.  هرگز نباید یک شیء به حالت نیمه آماده یا نیمه از بین‌ رفته وجود داشته باشد!
    2. وقتی که یک شیء ساخته می‌شود، آن شیء باید در حالت آماده باش برای استفاده باشد.
    3. وقتی یک شیء از محدوده خارج می‌شود، باید منابع اختصاص یافتهٔ خود را در حافظه آزاد کند (کاربر مجبور به انجام کار دیگری نیست).

    آیا RAII عنوان بدی برای مفهوم این تکنیک است!

    از نظر خالق سی‌پلاس‌پلاس نام بهتر می‌تواند به صورت زیر باشد:

    مدیریت منابع مبتنی بر حوزه (محدوده یا دامنه) : Scope Based Resource Management

    چیزی که تکنیک RAII را نقض می‌کند چیست؟

    اشاره‌گر‌های خام و تخصیص حافظه

    1. فراخوانی با کلمهٔ کلیدی new برای دست آوردن یا اختصاص دادن منبع (حافظه).
    2. فراخوانی با کلمهٔ کلیدی 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 برای این امر در چیست؟

    کلاسی داشته باشید که :

    1. حافظه را هنگام مقدار‌دهی اولیه تخصیص دهد.
    2. حافظه را هنگام فراخوانی مخرب (ویرانگر) آزاد کند.
    3. دسترسی به اشاره‌گر‌های زیرین را امکان‌پذیر کند.

    اشاره‌گر‌های هوشمند (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.

     

    ویرایش شده در توسط کامبیز اسدزاده

    • پسندیدن 4
    • تشکر شده 2


    بازخورد کاربر

    نظرهای پیشنهاد شده

    هیچ دیدگاهی برای نمایش وجود دارد.



    مهمان
    از هم اکنون ارسال دیدگاه قفل گردید

  • کاربران آنلاین در این صفحه   0 کاربر

    هیچ کاربر عضوی،در حال مشاهده این صفحه نیست.

×
×
  • جدید...