برنامه نویسی شی گرا در C++ — آموزش رایگان، به زبان ساده و جامع
زبان برنامه نویسی C++ (سی پلاسپلاس) یکی از اولین زبانهایی است که برنامه نویسی شی گرا (OOP) در آن پیادهسازی و در سطح گستردهای توسط برنامه نویسان مورد استفاده قرار گرفته است. C و C++ از جمله زبانهای برنامه نویسی به حساب میآیند که اکثراً در دانشگاهها تحت عنوان دروس مبانی برنامهنویسی و برنامهنویسی پیشرفته تدریس میشوند و در واقع یکی از اولین زبانهایی هستند که برای آموزش مقدماتی دانشجویان رشته کامپیوتر و برنامهنویسان مبتدی مورد استفاده قرار میگیرند. بنابراین، یادگیری برنامه نویسی شی گرا در C++ اهمیت ویژهای دارد. بدین سبب، این مقاله با هدف آموزش مباحث و مفاهیم برنامه نویسی شی گرا در C++ و بیان مفهوم کلاس در C++ و سایر مفاهیم مرتبط ارائه شده است. به عنوان مقدمه، در ابتدا به این مسئله پرداخته شده است که برنامه نویسی شی گرا چیست؟
برنامه نویسی شی گرا چیست ؟
همانطور که از نامش پیداست، در برنامه نویسی شی گرا (Object Oriented Programming | OOP) از اشیا استفاده میشود. برنامه نویسی شی گرا برای ساختاردهی به یک برنامه نرمافزاری و تبدیل آن به قطعات ساده و قابل استفاده مجدد است. این قطعات درست مثل نقشه ساخت هستند و به آنها «کلاس» (Class) گفته میشود.
هدف برنامه نویسی شی گرا ، پیادهسازی نهادهای جهان واقعی همچون وراثت (ارثبری)، پنهانسازی، چندریختی (Polymorphism) و دیگر موارد در برنامهنویسی است. هدف اصلی OOP پیوند دادن دادهها و توابعی است که روی دادهها عملیات انجام میدهند.
در این مقصود، پیوند باید بهگونهای شکل بگیرد که امکان دسترسی به آن دادهها در هیچ بخش دیگری از کدها به غیر از تابع وجود نداشته باشد. در سالهای اخیر، قابلیت برنامه نویسی شی گرا در C++ و بسیاری از زبانهای برنامه نویسی دیگر فراهم شده است. برای استفاده از این قابلیت، ابتدا باید درکی از دلیل استفاده از برنامه نویسی شی گرا وجود داشته باشد. بنابراین، در ادامه به این سوال پاسخ داده شده است که چرا نیاز به برنامه نویسی شی گرا در C++ و سایر زبانهای شی گرا وجود دارد؟
چرا نیاز به برنامه نویسی شی گرا در C++ وجود دارد؟
در این بخش از مقاله برنامه نویسی شی گرا در C++ ، به چرایی نیاز به شی گرایی در C++ و سایر زبانهای شی گرا پرداخته شده است.
ابتدا با یک مثال ساده، استفاده از اجزا و قطعات کوچکتر برای تولید یک موجودیت بزرگتر در سختافزار و نرمافزار با هم مقایسه شدهاند و سپس برای روشن شدن دلیل استفاده از شی گرایی، زبانهای برنامه نویسی سنتی یا رویهگرا (Procedural) شرح داده شدهاند. پس از آن، به معرفی مفهوم برنامه نویسی شی گرا در C++ و سایر زبانهای شی گرا و شرح ویژگیهای آن پرداخته شده است.
تفاوت سرهم کردن اجزا سختافزاری با اجزا نرمافزاری
اگر فردی بخواهد کامپیوتر شخصی خودش را سرهم (مونتاژ) کند، به فروشگاههای قطعات سختافزار کامپیوتر مراجعه خواهد کرد و یک مادربرد، پردازنده، چند RAM، یک هارد دیسک، کیس، منبع تغذیه و سایر قطعات را خریداری میکند و آنها را سرهمبندی خواهد کرد. پس از آن، کامپیوتر را به برق وصل میکند و PC روشن میشود.
این فرد دیگر نگرانی بابت این مسئله ندارد که آیا مادربورد چهار لایه یا شش لایه است یا آیا هارد دیسک چهار صفحه دارد یا شش صفحه یا اینکه ضخامت آن سه یا پنج اینچ است. البته، باید مطمئن شد که رابطها با هم همخوانی داشته باشند. به عنوان مثال، در صورتی که مادربورد تنها از رابط IDE پشتیبانی میکند، باید اطمینان حاصل کرد که به جای رابط SCSI، یک هارد دیسک با رابط IDE خریداری شود.
باید RAMهایی انتخاب شوند که دارای نرخ سرعت درستی باشند. با این حال، راهاندازی یک سیستم کامپیوتری با استفاده از قطعات سختافزاری کار دشواری نیست. در مورد نرمافزار چه طور؟ آیا میتوان یک برنامه نرمافزاری را مشابه مونتاژ سختافزار به راحتی سرهمبندی کرد؟ واضح است که اینطور نیست.
بر خلاف سختافزار، سرهمبندی یک نرمافزار با ادغام قطعات نرمافزاری کار بسیار دشواری است. از زمان ظهور کامپیوتر تا کنون، برنامههای نرمافزاری بسیاری نوشته شدهاند. اما، برای هر برنامه جدید، لازم بود چرخ را دوباره اختراع کرد و همه چیز را از اول نوشت. اما چرا نیاز به اختراع دوباره چرخ وجود داشت؟ برای پاسخ به این سوال باید به شرح و معرفی زبانهای سنتی رویهگرا پرداخته شود.
زبان های برنامه نویسی سنتی رویهگرا
آیا میتوان با زبانهای برنامه نویسی سنتی رویهگرا یا Procedural مثل Cobol ،Fortran ،C یا پاسکال قطعات نرمافزاری را سرهم کرد و یک برنامه ساخت؟
زبانهای برنامه نویسی رویهای نظیر C و پاسکال از کاستیهای قابل توجهی در ایجاد قطعات نرمافزاری قابل استفاده مجدد رنج میبرند. دو مشکل اصلی زبانهای برنامهنویسی سنتی رویهای به شرح زیر است:
- برنامههای نرمافزاری از توابع تشکیل شدهاند. توابع اغلب قابل استفاده مجدد نیستند. کپی کردن یک تابع از یک برنامه و انتقال آن به برنامه دیگر بسیار پیچیده است. زیرا احتمال دارد در تابع به سربرگها (Header)، متغیرهای سراسری و سایر توابع ارجاع داده شده باشد. به بیان دیگر، توابع به عنوان یک واحد خودکفا و قابل استفاده مجدد به خوبی کپسوله نشدهاند.
- زبانهای رویهای مناسب انتزاع سطح بالا برای حل کردن مسائل جهان واقعی نیستند. برای مثال، در برنامههای C از ساختارهایی نظیر تابع، آرایه، حلقه for و گزاره شرطی if-else استفاده میشود. این ساختارها بسیار سطح پایین هستند و انتزاعی کردن مسائل واقعی مثل سیستمهای CRM یا یک بازی کامپیوتری فوتبال در آنها بسیار دشوار است.
به طور خلاصه، زبانهای سنتی رویهای، ساختمان داده و الگوریتمهای نهادهای نرمافزاری را از هم جدا میکنند. در ادامه پاسخ به این سوال که چرا نیاز به برنامه نویسی شی گرا در C++ و سایر زبانهای شی گرا وجود دارد، باید پرسید که زبانهای برنامه نویسی شی گرا قرار است چه مشکلاتی را برطرف کنند؟ در ادامه به این سوال پاسخ داده شده است.
زبان های برنامه نویسی شی گرا
زبان های برنامه نویسی شی گرا برای غلبه بر مشکلات زیر طراحی شدهاند:
- واحد اساسی برنامه نویسی شی گرا در C++ یک «کلاس» است که هم صفتهای ایستا و هم رفتارهای پویا را در درون یک جعبه کپسوله و واسط عمومی را برای استفاده از این جعبهها تعیین میکند. به دلیل اینکه کلاس به خوبی کپسوله شده است (در مقایسه با یک تابع)، استفاده مجدد از این کلاسها آسانتر خواهد بود. به بیان دیگر، برنامه نویسی شی گرا در C++ ، ساختمانهای داده و الگوریتمهای نهاد نرمافزار را در درون یک جعبه یکسان ادغام میکند.
- زبانهای برنامه نویسی شی گرا امکان سطح انتزاع بالاتر را برای حل مسائل جهان واقعی فراهم میکنند. یک زبان سنتی رویهای مثل زبان برنامه نویسی C و پاسکال، برنامهنویس را وادار به فکر کردن راجع به ساختار کامپیوتر میکنند. مثلاً برنامهنویس باید ساختار بیتها و بایتهای حافظه، آرایه، حلقه و سایر موارد را در نظر بگیرد. این موضوع باعث میشود به جای اینکه فرد بر حل مسئله تمرکز کند، درگیر مسائل حاشیهای شود. زبانهای برنامه نویسی شی گرا نظیر C# ،C++ و جاوا امکان تفکر در محدوده مسئله را فراهم میکنند و از اشیا نرمافزاری برای بازنمایی و انتزاعی کردن نهادهای فضای مسئله در حل آن استفاده میکنند.
در ادامه این بخش، مثالی برای درک بهتر دلیل استفاده از برنامه نویسی شی گرا در C++ ارائه شده است.
مثالی برای درک بهتر دلیل استفاده از برنامه نویسی شی گرا در C++
به عنوان مثال، میتوان توسعه یک بازی فوتبال کامپیوتری را در نظر گرفت. چنین برنامه نرمافزاری به عنوان یک اپلیکیشن پیچیده در نظر گرفته میشود.
مدلسازی این بازی در زبانهای رویهگرا بسیار دشوار است. اما، با استفاده از زبانهای برنامه نویسی شی گرا میتوان برنامه را مطابق با «موارد واقعی» که در بازیهای فوتبا اتفاق میافتند، مدلسازی کرد:
- بازیکن
- توپ
- داور
- زمین
- تماشاچی
- وضعیت آب و هوا
مهمتر از همه، میتوان برخی از این کلاسها (مثل توپ و تماشاچی) را بدون تغییر یا با تغییرات اندک، مجدداً در یک اپلیکیشن دیگر (مثل یک بازی بسکتبال کامپیوتری) استفاده کرد. بنابراین، اهمیت و دلیل استفاده از برنامه نویسی شی گرا در C++ و سایر زبانهای شی گرا در این بخش روشن شد. ادامه مقاله برنامه نویسی شی گرا در C++ ، به شرح مزایای برنامه نویسی شی گرا اختصاص دارد.
مزایای برنامه نویسی شی گرا چیست؟
به طور کلی میتوان گفت، مزایای برنامه نویسی شی گرا در C++ و دیگر زبانهای شی گرا شامل قابلیت استفاده مجدد (Reusability)، افزونگی دادهها، قابلیت تعمیر و نگهداری (Maintenance)، امنیت و سایر موارد است. زبانهای رویهگرا بر رویهها (روال | Procedure) تمرکز دارند و تابع نهاد اساسی آنها به حساب میآید.
ابتدا باید تمام توابع را سنجید و سپس به نحوه نمایش دادهها فکر کرد. اما، زبانهای برنامه نویسی شی گرا بر اجزایی تمرکز دارند که کاربر مشاهده میکند و شی نهاد اساسی آنها است. فناوری شی گرا مزایای بسیاری دارد که برخی از آنها در ادامه فهرست شدهاند.
- یکی از مزایای برنامه نویسی شی گرا ، سادگی در طراحی نرمافزار است. زیرا به جای مشغول شدن ذهن به بیتها و بایتهای ماشین، میتوان در فضای مسئله تمرکز کرد. افراد در برنامه نویسی شی گرا با مفاهیم و انتزاع سطح بالا مواجه هستند. سادگی در طراحی منجر به تاثیرگذاری و کارآمدی بیشتر در توسعه نرمافزار میشود.
- سادگی در تعمیر و نگهداری نرمافزار یکی دیگر از نقاط مثبت برنامه نویسی شی گرا به حساب میآید. درک نرمافزار شی گرا آسانتر است و بدین سبب، تست، عیبیابی و نگهداری از آن نیز به سادگی انجام میشود.
- بدون شک، قابلیت استفاده مجدد از قطعات نرمافزاری نیز یکی از نقاط برتری مهم در برنامه نویسی شی گرا محسوب میشود. نیازی به اختراع دوباره چرخ و بازنویسی همان توابع قبلی برای شرایط متفاوت وجود ندارد. استفاده مجدد از کدهایی که قبلاً نوشته شدهاند و کاملاً تست و تضمین شده هستند، سریعترین و ایمنترین راه برای توسعه یک اپلیکیشن جدید محسوب میشود.
خصوصیات یک زبان برنامه نویسی شی گرا چیست؟
یک زبان برنامه نویسی شی گرا دارای چهار خصوصیت یا مشخصه بستهبندی (کپسولهسازی)، ارثبری، چندریختی و انتزاع است. ضمن اینکه، مفاهیم کلاس و شی نیز از جمله موجودیتهای کلیدی در شی گرایی به حساب میآیند.
در ادامه، هر یک از خصوصیات یا ویژگیهای یک زبان برنامه نویسی شی گرا که به آنها ستونهای شی گرایی نیز گفته میشود، به اختصار شرح داده شدهاند. در بخشهای بعدی مقاله برنامه نویسی شی گرا در C++ ، این ستونهای اصلی با جزئیات بیشتری آموزش داده شدهاند.
- بستهبندی (کپسولهسازی | Encapsulation): به بیان ساده، بستهبندی به معنی جمعآوری داده و نگهداری از آن به شکلی امن و به دور از واسطهای خارجی است. کپسولهسازی ایده بستهبندی دادهها و متُدهای عامل روی دادهها را در قالب یک نهاد بیان میدارد.
- ارثبری (Inheritance): ارثبری یا وراثت فرآیندی است که یک کلاس میتواند از طریق آن از یک کلاس پایه مشتق شود. کلاس جدید علاوه بر تمام ویژگیهای کلاس پایه، ویژگیهای خاص خودش را نیز خواهد داشت. ارثبری منجر به افزایش امکان استفاده مجدد از کدها میشود.
- چندریختی (Polymorphism): چندرختی به توانایی یک شی در ظاهر شدن به اشکال مختلف گفته میشود. یعنی هر کلاس فرزند میتواند هر شکلی از یک کلاس در رده والد و قطعاً در رده خودش را به خود بگیرد. به بیان دیگر، شی کلاس فرزند میتواند به هر مرجع کلاسی در رده والد و رده خودش تخصیص داده شود.
- انتزاع (Abstraction): انتزاع در برنامه نویسی شی گرا به معنی توانایی بازنمایی (نمایش) دادهها در یک سطح کاملاً مفهومی و بدون جزئیات است. در انتزاع تنها صفتهای ضروری نمایش داده و اطلاعات غیر ضروری پنهان میشوند.
ادامه مقاله برنامه نویسی شی گرا در C++ ، به شرح مفهوم کلاس اختصاص دارد.
کلاس در C++ چیست؟
عنصر اصلی C++ در برنامه نویسی شی گرا ، کلاس (Class) است. کلاس یک نوع داده تعریف شده توسط کاربر است که دادهها و توابع عضو مربوط به خودش را نگهداری میکند. میتوان به این دادهها و توابع از طریق ایجاد یک نمونه (Instance) از آن کلاس دسترسی پیدا کرد. کلاس درست مثل یک نقشه ساخت برای یک شی است. برای مثال، میتوان کلاسی از خودروها را در نظر گرفت.
ممکن است خودروهای زیادی با اسامی و مدلهای مختلف وجود داشته باشند، اما همه آنها در مشخصههای یکسانی با هم مشترک هستند. مثلاً همه ماشینها چهار چرخ، محدودیت سرعت، مسافت پیموده شده و خصوصیات مشترک دیگری دارند. بنابراین، در اینجا ماشین کلاس و چرخها، سرعت و مسافت، خصوصیتها یا مشخصههای (Property) این کلاس به حساب میآیند. در ادامه، برخی نکات پیرامون مفهوم کلاس فهرست شده است.
- کلاس یک نوع داده تعریف شده توسط کاربر است که دارای اعضای داده (Data Member) و توابع عضو (Member Function) است.
- عضو داده، متغیرهای داده و توابع عضو توابعی هستند که برای دستکاری این متغیرها به کار میروند. این اعضای داده و توابع عضو به همراه هم مشخصهها و رفتار اشیا در کلاسها را تعریف میکنند.
- در مثال فوق از کلاس خودرو، عضو داده، همان محدودیت سرعت و مسافت طی شده است و توابع عضو میتوانند با ترمز گرفتن سرعت را کم کنند یا با فشردن پدال گاز سرعت را افزایش دهند.
در ادامه به معرفی بخشهای مختلف یک کلاس پرداخته شده است.
کلاس از چه بخش هایی تشکیل شده است؟
کلاس در C++ یک موجودیت قابل شناسایی است که خصوصیاتی دارد و رفتاری را از خود بروز میدهد. میتوان یک کلاس را مثل یک جعبه متشکل از سه بخش تصور کرد:
- نام کلاس (یا شناسه): هویت کلاس را تعیین میکند.
- اعضای داده یا متغیرها (یا صفتها، حالتها و فیلدها): شامل صفتهای ایستای کلاس میشوند.
- توابع عضو (یا متدها، رفتارها و عملیات): شامل عملیات پویای کلاس است.
به بیان دیگر، یک کلاس صفتهای ایستای (دادهها) و رفتارهای پویا (عملیاتی که روی داده انجام میشود) را در یک جعبه کپسوله میکند. به مجموعه اعضای داده و توابع عضو، اعضای کلاس (Class Members) گفته میشود.
عضو داده در C++ شی گرا چیست؟
یک عضو داده (متغیر) دارای یک نام (یا شناسه) و یک نوع است که مقداری از آن نوع خاص را نگهداری میکند. همچنین، یک عضو داده میتواند نمونهای از یک کلاس خاص باشد که در ادامه پیرامون آن بحث خواهد شد. در ادامه، نکاتی در مورد قرارداد نامگذاری (Naming Convention) عضو داده شرح داده شده است.
قرارداد نامگذاری عضو داده در C++ چگونه است؟
نام یک عضو داده باید یک اسم یا عبارت اسمی باشد که از چند کلمه تشکیل شده است. حرف آغازین اولین کلمه باید با حروف کوچک و اولین حرف سایر کلمات با حروف بزرگ نوشته شوند. برای مثال، نامهای fontSize ،roomNumber ،xMax yMin و xTopLeft از قرارداد نامگذاری اعضای داده در C++ پیروی میکنند. باید به این مسئله توجه داشت که نام متغیر یا همان عضو داده بر خلاف نام کلاس با حروف کوچک شروع میشود.
تابع عضو در C++ شی گرا چیست؟
همانطور که اشاره شد، یک تابع عضو وظایف زیر را بر عهده دارد:
- دریافت پارامترها از فراخواننده
- اجرای عملیات تعریف شده در بدنه تابع
- بازگرداندن یک نتیجه (یا Void) به فراخواننده؛ کلمه کلیدی Void مشخص میکند که تابع هیچ مقداری را بازنمیگرداند.
در ادامه معرفی مفهوم تابع عضو، به شرح قرارداد نامگذاری آن در C++ پرداخته شده است.
قرارداد نامگذاری تابع عضو در C++ به چه صورت است؟
نام یک تابع باید فعل یا عبارت فعلی باشد که از چندین کلمه تشکیل شده است. اولین کلمه با حروف کوچک و باقی کلمات با حرف بزرگ آغاز میشوند. برای مثال توابع فرضی getRadius() و getParameterValues() از قرارداد نامگذاری تابع عضو در C++ پیروی میکنند.
تفاوت نام متغیر و تابع در برنامه نویسی شی گرا در C++ چیست؟
باید در نظر داشت که نام متغیر اسم (نشانگر یک ویژگی ایستا)، اما نام تابع به صورت فعل (بیانگر یک عمل) است. البته، سایر قوانین نامگذاری متغیر و تابع یکسان هستند. با این حال، میتوان نوع متغیر یا تابع بودن را از طریق مفهوم نام آن تشخیص داد. توابع آرگومانهایی را در داخل پرانتز دریافت میکنند. خالی بودن پرانتز، بیانگر این مسئله است که تابع مربوطه هیچ آرگومانی را دریافت نمیکند. در این مقاله، با هدف شفافیت بیشتر، توابع با یک جفت پرانتز نشان داده میشوند.
یک نمونه از کلاس چیست ؟
یک نمونه یا Instance تحقق یک مورد خاص از یک کلاس است. به بیان دیگر، یک Instance، نمونهسازی از یک کلاس به حساب میآید. همانطور که در تعریف کلاس بیان شد، تمام نمونههای یک کلاس خصیصههای مشابهی دارند. برای مثال، میتوان کلاسی به نام «Student» را تعریف کرد و سه نمونه از کلاس Student به نامهای Paul ،Peter و Pauline ایجاد کرد. اصطلاح شی یا Object معمولاً به یک نمونه اشاره دارد. به طور کلی، اصطلاح شی به شکل آزادانهتر و گاهی برای اشاره به کلاس هم استفاده میشود. در بخش بعدی مقاله برنامه نویسی شی گرا در C++ توضیحات بیشتری راجع به اشیا در C++ ارائه شده است.
شی در C++ چیست؟
شی (Object) یک موجودیت قابل شناسایی است که خصوصیاتی دارد و رفتاری از خود بروز میدهد. شی به نمونهای (Instance) از یک کلاس گفته میشود. زمان تعریف یک کلاس، هیچ حافظهای تخصیص داده نمیشود. اما، وقتی آن کلاس نمونهسازی میشود (یک شی ایجاد میشود) به آن حافظه اختصاص داده خواهد شد. یک شی در حافظه فضا اشغال میکند و دارای یک نشانی شبیه به یک رکورد در زبان پاسکال یا ساختار (Structure) یا انجمن (Union) در زبان C است. وقتی که یک برنامه اجرا میشود، اشیا از طریق ارسال پیام با یکدیگر ارتباط برقرار میکنند.
هر شی حاوی داده و کدهایی برای دستکاری دادهها است. اشیا میتوانند بدون دانستن جزئیات دادهها یا کدهای یکدیگر به تعامل بپردازند. دانستن نوع پیام پذیرفته شده و نوع پاسخ بازگشت داده شده توسط اشیا کفایت میکند. در ادامه مقاله برنامه نویسی شی گرا در C++ ، هر یک از چهار ستون اصلی برنامه نویسی شی گرا که در ابتدای مقاله به صورت فهرستوار معرفی شدند، با جزئیات بیشتری ارائه شده است. ابتدا به شرح مفهوم کپسولهسازی یا بستهبندی در C++ پرداخته شده است.
کپسولهسازی در C++ چیست؟
کپسولهسازی در کاربرد رایج، به معنی پیچیدن یا بستهبندی داده و اطلاعات در یک قطعه واحد است. در برنامه نویسی شی گرا ،کپسولهسازی یا بستهبندی به اتصال دادهها و توابع مربوط به آن دادهها به یکدیگر گفته میشود. برای درک بهتر این مفهوم، در ادامه این بخش از برنامه نویسی شی گرا در C++ ، مثالی ساده و مطابق با جهان واقعی ارائه شده است.
مثالی در جهان واقعی برای درک بهتر مفهوم بستهبندی
به عنوان یک مثال جهان واقعی از کپسولهسازی، میتوان یک شرکت تجاری را در نظر گرفت. در یک شرکت، بخشهای مختلفی مثل بخش حسابداری، بخش مالی، بخش فروش و سایر بخشها وجود دارد. در بخش مالی تمام تراکنشهای مالی مدیریت و همه اسناد و دادههای مربوط به دارایی نگهداری میشوند. به همین شکل، در بخش فروش، تمام فعالیتها و اسناد مربوط به فروش شرکت مدیریت میشوند.
حال ممکن است در یک ماه خاص، وضعیتی به وجود بیاید که به هر دلیلی یکی مسئولین بخش مالی نیاز به همه دادههای مربوط به فروش داشته باشد. در چنین موردی، این فرد اجازه دسترسی مستقیم به دادههای بخش فروش را نخواهد داشت. او ابتدا باید با مسئول دیگری در بخش فروش در ارتباط باشد و اجازه دسترسی به دادهها را درخواست کند. در اینجا، دادههای بخش فروش و کارمندانی که میتوانند این دادهها را دستکاری کنند، همگی تحت نام «بخش فروش» بستهبندی شدهاند.
همچنین، کپسوله کردن به انتزاعیسازی یا مخفیسازی داده میانجامد. زیرا بستهبندی دادها را پنهان هم میکند. در مثال فوق، دادههای هر کدام از بخشها نظیر واحد فروش، واحد مالی یا حسابداری از هر بخش دیگری مخفی شدهاند. در توضیح تصویر فوق نیز باید گفت که میتوان یک کلاس را مانند یک کپسول در نظر گرفت که متدها و توابع به همراه متغیرها در داخل آن بستهبندی شدهاند. به این ترتیب، با روشن شدن مفهوم بستهبندی یا کپسوله کردن در C++، ادامه مقاله برنامه نویسی شی گرا در C++ به شرح مفهوم انتزاع دادهها یا Abstraction اختصاص دارد.
انتزاع دادهها در C++ چیست؟
انتزاع دادهها (Data Abstraction) یکی از ویژگیهای حیاتی و مهم برنامه نویسی شی گرا در C++ به حساب میآید. انتزاع داده یعنی تنها اطلاعات ضروری را نمایش داده و جزئیات مخفی شوند. در انتزاع داده، فقط اطلاعات ضروری درباره دادهها برای جهان بیرونی فراهم و جزئیات یا پیادهسازیهای پسزمینه پنهان میشوند. در اینجا نیز برای درک بهتر مفهوم انتزاع داده، مثالی از جهان واقعی ارائه شده است.
مثالی از جهان واقعی برای درک بهتر مفهوم انتزاع داده
به عنوان مثال، میتوان یک فرد در حال رانندگی را در جهانی واقعی در نظر گرفت. این فرد فقط میداند که فشردن پدال گاز سرعت خودرو را افزایش میدهد یا ترمز گرفتن ماشین را متوقف میکند. اما، این فرد از طرز کار اجزای داخلی ماشین سر در نمیآورد و اطلاعی از نحوه سرهمبندی پیشرانه یا ترمزها ندارد. این همان چیزی است که به آن انتزاع گفته میشود.
انتزاع با استفاده از کلاسها در C++
انتزاع را میتوان با استفاده از کلاسها در C++ پیادهسازی کرد. کلاسها در گروهبندی اعضای داده و توابع عضو با استفاده از مشخص کنندههای دسترسی (Access Specifier) به برنامهنویس کمک میکنند. یک کلاس میتواند تصمیم بگیرد که کدام عضو داده برای جهان بیرونی قابل مشاهده خواهد بود.
انتزاع در فایلهای سربرگ C++
به عنوان یک نوع دیگر از انتزاع در C++ میتوان فایلهای سربرگ (Header) را نام برد. برای مثال، میتوان متد pow() را در نظر گرفت که در فایل سربرگ math.h قرار دارد. هر گاه نیاز به محاسبه توان یک عدد وجود داشته باشد، میتوان به سادگی و بدون اطلاع از الگوریتمی که محاسبه توان یک عدد بر اساس آن انجام میشود، متد pow() در فایل سربرگ math.h را فراخوانی کرد و اعدادی را به عنوان آرگومان به آن ارجاع داد. یکی دیگر از ستونهای اصلی برنامه نویسی شی گرا در C++ مفهوم چندریختی است که در ادامه به شرح آن پرداخته شده است.
چندریختی در C++ چیست؟
کلمه چندریختی (Polymorphism) به معنی داشتن اشکال مختلف است. به بیان ساده، میتوان پلیمورفیسم یا چندریختی را امکان نمایش یک پیام به شکلهای گوناگون تعریف کرد. مطابق مفاهیم قبلی شی گرایی در C++ ، مثالی هم برای درک بهتر پلیمورفیسم در ادامه بیان شده است.
مثالی برای درک بهتر مفهوم چندریختی
به عنوان مثال، یک شخص میتواند در آنِ واحد دارای چندین خصیصه باشد. مثلاً یک زن میتواند در آنِ واحد هم یک مادر، هم همسر و هم یک کارمند باشد. بنابراین، همان شخص رفتارهای متفاوتی را در شرایط و وضعیتهای متفاوت از خود نشان میدهد. به طور مشابه، یک عملیات در نمونههای مختلف ممکن است رفتار متفاوتی را از خود بروز دهد. این رفتار به نوع دادههای استفاده شده در عملیات بستگی دارد.
سربار عملگر و سربار تابع در C++
C++ از سربار عملگر و سربار تابع پشتیبانی میکند:
- سربار عملگر (Operator Overloading): فرآیند واداشتن یک عملگر به نشان دادن رفتارهای متفاوت در موارد متفاوت را سرباری عملگر مینامند.
- سربار تابع (Function Overloading): به استفاده از یک نام تابع یکسان برای انجام انواع وظایف متفاوت در مواقع متفاوت، سربار تابع گفته میشود.
در ادامه این بخش از نوشته برنامه نویسی شی گرا در C++ ، مثالی برای درک بهتر مفهوم سربار تابع ارائه شده است.
مثالی برای درک بهتر سربار تابع
به فرض، باید تابعی برای جمع چند عدد صحیح نوشته شود. یعنی گاهی دو عد صحیح و گاهی سه عدد صحیح وجود دارد که باید جمع شوند. میتوان تابع جمع را با نامی یکسان، اما با پارامترهای متفاوت نوشت. در این صورت، با توجه به پارامترها، آن تابعی که مورد نیاز باشد فراخوانی خواهد شد. در تصویر زیر، مفهوم سربار تابع به عنوان یکی از نمودهای چندریختی در C++ شی گرا مصورسازی شده است.
بدین ترتیب، مفهوم چندریختی در C++ شی گرا شرح داده شد. اکنون در ادامه پرداختن به برنامه نویسی شی گرا در C++ ، نوبت به شرح یکی دیگر از پایههای اساسی شی گرایی در C++ یعنی ارثبری رسیده است.
ارث بری در C++ چیست؟
به قابلیت یک کلاس در اشتقاق خصیصهها و خصلتها از یک کلاس دیگر ارثبری گفته میشود. ارثبری یکی از مهمترین ویژگیهای برنامه نویسی شی گرا در C++ به شمار میرود.
در ادامه، برخی از اصطلاحاتی معرفی شدهاند که در مضمون ارثبری به کار برده میشوند.
- زیرکلاس (Sub Class): به کلاسی که خصیصههایی را از کلاس دیگر به ارث میبرد، زیرکلاس یا کلاس مشتق شده گفته میشود.
- ابَرکلاس (Super Class): کلاسی که خصیصههای آن به وسیله زیرکلاس به ارث برده شدهاند، ابرکلاس نامیده میشود.
- قابلیت استفاده مجدد (بازیافتپذیری | Reusability): ارثبری از مفهوم امکان استفاده مجدد یا «Reusability» پشتیبانی میکند. وقتی قصد ایجاد کلاس جدیدی وجود داشته باشد، در صورتی که کلاسی وجود داشته باشد که برخی از کدهای مورد نظر برای کلاس جدید را داشته باشد، میتوان کلاس جدید را از کلاس فعلی مشتق کرد. با انجام این کار، در واقع فیلدها و متدهای کلاس فعلی مجدداً استفاده میشوند.
مثالی برای درک بهتر مفهوم ارثبری
برای مثال، در صورتی که هر یک از حیواناتی مثل سگ، گربه و گاو به عنوان یک کلاس در نظر گرفته شوند، میتوان گفت که همه این کلاسها از کلاسی به نام حیوان مشتق شدهاند.
مقیدسازی پویا (نسبتدهی پویا) در C++
در مقیدسازی پویا (Dynamic Binding)، کدهایی که باید در پاسخ به فراخوانی تابع اجرا شوند، در زمان اجرا تعیین میشوند. C++ برای پشتیبانی از مقیدسازی پویا دارای توابع مجازی است. مقیدسازی پویا زمانی اتفاق میافتد که کامپایلر نتواند تمام اطلاعات مورد نیاز یک تابع برای فراخوانی در زمان کامپایل را تعیین کند.
مقیدسازی ایستا با استفاده از فراخوانیهای معمولی تابع، سربار تابع، سربار عملگر قابل محقق شدن است. در حالی که، رسیدن به مقیدسازی پویا با استفاده از توابع مجازی انجام میشود.
ارسال پیام در شی گرایی
اشیا از طریق ارسال و دریافت اطلاعات با یکدیگر ارتباط برقرار میکنند. ارسال پیام (Message Passing) برای یک شی درخواستی جهت اجرای یک رویه (Procedure) به کار میرود. بنابراین، به تابعی در شی دریافتی استناد خواهد شد که نتایج مورد نظر را تولید کند. ارسال پیام شامل تعیین نام شی و اطلاعات ارسالی است.
آموزش برنامه نویسی شی گرا در C++
پس از بیان مباحث نظری، در این بخش از مقاله برنامه نویسی شی گرا در C++ ، آموزش مفاهیم شی گرایی به همراه مثالهایی در زبان برنامه نویسی C++ ارائه شده است.
تعریف یک کلاس در C++ چگونه است ؟
در C++ برای تعریف یک کلاس از کلمه کلیدی «class» استفاده میشود. در تعریف کلاس دو بخش خصوصی و عمومی وجود دارد که در ادامه درباره آنها توضیحاتی ارائه خواهد شد. برای مثال، در برنامه نویسی شی گرا در C++ ، دو کلاس به نامهای Circle و SoccerPlayer به صورت زیر تعریف میشوند:
1class Circle { // نام کلاس
2private:
3 double radius; // اعضای داده (متغیرها)
4 string color;
5public:
6 double getRadius(); // توابع عضو
7 double getArea();
8}
1class SoccerPlayer { // نام کلاس
2private:
3 int number; // اعضای داده (متغیرها)
4 string name;
5 int x, y;
6public:
7 void run(); // توابع عضو
8 void kickBall();
9}
در ادامه، پیرامون قرارداد نامگذاری یا «Naming Convention» یک کلاس در C++ توضیحاتی ارائه شده است.
قرارداد نامگذاری کلاس در C++ به چه صورت است؟
نام یک کلاس در C++ باید یک اسم (فاعل) یا یک عبارت اسمی تشکیل شده از چندین کلمه باشد. همه کلمهها باید با حروف بزرگ شروع شوند. به این روش، «نگارش شتری» یا «Camel Case» گفته میشود. برای نام کلاس در C++ باید از اسم مفرد استفاده کرد. بهتر است نام کلاس در C++ معنادار و خود توصیفگر باشد.
چند مثال از نامگذاری کلاس در C++ در ادامه آمده است.
- SoccerPlayer
- HttpProxyServer
- FileInputStream
- PrintStream
- SocketFactory
نحوه تعریف یک کلاس شرح داده شد، حال در ادامه به نحوه ایجاد یک شی در C++ پرداخته شده است.
ایجاد اشیا در C++ چگونه است؟
برای ایجاد یک شی یا همان نمونههایی از یک کلاس باید موارد زیر را انجام داد:
- تعریف شناسه نمونه (Instance Identifier) یا همان نام شی از یک کلاس خاص
- فراخوانی یک سازنده (Constructor) برای ساخت نمونه که تخصیص حافظه برای نمونه و مقداردهی اولیه متغیرها را شامل میشود.
برای مثال، با فرض اینکه کلاسی به نام Circle وجود داشته باشد، میتوان نمونهها یا اشیایی از Circle را به صورت زیر تعریف کرد:
1// ایجاد سه نمونه از کلاس دایره:
2Circle c1(1.2, "red"); // شعاع، رنگ
3Circle c2(3.4); // شعاع، رنگ پیشفرض
4Circle c3; // شعاع و رنگ پیشفرض
از طرفی، میتوان سازنده را به طور خاص با استفاده از سینتکس زیر فراخوانی کرد:
1Circle c1 = Circle(1.2, "red"); // شعاع، رنگ
2Circle c2 = Circle(3.4); // شعاع، رنگ پیشفرض
3Circle c3 = Circle(); // شعاع و رنگ پیشفرض
عملگر نقطه در C++ چیست ؟
برای ارجاع به عضوی از یک شی (عضو داده یا تابع عضو) باید موارد زیر را انجام داد.
- ابتدا باید نمونه یا شی مورد نظر را شناسایی کرد.
- باید از عملگر نقطه (.) برای ارجاع به آن عضو در قالب «نام نمونه.نام عضو» استفاده کرد.
برای مثال، فرض میشود که کلاسی به نام Circle با دو عضو داده به نامهای radius و color و دو تابع به نامهای getRadius() و getArea() وجود دارد. به فرض، سه نمونه یا شی از کلاس Circle به نامهای c2 ،c1 و c3 ایجاد شده است. برای فراخوانی تابع getArea()، ابتدا باید نام نمونه مورد نظر، مثلاً c2 را مشخص و سپس از عملگر نقطه به صورت c2.getArea() برای فراخوانی تابع getArea() از نمونه c2 استفاده کرد. در ادامه این بخش از مقاله برنامه نویسی شی گرا در C++ ، مثالی از فراخوانی تابع از یک نمونه با عملگر نقطه آمده است:
1// تعریف و ساخت اشیا یا نمونههایی از کلاس دایره:
2Circle c1(1.2, "blue");
3Circle c2(3.4, "green");
4// فراخوانی تابع عضو با عملگر نقطه
5cout << c1.getArea() << endl;
6cout << c2.getArea() << endl;
7// ارجاع اعضای داده از طریق عملگر نقطه
8c1.radius = 5.5;
9c2.radius = 6.6;
فراخوانی تابع getArea() بدون مشخص کردن نمونه (شی) بیمعنی است، چرا که شعاع (در مثال فوق) مشخص نخواهد بود. زیرا ممکن است نمونههای بسیاری از دایره وجود داشته باشد که هر کدام شعاع مربوط به خود را داشته باشند.
به طورکلی، با فرض داشتن کلاسی به نام AClass با عضو دادهای به نام aData و یک تابع عضو به نام aFunction()، اگر یک نمونه به نام anInstance برای AClass ساخته شده باشد، از anInstance.aData و anInstance.aFunction() برای ارجاع به عضو داده و تابع عضو استفاده میشود. اکنون، با توجه به اینکه تا اینجا مفاهیم اساسی و اصلی برنامه نویسی شی گرا در C++ معرفی شدهاند، در ادامه مثالی برای یادگیری بهتر ارائه شده است.
مثال برنامه نویسی شی گرا در C++
با هدف جمعبندی مفاهیم و آموزههای ارائه شده تا اینجا، این بخش به مثالی از برنامه نویسی شی گرا در C++ اختصاص دارد. در این مثال، کلاسی به نام Circle تعریف شده است. این کلاس شامل دو عضو داده radius (از نوع داده double) و متغیر color (از نوع رشتهای) است.
همچنین، این کلاس سه تابع عضو به نامهای getColor() ، getRadius() و getArea() دارد. C2 ،C1 و C3 سه شی یا نمونه کلاس Circle هستند که مطابق آنچه در دیاگرام نمونهها در تصویر زیر نمایش داده شده است، در این مثال ساخته خواهند شد.
کدهای مربوط به این مثال که در فایل «CircleAIO.cpp» ذخیره میشوند، به صورت زیر است:
1/* The Circle class (All source codes in one file) (CircleAIO.cpp) */
2#include <iostream> // استفاده از توابع IO
3#include <string> // استفاده از رشته
4using namespace std;
5
6class Circle {
7private:
8 double radius; // عضو داده (متغیر)
9 string color; // عضو داده (متغیر)
10
11public:
12 // سازنده با مقادیر پیشفرض برای اعضای داده
13 Circle(double r = 1.0, string c = "red") {
14 radius = r;
15 color = c;
16 }
17
18 double getRadius() { // تابع عضو (دریافت کننده)
19 return radius;
20 }
21
22 string getColor() { // تابع عضو (دریافت کننده)
23 return color;
24 }
25
26 double getArea() { // تابع عضو
27 return radius*radius*3.1416;
28 }
29}; // تعریف کلاس باید با سمیکالون پایان پذیرد
30
31// تابع راهانداز برای تست
32int main() {
33 // ساخت یک نمونه از کلاس دایره
34 Circle c1(1.2, "blue");
35 cout << "Radius=" << c1.getRadius() << " Area=" << c1.getArea()
36 << " Color=" << c1.getColor() << endl;
37
38 // ساخت یک نمونه (شی) دیگر از کلاس دایره
39 Circle c2(3.4); // default color
40 cout << "Radius=" << c2.getRadius() << " Area=" << c2.getArea()
41 << " Color=" << c2.getColor() << endl;
42
43 // ساخت یک نمونه از کلاس دایره با استفاده از یک سازنده پیشفرض بدون آرگومان
44 Circle c3; // شعاع و رنگ پیشفرض
45 cout << "Radius=" << c3.getRadius() << " Area=" << c3.getArea()
46 << " Color=" << c3.getColor() << endl;
47 return 0;
48}
برای کامپایل کردن و اجرای برنامه با کامپایلر GNU GCC در ویندوز، باید از دستورات زیر استفاده کرد:
1> g++ -o CircleAIO.exe CircleAIO.cpp
2 // -o specifies the output file name
3
4> CircleAIO
5Radius=1.2 Area=4.5239 Color=blue
6Radius=3.4 Area=36.3169 Color=red
7Radius=1 Area=3.1416 Color=red
به این ترتیب، یک مثال ساده و اولیه برای مفاهیم پایه برنامه نویسی شی گرا در C++ ارائه شد. در ادامه این مقاله، به سایر مفاهیم شی گرایی در C++ به صورت عملی و با ذکر مثال پرداخته شده است. ادامه مقاله برنامه نویسی شی گرا در C++ به معرفی و شرح سازندهها یا Constructorها در C++ شی گرا اختصاص دارد.
سازنده ها در C++
یک سازنده تابعی مخصوص است که نام تابعی مشابه نام کلاس دارد. برای کلاس Circle که در مثال بالا تعریف شد، یک سازنده به صورت زیر تعریف میشود.
1// سازنده دارای نام یکسان با کلاس است
2Circle(double r = 1.0, string c = "red") {
3 radius = r;
4 color = c;
5}
کاربرد سازنده در C++ چیست ؟
یک سازنده برای ساختن و مقداردهی اولیه تمام اعضای داده استفاده میشود. برای ایجاد یک نمونه جدید از یک کلاس، باید نام نمونه تعریف و سازنده فراخوانی شود. برای مثال، این کار به صورت زیر انجام میشود:
1Circle c1(1.2, "blue");
2Circle c2(3.4); // رنگ پیشفرض
3Circle c3; // شعاع و رنگ پیشفرض
4 // باید توجه کرد که هیچ پرانتز خالی وجود ندارد
تفاوت سازنده و تابع چیست ؟
یک تابع سازنده با یک تابع معمولی در موارد زیر متفاوت است:
- نام سازنده با نام کلاس یکسان است.
- سازنده هیچ نوع بازگشتی ندارد (یا به طور مشخص مقدار Void را بازمیگرداند). بنابراین، اجازه استفاده از هیچ گزاره بازگشتی در داخل بدنه سازنده وجود ندراد.
- تنها یک بار امکان فراخوانی سازنده برای مقداردهی اولیه نمونه ساخته شده وجود دارد. پس از آن، نمیتوان سازنده را در برنامه فراخوانی کرد.
- سازندهها به ارث برده نمیشوند. (بعداً توضیح داده خواهد شد.)
آرگومانهای پیشفرض برای توابع
در C++ میتوان مقدار پیشفرض را برای آرگومانهای مؤخر (Trailing) یک تابع (شامل سازنده) در سربرگ تابع مشخص کرد. مثالی در این رابطه به صورت زیر آمده است:
1/* Test function default arguments (TestFnDefault.cpp) */
2#include <iostream>
3using namespace std;
4
5// نمونه اولیه تابع
6int sum(int n1, int n2, int n3 = 0, int n4 = 0, int n5 = 0);
7
8int main() {
9 cout << sum(1, 1, 1, 1, 1) << endl; // 5
10 cout << sum(1, 1, 1, 1) << endl; // 4
11 cout << sum(1, 1, 1) << endl; // 3
12 cout << sum(1, 1) << endl; // 2
13// cout << sum(1) << endl; // error: too few arguments
14}
15
16// تعریف تابع
17// مقادیر پیشفرض باید در نمونه اولیه تابع مشخص شوند
18// و نه در پیادهسازی تابع
19int sum(int n1, int n2, int n3, int n4, int n5) {
20 return n1 + n2 + n3 + n4 + n5;
21}
اصلاح کنندههای کنترل دسترسی عمومی و خصوصی
یک اصلاح کننده کنترل دسترسی (Access Control Modifier) را میتوان برای کنترل رویتپذیری یک عضو داده یا تابع عضو در داخل یک کلاس مورد استفاده قرار داد.
کار با دو اصلاح کننده کنترل دسترسی زیر آغاز میشود:
- عمومی (Public): عضو (داده یا تابع) برای همه در سیستم قابل دسترسی است.
- خصوصی (Private): عضو (داده یا تابع) تنها در کلاس مربوطه قابل دسترسی است.
برای مثال، در تعریف کلاس Circle (در مثالی که پیشتر آمد)، شعاع عضو داده به صورت خصوصی تعریف شده است. در نتیجه، شعاع در داخل کلاس Circle در دسترس خواهد بود، اما در خارج کلاس نمیتوان به آن دسترسی داشت. به بیان دیگر، نمیتوان از گذاره «c1.radius» برای اشاره به شعاع c1 در تابع main() استفاده کرد و در صورتی که این کار انجام شود، خطای زیر رخ خواهد داد:
CircleAIO.cpp:8:11: error: 'double Circle::radius' is private
میتوان متغیر radius را به بخش عمومی منتقل و گزاره را مجدد اجرا کرد. از طرف دیگر، تابع getRadius() در کلاس Circle به صورت عمومی تعریف شده است. بنابراین، میتوان این تابع را در تابع main() فراخوانی کرد.
پنهانسازی و کپسولهسازی اطلاعات
یک کلاس صفتهای ایستا و رفتارهای پویا را در یک جعبه سه بخشی بستهبندی و کپسوله میکند. وقتی که یک کلاس تعریف میشود، میتوان جعبه را مهر و موم کرد و آن را در قفسه گذاشت تا دیگران بتوانند بارها از آن استفاده کنند. هر شخصی میتواند جعبه را بردارد و از آن در اپلیکیشن خود استفاده کند.
این کار در یک زبان سنتی و رویهگرا مثل C قابل انجام نیست. زیرا صفتها یا متغیرهای ایستا در تمام برنامه و فایلهای سربرگ پخش شدهاند. نمیتوان بخشی از یک برنامه C را بُرید، آن را به برنامه دیگر اضافه کرد و از برنامه انتظار داشت بدون نیاز به تغییرات گسترده و به راحتی اجرا شود. عضو داده یک کلاس معمولاً با اصلاح کننده دسترسی خصوصی از جهان خارج پنهان میشود.
دسترسی به اعضای داده خصوصی از طریق توابع دستیابی (Assessor Function) مثل getRadius() و getColor() فراهم میشود. برنامه نویسی شی گرا در C++ از اصل پنهانسازی اطلاعات پیروی میکند. در این اصل، اشیا با استفاده از واسطهای کاملاً مشخص (توابع عمومی) با یکدیگر ارتباط برقرار میکنند. اشیا اجازه دانستن جزئیات اجرای دیگران را ندارند. جزئیات اجرا در داخل کلاس پنهان و کپسوله شده است. پنهانسازی اطلاعات استفاده مجدد از کلاس را آسان میکند. نکته مهم در بحث پنهانسازی و کپسولهسازی برنامه نویسی شی گرا در C++ این است که نباید هیچ عضو دادهای را عمومی کرد، مگر اینکه دلیل محکمی برای آن وجود داشته باشد.
گیرندهها و تنظیم کنندهها در C++
برای اینکه به کلاسهای دیگر اجازه خواندن یک عضو داده خصوصی با نام فرضی faradars داده شود، باید یک تابع get (یا getter یا accessor) به نام getFaradars() را فراخوانی کرد. یک تابع گیرنده یا Getter نباید دادهها را در قالب خام افشا کند. این تابع میتواند دادهها را پردازش و نمای دادهای که دیگران خواهند دید را محدود کند. توابع گیرنده نباید عضو داده را تغییر دهند. برای اینکه به دیگر کلاسها امکان تغییر مقدار یک داده خصوصی مثل متغیر faradars داده شود، باید از یک تابع تنظیم کننده (گذارنده | Set) استفاده شود.
به توابع تنظیم کننده Setter یا Mutator هم گفته میشود. برای متغیر faradars، تابع تنظیم کننده به صورت setFaradars() خواهد بود. یک تابع setter میتواند اعتبارسنجی داده (مثل بررسی دامنه) انجام دهد و دادههای خام را به نمایش داخلی تبدیل کند. برای مثال، در کلاس Circle، اعضای داده radius و color به صورت خصوصی تعریف شدهاند.
بنابراین، آنها تنها در داخل کلاس Circle در دسترس هستند و در خارج این کلاس و تابع main() قابل شناسایی نخواهند بود. نمیتوان مستقیماً از تابع اصلی main() به اعضای داده دسترسی داشت. کلاس دایره دو تابع دستیابی به نامهای getRadius() و getColor() فراهم میکند. این توابع به عنوان توابع عمومی تعریف شدهاند. تابع main() میتواند این توابع دستیابی عمومی را برای بازیابی متغیرهای radius و color از شی Circle به صورت c1.getRadius() و c1.getColor() فراخوانی کند.
هیچ راهی وجود ندارد که بتوان متغیرهای radius و color یک شی Circle را پس از ایجاد شدن در تابع main() تغییر داد. نمیتوان گزارههایی مثل c1.radius = 5.0 را برای تغییر شعاع نمونه c1 به کار برد. زیرا عضو داده radius در کلاس Circle به صورت خصوصی تعریف شده است و برای سایرین از جمله main() قابل مشاهده نخواهد بود. در صورتی که طراح کلاس Circle اجازه تغییر radius و color را پس از ساخت شی Circle بدهد، باید تابع تنظیم کننده مناسب را نیز مشابه مثال زیر فراهم کرده باشد:
1// تابع تنظیم کننده برای متغیر رنگ
2void setColor(string c) {
3 color = c;
4}
5
6// تابع تنظیم کننده برای متغیر شعاع
7void setRadius(double r) {
8 radius = r;
9}
با اجرا و پیادهسازی اصولی پنهانسازی اطلاعات، طراح کلاس (برنامهنویس) کنترل کاملی خواهد داشت نسبت به آنچه که کاربر یک کلاس میتواند یا نمیتواند انجام دهد.
کلمه کلیدی This در C++
میتوان از کلمه کلیدی «This» برای ارجاع به نمونه فعلی در داخل یک تعریف کلاس استفاده کرد. یکی از کاربردهای اصلی کلمه کلیدی This رفع ابهام بین نامهای اعضای داده و پارامترهای توابع است. برای مثال، باید به کدهای زیر دقت کرد:
1class Circle {
2private:
3 double radius; // متغیر عضو با نام شعاع
4 ......
5public:
6 void setRadius(double radius) { // آرگومان تابع که آن هم شعاع نام دارد
7 this->radius = radius;
8 // گذاره به متغیر عضو این نمونه اشاره دارد
9 // "radius" resolved to the function's argument.
10 }
11 ......
12}
در کدهای فوق، دو شناسه به نامهای radius وجود دارد. یکی از آنها عضو داده و دیگری پارامتر تابع است. این مسئله باعث بروز تداخل نام میشود. برای رفع تداخل نام، میتوان نام پارامتر تابع را از radius به r تغییر داد. اگرچه، radius بار معنایی بیشتری دارد. بنابراین، میتوان از کلمه کلیدی this برای رفع این تداخل نام استفاده کرد. عبارت «this->radius» در کدهای بالا به عضو داده اشاره دارد، در حالی که «radius» به پارامتر تابع اطلاق میشود.
کلمه کلید this در واقع یک اشارهگر (Pointer) به شی است. در عوض میتوان از یک پیشوند (مثل m_) یا یک پسوند (مثل ـ) برای نامگذاری اعضای داده استفاده کرد تا از توقف برنامه به دلیل تداخل نام جلوگیری شود. مثالی پیرامون این مسئله در ادامه آمده است:
1class Circle {
2private:
3 double m_radius; // or radius_
4 ......
5public:
6 void setRadius(double radius) {
7 m_radius = radius; // or radius_ = radius
8 }
9 ......
10}
کامپایلر C++ اعضای داده خود را به صورت داخلی با یک زیر خط در آغاز (ـ) و متغیرهای محلی را با دو زیر خط در ابتدایشان نامگذاری میکند. بنابراین باید از به کار بردن یک یا دو زیرخط در ابتدای نام متغیرها خودداری کرد.
تابع عضو Const در C++
یک تابع عضو ثابت که با کلمه کلیدی «const» در انتهای سربرگ تابع عضو مشخص میشود، نمیتواند هیچ عضو دادهای از یک شی را تغییر دهد. برای مثال در کدهای زیر تابع عضو getRadius() از نوع const تعریف شده است:
1double getRadius() const { // تابع عضو ثابت
2 radius = 0;
3 // error: assignment of data-member 'Circle::radius' in read-only structure
4 return radius;
5}
قرارداد برای گیرندهها، تنظیم کنندهها و سازندهها
توابع سازنده، گیرنده و تنظیم کننده برای عضو داده خصوصی به نام xxx از نوع T در کلاس Aaa دارای قراردادهای زیر است:
1class Aaa {
2private:
3 // A private variable named xxx of type T
4 T xxx;
5public:
6 // Constructor
7 Aaa(T x) { xxx = x; }
8 // OR
9 Aaa(T xxx) { this->xxx = xxx; }
10 // OR using member initializer list (to be explained later)
11 Aaa(T xxx) : xxx(xxx) { }
12
13 // A getter for variable xxx of type T receives no argument and return a value of type T
14 T getXxx() const { return xxx; }
15
16 // A setter for variable xxx of type T receives a parameter of type T and return void
17 void setXxx(T x) { xxx = x; }
18 // OR
19 void setXxx(T xxx) { this->xxx = xxx; }
20}
برای یک متغیر بولی xxx، گیرنده باید به جای getXxx() به صورت زیر و isXxx() نامگذاری شود:
1private:
2 // متغیر بولی خصوصی
3 bool xxx;
4public:
5 // گیرنده
6 bool isXxx() const { return xxx; }
7
8 // تنظیم کننده
9 void setXxx(bool x) { xxx = x; }
10 // OR
11 void setXxx(bool xxx) { this->xxx = xxx; }
سازنده پیشفرض در C++
به جای مقداردهی اولیه اعضای داده خصوصی در داخل بدنه سازنده به صورت زیر:
1Circle(double r = 1.0, string c = "red") {
2 radius = r;
3 color = c;
4}
میتوان از یک سینتکس جایگزین به نام «لیست مقداردهنده اولیه عضو» به صورت زیر استفاده کرد:
1Circle(double r = 1.0, string c = "red") : radius(r), color(c) { }
فهرست مقداردهنده اولیه عضو بعد از سربرگ سازنده قرار داده و با یک علامت دو نقطه جداسازی میشود. هر مقداردهنده اولیه به صورت قالب ata_member_name(parameter_name) است. برای نوع بنیادی، مقداردنده اولیه معادل data_member_name = parameter_name است. در مورد شی، سازنده برای ساخت شی فراخوانی خواهد شد. بدنه سازنده که در این مورد خالی است، پس از کامل شدن فهرست مقداردهنده اولیه عضو اجرا خواهد شد. توصیه میشود که از فهرست مقداردهنده اولیه عضو برای مقداردهی اولیه اعضای داده استفاده شود. زیرا معمولاً این روش نسبت به تخصیصدهی در داخل بدنه سازنده بهینهتر است.
مخرب در C++
یک مخرب (Destructor)، مشابه سازنده (Constructor)، یک تابع مخصوص است که نام یکسانی با نام کلاس دارد، با این تفاوت که یک پیشوند «~» مثل Circle()~ نیز قبل از نام کلاس قرار داده میشود. مخرب در زمان از بین بردن یک شی به طور ضمنی فراخوانی میشود.
در صورتی که یک سازنده تعریف نشود، کامپایلر یک مخرب پیشفرض فراهم میکند که کاری انجام نمیدهد.
1class MyClass {
2public:
3 // مخرب پیشفرضی که کاری انجام نمیدهد
4 ~MyClass() { }
5......
6}
یک نکته تخصصی در این خصوص این است که، اگر کلاس حاوی عضو دادهای باشد که به طور پویا (از طریق new یا new[ ]) تخصیص داده شده است، باید حافظه را از طریق delete یا delete[ ] آزادسازی کرد.
سازنده Copy در C++
یک سازنده Copy شی جدیدی را به وسیله کپی کردن یک شی فعلی از همان نوع میسازد. به بیان دیگر، یک سازنده Copy آرگومانی را میگیرد که شیئی از همان کلاس است.
در صورت عدم تعریف یک سازنده Copy، کامپایلر یک سازنده کپی پیشفرض را فراهم میکند که تمام اعضای داده شی داده شده را کپی خواهد کرد. مثالی در ادامه آمده است:
1Circle c4(7.8, "blue");
2cout << "Radius=" << c4.getRadius() << " Area=" << c4.getArea()
3 << " Color=" << c4.getColor() << endl;
4 // Radius=7.8 Area=191.135 Color=blue
5
6// ساخت یک شی جدیدی با کپی کردن یک شی فعلی
7// از طریق آنچه بدان سازنده کپی پیشفرض گویند
8Circle c5(c4);
9cout << "Radius=" << c5.getRadius() << " Area=" << c5.getArea()
10 << " Color=" << c5.getColor() << endl;
11 // Radius=7.8 Area=191.135 Color=blue
سازنده Copy منحصراً اهمیت دارد. زیرا وقتی که یک شی با مقدار به یک تابع ارجاع داده میشود، سازنده Copy برای ایجاد یک همتا (Clone) از آرگومان به کار میرود.
نکات پیشرفته درباره سازنده Copy
چند نکته مهم راجع به سازنده Copy به شرح زیر است:
- Pass-by-value برای شی به معنی فراخوانی سازنده Copy است. برای اجتناب از سربار ایجاد یک کپی همتا، معمولاً بهتر است ارجاع به وسیله مرجع به ثابت (pass-by-reference-to-const) انجام شود. این روش اثر جانبی در اصلاح شی فراخواننده نخواهد داشت.
- سازنده کپی دارای تفسیری (امضا | Signature) به صورت زیر است:
1class MyClass {
2private:
3 T1 member1;
4 T2 member2;
5public:
6 // سازنده کپی پیشفرض که شیئی را از طریق کپی عضوگونه ایجاد میکند
7 MyClass(const MyClass & rhs) {
8 member1 = rhs.member1;
9 member2 = rhs.member2;
10 }
11......
12}
- سازنده Copy پیشفرض کپی سایهای (Shadow Copy) انجام میدهد. سازنده Copy پیشفرض، دادههای تخصیص یافته به صورت پویای ایجاد شده به وسیله عملگر new یا new[ ] را کپی نمیکند.
عملگر جایگزینی کپی در C++
کامپایلر همچنین یک عملگر جایگزینی پیشفرض (=) را هم فراهم میکند.
از عملگر جایگزینی میتوان برای تخصیص یک شی به شی دیگر از همان کلاس به وسیله کپی عضوگونه (Memberwise) استفاده کرد. برای مثال، با استفاده از کلاس Circle که پیشتر معرفی شد:
1Circle c6(5.6, "orange"), c7;
2cout << "Radius=" << c6.getRadius() << " Area=" << c6.getArea()
3 << " Color=" << c6.getColor() << endl;
4 // Radius=5.6 Area=98.5206 Color=orange
5cout << "Radius=" << c7.getRadius() << " Area=" << c7.getArea()
6 << " Color=" << c7.getColor() << endl;
7 // Radius=1 Area=3.1416 Color=red (default constructor)
8
9c7 = c6; // جایگزینی کپی عضوگونه
10cout << "Radius=" << c7.getRadius() << " Area=" << c7.getArea()
11 << " Color=" << c7.getColor() << endl;
12 // Radius=5.6 Area=98.5206 Color=orange
در ادامه این بخش از مقاله برنامه نویسی شی گرا در C++ ، برخی نکات پیشرفته پیرامون عملگر جایگزینی کپی (=) آمده است.
نکات پیشرفته درباره جایگزینی کپی
برخی نکات پیشرفته پیرامون جایگزینی کپی به شرح زیر است:
- میتوان برای لغو پیشفرض، عملگر جایگزینی را سرریز (Overload | یعنی عملگر بیش از دو تعریف داشته باشد) کرد.
- به جای یک عملگر جایگزینی کپی از سازنده کپی در تعریف شی استفاده میشود:
1Circle c8 = c6; // فراخوانی سازنده کپی و نه عملگر جایگزینی کپی
2 // Same as Circle c8(c6)
- عملگر جایگزینی کپی، کپی سایهای انجام میدهد. این عملگر اعضای داده تخصیص یافته پویای ایجاد شده از طریق عملگر new یا new[ ] را کپی نمیکند.
- سازنده جایگزینی کپی دارای تفسیری (امضا | Signature) به صورت زیر است:
class MyClass { private: T1 member1; T2 member2; public: // عملگر جایگزینی کپی پیشفرض که یک شی را از طریق کپی عضوگونه جایگزین میکند MyClass & operator=(const MyClass & rhs) { member1 = rhs.member1; member2 = rhs.member2; return *this; } ...... }
- عملگر جایگزینی کپی با سازنده کپی تفاوت دارد. این تفاوت به این صورت است که عملگر جایگزینی کپی باید محتوای هدف تخصیص داده شده به صورت پویا را آزاد کند و از جایگزینی با خودش جلوگیری کند. عملگر جایگزینی باید یک مرجع از آن شی را برای فراهم کردن امکان انجام عملیات زنجیروار (Chaining Operation) مثل x=y=z بازگرداند.
- سازنده پیشفرض، مخرب پیشفرض، سازنده کپی پیشفرض و عملگرهای کپی جایگزینی پیشفرض به عنوان توابع عضو مخصوص شناخته میشوند. این توابع عضو مخصوص، در صورتی که در برنامه استفاده شده باشند ولی به طور ضمنی تعریف نشده باشند، کامپایلر به صورت خودکار یک کپی از آنها تولید خواهد کرد.
جداسازی سربرگ و اجرا در C++
برای مهندسی بهتر نرمافزار، پیشنهاد میشود که تعریف و اجرای کلاس در دو فایل جداگانه نگهداری شوند: تعریف کلاس در یک فایل سربرگ (Header) با پسوند «h.» و فایل اجرایی با پسوند «cpp.» ذخیره میشوند. این روش به عنوان جداسازی رابط عمومی (تعریف هدر) و اجرا (پیادهسازی) شناخته میشود. رابط توسط طراح تعریف میشود، اما اجرا میتواند توسط دیگران تهیه شود.
با وجود اینکه رابط ثابت است، تولید کنندگان مختلف میتوانند اجراهای متفاوتی را فراهم سازند. علاوه بر این، تنها فایلهای سربرگ هستند که برای کاربران افشا میشوند. اجرا میتواند در یک فایل شی با پسوند «o.» یا در یک کتابخانه فراهم شود. کدهای منبع نباید در اختیار کاربران قرار داده شوند. برای روشنتر شدن مباحث ذکر شده، در ادامه مقاله برنامه نویسی شی گرا در C++ یک مثال ارائه شده است.
مثال برنامه نویسی شی گرا در C++ : کلاس Circle
به جای قرار دادن همه کدها در یک فایل واحد، باید با تقسیم کدها در سه فایل، رابط و پیادهسازی را از هم جدا کرد. این سه فایل به صورت زیر است:
- Circle.h : رابط عمومی کلاس Circle را تعریف میکند.
- Circle.cpp : پیادهسازی کلاس Circle را فراهم میسازد.
- TestCircle.cpp : یک برنامه پیشبرنده آزمون برای کلاس Circle
کدهای مربوط به فایل سربرگ
کدهای فایل سربرگ Circle.h به صورت زیر است:
1/* The Circle class Header (Circle.h) */
2#include <string> // استفاده از رشته
3using namespace std;
4
5// تعریف کلاس دایره
6class Circle {
7private: // تنها قابل دسترسی توسط اعضای این کلاس
8 // اعضای داده (متغیرهای) خصوصی
9 double radius;
10 string color;
11
12public: // قابل دسترسی توسط همه
13 // تعریف نمونه اولیه توابع عضو
14 // سازنده با مقادیر پیشفرض
15 Circle(double radius = 1.0, string color = "red");
16
17 // گیرندهها و تنظیم کنندههای عمومی برای اعضای داده خصوصی
18 double getRadius() const;
19 void setRadius(double radius);
20 string getColor() const;
21 void setColor(string color);
22
23 // تابع عضو عمومی
24 double getArea() const;
25};
در ادامه، نکاتی پیرامون کدهای بالا ارائه شده است:
- فایل سربرگ شامل گذارههای تعریف است. این گذارهها نامها و نوعها و نمونههای اولیه تابع را بدون جزئیات پیادهسازی به کامپایلر اطلاع میدهند.
- C++ نسخه 98/03 به برنامهنویس اجازه نمیدهد که یک مقدار اولیه را به عضو داده تخصیص دهد. (به غیر از اعضای ایستای نوع Const) اعضای داده باید از طریق سازنده مقداردهی اولیه شوند. برای مثال:
1double radius = 1.0;
2 // error: ISO C++ forbids in-class initialization of non-const static member 'radius'
در کد بالا، خطایی با این مفهوم صادر میشود: «خطا: استاندارد ISO در C++، مقداردهی اولیه درون کلاسی عدد غیر ثابت ایستای radius را منع میکند.» اما در C++ نسخه ۱۱، امکان مقداردهی اولیه اعضای داده وجود دارد.
- میتوان مقدار پیشفرض را برای آرگومانهای تابع در سربرگ لحاظ کرد. برای مثال:
1Circle(double radius = 1.0, string color = "red");
- سربرگ حاوی نمونه اولیه تابع است. نام پارامترها توسط کامپایلر در نظر گرفته نمیشوند، اما برای مستندسازی مناسب هستند. برای مثال، میتوان نام پارامترها را در نمونه اولیه مثل خط کد زیر ذکر نکرد:
1Circle(double = 1.0, string = "red"); // without identifiers
2 // نیازی به ذکر شناسهها در نمونه اولیه نیست، ولی برای مستندسازی بهتر است لحاظ شوند
فایلهای سربرگ باید حاوی مقادیر ثابت، نمونههای اولیه تابع و تعریفهای کلاس یا سازه باشند. کدهای مربوط به فایل پیادهسازی در ادامه آمده است.
کدهای مربوط به پیادهسازی (اجرا)
کدهای مربوط به اجرا یا پیادهسازی که در فایل Circle.cpp ذخیره میشوند، به صورت زیر است:
1/* The Circle class Implementation (Circle.cpp) */
2#include "Circle.h" // سربرگ تعریف شده توسط کاربر در همان پوشه
3
4// سازنده
5// مقادیر پیشفرض باید تنها در تعریف فایل تعیین شوند
6// و نمیتوانند در تعریف تکرار شود
7Circle::Circle(double r, string c) {
8 radius = r;
9 color = c;
10}
11
12// گیرنده عمومی برای عضو داده خصوصی شعاع
13double Circle::getRadius() const {
14 return radius;
15}
16
17// تنظیم کننده عمومی برای عضو داده خصوصی شعاع
18void Circle::setRadius(double r) {
19 radius = r;
20}
21
22// گیرنده عمومی برای عضو داده خصوصی رنگ
23string Circle::getColor() const {
24 return color;
25}
26
27// تنظیم کننده عمومی برای عضو داده خصوصی رنگ
28void Circle::setColor(string c) {
29 color = c;
30}
31
32// یک تابع عضو عمومی
33double Circle::getArea() const {
34 return radius*radius*3.14159265;
35}
در ادامه، نکاتی پیرامون کدهای این مثال ارائه شده است:
- فایل اجرایی تعاریف توابعی را فراهم میسازد که از تعریف در فایل سربرگ حذف شدهاند.
- include "Circle.h"# : کامپایلر ابتدا سربرگهایی (نظیر "Circle.h") را جستجو میکند که بین علامت نقلقول در آدرس پوشه فعلی آمدهاند. سپس، کامپایلر پوشههای سیستمی را به حساب میآورد. برای سربرگهایی که بین علامت بزرگتر و کوچکتر (مثل <iostream>) قرار میگیرند، کامپایلر پوشه فعلی را جستجو نمیکند و تنها پوشههای شامل شده سیستمی را در نظر میگیرد. از این رو، باید برای سربرگهای تعریف شده توسط کاربر، از علامت نقلقول (" ") استفاده کرد.
- Circle::Circle(double r, string c) { : باید className «::» را در مقابل تمام نامهای اعضا شامل کرد (به آن «عملگر وضوح دامنه کلاس» گفته میشود). به این ترتیب، به کامپایلر اطلاع داده میشود که این عضو متعلق به یک کلاس خاص است.
- محدوده کلاس: نامهایی که در داخل یک کلاس تعریف شدهاند، دارای «محدوده کلاس» هستند. آنها تنها در داخل کلاس قابل دیده شدن هستند. بنابراین، میتوان از یک نام یکسان در دو کلاس متفاوت استفاده کرد. برای استفاده از این نامها در خارج از کلاس، نیاز به «عملگر وضوح دامنه کلاس» (::) وجود دارد.
- نمیتوان آرگومانهای پیشفرض را در پیادهسازی قرار داد (آنها باید در سربرگ قرار داده شوند). برای مثال:
1Circle::Circle(double r = 1.0, string c = "red") { // error!
اکنون پس از آمادهسازی و نوشتن کدهای کلاس Circle، زمان آن فرا رسیده است تا این کدها کامپایل شوند.
کامپایل کردن کلاس Circle
میتوان فایل Circle.cpp را به یک فایل شی به نام Circle.o به وسیله دستور -c در GNU GCC کامپایل کرد:
1> g++ -c Circle.cpp
2 // option –c for compile-only, output is Circle.o
برای استفاده از کلاس Circle، کاربر نیاز به Circle.h و Circle.o دارد و نیازی به فایل Circle.cpp نیست. به بیان دیگر، نیازی به فاش کردن کدهای منبع وجود ندارد. بلکه، تنها تعریف عمومی و کدهای شی مورد نیاز است.
به این ترتیب، پس از کامپایل کردن کدها، میتوان کلاس Circle را آزمایش و از آن استفاده کرد.
کدهای مربوط به آزمایش کلاس Circle
کدهای مربوط به آزمایش کلاس Circle در فایل TestCircle.cpp به صورت زیر است:
1/* A test driver for the Circle class (TestCircle.cpp) */
2#include <iostream>
3#include "Circle.h" // استفاده از کلاس دایره
4using namespace std;
5
6int main() {
7 // Construct an instance of Circle c1
8 Circle c1(1.2, "red");
9 cout << "Radius=" << c1.getRadius() << " Area=" << c1.getArea()
10 << " Color=" << c1.getColor() << endl;
11
12 c1.setRadius(2.1); // Change radius and color of c1
13 c1.setColor("blue");
14 cout << "Radius=" << c1.getRadius() << " Area=" << c1.getArea()
15 << " Color=" << c1.getColor() << endl;
16
17 // Construct another instance using the default constructor
18 Circle c2;
19 cout << "Radius=" << c2.getRadius() << " Area=" << c2.getArea()
20 << " Color=" << c2.getColor() << endl;
21 return 0;
22}
کامپایل کردن برنامه آزمایش کلاس Circle
برای کامپایل کردن برنامه آزمایشی (فایل TestCircle.cpp) با کد شی Circle.o (و سربرگ Circle.h) باید از دستورات زیر استفاده کرد:
1> g++ -o TestCircle.exe TestCircle.cpp Circle.o
2 // option -o specifies the output filename
همچنین، میتوان فایل TestCircle.cpp را با کد منبع Circle.cpp نیز کامپایل کرد:
1> g++ -o TestCircle.exe TestCircle.cpp Circle.cpp
به این ترتیب، مباحث مربوط به برنامه نویسی شی گرا در C++ در این مقاله به پایان میرسد. در بخش پایانی این مطلب، دورههای آموزشی مرتبط با برنامه نویسی شی گرا در C++ و دیگر زبانها برای آموزش و یادگیری بیشتر معرفی شدهاند.
جمعبندی
مقاله برنامه نویسی شی گرا در C++ با توجه به اهمیت این مفاهیم در حرفه برنامه نویسی ارائه شده است. به طور خلاصه، شی گرایی به معنی تولید قطعات و اجزا نرمافزاری مستقل با هدف امکان استفاده مجدد از آنها و سادهسازی مراحل توسعه نرمافزار است.
شی گرایی دارای چند خصوصیت ویژه است که باید در پیادهسازی آن رعایت شوند. زبان برنامه نویسی C++ یکی از اولین زبانهایی به شمار میرود که از شی گرایی در آن استفاده شده است. در مقاله برنامه نویسی شی گرا با C++ ، مباحثی از قبیل چیستی شی گرایی، دلیل نیاز به شی گرایی، مزایای شی گرایی، مفاهیم شی گرای در C++ از جمله
سلام
ممنون از مقاله کاربردی و مفیدتون . فقط یه مورد…
در قسمت نام گذاری کلاس ها گفتید حرف اول هر کلمه بزرگ باشد که این میشه روش Pascal Case و نه Camel Case !
با سلام و احترام؛
سپاس از دقت نظر شما.
با توجه به منابع موجود، گویا در شیوه نگارش «CamelCase» میتوانیم حرف اول را یا با حروف کوچک یا با حروف بزرگ بنویسیم که در حالت دوم مانند موردی که ذکر کردین فرقی با شیوه «PascalCase» ندارد. یعنی «YouTube» و «toString» هر دو کملکیس هستند.
از همراهی شما با مجله فرادرس بسیار سپاسگزاریم.
ممنون از شما توضیحات بسیار کامل و روان بود
ممنون از این مقاله کاربردی
با سلام و احترام؛
صمیمانه از همراهی شما با مجله فرادرس و ارائه بازخورد سپاسگزاریم.
از اینکه این مطلب مورد توجه شما قرار گرفته بسیار خشنود هستیم.
برای شما آرزوی سلامتی و موفقیت داریم.