Learn C++ STL

lesson ৩ / ৯ · C থেকে যতটুকু C++ দরকার

Module ১ · C থেকে যতটুকু C++ দরকার

reference: একই বাক্সের দ্বিতীয় নাম

Freeপড়া

এই lesson-এ যা শিখবে

  • Reference declare করতে পারবে, আর আগে থেকে থাকা একটা variable-এর দ্বিতীয় নাম হিসেবে সেটা ব্যবহার করতে পারবে।
  • এমন function লিখতে পারবে যেটা reference parameter দিয়ে caller-এর variable বদলায়, আর এমন function যেটা বড় কোনো জিনিস const& দিয়ে copy না করেই পড়ে।
  • Reference যে তিনটা নিয়ম মানে সেগুলো বলতে পারবে, কোনোটা ভাঙলে GCC 12-এর বার্তা পড়ে বুঝতে পারবে, আর pointer নাকি reference, সেটা বেছে নিতে পারবে।

গত মাসে Maria C-তে একটা swap function লিখেছিল। ওটা দুইটা pointer নিত, প্রতিটা ব্যবহারের আগে একটা করে তারা বসাত, আর প্রতিটা call-এ লাগত একটা করে ampersand। Bob একটা তারা দিতে ভুলে গিয়েছিল, আর সেই ভুল খুঁজতে ওর পুরো একটা সন্ধ্যা গেছে। C++-এ একই function লেখা যায় কোনো তারা ছাড়া, call-এ কোনো ampersand ছাড়া। এর নাম reference, আর এর পরের প্রতিটা STL lesson দাঁড়িয়ে আছে এটার ওপর ভর দিয়ে।

Maria-র swap, C-তে আর C++-এ

এই হলো Maria-র C version। C-তে function তার argument-গুলোর copy পায়, তাই caller-এর x আর y বদলাতে হলে ওর লাগে ওদের address।

#include <stdio.h>

void swap_ptr(int *a, int *b)
{
    int keep = *a;
    *a = *b;
    *b = keep;
}

int main(void)
{
    int x = 3;
    int y = 5;

    swap_ptr(&x, &y);
    printf("x = %d, y = %d\n", x, y);
    return 0;
}
x = 5, y = 3

আর এই হলো C++-এ একই কাজ, reference দিয়ে।

#include <iostream>

void swapRef(int& a, int& b)
{
    int keep = a;
    a = b;
    b = keep;
}

int main()
{
    int x = 3;
    int y = 5;

    swapRef(x, y);
    std::cout << "x = " << x << ", y = " << y << '\n';
    return 0;
}
x = 5, y = 3

Output একদম এক। বদলেছে তিনটা জিনিস, table-এ সেগুলো পাশাপাশি রাখা হলো।

C, pointer দিয়েC++, reference দিয়ে
Parameterint *aint& a
ভিতরে ব্যবহার*a = *b;a = b;
Callswap_ptr(&x, &y);swapRef(x, y);

মানে C++-এর swap একই কাজ করে, call-এ কোনো address নেওয়া লাগে না, আর function-এর ভিতরেও কোনো তারা লাগে না।

Reference মানে একই বাক্সের দ্বিতীয় নাম

Reference হলো আগে থেকে থাকা একটা variable-এর দ্বিতীয় নাম। এটা copy না, আবার address রাখা আলাদা কোনো variable-ও না। Reference-এর ওপর যা করবে, সেটা আসলে হবে মূল বাক্সটার ওপরেই।

Reference declare করার গড়ন

type& name = existing_variable;
  • type হলো যে বাক্সের নাম দিচ্ছ তার type, যেমন int।
  • Declaration-এ type-এর ঠিক পরে & মানে "এর reference"।
  • name হলো নতুন, দ্বিতীয় নামটা।
  • existing_variable হলো যে বাক্সের নাম দেওয়া হচ্ছে; এই লাইনেই একবার ঠিক হয়, আর সারাজীবনের জন্য।

দুই নামের একটা বাক্স চোখের সামনে দেখো।

#include <iostream>

int main()
{
    int score = 7;
    int& r = score;

    std::cout << "score = " << score << ", r = " << r << '\n';
    r = 8;
    std::cout << "after r = 8, score = " << score << '\n';
    score = 9;
    std::cout << "after score = 9, r = " << r << '\n';
    std::cout << "same box: " << (&r == &score) << '\n';
    return 0;
}
score = 7, r = 7
after r = 8, score = 8
after score = 9, r = 9
same box: 1

