static
Sau bài này bạn sẽ làm được
- Phân biệt hai nghĩa của static tùy theo vị trí khai báo
- Dùng biến static trong hàm để nhớ giá trị giữa các lần gọi
- Dùng static ở phạm vi file để đóng gói
- Đọc được bảng năm lớp lưu trữ của C
Từ khóa static là ví dụ rõ nhất trong C về chuyện một từ mang hai nghĩa chẳng liên quan gì tới nhau. Nghĩa nào áp dụng phụ thuộc hoàn toàn vào chỗ bạn đặt nó. Hiểu nhầm chỗ này là chuyện rất phổ biến.
#Một từ khóa, hai nghĩa
| Đặt ở đâu | Nghĩa là gì | Đổi cái gì |
|---|---|---|
| Trong thân hàm | Biến sống suốt chương trình, nhớ giá trị giữa các lần gọi | Vòng đời, phạm vi giữ nguyên |
| Ngoài mọi hàm | Tên chỉ nhìn thấy được trong file này | Liên kết, vòng đời giữ nguyên |
static int a = 0; /* ngoài hàm: giới hạn trong file */
void f(void)
{
static int b = 0; /* trong hàm: nhớ giá trị giữa các lần gọi */
}
static void g(void) { } /* ngoài hàm: hàm chỉ dùng nội bộ */Điểm chung duy nhất của hai nghĩa là cả hai đều liên quan tới thứ gì đó tồn tại lâu dài, nhưng thao tác chúng thực hiện là hoàn toàn khác nhau. Hãy học chúng như hai từ khóa riêng biệt tình cờ được viết giống nhau.
#Nghĩa 1: static trong hàm
#include <stdio.h>
void khong_static(void)
{
int n = 0; /* tạo lại và gán lại mỗi lần gọi */
n++;
printf("%d ", n);
}
void co_static(void)
{
static int n = 0; /* khởi tạo đúng một lần trong cả chương trình */
n++;
printf("%d ", n);
}
int main(void)
{
for (int i = 0; i < 3; ++i) khong_static();
printf("\n");
for (int i = 0; i < 3; ++i) co_static();
printf("\n");
return 0;
}1 1 1 1 2 3
.data hoặc .bss chứ không phải trên ngăn xếp, nên nó sống suốt chương trình. Nhưng cái tên vẫn chỉ nhìn thấy được trong khối chứa nó.| int n = 0; | static int n = 0; | |
|---|---|---|
| Nằm ở đâu | Ngăn xếp | .data nếu khác 0, .bss nếu bằng 0 |
| Khởi tạo khi nào | Mỗi lần vào hàm | Một lần duy nhất, trước khi main chạy |
| Giá trị sau khi hàm kết thúc | Mất | Giữ nguyên |
| Giá trị mặc định nếu không gán | Rác | Bằng 0 |
| Giá trị khởi tạo được phép là gì | Bất kỳ biểu thức nào | Chỉ hằng biết được lúc biên dịch |
| An toàn đa luồng | Có | Không |
Ba trường hợp dùng đúng
/* 1. Sinh mã định danh tăng dần */
int lay_id(void)
{
static int id = 0;
return ++id;
}
/* 2. Khởi tạo trễ một bảng tra tốn công tính */
const double *bang_sin(void)
{
static double bang[360];
static int da_tinh = 0;
if (!da_tinh) {
for (int i = 0; i < 360; ++i)
bang[i] = sin(i * 3.14159265358979 / 180.0);
da_tinh = 1;
}
return bang;
}
/* 3. Đếm số lần một hàm được gọi, dùng khi gỡ lỗi */
void ham_nghi_ngo(void)
{
static unsigned long lan = 0;
if (++lan % 100000 == 0)
fprintf(stderr, "da goi %lu lan\n", lan);
}#Nghĩa 2: static ngoài hàm
static int bi_mat = 42; /* chỉ file1.c thấy */
static void ham_noi_bo(void) { } /* chỉ file1.c gọi được */
int cong_khai = 10; /* file khác dùng extern là thấy */
void ham_cong_khai(void) { }extern int bi_mat; /* biên dịch qua, nhưng liên kết hỏng */
extern int cong_khai; /* dùng được bình thường */
void f(void)
{
printf("%d\n", cong_khai);
printf("%d\n", bi_mat);
}/usr/bin/ld: file2.o: in function `f': file2.c:8: undefined reference to `bi_mat'
Kiểm chứng bằng bảng ký hiệu
0000000000000000 d bi_mat 0000000000000004 D cong_khai 000000000000000b T ham_cong_khai 0000000000000000 t ham_noi_bo
| Ký tự trong nm | Nghĩa |
|---|---|
| T | Hàm ở vùng mã, liên kết ngoài, file khác gọi được |
| t | Hàm ở vùng mã, liên kết nội bộ, chỉ file này |
| D | Dữ liệu đã khởi tạo, liên kết ngoài |
| d | Dữ liệu đã khởi tạo, liên kết nội bộ |
| B | Dữ liệu chưa khởi tạo ở .bss, liên kết ngoài |
| U | Ký hiệu chưa xác định, cần tìm ở file khác |
Quy tắc dễ nhớ: chữ hoa nghĩa là công khai, chữ thường nghĩa là nội bộ. Đây là công cụ nhanh nhất để kiểm tra một file đối tượng thật sự xuất ra những gì.
/* thongke.c */
int bo_dem_tam[1024];
int so_sanh(const void *a, const void *b) { ... }
void sap_xep_noi_bo(int *a, size_t n) { ... }
double trung_vi(int *a, size_t n) { ... }
/* Bốn tên lọt ra toàn chương trình. File khác vô tình đặt trùng
tên so_sanh là hỏng lúc liên kết. *//* thongke.c */
static int bo_dem_tam[1024];
static int so_sanh(const void *a, const void *b) { ... }
static void sap_xep_noi_bo(int *a, size_t n) { ... }
double trung_vi(int *a, size_t n) { ... }
/* Chỉ trung_vi lọt ra ngoài, đúng bằng nội dung của thongke.h. */#extern và liên kết
Từ khóa extern nói rằng tên này tồn tại ở đâu đó, hãy để trình liên kết đi tìm. Nó là một khai báo chứ không phải định nghĩa, nên không cấp phát vùng nhớ nào.
#ifndef CAUHINH_H
#define CAUHINH_H
extern int g_muc_do_chi_tiet; /* khai báo, đặt trong file tiêu đề */
#endif#include "cauhinh.h"
int g_muc_do_chi_tiet = 1; /* định nghĩa, đúng một lần, trong một file .c */Ba mức liên kết trong C
| Mức liên kết | Ai có | Trình liên kết thấy không |
|---|---|---|
| Liên kết ngoài | Hàm và biến ở phạm vi file, mặc định | Có, mọi file đều tham chiếu được |
| Liên kết nội bộ | Thứ ở phạm vi file có static | Không, chỉ file đó dùng |
| Không liên kết | Biến cục bộ, tham số hàm | Không, tên chỉ tồn tại lúc biên dịch |
#Bảng năm lớp lưu trữ
| Cách khai báo | Nằm ở đâu | Sống bao lâu | Ai thấy được |
|---|---|---|---|
| int x; trong hàm | Ngăn xếp | Trong khối | Khối chứa nó |
| static int x; trong hàm | .data hoặc .bss | Toàn chương trình | Khối chứa nó |
| int x; ngoài hàm | .data hoặc .bss | Toàn chương trình | Toàn chương trình |
| static int x; ngoài hàm | .data hoặc .bss | Toàn chương trình | Chỉ file này |
| extern int x; | Không cấp phát, chỉ tham chiếu | Theo nơi định nghĩa thật | Nơi có khai báo |
| register int x; | Gợi ý thanh ghi | Trong khối | Khối chứa nó, không lấy địa chỉ được |
| _Thread_local int x; | Vùng riêng của luồng | Theo vòng đời luồng | Tùy nơi khai báo |
Bài tập tự kiểm tra
#include <stdio.h>
int a = 1;
static int b = 2;
void f(void)
{
int c = 3;
static int d = 4;
c++;
d++;
printf("%d %d %d\n", c, d, a + b);
}
int main(void)
{
f();
f();
f();
return 0;
}Trước khi chạy, hãy tự trả lời: chương trình in ra ba dòng gì, biến nào nằm ở .data, biến nào ở .bss, và biến nào không xuất hiện trong bảng ký hiệu của file đối tượng. Đáp án ở ngay dưới.
4 5 3 4 6 3 4 7 3
0000000000000000 D a 0000000000000004 d b 0000000000000008 d d.0 0000000000000000 T f 0000000000000020 T main
Biến c không hề xuất hiện vì nó không có liên kết. Biến d xuất hiện với một hậu tố do trình biên dịch tự thêm để tránh trùng tên với biến d có thể có ở hàm khác. Cả ba biến còn lại là a, b và d đều nằm ở .data vì đều có giá trị khởi tạo khác 0.
Tự làm thử
- Viết
int lay_id(void)dùng biến static, gọi năm lần và in kết quả. - Viết hàm trả về con trỏ tới bộ đệm static, gọi nó hai lần trong cùng một
printfvà giải thích kết quả nhận được. - Tạo hai file, mỗi file có một hàm
statictrùng tên nhau. Chứng minh rằng liên kết vẫn thành công. Sau đó bỏstaticvà xem lỗi. - Chạy
nmlên file đối tượng của bạn, đối chiếu từng ký hiệu với bảng chữ hoa chữ thường trong bài. - Đặt một biến toàn cục có giá trị đầu bằng 0 và một biến có giá trị đầu khác 0, dùng
sizeđể xem vùng nào tăng lê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
statictrong hàm làm biến sống suốt chương trình và nhớ giá trị giữa các lần gọi, phạm vi vẫn chỉ trong khối.staticngoài hàm giới hạn tên trong một file. Đây là cách C làm việc đóng gói.- Giá trị khởi tạo của biến static phải là hằng biết được lúc biên dịch.
- Trả về con trỏ tới bộ đệm static là cái bẫy: lời gọi sau ghi đè kết quả của lời gọi trước. Hãy để người gọi cấp bộ đệm.
- File tiêu đề chỉ chứa
extern, định nghĩa thật nằm ở đúng một file.c. Dùngnmđể kiểm chứng ký hiệu nào công khai.