Bài 14.416 phút đọc
Bộ nhớ toàn cục
Sau bài này bạn sẽ làm được
- Phân biệt khai báo với định nghĩa biến toàn cục
- Hiểu vì sao định nghĩa dự phòng là cái bẫy
- Bật cờ để trình liên kết bắt lỗi trùng định nghĩa
- Quản lý trạng thái toàn cục một cách kỷ luật
Biến toàn cục nằm cùng vùng với biến tĩnh, nhưng nó có một đặc điểm riêng gây ra rất nhiều lỗi liên kết: nó nhìn thấy được từ mọi file. Bài này nói về cách khai báo đúng, một cái bẫy lịch sử của C, và kỷ luật cần có.
#Khai báo và định nghĩa
Định nghĩa
Câu lệnh thật sự cấp phát vùng nhớ cho biến. Trong cả chương trình chỉ được có đúng một định nghĩa cho mỗi biến toàn cục.
Khai báo
Câu lệnh nói rằng biến này tồn tại ở đâu đó, để trình biên dịch biết kiểu của nó. Lặp lại bao nhiêu lần cũng được.
int g_dem = 0; /* ĐỊNH NGHĨA: cấp phát 4 byte ở .bss */
extern int g_dem; /* KHAI BÁO: không cấp gì, chỉ tham chiếu */| Định nghĩa | Khai báo | |
|---|---|---|
| Viết thế nào | int g_dem = 0; | extern int g_dem; |
| Cấp phát bộ nhớ | Có | Không |
| Số lần cho phép | Đúng một, trong cả chương trình | Bao nhiêu cũng được |
| Đặt ở đâu | Trong đúng một file .c | Trong file .h |
| Có giá trị khởi tạo | Được | Không được |
cauhinh.h
#ifndef CAUHINH_H
#define CAUHINH_H
extern int g_muc_do_chi_tiet; /* khai báo */
extern char g_ten_chuong_trinh[64];
extern double g_nguong;
#endifcauhinh.c
#include "cauhinh.h"
int g_muc_do_chi_tiet = 1; /* định nghĩa, đúng một lần */
char g_ten_chuong_trinh[64] = "app";
double g_nguong = 0.5;main.c
#include <stdio.h>
#include "cauhinh.h"
int main(void)
{
printf("%d %s %.2f\n",
g_muc_do_chi_tiet, g_ten_chuong_trinh, g_nguong);
return 0;
}#Định nghĩa dự phòng, cái bẫy lịch sử
Điều kỳ lạ là đoạn mã sau vẫn liên kết được trên nhiều trình biên dịch, dù nó vi phạm quy tắc một định nghĩa.
a.c
int g_dem; /* không có giá trị khởi tạo */b.c
int g_dem; /* cũng vậy, nhưng vẫn liên kết được */Định nghĩa dự phòng
Khai báo biến ở phạm vi file không có từ khóa
extern và không có giá trị khởi tạo. Trình liên kết được phép gộp nhiều định nghĩa dự phòng thành một biến duy nhất.| Cách viết | Là gì | Nhiều file cùng viết thì sao |
|---|---|---|
| int g; | Định nghĩa dự phòng | Gộp thành một, không báo lỗi |
| int g = 0; | Định nghĩa thật | Lỗi trùng định nghĩa |
| extern int g; | Khai báo thuần túy | Không sao, đúng cách |
| extern int g = 0; | Định nghĩa thật, extern bị bỏ qua | Lỗi trùng định nghĩa |
#Mô hình đúng với nhiều file
Ba cách viết sai
/* 1. Định nghĩa trong .h: trùng định nghĩa */
/* cauhinh.h */
int g_dem = 0;
/* 2. Định nghĩa dự phòng ở nhiều file: có thể im lặng gộp */
/* a.c */ int g_dem;
/* b.c */ int g_dem;
/* 3. Khai báo lệch kiểu: liên kết qua nhưng chạy sai */
/* a.c */ int g_dem = 0;
/* b.c */ extern long g_dem;Cách đúng duy nhất
/* cauhinh.h: CHỈ khai báo */
#ifndef CAUHINH_H
#define CAUHINH_H
extern int g_dem;
#endif
/* cauhinh.c: định nghĩa đúng MỘT lần */
#include "cauhinh.h"
int g_dem = 0;
/* Mọi file khác chỉ include cauhinh.h */
#include "cauhinh.h"Thủ thuật một file tiêu đề duy nhất
cauhinh.h
#ifndef CAUHINH_H
#define CAUHINH_H
/* Trong đúng một file .c, định nghĩa CAUHINH_TRIEN_KHAI trước khi include. */
#ifdef CAUHINH_TRIEN_KHAI
#define CH_BIEN(kieu, ten, gt) kieu ten = gt
#else
#define CH_BIEN(kieu, ten, gt) extern kieu ten
#endif
CH_BIEN(int, g_muc_do, 1);
CH_BIEN(double, g_nguong, 0.5);
#endifcauhinh.c
#define CAUHINH_TRIEN_KHAI
#include "cauhinh.h"#Vòng đời và thứ tự khởi tạo
| Thời điểm | Trạng thái biến toàn cục |
|---|---|
| Trước khi nạp chương trình | Chưa tồn tại |
| Ngay sau khi nạp | Đã có giá trị khởi tạo, hoặc bằng 0 |
| Trong suốt thời gian chạy | Tồn tại, ai cũng sửa được |
| Sau khi main trả về | Vẫn tồn tại cho tới khi tiến trình kết thúc |
| Sau khi tiến trình kết thúc | Hệ điều hành thu hồi toàn bộ |
Biến toàn cục vẫn sống sau khi main trả về
sau-main.c
#include <stdio.h>
#include <stdlib.h>
static int g = 7;
static void khi_thoat(void)
{
printf("atexit doc duoc g = %d\n", g); /* vẫn hợp lệ */
}
int main(void)
{
atexit(khi_thoat);
g = 99;
return 0;
}terminal
./sau-main
atexit doc duoc g = 99
Đây là khác biệt quan trọng với biến cục bộ của main: những biến đó chết ngay khi main trả về, nên hàm đăng ký bằng atexit không được phép đụng vào chúng.
#Kỷ luật với trạng thái toàn cục
Bài 8.8 đã liệt kê ba lý do tránh biến toàn cục. Nếu bạn buộc phải dùng, đây là năm quy tắc giảm thiệt hại.
| Quy tắc | Nội dung | Vì sao |
|---|---|---|
| 1 | Thêm static nếu chỉ dùng trong một file | Trình liên kết không thấy tên, tránh trùng và cho tối ưu mạnh hơn |
| 2 | Thêm const nếu không bao giờ sửa | Trình biên dịch chặn mọi phép ghi, và biến vào vùng chỉ đọc |
| 3 | Đặt tiền tố g_ hoặc tên gói | Người đọc biết ngay đây là trạng thái dùng chung |
| 4 | Không cho ai đọc ghi trực tiếp, chỉ qua hàm | Bạn kiểm soát được ai đổi và đổi khi nào |
| 5 | Ghi rõ ai được sửa và khi nào trong chú thích | Đây là hợp đồng duy nhất mà C có |
Biến toàn cục mở
cauhinh.h
extern int g_muc_do; /* ai cũng đọc ghi được, bất cứ lúc nào */Đóng gói sau hàm truy cập
cauhinh.h
/* cauhinh.h */
int ch_lay_muc_do(void);
int ch_dat_muc_do(int muc_do); /* trả về 0 nếu hợp lệ */
/* cauhinh.c */
static int g_muc_do = 1; /* static: không file nào khác thấy */
int ch_lay_muc_do(void)
{
return g_muc_do;
}
int ch_dat_muc_do(int muc_do)
{
if (muc_do < 0 || muc_do > 3) return -1;
g_muc_do = muc_do;
return 0;
}Trường hợp biến toàn cục thật sự đúng
#include <signal.h>
/* Cờ báo dừng do trình xử lý tín hiệu đặt.
Kiểu volatile sig_atomic_t là kiểu DUY NHẤT chuẩn cho phép
trình xử lý tín hiệu ghi vào một biến toàn cục. */
static volatile sig_atomic_t g_can_dung = 0;
static void xu_ly_tin_hieu(int so)
{
(void)so;
g_can_dung = 1; /* chỉ được phép làm đúng chuyện này */
}
int main(void)
{
signal(SIGINT, xu_ly_tin_hieu);
while (!g_can_dung) {
/* làm việc */
}
printf("Da dung sach se\n");
return 0;
}Tự làm thử
- Tạo ba file theo mô hình đúng, kiểm chứng chương trình biên dịch và chạy được.
- Đặt định nghĩa vào file tiêu đề để tạo lỗi trùng định nghĩa, chép lại thông báo của trình liên kết.
- Viết
int g;ở hai file, biên dịch với-fcommonvà với-fno-common, so hai kết quả. - Khai báo cùng một biến với hai kiểu khác nhau ở hai file, quan sát chuyện gì xảy ra khi ghi ở file này và đọc ở file kia.
- Đổi kiểu biến trong file tiêu đề mà không đổi ở file định nghĩa, xác nhận trình biên dịch bắt được.
- Bọc một biến toàn cục sau cặp hàm đọc ghi có kiểm tra, dùng gdb đặt điểm dừng ở hàm ghi.
- Viết chương trình dừng sạch bằng cờ
volatile sig_atomic_t, thử bỏvolatilevà biên dịch với-O2.
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
- Định nghĩa cấp phát bộ nhớ và chỉ được có một. Khai báo bằng
externthì lặp lại bao nhiêu cũng được. - File tiêu đề chỉ chứa
extern, định nghĩa nằm ở đúng một file.c, và file đó cũng phải nạp file tiêu đề của chính nó. - Định nghĩa dự phòng là cái bẫy lịch sử. Luôn bật
-fno-common, và nó đã là mặc định từ GCC 10. - Biến toàn cục đã có giá trị trước cả dòng đầu tiên của main, và vẫn sống sau khi main trả về.
- Nếu buộc phải dùng trạng thái toàn cục, hãy đóng gói nó sau cặp hàm đọc ghi và đánh dấu biến là
static.