r দিয়ে লিখলে score বদলে গেল, আবার score-এ লিখলে r যা পড়ে সেটাও বদলে গেল। শেষ লাইনটা জিজ্ঞেস করে, দুই নামের address কি একই? সত্যি comparison-কে std::cout print করে 1 হিসেবে, তাই উত্তর হলো হ্যাঁ।

ছবিটা এরকম। বাঁ দিকে একটা বাক্স, তার গায়ে দুইটা নাম। ডান দিকে তুলনার জন্য একটা pointer, p = &score: pointer নিজেই আরেকটা বাক্স, তার নিজের address আছে, আর ভিতরে রাখা আছে প্রথম বাক্সের address।

0x7ffd10-এ একটা int বাক্স, ভিতরে 9, আর গায়ে দুইটা নাম: score আর r; r-এর জন্য আলাদা কোনো বাক্স নেই।

score হলো 0x7ffd10-এর বাক্স, ভিতরে 9; p হলো 0x7ffd18-এর আরেকটা বাক্স, ভিতরে 0x7ffd10, মানে score-এর address।

তাই r হলো না score-এর copy, না তার pointer। এটা score-ই, শুধু দ্বিতীয় একটা নামে।

& কেন দুই রকম মানে দেয়

Maria সাথে সাথে ধরে ফেলল: C-তে তো & মানে ছিল "এর address"। এখন এর মানে "এর reference"-ও। দুইটাই সত্যি, আর চিহ্নটা কোথায় দাঁড়িয়ে আছে, সেটাই বলে দেয় কোনটা।

#include <iostream>

int main()
{
    int x = 5;
    int& r = x;
    int* p = &x;

    std::cout << r << ' ' << *p << '\n';
    std::cout << (6 & 3) << '\n';
    return 0;
}
5 5
2
& কোথায় বসেউদাহরণমানে
declaration-এ, type-এর পরেint& r = x;r একটা int-এর reference
expression-এ, variable-এর সামনেint* p = &x;x-এর address, C-র মতোই
দুইটা মানের মাঝখানে6 & 3bitwise AND, C-র মতোই: 110 AND 011 হলো 010, মানে 2

তাই &-এর বাঁ পাশটা দেখো। বাঁয়ে type থাকলে reference, বাঁয়ে কিছু না থাকলে address, আর দুই পাশে দুইটা মান থাকলে bitwise AND।

Reference দিয়ে পাঠানো: function কাজ করে caller-এর বাক্সেই

Function-এর parameter-ও reference হতে পারে। তখন call যতক্ষণ চলে, parameter-টা থাকে caller-এর variable-এর দ্বিতীয় নাম হয়ে। একে বলে passing by reference। সাধারণ যে পথে function নিজের একটা copy পায়, সেটা passing by value।

Maria-র swapRef(x, y) ধাপে ধাপে দেখো। Call শুরু হতেই a আর b হয়ে যায় main-এর বাক্সগুলোর দ্বিতীয় নাম, তাই ভিতরে কিছু copy হয়ে ঢোকে না, ফেরত পাঠানোরও কিছু থাকে না।

main x = 3 আর y = 5 নিয়ে swapRef(x, y) call করে। Call-এর ভিতরে a আর b হলো main-এর x আর y-এর দ্বিতীয় নাম, তাই return-এর পর x হয় 5, y হয় 3।

Reference parameter থাকলে একটা function একাধিক ফলও ফেরত দিতে পারে। Kenji-র দোকানের till প্রতিটা বিক্রি দুইটা চলতি হিসাবে টুকে রাখে।

#include <iostream>

void addSale(int price, int& count, long long& total)
{
    count = count + 1;
    total = total + price;
}

int main()
{
    int count = 0;
    long long total = 0;

    addSale(120, count, total);
    addSale(80, count, total);
    addSale(45, count, total);
    std::cout << count << " sales, total " << total << '\n';
    return 0;
}
3 sales, total 245

price যায় value হিসেবে, কারণ function এটা শুধু পড়ে। count আর total যায় reference হিসেবে, কারণ function-কে main-এর নিজের দুইটা বাক্স বদলাতে হবে।

Bob এখানে একটা জিনিস খেয়াল করল। scanf("%d", &x)-এ ampersand লাগে, অথচ std::cin >> x-এ লাগে না। কারণটা এই lesson-ই: int পড়ার >> তার ডান পাশেরটা নেয় int& হিসেবে। তাই std::cin সরাসরি তোমার বাক্সে লেখে, আর ampersand-টা উধাও হয়ে যায়।

