Bài 6.3: Phạm vi biến và scope rule trong C++

Chào mừng trở lại với chuỗi bài viết của chúng ta về C++! Hôm nay, chúng ta sẽ cùng nhau khám phá một khái niệm vô cùng quan trọngthen chốt trong lập trình: Phạm vi biến (Variable Scope) và các Quy tắc phạm vi (Scope Rules). Việc hiểu rõ phạm vi biến sẽ giúp bạn quản lý dữ liệu hiệu quả hơn, tránh được những lỗi phổ biến liên quan đến truy cập biến, và viết code sạch sẽ, dễ đọc.

Hãy tưởng tượng chương trình của bạn như một tòa nhà phức tạp với nhiều phòng ban, tầng lầu khác nhau. Mỗi "phòng" hoặc "khu vực" có những quy định riêng về việc ai có thể truy cập cái gì. Phạm vi biến cũng tương tự như vậy: nó định nghĩa khu vực trong mã nguồn mà tại đó một biến có thể được truy cập hoặc tham chiếu đến.

Phạm vi của một biến không chỉ xác định nơi nó hiển thị, mà còn ảnh hưởng đến thời gian sống (lifetime) của biến đó trong suốt quá trình thực thi chương trình.

Trong C++, có nhiều loại phạm vi khác nhau. Chúng ta sẽ đi sâu vào các loại phổ biến nhất:

  1. Global Scope (Phạm vi Toàn cục)
  2. Block Scope (Phạm vi Khối)
  3. Namespace Scope (Phạm vi Không gian tên)

Chúng ta cũng sẽ nói về hiện tượng Variable Shadowing (Che khuất biến), một vấn đề thường gặp khi làm việc với các phạm vi lồng nhau.

1. Global Scope (Phạm vi Toàn cục)

Biến được khai báo ở ngoài tất cả các hàm, khối lệnh ({}), lớp (class), hoặc không gian tên (namespace) sẽ có phạm vi toàn cục.

  • Khả năng truy cập: Biến toàn cục có thể được truy cập từ bất kỳ đâu trong chương trình sau điểm khai báo của nó (kể cả từ các hàm khác, các lớp khác, v.v.).
  • Thời gian sống: Biến toàn cục được tạo ra khi chương trình bắt đầu thực thi và tồn tại cho đến khi chương trình kết thúc.
  • Lưu ý: Việc sử dụng quá nhiều biến toàn cục thường không được khuyến khích vì nó có thể làm cho chương trình khó theo dõi, khó kiểm thử và dễ xảy ra xung đột tên biến.

Hãy xem một ví dụ đơn giản:

#include <iostream>

// Khai báo biến toàn cục
int global_variable = 100;

void accessGlobal() {
    // Có thể truy cập biến toàn cục từ đây
    cout << "Trong ham accessGlobal, global_variable = " << global_variable << endl;
}

int main() {
    // Cũng có thể truy cập biến toàn cục từ ham main
    cout << "Trong ham main, global_variable = " << global_variable << endl;

    accessGlobal();

    return 0;
}

Giải thích:

Biến global_variable được khai báo ở ngoài hàm main và hàm accessGlobal. Do đó, nó có phạm vi toàn cục và có thể được sử dụng bên trong cả hai hàm này một cách dễ dàng.

2. Block Scope (Phạm vi Khối)

Đây là loại phạm vi phổ biến nhất cho các biến mà bạn khai báo bên trong các hàm. Một biến được khai báo bên trong một cặp dấu ngoặc nhọn {} (một khối lệnh) sẽ có phạm vi khối.

  • Khả năng truy cập: Biến này chỉ có thể được truy cập bên trong chính khối lệnh mà nó được khai báo, từ điểm khai báo đến cuối khối.
  • Thời gian sống: Biến khối (thường được gọi là biến cục bộ) được tạo ra khi luồng thực thi đi vào khối đó và bị hủy (bộ nhớ được giải phóng) khi luồng thực thi thoát khỏi khối đó.
  • Các khối lệnh phổ biến:
    • Thân hàm ({...})
    • Khối lệnh của câu điều kiện if, else, else if ({...})
    • Khối lệnh của vòng lặp for, while, do-while ({...})
    • Bất kỳ cặp ngoặc nhọn độc lập nào ({...})

