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

این فصل دربارهٔ پردازش فایل ها یا همون File Processing بود. بیشتر نکاتش رو از قبل می‌دونستم و صرفا برام مرور بود.

کلاس های مورد استفاده‌مون

برای انجام پردازش فایلی باید هدر های iostream و fstream رو اینکلود کنیم. توی fstream این سه تا typedef وجود داره:

  • typedef basic_ifstream<char> ifstream
  • typedef basic_ofstream<char> ofstream
  • typedef basic_fstream<char> fstream

همونطور که می‌بینید این سه تا typdef برای نوع دادهٔ char ویژه سازی شده(specialized).

توی این کلاس ها اپراتور bool هم overload شده. به این معنی که میتونیم معتبر(valid) بودن یک شئ رو به این شکل بررسی کنیم:

fstream myFile("something.txt", ios::out);
if (myFile) {
// do somthing
}

باز کردن فایل ها

برای باز کردن فایل ها چند حالت(mode) وجود داره که هرکدوم دارای ویژگی های خاصی هستن. نحوه‌ی نوشتنش و اینکه هرکدومشون چه کاری انجام می‌دن پایین تر نوشته شده:

ofstream handler("filename", ios::out)

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

نکتهٔ دیگه اینکه میتونیم این حالت هارو به صورت همزمان باهم دیگه استفاده بکنیم. چطوری؟ هرکدومشون رو با علامت «|» (bitwise or) از هم جدا می‌کنیم:

ofstream handler("filename", ios::in | ios::out | ios::binary)

پیمایش در فایل

میتونیم به سی++ بگیم که از کجای فایل میخوایم شروع کنیم به نوشتن/خوندن. وقتی یک فایل رو(هم برای خواندن و هم نوشتن) باز می‌کنیم، دو اشاره گر ایجاد میشه که به ابتدای فایل(بایت شماره صفر) اشاره می‌کنن. یکی از این اشاره گر ها برای خوندن از فایل و دیگری برای نوشتن در فایل هست. برای تغییر مکان اشاره گرِ خوندن، از تابع seekg و برای تغییر مکان اشاگر نوشتن از تابع seekp استفاده می‌کنیم.

// position to the nth byte of fileObject (assumes ios::beg)
fileObject.seekg(n);
// position n bytes in fileObject
fileObject.seekg(n, ios::cur);
// position n bytes back from end of fileObject
fileObject.seekg(n, ios::end);
// position at end of fileObject
fileObject.seekg(0, ios::end);

خوندن و نوشتن در فایل

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

فرض کنیم یه فایلی داریم که متن های داخلش به این فرمت هستند:

100 "some text" 200

همونطور که میدونیم، input stream ها اسپیس رو به عنوان کلمه‌ی کلیدی برای جدا کردن آرگومان ها در نظر میگیرن. بنابراین اگر به شکل معمول زیر اقدام به خواندن خط بالا بکنیم، به مشکل برخواهیم خورد.

InputStream >> number1 >> text >> number2;

یکی از راه های موجود برای حل این مشکل، استفاده از یک stream manipulator به اسم quoted هست که در هدر <iomanip> قرار داره. حالا کدی که نوشتیم رو تغییر میدیم تا به درستی ورودی هارو از هم تشخیص بده.

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

InputStream >> number1 >> quoted(text) >> number2;

خوندن و نوشتن به صورت Random-Access

نمیخوام از مزیت های این نوع دسترسی به فایل صحبت بکنم چون احساس می‌کنم مطلبی نبوده که به عنوان یک چیز جدید توی این کتاب یاد گرفته‌م. به همین حد اکتفا می‌کنم که در اکثر موارد این روش بسیار سریع تر از روش Sequential Access هست.

در این روش که فایل رو در حالت باینری باز می‌کنیم، باید داده هامون رو به char * تبدیل کنیم.

اگر فرض بگیریم که int number{10};، برای نوشتن در فایل:

outFile.write(reinterpret_cast<const char*>(&number), sizeof(number));

و برای خوندن از فایل:

outFile.read(reinterpret_cast<char*>(&number), sizeof(number));

پارامتر اول تابع های read و write، داده ای هستن که میخوایم بنویسیم یا بخونیم و باید به صورت بایت به بایت خونده بشن. که برای اینکار، از char*استفاده می‌کنیم. پارامتر دوم همونطور که احتمالا فهمیدید، سایز داده ای هست که میخوایم بخونیم یا بنویسیم.

درباره انواع cast ها در سی++ احتمالا یه پست جدا می‌نویسم!

در آخر

برای صدمین بار رها کردن مطالعه، باز اومدم. اینبار هم مثل ۹۹ بار قبلی میگم که عزم بیشتری دارم 🙂

این فصل خیلی چیز جدیدی برام نداشت. فصل بعدی دربارهٔ کتابخانه استاندارد سی++‌ و container هاست.

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

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

تفاوت 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 عه که دانش ابتدایی ما برای برنامه نویسی شئ‌گرا رو کامل میکنه.

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

فصل نهم از کتاب 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 عه.