মানে reference parameter দিয়ে function caller-এর নিজের বাক্সেই কাজ করে, C ঠিক এই কাজটাই করত pointer দিয়ে, শুধু তারাগুলো ছাড়া।

Reference যে তিনটা নিয়ম মানে

Pointer খালি থাকতে পারে, অন্য দিকে ঘুরিয়ে দেওয়া যায়, আবার কোনো মান ছাড়াও ফেলে রাখা যায়। Reference এগুলোর কোনোটাই পারে না। প্রতিটা নিয়ম হয় compiler নিজে check করে, নয়তো একটা run দেখিয়ে দেয়।

নিয়ম 1: জন্মের সময়ই reference একটা বাক্সের নাম নিয়ে আসে। বাক্স ছাড়া reference compiler মানে না।

int& r;

GCC 12 Playground-এর command line-এ আর -Wall -Wextra-এ একই কথা বলে: error: 'r' declared as reference but not initialized।

নিয়ম 2: reference কখনো null হয় না। "কিছুই না"-র reference দিয়ে শুরু করার কোনো উপায় নেই।

int& r = nullptr;

GCC 12 প্রতিটা command line-এ এটা ফিরিয়ে দেয়: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'std::nullptr_t'। nullptr হলো C++-এর null pointer, C-র NULL, শুধু তার নিজের একটা type আছে।

নিয়ম 3: reference কখনো অন্য বাক্সে সরে যায় না। এটা compile হয়ে যায়, তাই খেয়াল করে দেখো কী ঘটে।

#include <iostream>

int main()
{
    int a = 10;
    int b = 20;
    int& r = a;

    r = b;
    r = 99;
    std::cout << a << ' ' << b << ' ' << r << '\n';
    return 0;
}
99 20 99

r = b; দেখে মনে হয় r এখন থেকে b-এর নাম। আসলে না: এটা 20 copy করে a-তে, যে বাক্সের নাম r জন্ম থেকে বহন করছে। তাই r = 99-ও গিয়ে পড়ে a-তে, আর b-এর 20 যেমন ছিল তেমনই থাকে।

মানে reference একবারই ঠিক হয়, declaration-এর লাইনে, একটা আসল বাক্সের সাথে, আর scope শেষ না হওয়া পর্যন্ত সেই বাক্সেরই নাম থাকে।

const&: বড় জিনিস copy না করেই পড়া

Value হিসেবে পাঠালে argument-টা copy হয়। একটা int-এর জন্য সেটা চার byte, কেউ টেরও পায় না। কিন্তু দশ লাখ সংখ্যার একটা list হলে সেটা চল্লিশ লাখ byte, আর প্রতিটা call-এ আবার নতুন করে copy।

Const reference, লেখা হয় const type&, function-কে caller-এর বাক্সটাই দেয়, তবে শুধু পড়ার একটা নামে। কিছুই copy হয় না, আর function কথা দেয় যে বাক্সটা বদলাবে না। Compiler সেই কথা রাখতে বাধ্য করে।

void show(const int& x)
{
    x = 7;
}

GCC 12, প্রতিটা command line-এ: error: assignment of read-only reference 'x'।

এবার খরচটা মেপে দেখা যাক। Program-টা std::vector দিয়ে 1,000,000টা int-এর একটা list বানায়; vector হলো এমন array যে নিজের size জানে, Module 2-তে এটা শেখানো হবে। List-টার যোগফল বের করে 100 বার value parameter দিয়ে, তারপর 100 বার const& parameter দিয়ে, আর দুইটারই সময় মাপে <chrono> দিয়ে, যেটা standard ঘড়ি।

#include <chrono>
#include <iostream>
#include <vector>

long long sumByValue(std::vector<int> v)
{
    long long total = 0;
    for (int i = 0; i < (int)v.size(); i++) {
        total += v[i];
    }
    return total;
}

long long sumByConstRef(const std::vector<int>& v)
{
    long long total = 0;
    for (int i = 0; i < (int)v.size(); i++) {
        total += v[i];
    }
    return total;
}

