نکات جدیدی که از فصل ۱۲ سی++ دایتل یاد گرفتم

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

چند ریختی به ما اجازه میده که برنامه های گسترده ای بنویسیم و درواقع بتونیم برنامه نویسی general داشته باشیم بجای اینکه مجبور بشیم به صورت خاص برنامه بنویسیم(الآن براش مثالی توی ذهنم ندارم که کوتاه باشه).

کلمه‌ی virtual

معمولا وقتی از طریق یه پوینتر توابع رو فراخوانی می‌کنیم، توابع مربوط به [جنس اون] پوینتر صدا زده میشن نه توابع مربوط به کلاسی که بهش اشاره میشه. یعنی اگه تابعی با دو نسخه(یکی توی کلاس والد و یکی توی کلاس فرزند) وجود داشته باشه، اون نسخه ای صدا زده میشه که مربوط به type پوینتره. virtual به وجود اومده تا راه حلی برای این مشکل باشه، برای اینکه موقع صدا زدن یه تابع از طریق پوینتر/رفرنس، برنامه چک میکنه که اگر کلاس فرزند نسخه‌ی خودش از اون تابع رو داره، همون رو صدا بزنه در غیر این صورت نسخه‌ی مربوط به کلاس والد رو صدا می‌زنه.

درواقع توابع ویرچوال ابن امکان رو فراهم میکنن که بجای فراخوانی تابع مربوط به هندل، تابع مربوط به کلاس اشاره شده فراخوانی بشه.

بهتره که برای خوانایی بهتر توی هر سطح که توابع virtual والد رو بازنویسی می‌کنیم، قید کنیم که این یه تابع virtual عه.

Dynamic Binding

همونطور که بالاتر گفتیم اگر یکی از توابع به شکل virtual تعریف شده باشه، و این تابع از طریق یه پوینتر/رفرنس فراخوانی بشه، برنامه خودش تابع مناسب رو بر اساس جنسِ شئ ای که داره بهش اشاره میشه انتخاب میکنه. به این فرآیند میگن dynamic binding.

کلمه‌ی override

وقتی یه تابعی از کلاس والد رو توی کلاس فرزند بازنویسی می‌کنیم، بهتره که از کلمه‌ی override براش استفاده بکنیم، با استفاده کردن این کلمه، کامپایلر چک میکنه که آیا تابعی با امضا(signature) مشابه توی کلاس(های) والد وجود داره که بخواد بازنویسی بشه یا نه.

ضرورت virtual کردن destructor

مهمه که اگر میخوایم به صورت چند ریختی از کدمون استفاده بکنیم، حتما destructor رو به صورت virtual تعریف کنیم.

دلیلش اینه که اگر یک اشاره گر از نوع کلاس والد که داره به یک شئ از کلاس فرزند اشاره میکنه توسط کلمه‌ی delete پاک بشه، تابع نابودکننده مربوط به خودش صدا زده بشه و نه تابع نابودکننده والد.

کلمه‌ی default

چیز جدیدی که به سی++ ۱۱ اضافه شده کلمه‌ی default هست. وقتی میخوایم یه کانستراکتور و یا دیستراکتور پیش فرض داشته باشیم دیگه نیاز نیست که یه تابع با بدنه‌ی خالی بنویسیم. فقط کافیه توی اعلان، اون تابع رو default کنیم.

کلمه‌ی final

وقتی از کلمه‌ی final برای یه تابع استفاده میشه، دیگه کامپایلر اجازه نمیده که اون تابع توی کلاس های فرزند بازنویسی و override بشه.

وقتی از کلمه‌ی final برای تعریف یه کلاس استفاده بشه، کامپایلر اجازه نمیده که کلاس های دیگه از این کلاس ارث بری داشته باشن.

کلاس Abstract

تا اینجا ما میتونستیم از کلاس هامون اشیاء ای رو بسازیم. اما یه سری کلاس های دیگه هم وجود دارن که نمیشه ازشون هیچ شئ ای ساخت که به این کلاس ها میگن Abstract. به کلاس های معمولی میگن concrete.

کلاسی رو میشه ابسترکت نامید که حداقل یکی از توابع virtual اش به صورت pure باشه.