Hãy xem các ví dụ minh họa:

Ví dụ 1: Biến cục bộ trong hàm main
#include <iostream>

int main() {
    // local_variable có pham vi khoi (block scope) trong ham main
    int local_variable = 50;

    cout << "Trong ham main, local_variable = " << local_variable << endl;

    // Bien local_variable khong the truy cap duoc ben ngoai ham main
    // Neu thu truy cap o day, se bao loi bien chua khai bao
    // cout << some_other_variable << endl; // LOI COMPILER!

    return 0;
}

Giải thích:

local_variable chỉ tồn tại và có thể được truy cập bên trong phạm vi của hàm main (tức là bên trong cặp {} của main). Nếu bạn cố gắng sử dụng nó bên ngoài main, trình biên dịch sẽ báo lỗi.

Ví dụ 2: Biến trong khối if
#include <iostream>

int main() {
    int number = 10;

    if (number > 5) {
        // message co pham vi khoi ben trong khoi if
        string message = "Number is greater than 5";
        cout << message << endl;
    } // <-- Pham vi cua message ket thuc o day

    // Thu truy cap message ben ngoai khoi if
    // cout << message << endl; // LOI COMPILER!

    return 0;
}

Giải thích:

Biến message chỉ được tạo ra và có thể truy cập bên trong khối lệnh của câu điều kiện if. Ngay sau khi khối if kết thúc, message không còn tồn tại và không thể truy cập được nữa.

Ví dụ 3: Biến trong vòng lặp for

Biến được khai báo trực tiếp trong phần khởi tạo của vòng lặp for cũng có phạm vi đặc biệt liên quan đến vòng lặp đó.

#include <iostream>

int main() {
    for (int i = 0; i < 3; ++i) {
        // bien i co pham vi khoi (lien quan den vong lap for)
        cout << "i = " << i << endl;
        // j cung co pham vi khoi ben trong vong lap
        int j = i * 10;
        cout << "j = " << j << endl;
    } // <-- Pham vi cua i va j ket thuc o day

    // Thu truy cap i hoac j ben ngoai vong lap
    // cout << i << endl; // LOI COMPILER!
    // cout << j << endl; // LOI COMPILER!

    return 0;
}

Giải thích:

Biến i được khai báo ngay trong câu lệnh for. Phạm vi của nó kéo dài từ điểm khai báo đến cuối khối lệnh của vòng lặp for. Tương tự, biến j được khai báo bên trong khối của for, nên phạm vi của nó cũng chỉ nằm trong khối đó.

3. Namespace Scope (Phạm vi Không gian tên)

Mặc dù không gian tên chủ yếu dùng để tổ chức mã nguồn và tránh xung đột tên, chúng cũng định nghĩa một loại phạm vi. Các biến, hàm, lớp, v.v., được khai báo bên trong một namespace có phạm vi trong không gian tên đó.

  • Khả năng truy cập: Để truy cập một thành viên trong không gian tên từ bên ngoài, bạn phải sử dụng tên không gian tên và toán tử phân giải phạm vi (::), ví dụ: cout, hoặc sử dụng khai báo using namespace để đưa tất cả các tên từ không gian tên đó vào phạm vi hiện tại.
  • Lưu ý: std là không gian tên tiêu chuẩn của C++ chứa hầu hết các thư viện tiêu chuẩn (như iostream, string, vector, v.v.).

Ví dụ:

#include <iostream> // cout va endl nam trong namespace std

namespace MyNamespace {
    // bien nay co pham vi trong MyNamespace
    int my_variable = 42;

    void printValue() {
        cout << "Trong MyNamespace, my_variable = " << my_variable << endl;
    }
}

int main() {
    // Truy cap bien va ham trong MyNamespace su dung ::
    cout << "Truy cap tu main: " << MyNamespace::my_variable << endl;
    MyNamespace::printValue();

    // Su dung using directive (can than khi su dung)
    // using namespace MyNamespace;
    // cout << "Sau using, co the truy cap: " << my_variable << endl; // Neu dung using

    return 0;
}

Giải thích:

my_variableprintValue được khai báo bên trong namespace MyNamespace. Để truy cập chúng từ main, chúng ta phải dùng MyNamespace:: để chỉ rõ chúng thuộc về không gian tên nào. coutendl cũng tương tự, chúng thuộc về không gian tên std.

4. Variable Shadowing (Che khuất biến)

Điều gì xảy ra khi bạn khai báo một biến cục bộ (trong phạm vi khối) có cùng tên với một biến toàn cục hoặc một biến ở phạm vi bên ngoài?

Trong trường hợp này, biến cục bộ sẽ che khuất hoặc bóng đè biến ở phạm vi bên ngoài bên trong phạm vi của biến cục bộ đó. Khi bạn sử dụng tên biến trong phạm vi cục bộ, trình biên dịch sẽ ưu tiên biến cục bộ.

Đây là một kỹ thuật có thể hữu ích trong một số trường hợp, nhưng cũng là nguyên nhân phổ biến gây ra lỗi nếu không cẩn thận.

Ví dụ:

#include <iostream>

// Bien toan cuc
int same_name_variable = 100;

int main() {
    // Bien cuc bo (che khuat bien toan cuc)
    int same_name_variable = 50;

    cout << "Bien cuc bo trong main: " << same_name_variable << endl; // In ra 50

    // De truy cap bien toan cuc khi bi che khuat, su dung toan tu pham vi ::
    cout << "Bien toan cuc (su dung ::): " << ::same_name_variable << endl; // In ra 100

    { // Bat dau mot khoi moi
        // Bien cuc bo khac (che khuat bien cuc bo trong main)
        int same_name_variable = 25;
        cout << "Bien cuc bo trong khoi inner: " << same_name_variable << endl; // In ra 25

        // Bien cuc bo trong main van bi che khuat o day, khong the truy cap truc tiep

    } // <-- Khoi inner ket thuc, bien same_name_variable = 25 bi huy

    cout << "Tro lai main, bien cuc bo trong main: " << same_name_variable << endl; // In ra 50

    return 0;
}

Giải thích:

  • Ban đầu, same_name_variable toàn cục có giá trị 100.
  • Trong main, chúng ta khai báo một biến cục bộ cùng tên và gán giá trị 50. Từ điểm này đến cuối main (trừ các khối lồng bên trong nơi nó bị che khuất tiếp), khi bạn dùng same_name_variable, bạn đang nói đến biến cục bộ này.
  • Sử dụng toán tử :: (toán tử phân giải phạm vi), ::same_name_variable cho phép chúng ta chỉ rõ rằng mình muốn truy cập biến same_name_variable trong phạm vi toàn cục.
  • Bên trong khối {} con, chúng ta lại khai báo một biến cùng tên với giá trị 25. Biến này che khuất biến cục bộ của main. Khi dùng same_name_variable trong khối này, nó sẽ tham chiếu đến biến có giá trị 25.
  • Khi thoát khỏi khối con, biến cục bộ có giá trị 25 bị hủy. Biến cục bộ của main (giá trị 50) lại trở thành biến hiển thị khi sử dụng tên same_name_variable trong phần còn lại của main.

Hiện tượng che khuất biến nhấn mạnh tầm quan trọng của việc hiểu phạm vi: C++ luôn tìm kiếm biến bắt đầu từ phạm vi gần nhất và mở rộng dần ra các phạm vi bên ngoài.

Tóm tắt các quy tắc phạm vi chính

  • Biến toàn cục (Global Scope) có thể truy cập từ bất kỳ đâu sau khi khai báo và tồn tại suốt thời gian chạy của chương trình.
  • Biến cục bộ (Block Scope) chỉ có thể truy cập bên trong khối lệnh nơi nó được khai báo và tồn tại cho đến khi kết thúc khối lệnh đó.
  • Khi có biến cùng tên ở các phạm vi khác nhau, biến ở phạm vi bên trong hơn sẽ che khuất biến ở phạm vi bên ngoài hơn.
  • Toán tử phân giải phạm vi :: có thể được dùng để truy cập biến toàn cục khi nó bị che khuất bởi biến cục bộ.
  • Không gian tên (Namespace Scope) dùng để nhóm các định danh và cần sử dụng toán tử :: hoặc using directive để truy cập từ bên ngoài không gian tên đó.

Comments

There are no comments at the moment.