int main()
{
    std::vector<int> big(1000000, 1);
    const int calls = 100;

    auto t0 = std::chrono::steady_clock::now();
    long long a = 0;
    for (int c = 0; c < calls; c++) {
        a += sumByValue(big);
    }
    auto t1 = std::chrono::steady_clock::now();
    long long b = 0;
    for (int c = 0; c < calls; c++) {
        b += sumByConstRef(big);
    }
    auto t2 = std::chrono::steady_clock::now();

    std::chrono::duration<double, std::milli> byValue = t1 - t0;
    std::chrono::duration<double, std::milli> byRef = t2 - t1;
    std::cout << "by value:     " << a << " in " << byValue.count() << " ms\n";
    std::cout << "by const ref: " << b << " in " << byRef.count() << " ms\n";
    return 0;
}

সময়গুলো প্রতিবার run-এ একটু একটু বদলায়, তাই এগুলো output block-এ না রেখে table-এ রাখা হলো। এটা Compiler Explorer-এ একবারের run, x86-64 GCC 12.2, Playground-এর flag-এ, -O2 -std=c++17।

ParameterPrint হওয়া যোগফল100টা call-এ সময়প্রতি call-এ copy
std::vector<int> v, value হিসেবে10000000045.6 ms4,000,000 byte
const std::vector<int>& v10000000016.8 msকিছুই না

দুইটাই একই যোগফল দেয়। Value version প্রায় 2.7 গুণ ধীর, আর বাড়তি প্রতিটা millisecond খরচ হয়েছে copy করতে: যে সংখ্যাগুলো কখনো বদলায়নি, সেগুলো পড়ার জন্য 100টা call-এ 400 MB টানাটানি।

প্রচলিত পরামর্শটা C++ Core Guidelines-এর, rule F.16: int, double, char-এর মতো ছোট জিনিস value হিসেবে পাঠাও, আর এর চেয়ে বড় যেকোনো কিছু const& দিয়ে। তাই বড় জিনিস পড়ার পথ হলো const&: caller-এর নিজের বাক্স, শুধু পড়ার জন্য, copy-র কোনো খরচ ছাড়া।

আগাম এক ঝলক: reference দিয়ে range-for

Lesson 04 শেখাবে range-for, এমন একটা loop যেটা array-র প্রতিটা বাক্সে একে একে যায়। এর একটা অংশ এখানেই মানায়, কারণ সেটাও আবার reference।

#include <iostream>

int main()
{
    int marks[4] = {70, 85, 62, 91};

    for (int m : marks) {
        m = m + 5;
    }
    std::cout << "after a copy loop:      " << marks[0] << ' ' << marks[3] << '\n';

    for (int& m : marks) {
        m = m + 5;
    }
    std::cout << "after a reference loop: " << marks[0] << ' ' << marks[3] << '\n';
    return 0;
}
after a copy loop:      70 91
after a reference loop: 75 96

প্রথম loop-এ m প্রতিটা বাক্সের একটা copy, তাই তাতে 5 যোগ করলে marks-এ কিছুই বদলায় না। দ্বিতীয়টায় m একে একে প্রতিটা বাক্সের দ্বিতীয় নাম, তাই নম্বরগুলো সত্যিই বদলে যায়। পুরো ফারাকটা একটা &-এর।

Pointer নাকি reference: কখন কোনটা

Reference pointer-কে সরিয়ে দেয় না। দুইটাই এমন কাজ পারে যা অন্যটা পারে না, table-এ দুইটা পাশাপাশি রাখা হলো।

Pointer নাকি reference, সারি ধরে ধরে প্রশ্ন Pointer: int* p Reference: int& r Null হতে পারে? হ্যাঁ: p = nullptr না, কখনোই না অন্য বাক্সে সরানো যায়? হ্যাঁ: p = &other না, জন্মের সময়ই ঠিক বাক্সে পৌঁছানোর উপায় *p r, সাধারণ নামটাই নিজের আলাদা বাক্স? হ্যাঁ, নিজস্ব address সহ না: &r আসলে ওই বাক্সের p + 1-এর মতো হিসাব? হ্যাঁ, array ধরে না C-তে আছে? হ্যাঁ না, শুধু C++-এ

Function যে parameter বদলাবে, তার জন্য reference নাও, আর যে বড় জিনিস শুধু পড়বে, তার জন্য const&। Pointer নাও তখন, যখন "কিছুই না" একটা আসল উত্তর, যখন array ধরে এগোতে হবে, বা যখন code-টা C-র সাথে ভাগ করে চলে। STL প্রায় সব জায়গায় তোমার হাতে reference দেয়, আর pointer-এর মতো যে একটা জিনিস সে যোগ করে, iterator, সেটা দেখাবে Module 11।