برای اینکه یک تابع رو pure کنیم، باید اعلان تابع رو برابر 0 قرار بدیم که اون 0 نشان pure specifier هست. با pure اعلام کردن یک تابع دیگه نباید براش پیاده سازی ای نوشت بنابراین همه‌ی کلاس های فرزند باید توابع pure رو پیاده سازی کنن وگرنه خودشونم ابسترکت میشن.

چندریختی به صورت عمیق تر!

یه رفتار پلی مورفیک از سه سطح از پوینتر ها تشکیل شده.

مرحله اول، vtable

وقتی کاپایلر کلاسی رو که دارای توابع virtual عه رو کامپایل میکنه، برای اون کلاس یدونه Virtual function Table (vtable) میسازه. کار این جدول چیه؟ آدرس توابع virtual شده‌ی اون کلاس رو داخل خودش ذخیره می‌کنه. وقتی در زمان اجرا میخوایم با استفاده از dynamic binding یه تابع virtual رو فراخوانی کنیم، برنامه توی vtable کلاس مربوطه میگرده تا تابع درست رو پیدا و اجرا بکنه.

مرحله دوم

وقتی یه آبجکت از یک کلاس دارای توابع ویرچوال ساخته میشه، کامپایلر به اون آبجکت یه پوینتر اختصاص میده(این پوینتر رو معمولا در اول آبجکت میذاره) که اون پوینتر به vtable مربوط به اون کلاس اشاره می‌کنه.

مرحله سوم

پوینتریه که به خودِ شئ اشاره می‌کنه. به عنوان مثال ما یک وکتور از همه اشیاء از کلاس های مشتق شده از کلاس A داریم. پوینتر هایی که توی این وکتور هستند(که به اشیاء اشاره می‌کنن) به عنوان سطح سوم پوینتر ها محسوب میشن.

بنابراین برای اجرای یه تابع ویرچوال که به صورت dynamic binding میخواد صدا زده بشه، حداقل ۳ بار pointer dereferencing اتفاق میوفته که این موجب افزایش زمان اجرایی میشه. همچنین ذخیره کردن پوینتر مرحله ۲ و خودِ vtable هم باعث استفاده بیشتر از مموری میشه. اگر پرفورمنس و سرعت توی برنامه ای که داریم می‌نویسیم یک اصل بسیار مهم و سفت و سخته،‌ بهتره که از پلی مورفیسم استفاده نکنیم.

RunTime Type Information

تا اینجا وقتی به صورت پلی‌مورفیسم کار می‌کردیم، نیاز نبود بدونیم هر آبجکت دقیقا از چه نوعیه. اما ممکنه گاهی این نیاز رو پیدا بکنیم. با استفاده از قابلیت RTTI یا همون RunTime Type Information و قابلیت dynamic cast میتونیم در زمان اجرا بفهمیم که شئ ما از چه نوعیه و رفتار متناسب با خودش رو باهاش انجام بدیم. با استفاده از dymanic_cast میتونیم یه اشاره گر از جنس کلاس والد رو که داره به یکی از کلاس های فرزند اشاره می‌کنه به یک اشاره گر از نوع خودِ کلاس فرزند تغییر بدیم. فرقش با static_cast اینه که تایپ چک انجام میده و اگر کلاسی که داره بهش اشاره میشه از نوع کلاسی نباشه که میخواد بهش cast بشه، تبدیل انجام نمیشه.

همچنین با استفاده از typeid میتونیم در زمان اجرا بفهمیم که یه شئ از چه نوعیه. با استفاده از متود name اش میتونیم اسم جنسِ یه شئ رو به صورت یه رشته بگیریم.

در پایان

فصل بعدی توی I/O بیشتر عمیق میشیم.

نکات جدیدی که از فصل ۱۱ سی++ دایتل یاد گرفتم

این فصل از دایتل درباره‌ی ویژگی ارث‌بری توی سی++ بود. نکاتی که برام جدیده رو توی این پست نوشتم.

تفاوت is-a relationship و has-a relationship

وقتی یک کلاسی(A) از یک کلاس دیگه(B) ارث بری می‌کنه، میگیم که رابطه‌شون از نوع is-a relationship هست. یعنی کلاس A نوعی از کلاس B عه؛ مثالش؟ «خودرو» و «وسیله نقلیه». خودرو از وسیله‌ی نقلیه مشتق شده(نمیدونم این تعبیرِ مشتق شدن درسته یا نه) و تمام ویژگی های یک وسیله‌ی نقلیه رو داره. درواقع، «خودرو یک وسیله‌ی نقلیه‌ست».

