نحوه راه اندازی گوشی های هوشمند و رایانه های شخصی. پرتال اطلاعاتی
  • خانه
  • ویندوز 8
  • استفاده از حذف جدید برای پیاده سازی آرایه ها. §11 آرایه ها و اشاره گرها

استفاده از حذف جدید برای پیاده سازی آرایه ها. §11 آرایه ها و اشاره گرها

15.8. اپراتورهای جدید و حذف

به طور پیش فرض، تخصیص یک شی کلاس از یک پشته و آزاد کردن حافظه اشغال شده با استفاده از عملگرهای global new() و delete() تعریف شده در کتابخانه استاندارد C++. (ما در بخش 8.4 به این عملگرها نگاه کردیم.) اما یک کلاس نیز می تواند پیاده سازی کند استراتژی خودمدیریت حافظه با ارائه اپراتورهای عضو به همین نام. اگر در یک کلاس تعریف شده باشند، به جای عملگرهای سراسری برای تخصیص و آزادسازی حافظه برای اشیاء این کلاس فراخوانی می شوند.

بیایید عملگرهای new() و delete() را در کلاس Screen تعریف کنیم.

اپراتور عضو new() باید مقداری از نوع void* را برگرداند و به عنوان اولین پارامتر خود مقداری از نوع size_t را در نظر بگیرد، جایی که size_t typedef تعریف شده در فایل هدر سیستم است. این هم اعلامیه او:

void *operator new(size_t);

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

صفحه *ps = صفحه نمایش جدید.

یک شی Screen در پشته ایجاد می کند و از آنجایی که این کلاس یک عملگر new() دارد، فراخوانی می شود. پارامتر size_t اپراتور به طور خودکار به مقدار مقدار دهی اولیه می شود اندازه مساویصفحه نمایش بر حسب بایت

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

با استفاده از عملگر تفکیک دامنه جهانی، می توانید global new() را فراخوانی کنید حتی اگر کلاس Screen نسخه خود را تعریف کند:

صفحه نمایش *ps = ::new Screen;

عملگر void delete(void *);

وقتی عملوند delete یک اشاره گر به یک شی از نوع کلاس باشد، کامپایلر بررسی می کند که آیا عملگر ()delete در آن کلاس تعریف شده است یا خیر. اگر بله، برای آزاد کردن حافظه فراخوانی می شود، در غیر این صورت - نسخه جهانیاپراتور. دستورالعمل های بعدی

حافظه اشغال شده توسط شی Screen که با ps به آن اشاره شده است را آزاد می کند. از آنجایی که Screen دارای یک اپراتور عضو (delete) است، این همان چیزی است که استفاده می شود. پارامتر عملگر از نوع void* به طور خودکار به مقدار ps مقداردهی می شود. افزودن ()delete به یک کلاس یا حذف آن از کلاس هیچ تاثیری بر کد کاربر ندارد. تماس برای حذف هم برای اپراتور جهانی و هم برای اپراتور عضو یکسان به نظر می رسد. اگر کلاس Screen نداشت اپراتور خود delete()، سپس تماس صحیح باقی می ماند، فقط اپراتور جهانی به جای اپراتور عضو فراخوانی می شود.

با استفاده از عملگر تفکیک دامنه جهانی، می توانید delete() را فراخوانی کنید حتی اگر Screen نسخه خود را تعریف کرده باشد:

که در مورد کلیعملگر delete() مورد استفاده باید با عملگر new() که حافظه با آن تخصیص داده شده مطابقت داشته باشد. به عنوان مثال، اگر ps به یک منطقه حافظه اختصاص داده شده توسط global new اشاره کند، باید از delete() جهانی برای آزاد کردن آن استفاده شود.

عملگر delete() تعریف شده برای نوع کلاس می تواند به جای یک پارامتر دو پارامتر بگیرد. پارامتر اول باید همچنان از نوع void* باشد و دومی باید از نوع از پیش تعریف شده size_t باشد (به یاد داشته باشید که شامل فایل هدر):

// جایگزین می کند

// عملگر void delete(void *);

اگر پارامتر دوم وجود داشته باشد، کامپایلر به طور خودکار آن را با مقداری برابر با اندازه در بایت شی آدرس دهی شده توسط پارامتر اول مقداردهی اولیه می کند. (این گزینه در یک سلسله مراتب کلاس مهم است، جایی که عملگر delete() را می توان توسط یک کلاس مشتق شده به ارث برد. وراثت با جزئیات بیشتر در فصل 17 مورد بحث قرار گرفته است.)

بیایید پیاده سازی عملگرهای new() و delete() در کلاس Screen را با جزئیات بیشتری بررسی کنیم. استراتژی تخصیص حافظه ما بر این اساس خواهد بود لیست پیوندیاشیاء صفحه، که ابتدا توسط عضو freeStore به آن اشاره می شود. هر بار که عملگر عضو new() فراخوانی شود، شی بعدی در لیست برگردانده می شود. هنگامی که delete() فراخوانی می شود، شی به لیست برمی گردد. اگر هنگام ایجاد یک شی جدید، لیست آدرس‌دهی شده به freeStore خالی باشد، اپراتور جهانی new() فراخوانی می‌شود تا یک بلوک حافظه کافی برای ذخیره اشیاء screenChunk کلاس Screen به دست آورد.

هم screenChunk و هم freeStore فقط مورد توجه Screen هستند، بنابراین ما آنها را به اعضای خصوصی تبدیل خواهیم کرد. علاوه بر این، برای تمام اشیاء ایجاد شده کلاس ما، مقادیر این اعضا باید یکسان باشد و بنابراین، آنها باید ثابت اعلام شوند. برای پشتیبانی از ساختار لیست پیوندی اشیاء Screen، به عضو سوم بعدی نیاز داریم:

void *operator new(size_t);

عملگر void delete(void *, size_t);

صفحه نمایش ثابت *فروشگاه آزاد.

static const int screenChunk.

اینجا یکی از پیاده سازی های ممکنعملگر new() برای کلاس Screen:

#include "Screen.h"

#include cstddef

// اعضای استاتیک مقداردهی اولیه می شوند

// V فایل های منبعبرنامه ها، نه در فایل های هدر

صفحه نمایش *صفحه نمایش::freeStore = 0;

const int Screen::screenChunk = 24;

void *صفحه نمایش::اپراتور جدید (اندازه_t)