Example 1: Zara reading-টাকে sensor-এর সীমার ভিতরে রাখে

Zara-র thermometer পড়তে পারে -40 থেকে 60 ডিগ্রি পর্যন্ত। এর বাইরে যা আসে, সেটা গোলমাল, তাই ও সেটাকে কাছের প্রান্তে টেনে আনে। Function reading-টাকে যেখানে আছে সেখানেই বদলে দেয়।

#include <iostream>

void clampTemp(int& t, int low, int high)
{
    if (t < low) {
        t = low;
    }
    if (t > high) {
        t = high;
    }
}

int main()
{
    int a = 25;
    int b = -55;
    int c = 70;

    clampTemp(a, -40, 60);
    clampTemp(b, -40, 60);
    clampTemp(c, -40, 60);
    std::cout << a << ' ' << b << ' ' << c << '\n';
    return 0;
}
25 -40 60

25 সীমার ভিতরেই ছিল, তাই যেমন ছিল তেমন রইল। -55 আর 70 টেনে আনা হলো দুই প্রান্তে। t একটা reference, তাই প্রতিটা call main-এর নিজের variable বদলেছে; low আর high শুধু পড়া হয়, তাই ওরা সাধারণ int মান।

Run in Compiler
Example 2: একটা নম্বর পড়ো, আর বলো সেটা ঠিক ছিল কি না

Bob এখনো scanf লিখেই যাচ্ছে। "আমার variable-এ পড়ো, আর বলো কাজ হলো কি না", এর C++ রূপটা এরকম: function একটা reference parameter ভরে দেয়, আর ফেরত দেয় একটা bool।

#include <iostream>

bool readMark(int& mark)
{
    std::cin >> mark;
    return mark >= 0 && mark <= 100;
}

int main()
{
    int mark = 0;
    int good = 0;
    int bad = 0;

    for (int i = 0; i < 4; i++) {
        if (readMark(mark)) {
            good++;
        } else {
            bad++;
            std::cout << "rejected " << mark << '\n';
        }
    }
    std::cout << good << " good, " << bad << " bad\n";
    return 0;
}
rejected 120
rejected -3
2 good, 2 bad

এই output input 88 120 -3 45-এর জন্য। main-এর mark ভরে দেয় function, যেভাবে scanf ভরত &mark, তাই বাতিল হওয়া মানটা main print করতে পারে।

Run in Compiler
Example 3: Amara-র ছাড়ের দামের তালিকা

Amara n-টা দাম পড়ে, প্রতিটা থেকে একটা শতাংশ ছাড় দেয়, আর দোকানের নামের নিচে তালিকাটা print করে। একটা function একটা দাম বদলায়, আরেকটা শুধু তালিকা আর নামটা পড়ে। C++-এ const int MAX_N = 100; সত্যিকারের constant, তাই এটা দিয়ে array-র size দেওয়া যায়; C track #define ব্যবহার করত, কারণ C এটা মানে না।

#include <iostream>
#include <string>

const int MAX_N = 100;

void applyDiscount(int& price, int percent)
{
    price = price - price * percent / 100;
}

void printList(const std::string& shop, const int prices[], int n)
{
    std::cout << shop << ':';
    for (int i = 0; i < n; i++) {
        std::cout << ' ' << prices[i];
    }
    std::cout << '\n';
}

int main()
{
    int prices[MAX_N] = {0};
    int n = 0;
    int percent = 0;
    std::string shop = "Amara's Books";

    std::cin >> n;
    for (int i = 0; i < n; i++) {
        std::cin >> prices[i];
    }
    std::cin >> percent;

    printList(shop, prices, n);
    for (int i = 0; i < n; i++) {
        applyDiscount(prices[i], percent);
    }
    printList(shop, prices, n);
    return 0;
}
Amara's Books: 400 250 120 90
Amara's Books: 300 188 90 68

এই output input 4, 400 250 120 90 আর 25-এর জন্য। prices[i] একটা int বাক্স, তাই applyDiscount তার একটা দ্বিতীয় নাম পায় আর সেটাকেই বদলায়। 250 থেকে 25% বাদ দিলে হয় 187.5, আর integer ভাগ রাখে 188 (250 - 6250 / 100 = 250 - 62)।

printList নামটা নেয় const std::string& হিসেবে, শুধু পড়ার একটা নাম, কোনো copy নেই। Array আসে pointer হয়ে, ঠিক C-র মতো, আর const কথা দেয় যে function ওতে লিখবে না।

Run in Compiler