اما وقتی یک کلاس در data-member های خود یک شئ از یک کلاس دیگه رو داره(composition) میگیم رابطه‌شون has-a relationship هست. توی مثال خودرو، میشه فرمون خودرو رو مثال زد که خودش یک شئ هست و عضوی از خودرو محسوب میشه(نه نوعی از خودرو!).

Constructor

در حالت عادی، توابع سازنده(constructor)، نابودکننده(destructor) و اوپراتور تخصیص (operator= ) از کلاس والد ارث برده نمیشن.

وقتی یک شئ از کلاس فرزند(B) ساخته میشه اگه کلاس والد(A) دارای default constructor باشه، بدون اینکه کانستراکتور والد رو صراحتا صدا بزنیم، خودش به صورت ضمنی اجرا میشه. اما اگه کلاس A دارای default constructor نباشه باید حتما خودمون کانستراکتور کلاس A رو توی کانستراکتور کلاس B صدا بزنیم.

دسترسی به عناصر private والد

کلاس فرزند نمیتونه به طور مستقیم به عناصر private کلاس والد دسترسی داشته باشه. برای اینکه بشه اینکار رو انجام داد، اون عناصر مورد نظر توی کلاس والد باید به عنوان protected تعریف شده باشن یا اینکه کلاس فرزند از طریق توابع public کلاس والد اقدام به دسترسی گرفتن به عناصر پرایوت بکنه.

با استفاده از protected access specifier به دلیل اینکه میتونید به صورت مستقیم به عناصر مورد دسترسی داشته باشید، برنامه‌تون کمی سریعتر میشه. در مقابل استفاده از توابع public باعث میشه برنامه‌تون کمی کند تر باشه.

اما این روش(protected) مشکلاتی هم داره:

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

دوم اینکه برنامه رو شکننده (اصطلاحا fragile) میکنه. یعنی کلاس فرزند جای اینکه بر پایه خدماتی(service) باشه که کلاس والد ارائه میده، بر پایه‌ی پیاده سازی (implementation) کلاس والد توسعه پیدا میکنه. مشکلش چیه؟ از نظر مهندسی نرم افزار بهتره که تغییرات رو localize بکنیم. یعنی برنامه طوری نباشه که اگر اسم یکی از عناصر رو توی کلاس والد تغییر دادیم، مجبور بشیم تمام جاهایی از کلاس فرزندمون که به اون عضو رفرنس دادیم رو تغییر بدیم و استفاده از protected پتانسیل بوجود اومدن چنین مشکلی رو ایجاد می‌کنه.

مگه نگفتی استفاده از توابع public برنامه رو کندتر می‌کنه؟

چرا. اما بهتره که ما اصول مهندسی نرم افزار رو رعایت کنیم و بهینه کردن کد(optimization) توی این مورد رو به دست کامپایلر بسپاریم. چون خیلی وقتا کامپایلر خودش بهینه سازی های خوبی انجام میده(مثلا ممکنه توابع set و get رو به حالت inline دربیاره). به قول معروف، Do not second-guess the compiler .

فرق بین ارث بری public و بقیه

خود کتاب یه جدول خیلی خوب رسم کرده که من عکسش رو میذارم. به نظرم خوندن همون کافیه 🙂

جدولی که توضیح میده هرکدوم از انواع ارث بری public, private, protected چه ویژگی هایی دارن.

ارث بردن constructor

بالاتر گفتم که در حالت عادی کانستراکتور از کلاس والد به ارث برده نمیشه. اما توی C++ 11 ویژگی ای اضافه شده که اجازه میده کانستراکتور والد توسط کلاس فرزند به ارث برده بشه.

برای اینکار باید یه همچین چیزی توی تعریف کلاستون بنویسید:

using BaseClass :: BaseClass ;

که BaseClass اسم کلاس والد هست. با اینکار، کامپایلر به ازای کانستراکتور های موجود توی کلاس والد، یه کانستراکتور توی کلاس فرزند میسازه که به طور خودکار تابع متناظرش توی کلاس والد رو صدا بزنه و data-member های کلاس فرزند رو default initialize بکنه.

