Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 8.816 phút đọc

Phạm vi biến

Sau bài này bạn sẽ làm được

  • Phân biệt biến cục bộ với biến toàn cục trên năm tiêu chí
  • Nhận ra hiện tượng che khuất biến và bật cờ cảnh báo
  • Giải thích ba lý do nên tránh biến toàn cục
  • Thay biến toàn cục bằng tham số hoặc bằng ngữ cảnh truyền vào

Phạm vi trả lời câu hỏi tên này nhìn thấy được ở đâu. Vòng đời trả lời câu hỏi vùng nhớ này sống được bao lâu. Hai khái niệm khác nhau, hay bị gộp làm một, và hiểu lẫn lộn chúng là nguồn của rất nhiều lỗi khó tìm.

#Bốn mức phạm vi

Mức phạm viNhìn thấy ở đâuVí dụ
Phạm vi khốiTừ chỗ khai báo tới ngoặc nhọn đóng gần nhấtBiến khai báo trong thân hàm hoặc trong thân vòng lặp
Phạm vi nguyên mẫuChỉ trong cặp ngoặc của một khai báo hàmTên tham số trong int f(int a, int b);
Phạm vi fileTừ chỗ khai báo tới cuối fileBiến và hàm khai báo ngoài mọi hàm
Phạm vi hàmToàn bộ một hàm, bất kể khối nàoChỉ áp dụng cho nhãn dùng với goto
pham-vi.c
int toan_cuc = 1;            /* phạm vi file, thấy từ đây tới cuối file */

void f(void)
{
    int cuc_bo = 2;          /* phạm vi khối: cả thân hàm f */

    if (cuc_bo > 0) {
        int trong_if = 3;    /* phạm vi khối: chỉ trong thân if */

        printf("%d\n", trong_if);
    }

    /* printf("%d\n", trong_if);  <- lỗi: 'trong_if' undeclared */

    for (int i = 0; i < 3; ++i) {
        /* i chỉ tồn tại trong vòng lặp, đây là cú pháp từ C99 */
    }

    /* printf("%d\n", i);  <- lỗi tương tự */
}

#Cục bộ so với toàn cục

Tiêu chíBiến cục bộBiến toàn cục
Nằm ở đâu trong bộ nhớNgăn xếpVùng .data hoặc .bss
Sống bao lâuTừ lúc vào khối tới lúc ra khỏi khốiSuốt thời gian chương trình chạy
Giá trị ban đầu nếu không gánRác, không xác định đượcTự động bằng 0
Ai nhìn thấyChỉ khối chứa nóCả file, và cả chương trình nếu dùng extern
Đa luồngMỗi luồng có bản riêng, an toànDùng chung, phải tự khóa
Chi phí cấp phátGần bằng khôngKhông có, đã cấp sẵn lúc nạp
Biến cục bộ nằm trên ngăn xếp và biến mất khi hàm kết thúc. Biến toàn cục và biến static nằm ở .data hoặc .bss, sống suốt chương trình.

Vì sao biến toàn cục lại tự bằng 0

Không phải trình biên dịch chăm chỉ sinh mã gán. Biến toàn cục chưa khởi tạo được xếp vào vùng .bss, và vùng này không chiếm chỗ trong file thực thi, chỉ ghi lại nó cần bao nhiêu byte. Khi nạp chương trình, hệ điều hành cấp một vùng nhớ đã được xóa sạch về 0. Đó là quy định bảo mật: một tiến trình không được phép nhìn thấy dữ liệu còn sót của tiến trình khác.

terminal
# Xem kích thước từng vùng trong file thực thi
size ./bai
   text    data     bss     dec     hex filename
   1876     620       8    2504     9c8 ./bai

#Che khuất biến

Che khuất
Khi một biến ở phạm vi hẹp trùng tên với biến ở phạm vi rộng hơn, cái hẹp che mất cái rộng trong toàn bộ phạm vi của nó. Đây là hành vi hợp lệ của C, không phải lỗi.
che-khuat.c
#include <stdio.h>

int x = 10;                  /* toàn cục */

void f(void)
{
    int x = 20;              /* che khuất biến toàn cục */

    {
        int x = 30;          /* che khuất tiếp biến ở tầng trên */

        printf("%d\n", x);   /* 30 */
    }

    printf("%d\n", x);       /* 20 */
}

int main(void)
{
    f();
    printf("%d\n", x);       /* 10, biến toàn cục chưa hề bị đổi */

    return 0;
}
terminal
gcc -std=c17 -Wall -Wextra -Wshadow che-khuat.c -o t
che-khuat.c:7:9: warning: declaration of 'x' shadows a global declaration [-Wshadow]
che-khuat.c:10:13: warning: declaration of 'x' shadows a previous local [-Wshadow]

Che khuất giữa tham số và biến toàn cục cũng bị bắt bởi cùng cờ đó. Trường hợp che khuất duy nhất được coi là chấp nhận được là biến đếm i trong các vòng lặp lồng nhau ở những khối tách biệt hoàn toàn.