اگر (!فروشگاه آزاد) (

// لیست پیوندی خالی است: get بلوک جدید

// عملگر جهانی new نامیده می شود

size_t chunk = screenChunk * اندازه;

reinterpret_cast Screen* (کاراکتر جدید[ تکه ]);

// بلوک دریافتی را در لیست قرار دهید

p != &freeStore[ screenChunk - 1 ];

freeStore = freeStore-next;

و در اینجا اجرای عملگر delete() است:

void Screen:: حذف اپراتور (void *p، size_t)

// شی "راه دور" را به عقب وارد کنید،

// به لیست رایگان

(Static_cast Screen* (p))-next = freeStore;

freeStore = static_cast Screen* (p);

عملگر new() را می توان در یک کلاس بدون یک delete() متناظر اعلام کرد. در این حالت، اشیاء با استفاده از عملگر جهانی به همین نام آزاد می شوند. همچنین مجاز است که اپراتور delete() را بدون new() اعلام کند: اشیاء با استفاده از عملگر جهانی به همین نام ایجاد خواهند شد. با این حال، معمولاً این عملگرها به طور همزمان اجرا می شوند، مانند مثال بالا، زیرا توسعه دهنده کلاس معمولاً به هر دو نیاز دارد.

آنها اعضای ایستا کلاس هستند، حتی اگر برنامه نویس به صراحت آنها را به عنوان چنین اعلام نکند، و مشمول محدودیت های معمول برای چنین توابع عضوی هستند: آنها از این نشانگر عبور نمی کنند، و بنابراین فقط می توانند مستقیماً به اعضای ایستا دسترسی داشته باشند. (به بحث در مورد توابع عضو استاتیک در بخش 13.5 مراجعه کنید.) دلیل ایستا شدن این عملگرها این است که آنها قبل از ساخته شدن شی کلاس (new()) یا پس از نابودی آن (delete()) فراخوانی می شوند.

تخصیص حافظه با استفاده از عملگر new() برای مثال:

Screen *ptr = new Screen(10,20);

// کد شبه در C++

ptr = Screen::operator new(sizeof(Screen));

صفحه نمایش::صفحه نمایش(ptr, 10, 20);

به عبارت دیگر، عملگر new() کلاس ابتدا برای تخصیص حافظه برای شیء فراخوانی می شود و سپس شی توسط سازنده مقداردهی اولیه می شود. اگر new() ناموفق باشد، استثنایی از نوع bad_alloc مطرح می شود و سازنده فراخوانی نمی شود.

آزاد کردن حافظه با استفاده از عملگر delete() برای مثال:

معادل اجرای متوالیچنین دستوراتی:

// کد شبه در C++

صفحه نمایش::~صفحه نمایش(ptr);

صفحه نمایش:: حذف اپراتور (ptr، sizeof(*ptr));

بنابراین، هنگامی که یک شی از بین می رود، ابتدا تخریبگر کلاس فراخوانی می شود و سپس عملگر ()delete تعریف شده در کلاس برای آزادسازی حافظه فراخوانی می شود. اگر ptr 0 باشد، نه destructor و نه delete() فراخوانی نمی شود.

15.8.1. اپراتورهای جدید و حذف

عملگر new() که در بخش فرعی قبلی تعریف شده است، تنها زمانی فراخوانی می شود که حافظه برای یک شی اختصاص داده شود. بنابراین، در این دستورالعمل new() از کلاس Screen نامیده می شود:

Screen *ps = new Screen(24, 80);

در حالی که در زیر عملگر جهانی new() برای تخصیص حافظه از پشته برای آرایه ای از اشیاء از نوع Screen فراخوانی می شود:

// با نام Screen::operator new()

صفحه نمایش *psa = صفحه نمایش جدید;

کلاس همچنین می تواند عملگرهای new() و delete() را برای کار با آرایه ها اعلام کند.

عملگر عضو new() باید مقداری از نوع void* را برگرداند و مقداری از نوع size_t را به عنوان اولین پارامتر خود در نظر بگیرد. در اینجا اعلامیه او برای Screen است:

void *operator new(size_t);

هنگام استفاده از new برای ایجاد آرایه ای از اشیاء از نوع کلاس، کامپایلر بررسی می کند که آیا کلاس دارای یک عملگر new() تعریف شده است یا خیر. اگر بله، برای تخصیص حافظه برای آرایه فراخوانی می شود، در غیر این صورت، global new() فراخوانی می شود. که در دستورالعمل های زیرآرایه ای از ده شیء Screen در پشته ایجاد می شود:

صفحه *ps = صفحه نمایش جدید.

این کلاس دارای عملگر new() است، به همین دلیل برای تخصیص حافظه فراخوانی می شود. پارامتر size_t آن به طور خودکار به مقدار حافظه، بر حسب بایت، مقداردهی اولیه می شود که برای نگهداری ده شیء Screen لازم است.

حتی اگر یک کلاس دارای یک اپراتور عضو new() باشد، برنامه نویس می تواند () global new را برای ایجاد یک آرایه با استفاده از عملگر جهانی scope رزولوشن فراخوانی کند:

صفحه نمایش *ps = ::new Screen;

عملگر delete() که عضوی از کلاس است، باید از نوع void باشد و void* را به عنوان اولین پارامتر خود در نظر بگیرد. در اینجا تبلیغ صفحه نمایش او به نظر می رسد:

عملگر void delete(void *);

برای حذف آرایه ای از اشیاء کلاس، delete باید به این صورت فراخوانی شود:

هنگامی که عملوند delete یک اشاره گر به یک شی از نوع کلاس است، کامپایلر بررسی می کند که آیا عملگر delete() در آن کلاس تعریف شده است یا خیر. اگر بله، برای آزاد کردن حافظه فراخوانی می شود، در غیر این صورت، نسخه جهانی آن فراخوانی می شود. یک پارامتر از نوع void* به طور خودکار به مقدار آدرس ابتدای ناحیه حافظه که آرایه در آن قرار دارد مقداردهی اولیه می شود.

حتی اگر یک کلاس دارای یک اپراتور عضو ()delete باشد، برنامه نویس می تواند با استفاده از عملگر تفکیک دامنه جهانی، delete() را فراخوانی کند:

افزودن یا حذف اپراتورهای new() یا delete() به یک کلاس بر روی کد کاربر تأثیری ندارد: تماس‌های اپراتورهای سراسری و عضو یکسان به نظر می‌رسند.

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

هنگامی که یک آرایه از بین می رود، ابتدا از تخریبگر کلاس فراخوانی می شود تا عناصر را از بین ببرد و سپس عملگر ()delete برای آزاد کردن تمام حافظه فراخوانی می شود. مهم است که از نحو صحیح استفاده کنید. اگر دستورالعمل

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

عملگر عضو delete() می تواند دو پارامتر به جای یک پارامتر داشته باشد و دومی باید از نوع size_t باشد:

// جایگزین می کند

// عملگر void delete(void*);

عملگر void delete(void*, size_t);

اگر پارامتر دوم وجود داشته باشد، کامپایلر به طور خودکار آن را با مقداری برابر با مقدار حافظه اختصاص داده شده برای آرایه در بایت مقداردهی اولیه می کند.

از کتاب راهنمای راهنمادر C++ نویسنده استراستراپ بیارن

R.5.3.4 عملیات حذف عملیات حذف یک شی ایجاد شده با استفاده از new.deallocation-expression را از بین می برد: ::opt delete cast-expression::opt delete cast-expression نتیجه از نوع void است. عملوند حذف باید یک اشاره گر باشد که جدید را برمی گرداند. تاثیر استفاده از عملیات حذف

از کتاب مایکروسافت ویژوال C++ و MFC. برنامه نویسی برای ویندوز 95 و ویندوز NT نویسنده فرولوف الکساندر ویاچسلاوویچ

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

از کتاب استفاده موثر C++. 55 راه های درستساختار و کد برنامه های خود را بهبود بخشید توسط مایرز اسکات

قانون 16: از همان اشکال new استفاده کنید و قطعه زیر را حذف کنید چه مشکلی دارد؟std::string *stringArray = new std::string;...delete stringArray;در نگاه اول، همه چیز در در نظم کامل- استفاده از new مربوط به استفاده از حذف است، اما چیزی در اینجا کاملاً اشتباه است. رفتار برنامه

از کتاب ویندوز میزبان اسکریپتبرای ویندوز 2000/XP نویسنده پوپوف آندری ولادیمیرویچ

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

برگرفته از کتاب استانداردهای برنامه نویسی در سی پلاس پلاس. 101 قانون و توصیه نویسنده الکساندرسکو آندری

روش حذف اگر پارامتر نیرو نادرست باشد یا مشخص نشده باشد، با استفاده از روش Delete حذف دایرکتوری با ویژگی فقط خواندنی غیرممکن خواهد بود. تنظیم نیرو بر روی true باعث می شود که این دایرکتوری ها بلافاصله حذف شوند. هنگام استفاده از روش Delete، مهم نیست که آیا مشخص شده

از کتاب مرجع فلش نویسنده تیم نویسندگان

روش حذف اگر پارامتر نیرو نادرست باشد یا مشخص نشده باشد، با استفاده از روش Delete حذف فایلی با ویژگی فقط خواندنی غیرممکن خواهد بود. تنظیم نیرو بر روی true باعث می شود چنین فایل هایی فوراً حذف شوند. نکته می توانید به جای روش Delete از روش DeleteFile استفاده کنید.

از کتاب Firebird DATABASE DEVELOPER'S GUIDE توسط بوری هلن

عملگرهای رابطه ای و منطقی از عملگرهای رابطه ای برای مقایسه مقادیر دو متغیر استفاده می شود. این عملگرها در جدول توضیح داده شده است. P2.11، فقط می تواند مقادیر منطقی true یا false را برگرداند. جدول P2.11. اپراتورهای رابطه ای شرایط اپراتور، زمانی که

برگرفته از کتاب لینوکس و یونیکس: برنامه نویسی پوسته. راهنمای توسعه دهنده. توسط Tainsley David

45. new و delete همیشه باید با هم طراحی شوند. خلاصه هر اضافه باری از عملگر void* new(parms) در یک کلاس باید با اضافه بار متناظر با اپراتور void delete (void* , parms) همراه باشد، که در آن parms لیستی از انواع است. پارامترهای اضافی(که اولین مورد همیشه std::size_t است). یکسان

از کتاب SQL Help نویسنده

حذف - حذف یک شی، عنصر آرایه یا متغیر حذف (اپراتور) این عملگر برای حذف یک شی، ویژگی شی، عنصر آرایه یا متغیرها از یک اسکریپت استفاده می شود. نحو: حذف شناسه؛ آرگومان ها: توضیحات: عملگر حذف یک شی یا متغیر، نام

برگرفته از کتاب درک SQL توسط گروبر مارتین

بیانیه را حذف کنید درخواست حذفبرای حذف کل ردیف های جدول استفاده می شود. SQL به یک دستور DELETE اجازه نمی دهد تا ردیف های بیش از یک جدول را حذف کند. یک درخواست DELETE که فقط یکی را اصلاح می کند خط فعلیمکان نما حذف موقعیت نامیده می شود.

از کتاب نویسنده

15.8. عملگرهای new و delete به طور پیش‌فرض، تخصیص یک شی کلاس از یک پشته و آزاد کردن حافظه اشغال شده توسط آن با استفاده از عملگرهای new() و delete() جهانی تعریف شده در کتابخانه استاندارد C++ انجام می‌شود. (ما در بخش 8.4 به این عملگرها نگاه کردیم.) اما یک کلاس می تواند پیاده سازی کند

از کتاب نویسنده

15.8.1. عملگرهای new و delete عملگر new() که در بخش فرعی قبلی تعریف شده است، تنها زمانی فراخوانی می شود که حافظه برای یک شی منفرد تخصیص داده شود. بنابراین، در این دستورالعمل، new() از کلاس Screen نامیده می شود: Screen::operator new()Screen *ps = new Screen(24, 80)؛ در حالی که زیر نامیده می شود.

همانطور که می دانید در زبان C از توابع malloc() و free() برای تخصیص پویا و آزادسازی حافظه استفاده می شود. با این حال، C++ شامل دو عملگر است که تخصیص و توزیع حافظه را کارآمدتر و ساده تر انجام می دهند. این اپراتورها جدید هستند و حذف می شوند. آنها شکل کلیدارای فرم:

متغیر اشاره گر = متغیر_نوع;

حذف متغیر اشاره گر

در اینجا، pointer-variable یک اشاره گر از نوع variable-type است. عملگر جدید حافظه را برای ذخیره مقداری از نوع variable_type اختصاص می دهد و آدرس آن را برمی گرداند. هر نوع داده ای را می توان با جدید قرار داد. عملگر حذف حافظه ای را که توسط pointer_variable به آن اشاره شده است آزاد می کند.

اگر عملیات تخصیص حافظه قابل انجام نباشد، عملگر جدید استثنایی از نوع xalloc را پرتاب می کند. اگر برنامه این استثنا را نگیرد، اجرای آن خاتمه می یابد. در حالی که این رفتار پیش فرض برای برنامه های کوتاه رضایت بخش است، اما در واقع برنامه های کاربردیبه طور معمول شما می خواهید استثنا را بگیرید و آن را به طور مناسب مدیریت کنید. برای گرفتن این استثنا، باید فایل سرصفحه استثنا.h را وارد کنید.

عملگر حذف فقط باید برای اشاره گرها به حافظه اختصاص داده شده با استفاده از عملگر جدید استفاده شود. استفاده از عملگر حذف با انواع دیگر آدرس ها می تواند مشکلات جدی ایجاد کند.

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

در زیر یک مثال ساده از استفاده از عملگرهای جدید و حذف آورده شده است. به استفاده از بلوک try/catch برای ردیابی خطاهای تخصیص حافظه توجه کنید.

#عبارتند از
#عبارتند از
int main()
{
int *p;
تلاش كردن (
p = int جدید; // تخصیص حافظه برای int
) گرفتن (xalloc xa) (
کوت<< "Allocation failure.\n";
بازگشت 1;
}
*p = 20; // به این مکان حافظه مقدار 20 را اختصاص می دهد
کوت<< *р; // демонстрация работы путем вывода значения
حذف p; // آزاد کردن حافظه
بازگشت 0;
}

این برنامه به متغیر p آدرس یک بلوک حافظه را به اندازه کافی بزرگ برای نگه داشتن یک عدد صحیح اختصاص می دهد. در مرحله بعد به این حافظه مقداری اختصاص داده می شود و محتویات حافظه روی صفحه نمایش داده می شود. در نهایت، حافظه اختصاص داده شده به صورت پویا آزاد می شود.

همانطور که اشاره شد، می توانید حافظه را با استفاده از عملگر جدید مقداردهی اولیه کنید. برای این کار باید مقدار مقداردهی اولیه را در پرانتز بعد از نام نوع مشخص کنید. به عنوان مثال، در مثال زیر، حافظه ای که توسط p به آن اشاره شده است به 99 مقداردهی اولیه می شود:

#عبارتند از
#عبارتند از
int main()
{
int *p;
تلاش كردن (
p = new int(99); // مقداردهی اولیه 99
) گرفتن (xalloc xa) (
کوت<< "Allocation failure.\n";
بازگشت 1;
}
کوت<< *p;
حذف p;
بازگشت 0;
}

برای تخصیص آرایه ها می توانید از new استفاده کنید. شکل کلی یک آرایه تک بعدی به صورت زیر است:

variable_pointer = متغیر_نوع [اندازه]؛

در اینجا اندازه تعداد عناصر آرایه را تعیین می کند. یک محدودیت مهم برای به خاطر سپردن در هنگام قرار دادن یک آرایه وجود دارد: آن را نمی توان مقداردهی اولیه کرد.

برای آزاد کردن یک آرایه اختصاص داده شده به صورت پویا، باید از فرم زیر از عملگر حذف استفاده کنید:

حذف متغیر اشاره گر

در اینجا پرانتز به اپراتور حذف اطلاع می دهد تا حافظه اختصاص داده شده برای آرایه را آزاد کند.

برنامه زیر به آرایه ای از 10 عنصر شناور حافظه اختصاص می دهد. به عناصر آرایه مقادیری از 100 تا 109 اختصاص داده می شود و سپس محتویات آرایه روی صفحه چاپ می شود:

#عبارتند از
#عبارتند از
int main()
{
شناور *p;
int i;
تلاش كردن (
p = شناور جدید ; // دریافت دهمین عنصر آرایه
) catch(xalloc xa) (
کوت<< "Allocation failure.\n";
بازگشت 1;
}
// تخصیص مقادیر از 100 تا 109
برای (i=0; i<10; i + +) p[i] = 100.00 + i;
// خروجی محتویات آرایه
برای (i=0; i<10; i++) cout << p[i] << " ";
حذف p; // حذف کل آرایه
بازگشت 0;
}

C++ از سه نوع اصلی پشتیبانی می کند تخلیه (توزیع) حافظه، که ما قبلاً با دو مورد از آنها آشنا هستیم:

تخصیص حافظه استاتیکبرای و متغیرها وجود دارد. هنگام شروع برنامه، حافظه یک بار تخصیص داده می شود و در کل برنامه حفظ می شود.

تخصیص خودکار حافظهنگه می دارد برای و . هنگام ورود به بلوکی که این متغیرها در آن قرار دارند، حافظه تخصیص داده می شود و هنگام خروج از آن آزاد می شود.

موضوع این مقاله است.

تخصیص حافظه استاتیک و خودکار دو چیز مشترک دارند:

اندازه متغیر/آرایه باید در زمان کامپایل مشخص باشد.

حافظه به طور خودکار تخصیص داده می شود (زمانی که متغیری ایجاد یا از بین می رود).

در بیشتر موارد این مشکلی ندارد. با این حال، وقتی صحبت از کار با ورودی خارجی می شود، این محدودیت ها می تواند منجر به مشکلاتی شود.

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

اگر لازم باشد اندازه همه متغیرها را در زمان کامپایل اعلام کنیم، بهترین کاری که می‌توانیم انجام دهیم این است که حداکثر اندازه آنها را حدس بزنیم، به امید اینکه این اندازه کافی باشد:

نام شخصیت؛ // امیدواریم که کاربر نامی را با کمتر از 30 کاراکتر وارد کند! رکورد رکورد؛ // امیدواریم تعداد رکوردها 400 عدد بیشتر نباشد! هیولا هیولا; // حداکثر رندر چند ضلعی 30 هیولا. // این رندر سه بعدی بهتر است کمتر از 40000 چند ضلعی باشد!

این یک راه حل بد حداقل به سه دلیل است:

اولاً، اگر متغیرها واقعاً مورد استفاده قرار نگیرند یا به طور کامل استفاده نشده باشند، حافظه هدر می رود. به عنوان مثال، اگر برای هر نام 30 کاراکتر اختصاص دهیم، اما نام ها به طور متوسط ​​15 کاراکتر را اشغال کنند، مصرف حافظه دو برابر بیشتر از مقدار مورد نیاز خواهد بود. یا یک آرایه رندر را در نظر بگیرید: اگر فقط از 20000 چند ضلعی استفاده کند، حافظه با 20000 چند ضلعی عملاً تلف می شود (یعنی استفاده نمی شود)!

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

در ویژوال استودیو می توانید با اجرای برنامه زیر این موضوع را بررسی کنید:

int main() (آرایه int؛ // اختصاص 1 میلیون مقدار صحیح)

محدودیت حافظه 1 مگابایتی می تواند برای بسیاری از برنامه ها، به ویژه برنامه هایی که از گرافیک استفاده می کنند، مشکل ساز باشد.

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

خوشبختانه این مشکلات با استفاده از تخصیص حافظه پویا به راحتی حل می شوند. تخصیص حافظه پویاراهی برای اجرای برنامه ها برای درخواست حافظه از سیستم عامل در صورت نیاز است. این حافظه از حافظه پشته محدود برنامه تخصیص داده نمی شود، بلکه از یک فضای ذخیره سازی بسیار بزرگتر مدیریت شده توسط سیستم عامل - پشته (پشته ها) . در رایانه های مدرن، اندازه پشته می تواند گیگابایت حافظه باشد.

تخصیص متغیر پویا

برای تخصیص پویا حافظه برای یک متغیر، از عملگر استفاده کنید جدید:

int جدید; // به صورت پویا یک متغیر عدد صحیح را تخصیص دهید و فوراً نتیجه را کنار بگذارید (چون ما آن را در جایی ذخیره نمی کنیم)

در مثال بالا، ما درخواست تخصیص حافظه برای یک متغیر صحیح از سیستم عامل را داریم. عملگر جدید حاوی آدرس حافظه اختصاص داده شده برمی گردد.

یک اشاره گر برای دسترسی به حافظه اختصاص داده شده ایجاد می شود:

int *ptr = int جدید; // به صورت پویا یک متغیر عدد صحیح را اختصاص دهید و آدرس آن را به ptr اختصاص دهید تا بتوانیم بعداً به آن دسترسی پیدا کنیم.

سپس برای بدست آوردن مقدار می توانیم اشاره گر را تغییر دهیم:

*ptr = 8; // مقدار 8 را به حافظه اختصاص داده شده جدید اختصاص دهید

این یکی از مواردی است که نشانگرها مفید هستند. بدون اشاره گر به آدرس حافظه تازه تخصیص یافته، راهی برای دسترسی به آن نخواهیم داشت.

تخصیص حافظه پویا چگونه کار می کند؟

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

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

بر خلاف تخصیص حافظه استاتیک یا خودکار، خود برنامه مسئول درخواست و برگرداندن حافظه تخصیص یافته به صورت پویا است.

راه اندازی متغیرهای تخصیص یافته به صورت پویا

هنگامی که یک متغیر را به صورت پویا تخصیص می دهید، می توانید آن را از طریق یا مقدار دهی اولیه یکنواخت (در C++11) مقداردهی اولیه کنید:

int *ptr1 = new int (7); // استفاده از مقدار دهی اولیه int *ptr2 = new int ( 8 ); // از مقدار دهی اولیه یکنواخت استفاده کنید

حذف متغیرها

وقتی همه چیزهایی که نیاز بود قبلاً با یک متغیر تخصیص یافته به صورت پویا انجام شده است، باید صریحاً به C++ بگویید این حافظه را آزاد کند. برای متغیرهای فردی این کار با استفاده از عملگر انجام می شود حذف:

// فرض کنید که ptr قبلاً با استفاده از عملگر new delete ptr تخصیص داده شده است. // حافظه اشاره شده با ptr را به سیستم عامل بازگردانید ptr = 0; // ptr را یک نشانگر تهی کنید (از nullptr به جای 0 در C++11 استفاده کنید)

"حذف حافظه" به چه معناست؟

عملگر حذف در واقع چیزی را حذف نمی کند. به سادگی حافظه ای را که قبلاً به سیستم عامل تخصیص داده شده بود برمی گرداند. سپس سیستم عامل می تواند این حافظه را به برنامه دیگری (یا دوباره همان برنامه) اختصاص دهد.

اگرچه ممکن است به نظر برسد که ما در حال حذف هستیم متغیر، اما این درست نیست! یک متغیر اشاره گر همچنان همان محدوده قبلی را دارد و می توان مانند هر متغیر دیگری مقدار جدیدی به آن اختصاص داد.

توجه داشته باشید که حذف یک اشاره گر که به حافظه اختصاص داده شده به صورت پویا اشاره نمی کند می تواند مشکلاتی ایجاد کند.

تابلوهای آویزان

C++ هیچ تضمینی در مورد اینکه چه اتفاقی برای محتویات حافظه آزاد شده یا ارزش اشاره گر در حال حذف خواهد افتاد، نمی دهد. در بیشتر موارد، حافظه ای که به سیستم عامل بازگردانده می شود دارای همان مقادیری است که قبلا داشت. رهایی، و نشانگر همچنان به حافظه اشاره می کند، فقط قبلاً آزاد شده (حذف شده است).

اشاره گر به حافظه آزاد شده نامیده می شود علامت آویزان. عدم ارجاع یا حذف یک نشانگر آویزان نتایج غیرمنتظره ای ایجاد می کند. برنامه زیر را در نظر بگیرید:

#عبارتند از int main() ( int *ptr = new int؛ *ptr = 8؛ // مقدار را در محل حافظه اختصاص داده شده قرار دهید delete ptr؛ // حافظه را به سیستم عامل برگردانید. ptr اکنون یک نشانگر آویزان است std:: کوت<< *ptr; // разыменование висячого указателя приведет к неожиданным результатам delete ptr; // попытка освободить память снова приведет к неожиданным результатам также return 0; }

#عبارتند از

int main()

int * ptr = int جدید ; // به صورت پویا یک متغیر عدد صحیح را اختصاص دهید

* ptr = 8 ; // مقدار را در سلول حافظه اختصاص داده شده قرار دهید

حذف ptr // حافظه را به سیستم عامل برگردانید. ptr اکنون یک اشاره گر آویزان است

std::cout<< * ptr ; // عدم ارجاع یک اشاره گر آویزان نتایج غیرمنتظره ای ایجاد می کند

حذف ptr // تلاش برای آزادسازی مجدد حافظه نتایج غیرمنتظره ای نیز به همراه خواهد داشت

بازگشت 0 ;

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

فرآیند آزادسازی حافظه نیز ممکن است منجر به ایجاد شود چندیننشانه های آویزان به مثال زیر توجه کنید:

#عبارتند از int main() ( int *ptr = int جدید؛ // به صورت پویا یک متغیر عدد صحیح را اختصاص می دهد int *otherPtr = ptr؛ // otherPtr اکنون به همان حافظه اختصاص داده شده اشاره می کند که ptr delete ptr؛ // حافظه را به سیستم عامل باز می گرداند. ptr و otherPtr اکنون نشانگرهای آویزان هستند ptr = 0؛ // ptr اکنون nullptr است // اما otherPtr هنوز یک نشانگر آویزان است! بازگشت 0؛ )

#عبارتند از

int main()

int * ptr = int جدید ; // به صورت پویا یک متغیر عدد صحیح را اختصاص دهید

int * otherPtr = ptr ; // otherPtr اکنون به همان حافظه اختصاص داده شده به عنوان ptr اشاره می کند

حذف ptr // حافظه را به سیستم عامل برگردانید. ptr و otherPtr اکنون نشانگرهای آویزان هستند

ptr = 0 ; // ptr اکنون nullptr است

// با این حال otherPtr هنوز یک اشاره گر آویزان است!

بازگشت 0 ;

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

ثانیاً، هنگامی که یک اشاره گر را حذف می کنید، و اگر بلافاصله پس از حذف خارج نشد، باید آن را نول کنید، یعنی. مقدار را روی 0 (یا در C++11) قرار دهید. منظور ما از "خارج شدن از محدوده بلافاصله پس از حذف" این است که شما نشانگر را در انتهای بلوکی که در آن اعلام شده است حذف می کنید.

قانون: نشانگرهای حذف شده را روی 0 (یا nullptr در C++11) تنظیم کنید، مگر اینکه بلافاصله پس از حذف از محدوده خارج شوند.

اپراتور جدید

هنگام درخواست حافظه از سیستم عامل، در موارد نادر ممکن است در دسترس نباشد (یعنی در دسترس نباشد).

به طور پیش فرض، اگر new کار نمی کند، حافظه تخصیص داده نمی شود، سپس یک استثنا ایجاد می شود bad_alloc. اگر این استثنا به درستی مدیریت نشود (که خواهد بود، زیرا ما هنوز استثناها و رسیدگی به آنها را پوشش نداده ایم)، برنامه به سادگی با یک خطای استثنای کنترل نشده خاتمه می یابد (خراش می شود).

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

int *value = new (std::nothrow) int; // اگر تخصیص پویا یک متغیر عدد صحیح با شکست مواجه شود، نشانگر مقدار تهی می شود

در مثال بالا، اگر new یک اشاره گر با حافظه تخصیصی پویا برگرداند، یک اشاره گر تهی برگردانده می شود.

عدم ارجاع آن نیز توصیه نمی شود، زیرا منجر به نتایج غیرمنتظره می شود (به احتمال زیاد خرابی برنامه). بنابراین، بهترین کار این است که تمام درخواست‌های تخصیص حافظه را بررسی کنید تا مطمئن شوید که درخواست‌ها موفق هستند و حافظه تخصیص داده شده است.

int *value = new (std::nothrow) int; // درخواست تخصیص حافظه پویا برای یک مقدار صحیح اگر (!value) // زمانی که new null برمی گرداند (یعنی حافظه تخصیص داده نشده است) مورد را رسیدگی کند ( // handle this case std::cout<< "Could not allocate memory"; }

از آنجایی که عدم تخصیص حافظه توسط اپراتور جدید بسیار نادر است، معمولا برنامه نویسان انجام این بررسی را فراموش می کنند!

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

نشانگرهای تهی (نشانگرهایی با مقدار 0 یا nullptr) به ویژه در فرآیند تخصیص حافظه پویا مفید هستند. به نظر می رسد حضور آنها می گوید: "هیچ حافظه ای به این اشاره گر اختصاص داده نشده است." و این به نوبه خود می تواند برای انجام تخصیص حافظه شرطی استفاده شود:

// اگر ptr هنوز به حافظه اختصاص داده نشده است، آن را تخصیص دهید اگر (!ptr) ptr = new int;

حذف نشانگر تهی تأثیری ندارد. بنابراین موارد زیر ضروری نیست:

if (ptr) delete ptr;

if (ptr)

حذف ptr

در عوض می توانید به سادگی بنویسید:

حذف ptr

اگر ptr تهی نباشد، متغیر تخصیص یافته به صورت پویا حذف خواهد شد. اگر مقدار اشاره گر صفر باشد، هیچ اتفاقی نمی افتد.

نشت حافظه

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

تابع زیر را در نظر بگیرید:

void doSomething() (int *ptr = new int;)

عملگر جدید به شما اجازه می دهد تا حافظه را برای آرایه ها اختصاص دهید. او باز میگردد

اشاره گر به اولین عنصر آرایه در پرانتز مربع است. هنگام تخصیص حافظه برای آرایه های چند بعدی، همه ابعاد به جز سمت چپ باید ثابت باشند. بعد اول را می توان توسط متغیری مشخص کرد که مقدار آن در زمان استفاده از new برای کاربر مشخص است، به عنوان مثال:

int *p=new int[k]; // خطا نمی تواند از "int (*)" به "int *" تبدیل شود

int (*p)=new int[k]; // درست

هنگام تخصیص حافظه برای یک شی، مقدار آن تعریف نشده خواهد بود. با این حال، به یک شی می توان مقدار اولیه داد.

int *a = new int (10234);

از این گزینه برای مقداردهی اولیه آرایه ها نمی توان استفاده کرد. با این حال

به جای مقدار اولیه می توانید یک لیست جدا شده با کاما قرار دهید

مقادیری که هنگام تخصیص حافظه برای آرایه به سازنده ارسال می شود (جرم

siv اشیاء جدید مشخص شده توسط کاربر). حافظه برای مجموعه ای از اشیاء

فقط در صورتی قابل تخصیص است که کلاس مربوطه داشته باشد

یک سازنده پیش فرض وجود دارد.

matr())(; // سازنده پیش فرض

matr(int i، float j): a(i)،b(j) ()

( matr mt(3,.5);

matr *p1=new matr; // true p1 - اشاره گر به 2 شی

matr *p2=matr جدید (2,3.4); // نادرست است، مقداردهی اولیه امکان پذیر نیست

matr *p3=matr جدید (2,3.4); // p3 true – شیء اولیه

(int i; // جزء داده کلاس A

A()() // سازنده کلاس A

~A()() // تخریب کننده کلاس A

( A *a,*b; // شرح اشاره گرها به یک شی از کلاس A

float *c,*d; // شرح اشاره گر به عناصر نوع شناور

a = جدید A; // تخصیص حافظه برای یک شی از کلاس A

b=جدید A; // تخصیص حافظه برای آرایه ای از اشیاء کلاس A

c=new float; // تخصیص حافظه برای یک عنصر شناور

d = شناور جدید; // تخصیص حافظه برای آرایه ای از عناصر شناور

حذف a; // آزاد کردن حافظه اشغال شده توسط یک شی

حذف b; // آزاد کردن حافظه اشغال شده توسط آرایه ای از اشیاء

حذف c; // آزاد کردن حافظه یک عنصر شناور

حذف شده؛ ) // آزاد کردن حافظه آرایه ای از عناصر شناور

سازماندهی دسترسی خارجی به اجزای محلی کلاس (دوست)

ما قبلاً با قانون اساسی OOP - داده ها (داخلی) آشنا شده ایم

متغیرها) شی از تأثیرات خارجی محافظت می شوند و دسترسی به آنها می تواند باشد

فقط با استفاده از توابع (روش) شی به دست آورید. اما چنین مواردی وجود دارد

چای، زمانی که ما نیاز به سازماندهی دسترسی به داده های شی بدون استفاده داریم

یادگیری رابط (توابع) آن. البته، می توانید یک تابع عمومی جدید اضافه کنید

به یک کلاس برای دسترسی مستقیم به متغیرهای داخلی. با این حال، در

در بیشتر موارد، رابط یک شیء عملیات خاصی را اجرا می کند، و

ویژگی جدید ممکن است اضافی باشد. در همان زمان، گاهی اوقات وجود دارد

نیاز به سازماندهی دسترسی مستقیم به داده های داخلی (محلی).

دو شی متفاوت از یک تابع در عین حال، در C++ یک تابع نمی تواند

می تواند جزء دو کلاس مختلف باشد.

برای پیاده سازی این، تعیین کننده دوست در C++ معرفی شد. اگر برخی

تابع به عنوان یک تابع دوست برای برخی از کلاس ها تعریف می شود، سپس:

جزء تابع این کلاس نیست.

به تمام اجزای این کلاس (خصوصی، عمومی و محافظت شده) دسترسی دارد.

در زیر مثالی وجود دارد که در آن یک تابع خارجی دسترسی دارد

داده های کلاس داخلی

#عبارتند از

با استفاده از namespace std.

kls(int i,int J): i(I),j(J) () // سازنده

int max() (بازگشت i>j? i: j;) // تابع جزء کلاس kls

دوست مضاعف (int, kls&); // اعلامیه دوست از عملکرد خارجی سرگرم کننده

سرگرمی دوگانه (int i، kls &x) // تابع خارجی

(بازگشت (دوبل)i/x.i;

کوت<< obj.max() << endl;

در C(C++)، سه راه شناخته شده برای انتقال داده به یک تابع وجود دارد: با مقدار

بر روی برخی از شی موجود ممکن است. زمان های زیر قابل تشخیص است:

وجود پیوندها و اشاره گرها اولاً عدم امکان وجود صفر

پیوندها به این معنی است که نیازی به بررسی صحت آنها ندارند. و هنگام استفاده از یک اشاره گر، باید آن را برای یک مقدار غیر تهی بررسی کنید. ثانیا، اشاره گرها می توانند به اشیاء مختلف اشاره کنند، اما یک مرجع همیشه به یک شیء واحد اشاره می کند، زمانی که مقدار اولیه آن مشخص می شود. اگر می خواهید به یک تابع اجازه دهید مقادیر را تغییر دهد

پارامترها به آن منتقل می شوند، سپس در زبان C باید آنها را اعلان کنید

در سطح جهانی، یا کار با آنها در توابع از طریق ارسال به انجام می شود

این شامل نشانگرهایی به این متغیرها است. در C++، آرگومان ها را می توان به یک تابع ارسال کرد

رام علامت گذاری شده است &.

void fun1(int,int);

void fun2(int &,int &);

(int i=1,j=2; // i و j پارامترهای محلی هستند

کوت<< "\n адрес переменных в main() i = "<<&i<<" j = "<<&j;

کوت<< "\n i = "<چرا C++ وقتی واسا غرق شد بادبان می رود؟. اگر این برای شماست واقعامشکل، من می توانم std::unique_ptr را توصیه کنم ، و در آینده ممکن است استاندارد به ما dynarray بدهد.

اشیاء پویا

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

هنگامی که یک شی پویا ایجاد می شود، شخصی باید آن را حذف کند و انواع اشیاء را می توان به دو گروه تقسیم کرد: آنهایی که به هیچ وجه از روند حذف خود آگاه نیستند و آنهایی که به چیزی مشکوک هستند. خواهیم گفت که اولی یک مدل مدیریت حافظه استاندارد دارند و دومی یک مدل غیر استاندارد دارند.

انواع با مدل مدیریت حافظه استاندارد شامل همه انواع استاندارد از جمله کانتینرها می شود. در واقع کانتینر حافظه ای را که به خود اختصاص داده مدیریت می کند. برای او مهم نیست که چه کسی آن را ایجاد کرده یا چگونه حذف خواهد شد.

انواع با مدل مدیریت حافظه غیر استاندارد شامل، به عنوان مثال، اشیاء Qt هستند. در اینجا، هر شی یک والد دارد که مسئول حذف آن است. و شی از این موضوع می داند، زیرا از کلاس QObject به ارث می برد. این شامل انواع با تعداد مرجع نیز می شود، به عنوان مثال، آنهایی که برای کار با boost::intrusive_ptr طراحی شده اند.

به عبارت دیگر، یک نوع با یک مدل مدیریت حافظه استاندارد هیچ مکانیسم اضافی برای مدیریت طول عمر خود ارائه نمی دهد. این باید به طور کامل توسط کاربر انجام شود. اما نوع با مدل غیر استاندارد چنین مکانیزم هایی را فراهم می کند. به عنوان مثال، QObject دارای متدهای setParent() و children() است و حاوی لیستی از فرزندان است و نوع boost::intrusive_ptr به توابع intrusive_ptr_add_ref و intrusive_ptr_release متکی است و حاوی یک شمارنده مرجع است.

اگر یک نوع شی دارای یک مدل مدیریت حافظه استاندارد باشد، برای اختصار می گوییم که یک شی با مدیریت حافظه استاندارد است. به طور مشابه، اگر یک نوع شی دارای یک مدل مدیریت حافظه غیر استاندارد باشد، می گوییم که یک شی با مدیریت حافظه غیر استاندارد است.

در ادامه، به اشیاء هر دو مدل نگاه می کنیم. با نگاهی به آینده، شایان ذکر است که برای اشیاء با مدیریت حافظه استاندارد، قطعاً نباید از کدهای جدید و حذف در کد کلاینت استفاده کنید، و برای اشیاء با مدیریت حافظه غیر استاندارد، این بستگی به مدل خاص دارد.

* برخی استثناها: اصطلاح pimpl; یک شی بسیار بزرگ (به عنوان مثال، یک بافر حافظه).

** استثنا std::locale::facet است (به زیر مراجعه کنید).

اشیاء پویا با مدیریت حافظه استاندارد

اینها اغلب در عمل مشاهده می شوند. و این آنها هستند که باید سعی کنند در C ++ مدرن استفاده کنند، زیرا رویکردهای استاندارد، به ویژه در اشاره گرهای هوشمند، با آنها کار می کنند.

در واقع، نشانگرهای هوشمند، بله، پاسخ هستند. آنها باید بر طول عمر اجسام پویا کنترل داشته باشند. دو مورد از آنها در C++ وجود دارد: std::shared_ptr و std::unique_ptr. std::weak_ptr را در اینجا برجسته نمی کنیم، زیرا این فقط یک کمک کننده برای std::shared_ptr در موارد استفاده خاص است.

در مورد std::auto_ptr، رسماً از C++ با شروع C++17 حذف شد. روحش شاد!

من در اینجا به طراحی و استفاده از اشاره گرهای هوشمند نمی پردازم، زیرا ... این خارج از محدوده مقاله است. اجازه دهید فوراً به شما یادآوری کنم که آنها همراه با توابع فوق العاده std::make_shared و std::make_unique هستند و باید از آنها برای ایجاد اشاره گرهای هوشمند استفاده کرد.

آن ها به جای این:
std::unique_ptr کوکی (کوکی جدید (خمیر، شکر، دارچین))؛
باید به این صورت نوشته شود:
کوکی خودکار = std::make_unique (خمیر، شکر، دارچین)؛
مزیت های توابع ساخت نسبت به ایجاد آشکار اشاره گرهای هوشمند توسط هرب ساتر در GotW #89 و اسکات مایرز در C++ مدرن موثر، مورد 21 به زیبایی توصیف شده است. لیست نکات در اینجا:

  • برای هر دو توابع make:
    • امنیت از نظر استثنائات.
    • هیچ نام نوع تکراری وجود ندارد.
  • برای std::make_shared:
    • به دست آوردن بهره وری، زیرا بلوک کنترل در کنار خود شی تخصیص داده می شود که تعداد تماس ها با مدیر حافظه را کاهش می دهد و محلی بودن داده ها را افزایش می دهد. بهينه سازي.
توابع Make نیز تعدادی محدودیت دارند که در همان منابع به تفصیل شرح داده شده است:
  • برای هر دو توابع make:
    • شما نمی توانید از حذف کننده خود عبور کنید. این کاملا منطقی است، زیرا در داخل، توابع را بنا به تعریف، از استاندارد new استفاده کنید.
    • شما نمی توانید از مقداردهی اولیه مهاربندی شده استفاده کنید، و نه از تمام ویژگی های دیگر مرتبط با ارسال کامل (به C++ مدرن موثر، مورد 30 مراجعه کنید).
  • برای std::make_shared:
    • مصرف حافظه بالقوه برای اشیاء بزرگ با مراجع ضعیف طولانی مدت (std::weak_pointer).
    • مشکلات با اپراتورهای جدید و حذف در سطح کلاس لغو شده است.
    • اشتراک گذاری نادرست احتمالی بین یک شی و یک بلوک کنترلی (به سؤال در StackOverflow مراجعه کنید).
در عمل، این محدودیت ها نادر هستند و از مزایای آن کم نمی کنند. به نظر می رسد که نشانگرهای هوشمند تماس را برای حذف از ما پنهان می کنند و توابع make تماس با جدید را از ما پنهان می کنند. در نتیجه، کد قابل اعتمادتری دریافت کردیم که حاوی کدهای جدید یا حذف نیست.

به هر حال، ساختار توابع ساخت به طور جدی در گزارش های خود توسط Stefan Lavavey (معروف به STL) مورد بحث قرار گرفته است. در اینجا یک اسلاید شیوا از گزارش او به کامپایلر کمک نکنید:

اشیاء پویا با مدیریت حافظه غیر استاندارد

علاوه بر رویکرد استاندارد برای مدیریت حافظه از طریق اشاره گرهای هوشمند، مدل های دیگری نیز وجود دارد. به عنوان مثال، شمارش ارجاع و روابط والدین به فرزند.

اشیاء پویا با شمارش مرجع


یک تکنیک بسیار رایج که در بسیاری از کتابخانه ها استفاده می شود. بیایید کتابخانه OpenSceneGraph را به عنوان مثال در نظر بگیریم. این یک موتور سه بعدی کراس پلتفرم باز است که در C++ و OpenGL نوشته شده است.

بیشتر کلاس‌های موجود در آن از کلاس osg::Referenced ارث می‌برند که شمارش مراجع را به صورت داخلی انجام می‌دهد. متد ref() شمارنده را افزایش می دهد، روش unref() شمارنده را کاهش می دهد و هنگامی که شمارنده به صفر رسید، شی را حذف می کند.

این کیت همچنین شامل یک اشاره گر هوشمند osg::ref_ptr است که متد T::ref() را روی شی ذخیره شده در سازنده آن و متد T::unref() را در ویرانگر آن فراخوانی می کند. همین رویکرد در boost::intrusive_ptr استفاده می شود، فقط در آنجا توابع خارجی به جای متدهای ref() و unref() وجود دارد.

بیایید به یک قطعه کد که در OpenSceneGraph 3.0: راهنمای مبتدیان ارائه شده است نگاه کنیم:
osg::ref_ptr رئوس = new osg::Vec3Array; // ... osg::ref_ptr نرمال = جدید osg::Vec3Array; // ... osg::ref_ptr geom = new osg::هندسه; geom->setVertexArray(vertices.get()); geom->
ساختارهای بسیار آشنا مانند osg::ref_ptr p = T جدید. دقیقاً به همان روشی که از توابع std::make_unique و std::make_shared برای ایجاد کلاس های std::unique_ptr و std::shared_ptr استفاده می شود، می توانیم تابع osg::make_ref را برای ایجاد کلاس osg::ref_ptr بنویسیم. . این کار بسیار ساده و با قیاس با تابع std::make_unique انجام می شود:
فضای نام osg (الگو osg::ref_ptr make_ref(Args&&... args) (T(std::forward) جدید را برگرداند (ارگ)...); ))
بیایید این قطعه کد مجهز به تابع جدید خود را بازنویسی کنیم:
رئوس خودکار = osg::make_ref ()؛ // ... auto normals = osg::make_ref ()؛ // ... خودکار geom = osg::make_ref ()؛ geom->setVertexArray(vertices.get()); geom->setNormalArray(normals.get()); //...
تغییرات پیش پا افتاده هستند و به راحتی می توانند به صورت خودکار انجام شوند. به این روش ساده، ایمنی استثنایی، بدون نام نوع تکراری و مطابقت عالی با سبک استاندارد را دریافت می کنیم.

فراخوانی حذف قبلاً در متد osg::Referenced::unref() پنهان بود و اکنون فراخوانی جدید را در تابع osg::make_ref مخفی کرده ایم. بنابراین جدید نیست و حذف کنید.

* از نظر فنی، در این قطعه هیچ موقعیتی وجود ندارد که از نظر استثنایی ناامن باشد، اما در پیکربندی های پیچیده تر ممکن است مواردی وجود داشته باشد.

اشیاء دینامیک برای دیالوگ های بدون مدل در MFC


بیایید به یک مثال خاص برای کتابخانه MFC نگاه کنیم. این مجموعه ای از کلاس های C++ بر روی Windows API است. برای ساده سازی توسعه رابط کاربری گرافیکی در ویندوز استفاده می شود.

تکنیک جالبی که مایکروسافت رسما استفاده از آن را برای ایجاد دیالوگ های بدون حالت توصیه می کند. زیرا دیالوگ بی حالت است، کاملاً مشخص نیست که چه کسی مسئول حذف آن است. پیشنهاد می شود که خود را در روش حذف شده CDialog::PostNcDestroy() حذف کند. این روش پس از پردازش پیام WM_NCDESTROY، آخرین پیام دریافت شده توسط پنجره در چرخه حیات خود، فراخوانی می شود.

در مثال زیر، یک دیالوگ با کلیک بر روی دکمه ای در متد CMainFrame::OnBnClickedCreate() ایجاد می شود و در روش حذف شده CMyDialog::PostNcDestroy() حذف می شود.
void CMainFrame::OnBnClickedCreate() ( auto* pDialog = new CMyDialog(this); pDialog->ShowWindow(SW_SHOW); ) class CMyDialog: public CDialog ( public: CMyDialog(CWnd* pParent) (pParent_M)(Create)YALD محافظت شده: void PostNcDestroy() override ( CDialog::PostNcDestroy(); حذف این؛ ) );
در اینجا ما نه تماس جدید و نه تماس حذف را پنهان داریم. راه های زیادی برای شلیک به پای خود وجود دارد. علاوه بر مشکلات معمول با اشاره گرها، می توانید فراموش کنید که متد PostNcDestroy() را در گفتگوی خود نادیده بگیرید و در نتیجه حافظه نشت کند. هنگامی که تماس با جدید را مشاهده می کنید، ممکن است بخواهید در یک لحظه خاص خود را حذف کنید، که منجر به حذف مضاعف می شود. شما می توانید به طور تصادفی یک شیء محاوره ای در حافظه خودکار ایجاد کنید، دوباره یک حذف مضاعف دریافت می کنیم.

بیایید سعی کنیم تماس‌های جدید را پنهان کرده و در کلاس میانی CModelessDialog و کارخانه CreateModelessDialog حذف کنیم، که مسئول گفتگوهای بدون حالت در برنامه ما خواهد بود:
class CModelessDialog: public CDialog ( public: CModelessDialog(UINT nIDTemplate, CWnd* pParent) ( Create(nIDTemplate, pParent); ) protected: void PostNcDestroy() override (CDialog::PostNcDestroy(); حذف این؛ )); // کارخانه ایجاد قالب دیالوگ های معین مشتق شده* CreateModelessDialog(Args&&... args) ( // به جای static_assert در بدنه تابع، می توانیم از std::enable_if در هدر آن استفاده کنیم که به ما امکان استفاده از SFINAE را می دهد. // اما از آنجایی که اضافه بارهای دیگر این تابع هستند بعید به نظر می رسد، استفاده از راه حل ساده تر و بصری تر منطقی به نظر می رسد: static_assert(std::is_base_of ::value، "CreateModelessDialog باید برای نوادگان CModelessDialog فراخوانی شود"); خودکار* pDialog = مشتق جدید (std::forward (ارگ)...); pDialog->ShowWindow(SW_SHOW); بازگشت pDialog; )
خود کلاس متد PostNcDestroy() را که در آن delete را مخفی می‌کردیم لغو می‌کند و برای ایجاد کلاس‌های نسل از کارخانه‌ای که new را در آن مخفی کردیم استفاده می‌شود. ایجاد و تعریف یک کلاس descendant اکنون به شکل زیر است:
void CMainFrame::OnBnClickedCreate() ( CreateModelessDialog (این)؛ ) class CMyDialog: public CModelessDialog ( public: CMyDialog(CWnd* pParent) : CModelessDialog(IDD_MY_DIALOG, pParent) () );
البته ما همه مشکلات را به این شکل حل نکرده ایم. به عنوان مثال، یک شی همچنان می تواند در پشته تخصیص داده شود و دوبار حذف شود. شما می توانید از تخصیص یک شی در پشته فقط با تغییر خود کلاس شیء جلوگیری کنید، به عنوان مثال با افزودن یک سازنده خصوصی. اما هیچ راهی وجود ندارد که بتوانیم این کار را از کلاس پایه CModelessDialog انجام دهیم. البته می‌توانید کلاس CMyDialog را به طور کامل مخفی کنید و با پذیرش یک شناسه کلاس خاص، کارخانه را نه یک الگو، بلکه کلاسیک‌تر کنید. اما همه اینها از حوصله مقاله خارج است.

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

اشیاء پویا با رابطه والد و فرزند



آنها اغلب به خصوص در کتابخانه های توسعه رابط کاربری گرافیکی رخ می دهند. به عنوان مثال، Qt را در نظر بگیرید، یک کتابخانه معروف برای توسعه اپلیکیشن و UI.

اکثر کلاس ها از QObject ارث می برند. لیستی از کودکان را ذخیره می کند و زمانی که خودش حذف شد آنها را حذف می کند. یک اشاره گر به والد ذخیره می کند (می تواند تهی باشد) و می تواند والد را در طول زندگی تغییر دهد.

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

بنابراین من راه خوبی برای خلاص شدن از new و حذف در Qt نمی شناسم.

اشیاء پویا std::locale::facet


برای کنترل خروجی داده ها به جریان ها در C++، از اشیاء std::locale استفاده می شود. محلی مجموعه‌ای از جنبه‌ها است که نحوه نمایش داده‌های خاص را تعیین می‌کند. وجه ها شمارنده مرجع خود را دارند و هنگام کپی کردن محلی ها، وجه ها کپی نمی شوند، فقط اشاره گر کپی می شود و شمارنده مرجع افزایش می یابد.

زمانی که تعداد مراجع به صفر می رسد، خود محلی مسئول حذف وجه است، اما کاربر باید با استفاده از عملگر جدید وجه ایجاد کند (به بخش Notes در توضیح سازنده std::locale مراجعه کنید):
std::local default; std::locale myLocale(پیش‌فرض، std جدید::codecvt_utf8 );
این مکانیسم حتی قبل از معرفی نشانگرهای هوشمند استاندارد اجرا شد و از قوانین کلی استفاده از کلاس ها در کتابخانه استاندارد متمایز است.

می توانید یک بسته بندی ساده بسازید که محلی برای حذف موارد جدید از کد مشتری ایجاد کند. با این حال، این یک استثنا نسبتاً شناخته شده از قوانین کلی است و شاید ساختن باغ برای آن هیچ فایده ای نداشته باشد.

نتیجه

بنابراین، ابتدا به سناریوهایی مانند ایجاد آرایه های پویا و اشیاء پویا با مدیریت حافظه استاندارد نگاه کردیم. به جای جدید و حذف، از کانتینرهای استاندارد و ساخت توابع استفاده کردیم و کدهای ساده تر و قابل اعتمادتری دریافت کردیم.

سپس به تعدادی از نمونه‌های مدیریت حافظه غیر استاندارد نگاه کردیم و دیدیم که چگونه می‌توانیم کد را با حذف جدید و حذف در wrapper‌های مناسب بهتر کنیم. ما همچنین نمونه ای پیدا کردیم که در آن این رویکرد کار نمی کند.

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

  • از استفاده از جدید و حذف در کد خود اجتناب کنید. آنها را به عنوان عملیات مدیریت پشته دستی سطح پایین در نظر بگیرید.
  • از کانتینرهای استاندارد برای ساختارهای داده پویا استفاده کنید.
  • در صورت امکان از توابع make برای ایجاد اشیاء پویا استفاده کنید.
  • ایجاد پوشش برای اشیاء با مدل حافظه غیر استاندارد.

از نویسنده

من شخصاً با موارد زیادی از نشت و خرابی حافظه به دلیل استفاده بیش از حد از new و delete مواجه شده ام. بله، بیشتر این کد سال ها پیش نوشته شده است، اما بعد از آن برنامه نویسان جوان شروع به کار با آن می کنند و فکر می کنند که باید اینگونه نوشته شود.

امیدوارم این مقاله به عنوان یک راهنمای عملی باشد که می توان یک توسعه دهنده جوان را به آن فرستاد تا به بیراهه نرود.

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

PSدر روند بحث در مورد مقاله، من و همکارانم کلی بحث کردیم که درست است: "Myers" یا "Meyers". از یک طرف، "Meyers" برای گوش روسی آشناتر به نظر می رسد، و به نظر می رسد خود ما همیشه اینگونه صحبت کرده ایم. از سوی دیگر، "Myers" در ویکی استفاده می شود. اگر به کتاب های بومی شده نگاه کنید، به طور کلی چیزهای زیادی وجود دارد: به این دو گزینه، "Meyers" نیز اضافه شده است. در کنفرانس ها ناهمسان مردم حاضرآن را به روش های مختلف در نهایت ما موفق به کشف کردن شد، که او خود را "Myers" می نامد، که آنها در مورد آن تصمیم گرفتند.

پیوندها

  1. هرب ساتر راه حل GotW #89: اشاره گرهای هوشمند.
  2. اسکات مایرز C++ مدرن موثر، مورد 21، ص. 139.
  3. استفان تی. لاواوی، به کامپایلر کمک نکنید
  4. بیارن استروستروپ، زبان برنامه نویسی C++، 11.2.1، ص. 281.
  5. پنج افسانه محبوب در مورد C++.،قسمت 2
  6. میخائیل ماتروسوف، C++ بدون جدید و حذف.

برچسب ها:

افزودن برچسب

نظرات 134

بهترین مقالات در این زمینه