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 vi | Nhìn thấy ở đâu | Ví dụ |
|---|---|---|
| Phạm vi khối | Từ chỗ khai báo tới ngoặc nhọn đóng gần nhất | Biến khai báo trong thân hàm hoặc trong thân vòng lặp |
| Phạm vi nguyên mẫu | Chỉ trong cặp ngoặc của một khai báo hàm | Tên tham số trong int f(int a, int b); |
| Phạm vi file | Từ chỗ khai báo tới cuối file | Biến và hàm khai báo ngoài mọi hàm |
| Phạm vi hàm | Toàn bộ một hàm, bất kể khối nào | Chỉ áp dụng cho nhãn dùng với goto |
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ếp | Vùng .data hoặc .bss |
| Sống bao lâu | Từ lúc vào khối tới lúc ra khỏi khối | Suốt thời gian chương trình chạy |
| Giá trị ban đầu nếu không gán | Rác, không xác định được | Tự động bằng 0 |
| Ai nhìn thấy | Chỉ khối chứa nó | Cả file, và cả chương trình nếu dùng extern |
| Đa luồng | Mỗi luồng có bản riêng, an toàn | Dùng chung, phải tự khóa |
| Chi phí cấp phát | Gần bằng không | Không có, đã cấp sẵn lúc nạp |
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.
text data bss dec hex filename 1876 620 8 2504 9c8 ./bai
#Che khuất biến
#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;
}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.
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ì
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. */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ục | Cá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ũy | Struct ngữ cảnh truyền qua con trỏ |
| Bộ đệm dùng lại nhiều lần | Tham số ra do người gọi cấp |
| Kết quả trung gian giữa hai hàm | Giá trị trả về hoặc tham số ra |
| Biến nhớ giữa các lần gọi cùng một hàm | Biến static trong hàm, học ở Bài 8.9 |
Tự làm thử
- 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ả.
- 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
-Wshadowvà đọc từng cảnh báo. - Dùng
sizelê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. - 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.
- 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 ở
.datahoặc.bssvà tự bằng 0. - Che khuất biến là hợp lệ nhưng nguy hiểm. Bật
-Wshadowvì 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ỏ.