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>

using namespace std;

int x = 100;

void truy_cap_x() {
    cout << "Trong ham truy_cap_x, x = " << x << endl;
}

int main() {
    cout << "Trong ham main, x = " << x << endl;
    truy_cap_x();
    return 0;
}

Output:

Trong ham main, x = 100
Trong ham truy_cap_x, x = 100

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>

using namespace std;

int main() {
    int y = 50;
    cout << "Trong ham main, y = " << y << endl;
    return 0;
}

Output:

Trong ham main, y = 50

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>

using namespace std;

int main() {
    int so = 10;

    if (so > 5) {
        string s = "So lon hon 5";
        cout << s << endl;
    }

    return 0;
}

Output:

So lon hon 5

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>

using namespace std;

int main() {
    for (int i = 0; i < 3; ++i) {
        cout << "i = " << i << endl;
        int j = i * 10;
        cout << "j = " << j << endl;
    }
    return 0;
}

Output:

i = 0
j = 0
i = 1
j = 10
i = 2
j = 20

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>

using namespace std;

namespace KG {
    int a = 42;

    void in_gia_tri() {
        cout << "Trong KG, a = " << a << endl;
    }
}

int main() {
    cout << "Truy cap tu main: " << KG::a << endl;
    KG::in_gia_tri();

    return 0;
}

Output:

Truy cap tu main: 42
Trong KG, a = 42

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>

using namespace std;

int ten_chung = 100; // Bien toan cuc

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

    cout << "Bien cuc bo trong main: " << ten_chung << endl;
    cout << "Bien toan cuc (su dung ::): " << ::ten_chung << endl;

    { // Bat dau mot khoi moi
        int ten_chung = 25; // Bien cuc bo khac, che khuat bien cuc bo trong main
        cout << "Bien cuc bo trong khoi con: " << ten_chung << endl;
    }

    cout << "Tro lai main, bien cuc bo trong main: " << ten_chung << endl;

    return 0;
}

Output:

Bien cuc bo trong main: 50
Bien toan cuc (su dung ::): 100
Bien cuc bo trong khoi con: 25
Tro lai main, bien cuc bo trong main: 50

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.