এটা কোথায় কাজে লাগে

  • Standard std::swap। <utility>-এ এটা দুইটা argument-ই নেয় reference হিসেবে, Maria-র swapRef-এর একদম একই গড়নে। Lesson 05 এটা যেকোনো type-এ ব্যবহার করবে।
  • std::getline। যে string ভরবে, সেটা এটা নেয় reference হিসেবে, তাই std::getline(std::cin, line) লেখে তোমার নিজের line-এ (lesson 02)।
  • খোদ std::cin >> x। Standard library-র int-এর >> নেয় একটা int&, এজন্যই scanf-এ যেখানে ampersand লাগত, এখানে লাগে না।
  • C++ Core Guidelines। Bjarne Stroustrup আর Herb Sutter-এর সম্পাদনা করা এই guideline বলে: ছোট input value হিসেবে, বড়গুলো const& দিয়ে (F.16), আর যে parameter পড়াও হয় বদলানোও হয়, সেটা non-const reference দিয়ে (F.17)।

যে ভুলগুলো সবাই করে

১. Parameter-এ & দিতে ভুলে যাওয়া।

void swapValues(int a, int b)
{
    int keep = a;
    a = b;
    b = keep;
}

কোনো command line-এই কোনো বার্তা নেই, Playground-এর-টাতেও না, -Wall -Wextra-তেও না। Function নিজের copy দুইটা swap করে, আর main তখনো print করে 3 5। দুইটা ampersand বসিয়ে দাও: int& a, int& b। এই ভুলটা তুমি করবে, কারণ পার্থক্য মাত্র একটা অক্ষরের, যেখানে C তোমাকে সব জায়গায় তারা লিখিয়ে ছাড়ত।

২. int& parameter-এ সরাসরি একটা সংখ্যা পাঠানো।

swapRef(3, 5);

প্রতিটা command line-এ error: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int', তার পরে note: initializing argument 1 of 'void swapRef(int&, int&)'। Lvalue হলো এমন কিছু যার একটা বাক্স আছে আর নাম ধরে ডাকা যায়; rvalue, যেমন 3, এমন একটা মান যার নিজের কোনো বাক্স নেই। Reference-এর বাক্স লাগে, তাই মানগুলো আগে variable-এ রাখো। Note লাইনটা function আর argument-এর নাম বলে দেয়, খুঁজতে হবে ওখানেই।

৩. Local variable-এর reference return করা।

int& bad()
{
    int local = 42;
    return local;
}

GCC 12 Playground-এর command line-এই, কোনো -Wall ছাড়া, warning দেয়: warning: reference to local variable 'local' returned [-Wreturn-local-addr]। Function return করলেই local শেষ, তাই caller হাতে পায় এমন একটা বাক্সের নাম, যে বাক্স আর নেই। Compiler Explorer-এ, Playground-এর flag-এ, এটা print করতে গিয়ে program শেষ হয়েছে Program terminated with signal SIGSEGV (11) দিয়ে। Value হিসেবে return করো: int good()। এটা তুমি করতে যাবে, কারণ reference return করাটা শুনতে লাগে "copy ছাড়াই ফেরত দেওয়া"-র মতো।

৪. const variable-এর সাধারণ reference।

const int limit = 10;
int& r = limit;

প্রতিটা command line-এ error: error: binding reference of type 'int&' to 'const int' discards qualifiers। শুধু-পড়ার একটা বাক্সকে লেখার মতো নাম দিলে const-এর দেওয়া কথা ভেঙে যায়। শুধু পড়লে লেখো const int& r = limit;।

মাথা খাটাও

এই program কোনো বার্তা ছাড়াই compile হয়, আর print করে 5।

#include <iostream>

int main()
{
    const int& r = 5;
    std::cout << r << '\n';
    return 0;
}

const শব্দটা মুছে দাও, GCC 12 ভুল ২-এর বার্তা দিয়ে মানা করে দেবে। Reference-এর বাক্স লাগে, আর 5 তো কোনো variable না। তাহলে r কোন বাক্সের নাম, আর const থাকলে তফাতটা হয় কেন?

ভাবো, 5 সংখ্যাটাকেই লেখার মতো একটা নাম দিলে কী গোলমাল হতে পারত। তারপর ভাবো, যে কথা দেয় কখনো লিখবে না, তার জন্য compiler চুপচাপ কী বানিয়ে দিতে পারে।

অনুশীলন ১সহজ

