در C++، اعلانها غالباً از تعریفها جدا میشوند. اعلانها در بهاصطلاح فایلهای هدر دستهبندی میشوند و پیادهسازیهای مربوطه در فایلهای منبع قرار میگیرند. میتوانید فایلهای هدر را مانند یک API در نظر بگیرید. فایل هدر به شما میگوید یک پایگاه کد چه امکاناتی دارد، بیآنکه به جزئیات _چگونه_اش بپردازد.
رایجترین پسوند فایل برای فایلهای هدر .h است.
بعضی پروژهها از .hpp استفاده میکنند یا پسوند را کاملاً حذف میکنند.
تعریفها در یک فایل .cpp جداگانه قرار میگیرند.
برای کنار هم گذاشتن دوبارهی این بخشها، فایل منبع با الحاق کردن فایل هدر مربوطه شروع میشود.
اگر بخواهید کتابخانهای به اسم «quick_math» بنویسید که تابعی به اسم «super_root» ارائه میدهد و میخواهید از آن زیاد استفاده کنید، فایلها این شکلی خواهند بود:
// A file named quick_math.h
#pragma once
namespace quick_math {
double super_root(double x, int n);
}
// A file named quick_math.cpp
#include "quick_math.h"
#include <cmath>
double quick_math::super_root(double x, int n) {
while(n) { x = std::sqrt(x), --n;}
return x;
}
اگر لازم است هدری را الحاق کنید که فقط پیادهسازی به آن نیاز دارد، خط #include مربوطه فقط در فایل منبع لازم است.
هر چیزی که در هدر الحاق میشود، در فایل .cpp هم در دسترس است، مثل کتابخانهی string در مثال زیر.
توجه: بعد از اعلان در فایل هدر به ; نیاز هست، اما بعد از تعریف در فایل منبع لازم نیست.
بسیاری از تمرینهای C++ در Exercism با دو فایل تقریباً خالی شروع میشوند: هدر و منبع.
برای حل تمرین باید فایل *_test.cpp را بررسی کنید تا اسمها و فضاهای نام توابع مورد انتظار را ببینید.
کلاسها میتوانند بسیار پیچیده شوند و رابطهشان با تقسیمبندی هدر / منبع ممکن است گیجکننده باشد. یک چیدمان ممکن این است که همهی جزئیات پیادهسازی را در فایل منبع و همهی اعلانها و متغیرهای عضو را در هدر نگه دارید:
// A file named robot_flower.h
#if !defined(ROBOT_FLOWER_H)
#define ROBOT_FLOWER_H
#include <string>
namespace robots {
class Flower {
private:
bool needs_water{};
int size{};
std::string name{};
public:
Flower(std::string name, int size = 0);
void give_water();
std::string get_name();
int get_size();
void start_next_day();
};
}
#endif
// A file named robot_flower.cpp
#include "robot_flower.h"
robots::Flower::Flower(std::string name, int size) {this->name = "Robotica " + name; this->size = size;}
void robots::Flower::start_next_day() {if (!needs_water) ++size; needs_water = true;}
std::string robots::Flower::get_name() {return name;}
int robots::Flower::get_size() {return size;}
وقتی از هدر بهعنوان نمای کلی API استفاده میشود، همانجایی است که آدم برای اطلاعاتی مثل مقدارهای پیشفرض نگاه میکند.
بنابراین مقدار پیشفرض پارامتر size در سازنده در هدر مدیریت میشود، نه در پیادهسازی.
تعریفها در فایل منبع با فضای نام robots و نوع کلاس Flower پیشوند میگیرند.
گزینهی دیگر چیدمان، کتابخانهی فقط هدر است که اصلاً فایل .cpp ندارد:
// A file named robot_flower.h
#pragma once
#include <string>
namespace robots {
class Flower {
private:
bool needs_water{};
int size{};
std::string name{};
public:
Flower(std::string name, int size = 0) {this->name = "Robotica " + name; this->size = size;}
void give_water() {needs_water = false;}
std::string get_name() {return name;}
int get_size() {return size;}
void start_next_day() {if (!needs_water) ++size; needs_water = true;}
};
}
پروژهها ممکن است از ترکیب این چیدمانها استفاده کنند و بحثهای زیادی هست بر سر اینکه کدامیک برای هر مورد استفاده بهترین گزینه است.
ممکن است خط #pragma once را در فایل هدر مثال بالا دیده باشید.
به این «محافظ الحاق» میگویند و تضمین میکند که محتوای فایل در طول کامپایل فقط یک بار الحاق شود تا از بروز خطا جلوگیری شود.
گونهی دیگری از محافظ الحاق هم هست که پیچیدهتر است و با #ifndef شروع میشود و با #endif پایان مییابد.
این هم همان هدف را دنبال میکند و کاربردش در مثال کلاس Flower در بالا نشان داده شده است.
شما اولین روز کاریتان را در شرکتی استرالیایی به اسم «Doctor Data» شروع میکنید که در بازیابی اطلاعات تخصص دارد. شما مصاحبهی شغلیتان را به لطف دانشتان از C++ و کاوشگرهای فون نویمان با موفقیت پشت سر گذاشتید. چون فایلهای تست زیادی دیدهاید، رئیس جدیدتان میخواهد فایلهای منبع و هدر مربوطه را از روی کد تستی که شرکت بهتازگی بازیابی کرده بازسازی کنید.
در این تمرین قرار است فایلهای گمشده را بازسازی کنید. در حال حاضر محتوای فایلها دادههای زبالهی بیاستفاده است.
روال کار این تمرین مفهومی بسیار شبیه به ساختار تمرینهای عملی Exercism است. متن معرفی تمرین فقط یک بخش از مشخصات است. فایل تست، راهنمای قطعی شما برای حل مسئلهای مشخص است. بهدلیل نحوهی کار کامپایل C++، ممکن است نتایج تست تا زمانی که نسخهی حداقلی هر کلاس، تابع و شمارشی را که تستها لازم دارند پیادهسازی نکنید، در رابط وب نمایش داده نشوند. اگر خطاهای کامپایل دریافت کردید، ممکن است پس از رسیدگی به همهی وظیفهها و تستها از بین بروند.
چهار وظیفه دارید که همهشان به بازیابی کد گمشده در فایلهای هدر و منبع مربوط میشوند.
به دو خط بازیابیشده از فایلهای تست نگاه میکنید:
heaven::Vessel bob{"Robert Johansson", 1};
heaven::Vessel will{"Riker", 2, star_map::System::BetaHydri};
چشم تیزبین شما فوراً یک فضای نام به اسم heaven را تشخیص داد.
همچنین میبینید که باید کلاسی به اسم Vessel وجود داشته باشد.
ظاهراً میتوان سازنده را با دو یا سه آرگومان فراخوانی کرد.
آرگومان اول ظاهراً از نوع string است و دومی یک عدد است.
میتوان کلاس Vessel را با یک آرگومان سوم مقداردهی اولیه کرد.
آرگومان سوم از فضای نام star_map میآید.
این یک شمارشی از نوع System است.
حتی یکی از شمارشیها را هم به دست آوردهاید: BetaHydri.
فایلهای منبع و هدر را با اطلاعاتی که کشف کردهاید آماده کنید.
به دو فضای نام نیاز دارید: heaven و star_map.
فضای نام heaven کلاسی به اسم Vessel دارد که میتوان آن را با دو یا سه آرگومان فراخوانی کرد.
شمارشی System باید در فضای نام star_map قرار بگیرد و یک شمارشی به اسم BetaHydri دارد.
مراقب شمارشیهای دیگر باشید تا بتوانید شمارشی System را بهطور مداوم بهروزرسانی کنید.
خطهای بیشتری از تست را کشف میکنید:
heaven::Vessel bob{"Robert Johansson", 1};
REQUIRE(bob.current_system == star_map::System::Sol);
REQUIRE(bob.generation == 1);
heaven::Vessel bob5 = bob.replicate("Mario");
REQUIRE(bob5.current_system == star_map::System::Sol);
REQUIRE(bob5.generation == 2);
خطهای تازهیافتهی تست، یک تابع عضو دیگر از کلاس Vessel را آشکار میکنند: replicate.
این تابع یک رشته میگیرد و یک نمونهی دیگر از Vessel برمیگرداند.
از وظیفهی قبلی سرنخی دربارهی مقدار پیشفرض آرگومان سوم سازنده به دست میآورید.
همچنین دو متغیر عضو عمومی از کلاس Vessel و مشخصات تابع عضو replicate را میبینید.
تابع replicate و متغیرهای عضو عمومی current_system و generation را به فایلهای هدر و منبع اضافه کنید.
چند خط جالب دیگر هم در فایلهای تست بازیابیشده پیدا میکنید:
heaven::Vessel bob6{"Homer", 3, star_map::System::EpsilonEridani};
REQUIRE(bob6.busters == 0);
bob6.make_buster();
REQUIRE(bob6.busters == 1);
bool success = bob6.shoot_buster();
REQUIRE(success);
REQUIRE(bob6.busters == 0);
success = bob6.shoot_buster();
REQUIRE_FALSE(success);
ظاهراً کلاس Vessel یک متغیر عضو به اسم busters دارد که میتوان آن را با دو تابع عضو کلاس، یعنی make_buster و shoot_buster، تغییر داد.
تا وقتی اطلاعات دیگری پیدا نشده، حدس میزنید که تابع make_buster مقدار void برمیگرداند.
چون تستی برای مقدار خروجی shoot_buster وجود دارد، فرض میکنید که این تابع یک مقدار bool برمیگرداند.
این دو تابع و متغیر عضو را به کلاس Vessel اضافه کنید.
به جستوجوی شمارشیهای دیگر System ادامه دهید.
در حین بررسی فایلهای تست، فقط دو بخش کشفنشده از کد پیدا میکنید:
heaven::Vessel bob1{"Bob", 1, star_map::System::AlphaCentauri};
heaven::Vessel marv{"Marvin", 2, star_map::System::DeltaEridani};
heaven::Vessel milo{"Milo", 3, star_map::System::DeltaEridani};
heaven::Vessel howie{"Howard", 4, star_map::System::Omicron2Eridani};
REQUIRE("Bob" == heaven::get_older_bob(bob1, marv));
REQUIRE(heaven::in_the_same_system(marv, milo));
REQUIRE_FALSE(heaven::in_the_same_system(marv, howie));
دو تابع میبینید که عضو کلاس Vessel نیستند، چون با یک نمونه فراخوانی نشدهاند.
get_older_bob دو نمونهی Vessel را مقایسه میکند و یک string برمیگرداند.
in_the_same_system دو نمونهی Vessel را مقایسه میکند و یک bool برمیگرداند.
آخرین توابع گمشده را بر اساس خطهای بازیابیشدهی بالا پیادهسازی کنید.