رفتن به محتوای اصلی

آموزش برنامه‌نویسی

آموزش C++ با یک پروژه؛ vector، تابع، اشاره‌گر و reference را بفهم

برنامهٔ تحلیل نمره بساز و تفاوت کپی، reference، اشاره‌گر، size و capacity را با مثال ببین. همراه کد C++17، کامپایل و تست.

آموزش C++ با یک پروژه؛ vector، تابع، اشاره‌گر و reference را بفهم

تصویر اختصاصی مجلهٔ ساروی؛ ساخته‌شده با هوش مصنوعی

در C++ یک علامت & می‌تواند معنی مهمی داشته باشد: گاهی آدرس می‌گیرد و گاهی می‌گوید این نام، اشاره‌ای به یک شیء موجود است. اگر این تفاوت روشن نباشد، کدی که کامپایل می‌شود هم گیج‌کننده می‌ماند. با یک برنامهٔ سادهٔ نمره شروع می‌کنیم و هر مفهوم را به کاری که در برنامه انجام می‌دهد وصل می‌کنیم.

C++ را با اجرای مرحله‌ای یاد بگیر

۱. کامپایل با اجرا فرق دارد

فایل grades.cpp متن کد است. کامپایلر آن را بررسی و به برنامهٔ اجرایی تبدیل می‌کند. بعد برنامهٔ خروجی را اجرا می‌کنی. تغییر فایل cpp بدون کامپایل دوباره، فایل اجرایی قبلی را عوض نمی‌کند.

این نمونه به C++17 نیاز دارد چون از std::optional استفاده می‌کند. گزینه‌های هشدار کمک می‌کنند بعضی اشتباه‌ها زودتر دیده شوند. هشدار را فقط برای آرام کردن خروجی خاموش نکن؛ علتش را بفهم.

terminal.txt text
c++ -std=c++17 -Wall -Wextra -pedantic grades.cpp -o grades
./grades --self-test
# C++ grades: all checks passed
printf "12 16 20\n" | ./grades
# Average: 16.00

۲. نوع داده بخشی از قرارداد است

double value یعنی مقدار می‌تواند اعشاری باشد. std::vector<double> مجموعه‌ای از همین نوع نگه می‌دارد. وقتی مقدار از ورودی خوانده می‌شود، بررسی می‌کنیم عدد متناهی و بین صفر تا بیست باشد.

int برای شمارش صحیح مناسب است، اما میانگین سه نمره ممکن است اعشاری شود. اگر جمع و تقسیم را در نوع صحیح انجام دهی، بخش اعشاری حذف می‌شود. در این برنامه جمع با 0.0 آغاز می‌شود تا نوع محاسبه double بماند.

۳. vector آرایه‌ای است که اندازه‌اش رشد می‌کند

scores.push_back(value) یک نمره به انتهای vector اضافه می‌کند. size تعداد عضوهای واقعی است. capacity مقدار فضایی است که فعلاً برای رشد در اختیار vector قرار دارد. reserve فقط ظرفیت را تغییر می‌دهد و عضو تازه نمی‌سازد.

اشتباه رایج: vector خالی را reserve(10) می‌کنی و بعد scores[0]=18 می‌نویسی. هنوز عضو صفر وجود ندارد. برای اضافه کردن عضو از push_back استفاده کن، یا اگر واقعاً ده عضو می‌خواهی، اندازه را با سازنده یا resize مشخص کن.

برای دسترسی‌ای که باید محدوده را بررسی کند، at می‌تواند در اندیس نامعتبر استثنا بدهد. operator[] بررسی محدوده انجام نمی‌دهد. برای مبتدی، at هنگام عیب‌یابی کمک می‌کند اشتباه اندیس به رفتار نامعلوم تبدیل نشود.

vector-example.cpp cpp
std::vector<double> scores;
scores.reserve(10);  // size is still 0
scores.push_back(18.0);
std::cout << scores.size() << "\n"; // 1
std::cout << scores.at(0) << "\n"; // 18

۴. تابع average چه می‌گیرد و چه برمی‌گرداند؟

ورودی const std::vector<double>& scores است. & اینجا reference را معرفی می‌کند: تابع به همان vector دسترسی دارد و یک کپی کامل نمی‌سازد. const می‌گوید تابع از این مسیر عضوها را تغییر نمی‌دهد.

خروجی optional<double> است. برای فهرست خالی، میانگین تعریف نشده و nullopt برمی‌گردانیم. صفر جواب مناسبی برای این حالت نیست، چون میانگین واقعی صفر با نبود داده فرق دارد. فراخواننده قبل از استفاده، وجود مقدار را بررسی می‌کند.

average.cpp cpp
std::optional<double> average(const std::vector<double>& scores) {
    if (scores.empty()) return std::nullopt;
    const double total = std::accumulate(scores.begin(), scores.end(), 0.0);
    return total / static_cast<double>(scores.size());
}

۵. کپی و reference را با یک تغییر ببین