কঠিন একটা পরীক্ষার পর স্কুল প্রতিটা নম্বরে bonus যোগ করে। void addBonus(int& mark, int bonus) লেখো, যেটা একটা নম্বরে bonus যোগ করে, তবে 100-এর ওপরে কখনো নেয় না, আর array-র প্রতিটা বাক্সে এটা call করো।

Input. একটা লাইনে n, একটা লাইনে n-টা নম্বর, তারপর একটা লাইনে bonus।

Output. Bonus-এর পরের n-টা নম্বর, এক লাইনে, মাঝে একটা করে space।

Constraints. 1 <= n <= 100। প্রতিটা নম্বর 0 থেকে 100-এর মধ্যে, আর bonus-ও 0 থেকে 100-এর মধ্যে।

Sample. Input 3, 70 98 40 আর 5 দিলে আসে 75 100 45।

#include <iostream>

const int MAX_N = 100;

// Add bonus to mark, but never above 100.
void addBonus(int& mark, int bonus)
{
    // your code
}

int main()
{
    int marks[MAX_N] = {0};
    int n = 0;
    int bonus = 0;

    std::cin >> n;
    for (int i = 0; i < n; i++) {
        std::cin >> marks[i];
    }
    std::cin >> bonus;

    // Call addBonus on every mark, then print the marks on one line.

    return 0;
}

আলাদা করে গ্রেড হয় না। নম্বরগুলো vector-এ রাখলে আরও আরাম, সেটা Module 2 শেখাবে; এখানে সাধারণ array-ই যথেষ্ট।

Run in Compiler
অনুশীলন ২মাঝারি

David একটা function call থেকেই দিনের সবচেয়ে ঠান্ডা আর সবচেয়ে গরম reading চায়। void minMax(const int a[], int n, int& lo, int& hi) লেখো, যেটা দুইটা উত্তরই ফেরত দেয় তার দুইটা reference parameter দিয়ে।

Input. একটা লাইনে n, তারপর একটা লাইনে n-টা integer।

Output. এক লাইন: সবচেয়ে ছোট আর সবচেয়ে বড়, মাঝে একটা space।

Constraints. 1 <= n <= 100। প্রতিটা integer -1000000 থেকে 1000000-এর মধ্যে।

Sample. Input 5 আর 4 -2 9 0 9 দিলে আসে -2 9।

#include <iostream>

const int MAX_N = 100;

// Put the smallest of a[0] to a[n - 1] in lo and the largest in hi.
void minMax(const int a[], int n, int& lo, int& hi)
{
    // your code
}

int main()
{
    int a[MAX_N] = {0};
    int n = 0;

    std::cin >> n;
    for (int i = 0; i < n; i++) {
        std::cin >> a[i];
    }

    int lo = 0;
    int hi = 0;
    minMax(a, n, lo, hi);
    std::cout << lo << ' ' << hi << '\n';
    return 0;
}

আলাদা করে গ্রেড হয় না। Sample-এর আগে Zara দেখে নিত n = 1 আর শুধু ঋণাত্মক সংখ্যার একটা list। Lesson 06 এই দুইটা উত্তরই ফেরত দেবে একটা pair হিসেবে, সেটাই graded min-max-pair।

Run in Compiler
অনুশীলন ৩মাঝারি

Maria তাকে তিনটা parcel ওজন অনুযায়ী সাজায়, সবচেয়ে হালকাটা বাঁয়ে। ওর চাল একটাই: পাশাপাশি দুইটা দেখো, বাঁয়েরটা ভারী হলে জায়গা বদলে দাও। Starter সেই চালটা দিয়েছে void order(int& a, int& b) হিসেবে। এর ভিতরটা এমনভাবে লেখো যাতে call-এর পর a <= b হয়, তারপর এটা দিয়ে প্রতিটা লাইন সাজাও।

Input. প্রথম লাইনে t। পরের t-টা লাইনের প্রতিটায় তিনটা integer a, b আর c।

Output. t-টা লাইন, প্রতিটায় তার input লাইনের তিনটা integer ছোট থেকে বড় ক্রমে, মাঝে একটা করে space।

Constraints. 1 <= t <= 10000। প্রতিটা integer -1000000000 থেকে 1000000000-এর মধ্যে।

Sample. Input 3, তারপর 3 1 2, 5 5 1 আর -1 -2 -3 দিলে তিন লাইনে আসে 1 2 3, 1 5 5 আর -3 -2 -1।

#include <iostream>