چندتا نکته برای این کار وجود داره:

  • کانستراکتور های default, copy, move ارث بری نمیشن.
  • اگر توی کلاس فرزند کانستراکتوری باشه که ورودی هاش با ورودی های یک کانستراکتور توی کلاس والد یکسان باشه، اون کانستراکتور به ارث برده نمیشه.
  • اگه کانستراکتور دارای default argument باشه، به همون صورت به ارث نمیرسه بلکه به صورت چند کانستراکتور overload شده به ارث میرسه.
  • اگه یه کانستراکتور توی کلاس والد حذف بشه، توی کلاس فرزند هم حذف میشه.

در آخر

فصل بعدی درباره‌ی چند ریختی یا همون polymorphism عه که دانش ابتدایی ما برای برنامه نویسی شئ‌گرا رو کامل میکنه.

نکاتِ جدیدی که از فصل ۱۰ سی++ دایتل یادگرفتم

خب فصل ۱۰ ام از کتاب برنامه نویسی سی++ دایتل رو تموم کردم. اینجا خلاصه ای از چیزای جدیدی که یادگرفتم رو می‌نویسم.

string-object literal

توی سی++ ۱۴ میشه با اضافه کردن یک s به انتهای لیترال رشته‌ای‌مون(بعد از دابل کوتیشن، نه قبلش) اون رشته رو تبدیل به یه شئ از کلاس string کرد. مثال:

"Hello, World"s

Operator Overloading؛ به عنوان عضو کلاس یا نه؟

با اینکه من قبلا این بخش هارو توی کتاب C How To Program خونده بودم اما چون یادداشت نکرده بودم و خیلی وقت هم شده بود که حوصله‌ی برنامه نویسی رو نداشتم، یادم رفته بود.

مسئله اینه که چه وقتی باید تابع اوپراتوری که overload میشه رو جزئی از member function ها بذاریم و چه وقتی جزئی از non-member function. ساده‌ست، تنها زمانی میتونیم تابع اوپراتور رو به عنوان عضوی از کلاس قرار بدیم که شئِ کلاس ما به عنوان پارامتر در سمت چپ قرار بگیره. این کد رو ببینید:

istream& operator>>(istream& input, MyClass& object);

وقتی مینویسیم cin >> myobject درواقع انگار تابع رو به شکل operator>>(input, myobject) فراخوانی کردیم.

حالا از اونجایی که شئ ما توی پارامتر سمت چپ قرار نمیگیره، نمیتونیم این اوپراتور رو داخل کلاس overload کنیم و اگر اینکارو انجام دادیم، باید اینطوری بنویسیمش: myobject << cin

از طرف دیگه، برای اینکه بتونیم اوپراتور هامون رو به صورت commutative(جابجایی پذیر) تعریف کنیم، به دلایلی که بالاتر توضیح داده شد باید حداقل یکبار اوپراتورمون رو به عنوان non-member function تعریف کنیم.

نکته درباره‌ی Dynamic Allocation در عضو های کلاس

وقتی یک حافظه ای رو به صورت پویا برای یکی از اعضای کلاس‌مون (data-member) در نظر می‌گیریم باید حواسمون باشه که
Default memberwise assignment و Default copy constructor رو به حال خودشون رها نکنیم چون مشکلاتی مثل double free رو بوجود میارن. پس یادمون باشه که کپی کانستراکتور خودمون + علامت مساوی(=) خودمون رو تعریف کنیم.

حذف یه تابع از کلاس

یه تابع از کلاس (function member) رو میشه حذف کرد. قبلا برای اینکار، اون تابع رو جزء بخش private کلاس قرار می‌دادند اما الان به عنوان مثال میشه اینطوری نوشت:

const Array* operator=(const Array*) = delete;

کاربردش چیه؟ معمولا برای غیرفعال کردن توابعی که توسط کامپایلر به صورت خودکار ساخته میشن (auto generate)، مثل سازنده‌ی پیشفرض(Default constructor)، کپی کانستراکتور، اوپراتور تخصیص (=) و … استفاده می‌شن.

تعریف شئ از کلاس با initializer list

برای اینکه بتونیم یه شئ از کلاسمون رو با initializer list بسازیم، باید یه کانستراکتور داشته باشیم که ورودی‌ش از نوع initializer_listباشه. مثال:

MyClass::MyClass(initializer_list list);

تبدیل انواع مختلف به کلاس و بالعکس

ما میتونیم اشیاء ای که از کلاسمون میسازیم رو با استفاده از Conversion Constructor ها و Conversion Operator ها به انواع دیگه ای تبدیل کنیم.