اگر تابع vector را بدون & بگیرد، پارامتر یک کپی از مجموعه است. تغییر آن، مجموعهٔ اصلی را تغییر نمی‌دهد. اگر با reference بگیرد، تغییر در همان شیء اصلی اتفاق می‌افتد. تصمیم باید از قصد تابع بیاید.

add_bonus قرار است نمره‌ها را تغییر دهد، پس reference غیرconst می‌گیرد. در حلقه هم double& score می‌نویسیم تا هر نام score به عضو واقعی وصل شود. اگر & را حذف کنی، فقط متغیر موقت حلقه تغییر می‌کند.

bonus.cpp cpp
void add_bonus(std::vector<double>& scores, double bonus) {
    for (double& score : scores) {
        score = std::min(20.0, std::max(0.0, score + bonus));
    }
}
این تابع نمونهٔ آموزشی است و نمره را در بازهٔ صفر تا بیست نگه می‌دارد.

۶. اشاره‌گر، آدرس یک شیء را نگه می‌دارد

در int* pointer = &score، سمت چپ نوع اشاره‌گر به int را می‌گوید و سمت راست آدرس score را می‌گیرد. *pointer در عبارت، دسترسی به مقدار شیء اشاره‌شده است. همین علامت‌ها در تعریف و استفاده نقش متفاوتی دارند.

این مثال عمر ساده‌ای دارد: score تا پایان بلوک زنده است. برگرداندن آدرس متغیر محلی از تابع خطرناک است، چون پس از پایان تابع آن شیء دیگر وجود ندارد. برای این پروژه به new و delete نیاز نداریم؛ vector حافظهٔ عضوهایش را مدیریت می‌کند.

pointer.cpp cpp
int score = 18;
int* pointer = &score;
*pointer = 19;
std::cout << score << "\n"; // 19

۷. چرا pointer به عضو vector را بی‌دقت نگه نمی‌داریم؟

وقتی vector برای رشد، فضای تازه تخصیص می‌دهد، محل عضوها ممکن است عوض شود. اشاره‌گر و reference قبلی به عضوها ممکن است نامعتبر شوند. بنابراین «این آدرس قبلاً درست بود» دلیل کافی برای استفادهٔ بعدی نیست.

برای نسخهٔ اول این برنامه، عضوها را با حلقه پردازش کن و آدرسشان را بعد از عملیات رشد نگه ندار. قواعد عمر و مالکیت را با یک نمونهٔ کوچک یاد بگیر؛ اگر AI کدی با pointer خام پیشنهاد کرد، بپرس شیء را چه کسی می‌سازد، چه کسی آزاد می‌کند و تا کی زنده است.

۸. ورودی خراب را تشخیص بده

حلقهٔ while(std::cin >> value) تا وقتی خواندن عدد موفق است ادامه می‌دهد. پس از توقف، eof یعنی ورودی تمام شده؛ اگر eof نیست، احتمالاً متن نامعتبر وارد شده است. این دو حالت در برنامه جدا مدیریت می‌شوند.

برای پایان ورودی تعاملی، در macOS و Linux معمولاً Ctrl+D و در Windows معمولاً Ctrl+Z سپس Enter استفاده می‌شود. برای تست قابل تکرار، مثل فرمان بالای مقاله داده را از ورودی استاندارد بفرست.

نمرهٔ ۲۱ و متن abc را امتحان کن. برنامه باید پیام روشن بدهد و کد خروج غیرصفر داشته باشد. موفق بودن کامپایل فقط نشان می‌دهد ساختار کد پذیرفته شده؛ درستی قرارداد ورودی هنوز باید بررسی شود.

۹. تست‌های کوچک را قبل از گسترش نگه دار

فهرست خالی، میانگین [12,16,20]، حد پایین و بالای نمره و افزایش نمره نزدیک سقف را بررسی می‌کنیم. فایل کامل با --self-test همین حالت‌ها را اجرا می‌کند.

تمرین بعدی: کمترین و بیشترین نمره را اضافه کن، با خروجی مناسب برای فهرست خالی. سپس یک struct Student شامل name و score بساز و گزارش را برای چند دانشجو گسترش بده. قبل از مرتب‌سازی، تصمیم بگیر می‌خواهی ترتیب ورودی حفظ شود یا تغییر کند.

۱۰. از ساروی توضیح تصمیم بگیر

اگر خطای کامپایل داری، متن کامل اولین خطا و فرمان کامپایل را بده. ده خطای بعدی گاهی پیامد همان خطای اول‌اند. اگر برنامه خروجی غلط دارد، یک ورودی کوچک و نتیجهٔ مورد انتظار را اضافه کن.

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

حالا نوبت توست

این کار را خودت انجام بده.

  • پروژه را با C++17 و هشدارهای کامپایلر اجرا کن.
  • ورودی خالی و نمرهٔ خارج از بازه را آزمایش کن.
  • تفاوت کپی، reference و عمر شیء را با همین کد توضیح بده.

برای بررسی بیشتر

C++ Core Guidelines: عمر، مالکیت و رابط تابعپیش‌نویس استاندارد C++: vector

در انجام این کار به مشکل خوردی؟

از پشتیبانی کمک بگیر