// Put a and b in order: after the call, a <= b.
void order(int& a, int& b)
{
    // your code
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    int t = 0;
    std::cin >> t;
    for (int i = 0; i < t; i++) {
        int a = 0;
        int b = 0;
        int c = 0;
        std::cin >> a >> b >> c;

        // Use order() to put a, b and c in increasing order,
        // then print them on one line, separated by single spaces.
    }
    return 0;
}

order-three নামে গ্রেড হয়। Hidden test-এ আছে 1, 2, 3-এর ছয়টা ক্রমই, দুইটা বা তিনটা সমান মানের লাইন, আর সীমার একদম কিনারার মান দিয়ে 10000টা লাইন। & ছাড়া parameter হলে এর একটাও পাস করবে না।

Run in Compiler

সচরাচর যে প্রশ্নগুলো আসে

  • Reference কি আসলে ছদ্মবেশী pointer?

    ভিতরে ভিতরে compiler অনেক সময় reference parameter-কে address হিসেবেই পাঠায়, pointer যেভাবে পাঠাত। কিন্তু ভাষাটা তোমাকে pointer-এর কোনো ক্ষমতাই দেয় না: null নেই, সরানো নেই, হিসাব নেই, নিজের address নেই। এটাকে একটা নাম হিসেবেই ভাবো, কারণ তোমার program এটা দিয়ে শুধু ওইটুকুই করতে পারে।

  • Reference কত বড়?

    sizeof-কে জিজ্ঞেস করলে পাবে, যে বাক্সের নাম, তার size। double price = 9.5; আর double& r = price;-এর জন্য GCC 12-এ একটা run sizeof(r) আর sizeof(price)-এর জন্য print করেছে 8 8। খোদ reference-এর এমন কোনো size নেই যা তুমি জিজ্ঞেস করতে পারো।

  • const& যদি copy বাঁচায়, তাহলে সবকিছুই ওভাবে পাঠাই না কেন?

    int বা double-এর copy মাত্র কয়েক byte, খরচ বলতে কিছুই না। Reference দিয়ে পড়তে উল্টো একটু বেশিও লাগতে পারে, কারণ function-কে সেটা ধরে বাক্স পর্যন্ত যেতে হয়। তাই ছোট জিনিস value হিসেবে, বড় জিনিস const& দিয়ে, যেমনটা C++ Core Guidelines-এর rule F.16 বলে।

  • Reference-এর একটা array বানানো যায়?

    না। int& refs[2] = {a, b}; প্রতিটা command line-এ দেয় error: declaration of 'refs' as array of references। Array-র প্রতিটা বাক্স নিজেই একটা object, আর reference object না, শুধু একটা নাম।

  • C-তে reference নেই কেন?

    এটা C++-এর সংযোজন। C++-এর স্রষ্টা Bjarne Stroustrup লিখেছেন, তিনি এটা যোগ করেছিলেন মূলত operator overloading চালানোর জন্য, যেমন std::cin-এর >>। বড় জিনিস সস্তায় পাঠানোর সুবিধাটা এসেছে এর সাথেই।

মূল কথা

  • Reference, int& r = x;, হলো x-এর দ্বিতীয় নাম: একই বাক্স, copy-ও না, pointer-ও না।
  • Reference parameter দিয়ে function caller-এর variable বদলাতে পারে, কোনো তারা ছাড়া, call-এ কোনো ampersand ছাড়া।
  • const type& বড় জিনিস কোনো copy ছাড়াই পড়ে, আর এর মধ্য দিয়ে লেখার যেকোনো চেষ্টা compiler ফিরিয়ে দেয়।
  • Reference জন্মায় একটা বাক্সের নাম নিয়ে, কখনো null হয় না, কখনো সরে না: r = b; শুধু একটা মান copy করে।
  • Type-এর পরে & বানায় reference; variable-এর সামনে বসলে নেয় address, C-র মতোই।
  • "কিছুই না" যখন একটা উত্তর, যখন array ধরে এগোতে হবে, বা code যখন C-র সাথে ভাগ করা, তখন pointer নাও।

এরপর lesson 04-এ auto দিয়ে type লেখার কাজটা compiler-কে দিয়ে দেবে, আর এই lesson যে range-for-এর এক ঝলক দেখাল, সেটা পুরোটা শিখবে।

lesson ৩ শেষ

শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।

পরেরটা: auto আর range-for: কম লেখা, একই মানে

reference: একই বাক্সের দ্বিতীয় নাম | Learn C++ STL | Progsity