#Ba lý do tránh biến toàn cục

Lý do 1: không kiểm thử được từng phần

int g_cau_hinh;

int tinh(int x)
{
    return x * g_cau_hinh;   /* kết quả phụ thuộc thứ bạn không thấy trong nguyên mẫu */
}

Muốn kiểm thử tinh, bạn phải nhớ đặt g_cau_hinh trước, và nhớ khôi phục sau nếu bài kiểm thử khác cũng dùng nó. Nguyên mẫu hàm nói dối: nó bảo hàm chỉ cần một tham số, thực tế cần hai.

Lý do 2: không truy được ai đã sửa

Khi một biến toàn cục mang giá trị sai, thủ phạm có thể là bất kỳ hàm nào trong chương trình. Với biến cục bộ, danh sách nghi phạm chỉ gói trong vài chục dòng.

terminal
# gdb có công cụ cho chuyện này, nhưng đó đã là chữa cháy
(gdb) watch g_cau_hinh
Hardware watchpoint 2: g_cau_hinh

Old value = 0
New value = 7
khoi_tao () at cauhinh.c:41

Lý do 3: hỏng ngay khi có nhiều luồng

int g_dem = 0;

void *worker(void *arg)
{
    for (int i = 0; i < 1000000; ++i)
        g_dem++;             /* ba thao tác: đọc, cộng, ghi. Hai luồng giẫm lên nhau */

    return NULL;
}

/* Chạy bốn luồng, kết quả mong đợi là 4000000.
   Thực tế nhận được một con số nhỏ hơn và khác nhau mỗi lần chạy. */

#Thay biến toàn cục bằng gì

Trạng thái nằm ở biến toàn cục
int  g_tong = 0;
int  g_dem  = 0;

void them(int x)
{
    g_tong += x;
    g_dem++;
}

double trung_binh(void)
{
    return g_dem ? (double)g_tong / g_dem : 0.0;
}

/* Không thể theo dõi hai dãy số cùng lúc. */
Trạng thái truyền qua tham số
typedef struct {
    long long tong;
    size_t    dem;
} ThongKe;

void them(ThongKe *tk, int x)
{
    tk->tong += x;
    tk->dem++;
}

double trung_binh(const ThongKe *tk)
{
    return tk->dem ? (double)tk->tong / tk->dem : 0.0;
}

ThongKe a = { 0, 0 };
ThongKe b = { 0, 0 };   /* hai bộ đếm độc lập, không đụng nhau */

Mô hình bên phải gọi là ngữ cảnh truyền vào, và nó là cách gần như mọi thư viện C hiện đại được thiết kế. Bạn đã gặp nó mà không để ý: FILE * chính là một ngữ cảnh, và đó là lý do bạn mở được nhiều file cùng lúc.

Thứ bạn định làm toàn cụcCách thay thế
Hằng số cấu hình#define hoặc const ở phạm vi file, kèm static
Bộ đếm hoặc trạng thái tích lũyStruct ngữ cảnh truyền qua con trỏ
Bộ đệm dùng lại nhiều lầnTham số ra do người gọi cấp
Kết quả trung gian giữa hai hàmGiá trị trả về hoặc tham số ra
Biến nhớ giữa các lần gọi cùng một hàmBiến static trong hàm, học ở Bài 8.9

Tự làm thử

  1. In giá trị của một biến toàn cục và một biến cục bộ chưa khởi tạo. Chạy năm lần và ghi lại kết quả.
  2. Viết chương trình có ba tầng che khuất như ví dụ trong bài, biên dịch với -Wshadow và đọc từng cảnh báo.
  3. Dùng size lên chương trình của bạn, sau đó thêm một mảng toàn cục một triệu phần tử và chạy lại. Vùng nào tăng lên.
  4. Viết lại một chương trình dùng biến toàn cục thành phiên bản dùng struct ngữ cảnh, rồi chứng minh phiên bản mới theo dõi được hai bộ dữ liệu cùng lúc.
  5. Cố tình tạo lỗi che khuất khiến kết quả sai mà không có cảnh báo nào với -Wall -Wextra. Sau đó bật -Wshadow để bắt nó.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Phạm vi là tên nhìn thấy ở đâu, vòng đời là vùng nhớ sống bao lâu. Hai chuyện khác nhau.
  • Biến cục bộ nằm trên ngăn xếp và chứa rác nếu không khởi tạo. Biến toàn cục nằm ở .data hoặc .bss và tự bằng 0.
  • Che khuất biến là hợp lệ nhưng nguy hiểm. Bật -Wshadow vì cờ này không nằm trong -Wall.
  • Ba lý do tránh biến toàn cục: không kiểm thử được, không truy được ai sửa, và hỏng khi có nhiều luồng.
  • Thay biến toàn cục bằng tham số hoặc bằng một struct ngữ cảnh truyền qua con trỏ.