برای اینکه یه کانستراکتور تبدیل کننده داشته باشیم نیاز داریم که کانستراکتور ما بتونه با یک آرگومان صدا زده بشه.(این مسئله برای کپی کانستراکتور ها صادق نیست) مثال :

MyClass:MyClass(int a);

حالا با استفاده از این تابع میشه هم به صورت ضمنی و هم به صورت مستقیم یک int رو به شئ ای از کلاس خودمون تبدیل کنیم.

برای تعریف یک Conversion Operator میتونیم اینطوری عمل کنیم:

MyClass::operator string() const;

حالا اگر فرض کنیم a اسم شئ ما باشه، وقتی بنویسیم static_cast<string> (a) درواقع کامپایلر تابع a.operator string() رو صدا میزنه.

چرا توابع سازنده‌مون رو به شکل explicit تعریف کنیم؟

توابع سازنده ای که میتونن فقط با یک آرگومان صدا زده بشن ممکنه توسط کامپایلر اشتباها به عنوان Conversion Constructor تلقی بشن(چون این تابع ها هم با یک ورودی صدا زده میشن) و به صورت ضمنی عمل cast رو انجام بدن. درکل ما برای اینکه از تبدیل ضمنی چه توی کانستراکتور(اونایی که میتونن با یه آرگومان صدا زده بشن) و چه توی اوپراتور های تبدیلی( conversion operator) جلوگیری کنیم، اون توابع رو به صورت explicit تعریف می‌کنیم.

فصل بعدی درباره‌ی ارث‌بری هست 🙂

از فصل نهم کتاب برنامه نویسی سی++ دایتل چی یادگرفتم

فصل نهم از کتاب C++ How To Program (نگارش ۲۰۱۷) رو تموم کردم و توی این پست نکاتی که از این فصل یادگرفتم(و یادم مونده=) ) رو می‌نویسم.

تابع نابودگر (Destructor)

علاوه بر ترتیب اجرا شدنش برای اشیاء مختلف که به صورت «از بیرون به داخل» عه، وظیفه‌ی دیستراکتور درواقع Termination housekeeping عه. مثالش میشه کلاس file از STL که قبل از نابود شدن، فایل هایی که باز موندن توسط این تابع بسته میشن.

ممبر فانکشن های const

علاوه بر رعایت Least privilege principle در هنگام نوشتن توابع که موجب میشه تابع هایی که دیتا ممبر های کلاس رو تغییر نمیدن رو به صورت const تعریف کنیم، اگر شئ ای رو به صورت const توی کدِ کلاینت‌مون تعریف کنیم اونوقت تنها از متود هایی میتونیم استفاده کنیم که قبلا const تعریف شدن.

یک شئ به عنوان عضوی از کلاس (Composition)

نکته‌ی اول اینکه دیتا ممبر های یک کلاس به ترتیب تعریف شدنشون ساخته(کانستراکت) میشن نه به ترتیب نوشتنشون توی member-initializer list (ولی بهتره برای خوانایی کد به همون ترتیبی که تعریف شدن توی لیست شروع کننده هم نوشته بشن). همچنین، دیتا ممبر ها قبل از کانستراکت شدنِ شئ ای که داخلش قرار دارن ساخته میشن.

دیتا ممبر ها قبل از کانستراکت شدنِ شئ ای که داخلش قرار دارن ساخته میشن.

برای همینه که مهمه وقتی از Composition استفاده می‌کنیم حتما از member-initializer list استفاده کنیم. اگر اینکار رو انجام ندیم و داخل بدنه‌ی کانستراکتور اشیاء رو بسازیم، درواقع داریم اون ها رو دوباره مقدار دهی میکنیم.

چون یکبار دیفالت کانستراکتور خودشون موقع ساخته شدن شئ اصلی(= Enclosing Object) صدا زده میشه و بعد یکبار هم ما مقدار دهیشون میکنیم.

با استفاده از member-initializer list میتونیم از این دوباره کاری جلوگیری کنیم.

اشاره گر this

یه نکته ای که راجع به این اشاره گر فهمیدم این بود که اگر بر فرض اسم کلاس ما Test باشه، اشاره گر this از نوع const Test* عه

فصل بعدیِ کتاب

فصل بعدی درباره‌ی Operator Overloading عه.